agent response improvements

frdel committed Jun 16, 2025 at 20:14 UTC 73e6855345fbbb4e714de5348d6fb79b2d190d80
11 files changed +814 -537
agent.py
+78 -35
@@ -1,5 +1,6 @@
1 import asyncio
2 import nest_asyncio
3 +
4 nest_asyncio.apply()
5
6 from collections import OrderedDict
@@ -79,7 +80,7 @@ class AgentContext:
80 if not AgentContext._contexts:
81 return None
82 return list(AgentContext._contexts.values())[0]
82 -
83 +
84 @staticmethod
85 def all():
86 return list(AgentContext._contexts.values())
@@ -97,7 +98,8 @@ class AgentContext:
98 "name": self.name,
99 "created_at": (
100 Localization.get().serialize_datetime(self.created_at)
100 - if self.created_at else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
101 + if self.created_at
102 + else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
103 ),
104 "no": self.no,
105 "log_guid": self.log.guid,
@@ -106,7 +108,8 @@ class AgentContext:
108 "paused": self.paused,
109 "last_message": (
110 Localization.get().serialize_datetime(self.last_message)
109 - if self.last_message else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
111 + if self.last_message
112 + else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
113 ),
114 "type": self.type.value,
115 }
@@ -124,7 +127,11 @@ class AgentContext:
127 ) -> list[Log.LogItem]:
128 items: list[Log.LogItem] = []
129 for context in AgentContext.all():
127 - items.append(context.log.log(type, heading, content, kvps, temp, update_progress, id, **kwargs))
130 + items.append(
131 + context.log.log(
132 + type, heading, content, kvps, temp, update_progress, id, **kwargs
133 + )
134 + )
135 return items
136
137 def kill_process(self):
@@ -253,6 +260,8 @@ class LoopData:
260 self.extras_temporary: OrderedDict[str, history.MessageContent] = OrderedDict()
261 self.extras_persistent: OrderedDict[str, history.MessageContent] = OrderedDict()
262 self.last_response = ""
263 + self.params_temporary: dict = {}
264 + self.params_persistent: dict = {}
265
266 # override values with kwargs
267 for key, value in kwargs.items():
@@ -313,9 +322,12 @@ class Agent:
322
323 self.context.streaming_agent = self # mark self as current streamer
324 self.loop_data.iteration += 1
325 + self.loop_data.params_temporary = {} # clear temporary params
326
327 # call message_loop_start extensions
318 - await self.call_extensions("message_loop_start", loop_data=self.loop_data)
328 + await self.call_extensions(
329 + "message_loop_start", loop_data=self.loop_data
330 + )
331
332 try:
333 # prepare LLM chain (model, system, history)
@@ -328,15 +340,18 @@ class Agent:
340 padding=True,
341 background_color="white",
342 ).print(f"{self.agent_name}: Generating")
331 - log = self.context.log.log(
332 - type="agent", heading=f"{self.agent_name}: Generating"
343 + # create log message right away, more responsive
344 + self.loop_data.params_temporary["log_item_generating"] = (
345 + self.context.log.log(
346 + type="agent", heading=f"{self.agent_name}: Generating"
347 + )
348 )
349
350 async def stream_callback(chunk: str, full: str):
351 # output the agent response stream
352 if chunk:
353 printer.stream(chunk)
339 - self.log_from_stream(full, log)
354 + await self.handle_response_stream(full)
355
356 agent_response = await self.call_chat_model(
357 prompt, callback=stream_callback
@@ -414,10 +429,14 @@ class Agent:
429 # for extra in loop_data.extras_temporary.values():
430 # extras += history.Message(False, content=extra).output()
431 extras = history.Message(
417 - False,
418 - content=self.read_prompt("agent.context.extras.md", extras=dirty_json.stringify(
419 - {**loop_data.extras_persistent, **loop_data.extras_temporary}
420 - ))).output()
432 + False,
433 + content=self.read_prompt(
434 + "agent.context.extras.md",
435 + extras=dirty_json.stringify(
436 + {**loop_data.extras_persistent, **loop_data.extras_temporary}
437 + ),
438 + ),
439 + ).output()
440 loop_data.extras_temporary.clear()
441
442 # convert history + extras to LLM format
@@ -527,14 +546,14 @@ class Agent:
546 "fw.intervention.md",
547 message=message.message,
548 attachments=message.attachments,
530 - system_message=message.system_message
549 + system_message=message.system_message,
550 )
551 else:
552 content = self.parse_prompt(
553 "fw.user_message.md",
554 message=message.message,
555 attachments=message.attachments,
537 - system_message=message.system_message
556 + system_message=message.system_message,
557 )
558
559 # remove empty parts from template
@@ -705,34 +724,39 @@ class Agent:
724 if tool_request is not None:
725 raw_tool_name = tool_request.get("tool_name", "") # Get the raw tool name
726 tool_args = tool_request.get("tool_args", {})
708 -
727 +
728 tool_name = raw_tool_name # Initialize tool_name with raw_tool_name
729 tool_method = None # Initialize tool_method
730
731 # Split raw_tool_name into tool_name and tool_method if applicable
732 if ":" in raw_tool_name:
733 tool_name, tool_method = raw_tool_name.split(":", 1)
715 -
734 +
735 tool = None # Initialize tool to None
736
737 # Try getting tool from MCP first
738 try:
720 - import python.helpers.mcp_handler as mcp_helper
721 - mcp_tool_candidate = mcp_helper.MCPConfig.get_instance().get_tool(self, tool_name)
739 + import python.helpers.mcp_handler as mcp_helper
740 +
741 + mcp_tool_candidate = mcp_helper.MCPConfig.get_instance().get_tool(
742 + self, tool_name
743 + )
744 if mcp_tool_candidate:
745 tool = mcp_tool_candidate
746 except ImportError:
725 - PrintStyle(background_color="black", font_color="yellow", padding=True).print(
726 - "MCP helper module not found. Skipping MCP tool lookup."
727 - )
747 + PrintStyle(
748 + background_color="black", font_color="yellow", padding=True
749 + ).print("MCP helper module not found. Skipping MCP tool lookup.")
750 except Exception as e:
729 - PrintStyle(background_color="black", font_color="red", padding=True).print(
730 - f"Failed to get MCP tool '{tool_name}': {e}"
731 - )
751 + PrintStyle(
752 + background_color="black", font_color="red", padding=True
753 + ).print(f"Failed to get MCP tool '{tool_name}': {e}")
754
755 # Fallback to local get_tool if MCP tool was not found or MCP lookup failed
756 if not tool:
735 - tool = self.get_tool(name=tool_name, method=tool_method, args=tool_args, message=msg)
757 + tool = self.get_tool(
758 + name=tool_name, method=tool_method, args=tool_args, message=msg
759 + )
760
761 if tool:
762 await self.handle_intervention()
@@ -745,7 +769,9 @@ class Agent:
769 if response.break_loop:
770 return response.message
771 else:
748 - error_detail = f"Tool '{raw_tool_name}' not found or could not be initialized."
772 + error_detail = (
773 + f"Tool '{raw_tool_name}' not found or could not be initialized."
774 + )
775 self.hist_add_warning(error_detail)
776 PrintStyle(font_color="red", padding=True).print(error_detail)
777 self.context.log.log(
@@ -756,21 +782,29 @@ class Agent:
782 self.hist_add_warning(warning_msg_misformat)
783 PrintStyle(font_color="red", padding=True).print(warning_msg_misformat)
784 self.context.log.log(
759 - type="error", content=f"{self.agent_name}: Message misformat, no valid tool request found."
785 + type="error",
786 + content=f"{self.agent_name}: Message misformat, no valid tool request found.",
787 )
788
762 - def log_from_stream(self, stream: str, logItem: Log.LogItem):
789 + async def handle_response_stream(self, stream: str):
790 try:
791 if len(stream) < 25:
792 return # no reason to try
793 response = DirtyJson.parse_string(stream)
794 if isinstance(response, dict):
768 - # log if result is a dictionary already
769 - logItem.update(content=stream, kvps=response)
795 + await self.call_extensions(
796 + "response_stream",
797 + loop_data=self.loop_data,
798 + text=stream,
799 + parsed=response,
800 + )
801 +
802 except Exception as e:
803 pass
804
773 - def get_tool(self, name: str, method: str | None, args: dict, message: str, **kwargs):
805 + def get_tool(
806 + self, name: str, method: str | None, args: dict, message: str, **kwargs
807 + ):
808 from python.tools.unknown import Unknown
809 from python.helpers.tool import Tool
810
@@ -778,13 +812,22 @@ class Agent:
812 "python/tools", name + ".py", Tool
813 )
814 tool_class = classes[0] if classes else Unknown
781 - return tool_class(agent=self, name=name, method=method, args=args, message=message, **kwargs)
815 + return tool_class(
816 + agent=self, name=name, method=method, args=args, message=message, **kwargs
817 + )
818
819 async def call_extensions(self, folder: str, **kwargs) -> Any:
820 from python.helpers.extension import Extension
821
786 - classes = extract_tools.load_classes_from_folder(
787 - "python/extensions/" + folder, "*", Extension
788 - )
822 + cache = {} # some extensions can be called very often, like response_stream
823 +
824 + if folder in cache:
825 + classes = cache[folder]
826 + else:
827 + classes = extract_tools.load_classes_from_folder(
828 + "python/extensions/" + folder, "*", Extension
829 + )
830 + cache[folder] = classes
831 +
832 for cls in classes:
833 await cls(agent=self).execute(**kwargs)
prompts/default/agent.system.tool.response.md
+5 -1
@@ -3,7 +3,11 @@ final answer to user
3 ends task processing use only when done or no task active
4 put result in text arg
5 always write full file paths
6 -always use markdown for formatting including headers bold text and lists to improve readability
6 +always use markdown formatting headers bold text lists emojis improve readability
7 +prefer using tables
8 +output full file paths not only names they are clickable
9 +images shown with ![alt](img:///path/to/image.png)
10 +math with latex notation delimiters <$LATEX>...</$LATEX>
11 usage:
12 ~~~json
13 {
prompts/research_agent/agent.system.main.communication.md
+1 -1
@@ -42,7 +42,7 @@ No other text is allowed!
42 Only one JSON object allowed per reply
43
44 ### rules
45 -Math requires latex notation $...$ delimiters
45 +Math requires latex notation <$LATEX>...<$LATEX> delimiters
46 Code inside markdown must be enclosed in "~~~" and not "```"
47 dont use **
48
python/extensions/response_stream/.gitkeep
python/extensions/response_stream/_10_log_from_stream.py new
+28
@@ -0,0 +1,28 @@
1 +from python.helpers import persist_chat, tokens
2 +from python.helpers.extension import Extension
3 +from agent import LoopData
4 +import asyncio
5 +from python.helpers.log import LogItem
6 +from python.helpers import log
7 +
8 +
9 +class LogFromStream(Extension):
10 +
11 + async def execute(
12 + self,
13 + loop_data: LoopData = LoopData(),
14 + text: str = "",
15 + parsed: dict = {},
16 + **kwargs
17 + ):
18 +
19 + # create log message and store it in loop data temporary params
20 + if "log_item_generating" not in loop_data.params_temporary:
21 + loop_data.params_temporary["log_item_generating"] = self.agent.context.log.log(
22 + type="agent",
23 + heading=f"{self.agent.agent_name}: Generating",
24 + )
25 +
26 + # update log message
27 + log_item = loop_data.params_temporary["log_item_generating"]
28 + log_item.update(content=text, kvps=parsed)
\ No newline at end of file
python/extensions/response_stream/_20_live_response.py new
+41
@@ -0,0 +1,41 @@
1 +from python.helpers import persist_chat, tokens
2 +from python.helpers.extension import Extension
3 +from agent import LoopData
4 +import asyncio
5 +from python.helpers.log import LogItem
6 +from python.helpers import log
7 +
8 +
9 +class LiveResponse(Extension):
10 +
11 + async def execute(
12 + self,
13 + loop_data: LoopData = LoopData(),
14 + text: str = "",
15 + parsed: dict = {},
16 + **kwargs,
17 + ):
18 + try:
19 + if (
20 + not "tool_name" in parsed
21 + or parsed["tool_name"] != "response"
22 + or "tool_args" not in parsed
23 + or "text" not in parsed["tool_args"]
24 + or not parsed["tool_args"]["text"]
25 + ):
26 + return # not a response
27 +
28 + # create log message and store it in loop data temporary params
29 + if "log_item_response" not in loop_data.params_temporary:
30 + loop_data.params_temporary["log_item_response"] = (
31 + self.agent.context.log.log(
32 + type="response",
33 + heading=f"{self.agent.agent_name}: Responding",
34 + )
35 + )
36 +
37 + # update log message
38 + log_item = loop_data.params_temporary["log_item_response"]
39 + log_item.update(content=parsed["tool_args"]["text"])
40 + except Exception as e:
41 + pass
python/tools/response.py
+6 -4
@@ -1,13 +1,15 @@
1 from python.helpers.tool import Tool, Response
2
3 +
4 class ResponseTool(Tool):
5
5 - async def execute(self,**kwargs):
6 + async def execute(self, **kwargs):
7 return Response(message=self.args["text"], break_loop=True)
8
9 async def before_execution(self, **kwargs):
9 - self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
10 + # self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
11 + # don't log here anymore, we have the live_response extension now
12 + pass
13
11 -
14 async def after_execution(self, response, **kwargs):
13 - pass # do not add anything to the history or output
\ No newline at end of file
15 + pass # do not add anything to the history or output
webui/css/modals.css
+3 -3
@@ -58,7 +58,7 @@
58 }
59
60 .modal-header h2 {
61 - font-size: var(--font-size-large);
61 + /* font-size: var(--font-size-large); */
62 margin: 0;
63 }
64
@@ -340,9 +340,9 @@
340 }
341
342 /* Typography */
343 -h2 {
343 +/* h2 {
344 color: var(--color-primary);
345 -}
345 +} */
346
347 /* Responsive Design */
348 @media (max-width: 768px) {
webui/index.css
+547 -451
@@ -1,7 +1,9 @@
1 -@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
1 +@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap");
2
3 /* Add box-sizing globally for better cross-browser consistency */
4 -*, *::before, *::after {
4 +*,
5 +*::before,
6 +*::after {
7 box-sizing: border-box;
8 }
9
@@ -53,6 +55,7 @@
55
56 /* Font sizes */
57 --font-size-small: 0.8rem;
58 + --font-size-smaller: 0.9rem;
59 --font-size-normal: 1rem;
60 --font-size-large: 1.2rem;
61
@@ -60,7 +63,8 @@
63 --border-radius: 1.125rem;
64 --transition-speed: 0.3s;
65 --svg-filter: brightness(0) saturate(100%) var(--color-primary-filter);
63 - --color-primary-filter: invert(73%) sepia(17%) saturate(360%) hue-rotate(177deg) brightness(87%) contrast(85%);
66 + --color-primary-filter: invert(73%) sepia(17%) saturate(360%)
67 + hue-rotate(177deg) brightness(87%) contrast(85%);
68 }
69
70 /* Reset and Base Styles */
@@ -78,7 +82,7 @@ html {
82 overflow: hidden;
83 position: fixed;
84 -webkit-font-smoothing: antialiased; /* Improve font rendering */
81 - -moz-osx-font-smoothing: grayscale; /* Improve font rendering */
85 + -moz-osx-font-smoothing: grayscale; /* Improve font rendering */
86 }
87
88 body {
@@ -209,39 +213,39 @@ body,
213
214 /* Chats container */
215 .chat-container {
212 - display: flex;
213 - align-items: center;
214 - position: relative;
215 - width: 100%;
216 - min-height: 30px;
217 - gap: 0;
216 + display: flex;
217 + align-items: center;
218 + position: relative;
219 + width: 100%;
220 + min-height: 30px;
221 + gap: 0;
222 }
223
224 /* Update the chat-list-button padding to accommodate the vertical layout */
225 .chat-list-button {
222 - display: block;
223 - width: 100%;
224 - padding: 8px 5px;
225 - cursor: pointer;
226 - overflow: hidden;
227 - position: relative;
228 - border-radius: 4px;
229 - transition: background-color 0.2s ease-in-out;
226 + display: block;
227 + width: 100%;
228 + padding: 8px 5px;
229 + cursor: pointer;
230 + overflow: hidden;
231 + position: relative;
232 + border-radius: 4px;
233 + transition: background-color 0.2s ease-in-out;
234 }
235
236 /* Add some more padding to the list items to accommodate the vertical layout */
237 .chat-list-button.has-task-container {
234 - padding-top: 6px;
235 - padding-bottom: 6px;
238 + padding-top: 6px;
239 + padding-bottom: 6px;
240 }
241
242 /* Subtle background on hover for the entire row */
243 .chat-list-button:hover {
240 - background-color: rgba(255, 255, 255, 0.03);
244 + background-color: rgba(255, 255, 255, 0.03);
245 }
246
247 .light-mode .chat-list-button:hover {
244 - background-color: rgba(0, 0, 0, 0.02);
248 + background-color: rgba(0, 0, 0, 0.02);
249 }
250
251 .chat-name {
@@ -405,12 +409,12 @@ body,
409 }
410
411 /* Typography */
408 -h2,
412 +/* h2,
413 h3 {
414 color: var(--color-primary);
411 -}
415 +} */
416
413 -h2 {
417 +/* h2 {
418 margin-bottom: var(--spacing-sm);
419 margin-top: var(--spacing-lg);
420 }
@@ -421,7 +425,7 @@ h3 {
425
426 h4 {
427 margin: auto 0;
424 -}
428 +} */
429
430 #a0version {
431 color: var(--color-text);
@@ -431,7 +435,7 @@ h4 {
435 }
436
437 pre {
434 - font-family: 'Roboto Mono', monospace;
438 + font-family: "Roboto Mono", monospace;
439 font-optical-sizing: auto;
440 -webkit-font-optical-sizing: auto;
441 font-size: 0.75rem;
@@ -519,6 +523,10 @@ pre {
523 z-index: 1001;
524 }
525
526 +#progress-bar-box h4 {
527 + margin: 0;
528 +}
529 +
530 #progress-bar-h {
531 color: var(--color-primary);
532 display: flex;
@@ -526,7 +534,7 @@ pre {
534 justify-content: flex-start;
535 height: 1.2em;
536 text-overflow: ellipsis;
529 - white-space: nowrap; /* Added for text overflow */
537 + white-space: nowrap; /* Added for text overflow */
538 overflow: hidden;
539 font-weight: normal;
540 }
@@ -544,11 +552,11 @@ pre {
552
553 .shiny-text {
554 background: linear-gradient(
547 - to right,
548 - var(--color-primary-dark) 20%,
549 - var(--color-text) 40%,
550 - var(--color-text) 60%,
551 - var(--color-primary-dark) 60%
555 + to right,
556 + var(--color-primary-dark) 20%,
557 + var(--color-text) 40%,
558 + var(--color-text) 60%,
559 + var(--color-primary-dark) 60%
560 );
561 background-size: 200% auto;
562 color: transparent;
@@ -566,17 +574,27 @@ pre {
574 animation: fadeIn 0.5s;
575 -webkit-animation: fadeIn 0.5s;
576 margin-bottom: var(--spacing-sm);
577 + width: 100%;
578 + max-width: 100%;
579 }
580
581 .message {
582 background-color: var(--color-message-bg);
583 border-radius: var(--border-radius);
584 padding: 0.9rem var(--spacing-md) 0.7rem var(--spacing-md);
585 + overflow-x: auto;
586 + width: fit-content;
587 + max-width: 100%;
588 + box-sizing: border-box;
589 + display: block;
590 }
591
592 .user-container {
593 align-self: flex-end;
594 margin: var(--spacing-sm) var(--spacing-md);
595 + display: flex;
596 + justify-content: flex-end;
597 + width: 100%;
598 }
599
600 .ai-container {
@@ -602,7 +620,7 @@ pre {
620
621 .message-user > div {
622 padding-top: var(--spacing-xs);
605 - font-family: 'Roboto Mono', monospace;
623 + font-family: "Roboto Mono", monospace;
624 font-optical-sizing: auto;
625 -webkit-font-optical-sizing: auto;
626 font-size: var(--font-size-small);
@@ -724,6 +742,10 @@ pre {
742 border-bottom: 1px solid rgba(255, 255, 255, 0.15);
743 }
744
745 +.msg-heading {
746 + margin: 0;
747 +}
748 +
749 /* Message Actions */
750 .message-actions {
751 color: var(--color-text);
@@ -838,10 +860,10 @@ pre {
860 border-radius: 8px;
861 color: var(--color-text);
862 flex-grow: 1;
841 - font-family: 'Roboto Mono', monospace;
863 + font-family: "Roboto Mono", monospace;
864 font-optical-sizing: auto;
865 -webkit-font-optical-sizing: auto;
844 - font-size: 0.90rem;
866 + font-size: 0.9rem;
867 max-height: 7rem;
868 min-height: 3.05rem;
869 width: 100%;
@@ -883,8 +905,8 @@ pre {
905 }
906
907 #expand-button svg {
886 - width: 1.3rem;
887 - height: 1.3rem;
908 + width: 1.3rem;
909 + height: 1.3rem;
910 }
911
912 #chat-input::-webkit-scrollbar {
@@ -960,7 +982,7 @@ pre {
982 opacity: 0.8;
983 text-wrap: nowrap;
984 width: calc(50% - var(--spacing-xs));
963 - float:left;
985 + float: left;
986 margin: 0 var(--spacing-xs) var(--spacing-xs) 0;
987 padding: var(--spacing-sm) 0.75rem;
988 max-height: 2.3rem;
@@ -1250,7 +1272,7 @@ pre {
1272 }
1273
1274 .text-button:active {
1253 - opacity: 0.5;
1275 + opacity: 0.5;
1276 }
1277
1278 .text-button svg {
@@ -1266,9 +1288,9 @@ pre {
1288 /* Chat buttons (Send and Mic) */
1289
1290 #chat-buttons-wrapper {
1269 - gap: var(--spacing-xs);
1270 - padding-left: var(--spacing-xs);
1271 - }
1291 + gap: var(--spacing-xs);
1292 + padding-left: var(--spacing-xs);
1293 +}
1294
1295 .chat-button {
1296 border: none;
@@ -1379,7 +1401,7 @@ pre {
1401
1402 .remove-image {
1403 -webkit-transform: scale(1.1);
1382 - transform: scale(1.1);
1404 + transform: scale(1.1);
1405 }
1406
1407 /* Toggle Switch */
@@ -1429,8 +1451,8 @@ input:checked + .slider {
1451 }
1452
1453 input:checked + .slider:before {
1432 - -webkit-transform: translateX(1.0rem);
1433 - transform: translateX(1.0rem);
1454 + -webkit-transform: translateX(1rem);
1455 + transform: translateX(1rem);
1456 }
1457
1458 #chat-buttons-wrapper {
@@ -1538,7 +1560,7 @@ input:checked + .slider:before {
1560 border: 1px solid rgba(255, 255, 255, 0.15);
1561 }
1562
1541 -.image-viewer-img{
1563 +.image-viewer-img {
1564 width: 100%;
1565 }
1566
@@ -1614,22 +1636,23 @@ input:checked + .slider:before {
1636 }
1637 }
1638
1617 -@-webkit-keyframes fadeIn { /* Safari and older Chrome */
1639 +@-webkit-keyframes fadeIn {
1640 + /* Safari and older Chrome */
1641 from {
1642 opacity: 0;
1643 -webkit-transform: translateY(var(--spacing-sm));
1621 - transform: translateY(var(--spacing-sm));
1644 + transform: translateY(var(--spacing-sm));
1645 }
1646 to {
1647 opacity: 1;
1648 -webkit-transform: translateY(0);
1626 - transform: translateY(0);
1649 + transform: translateY(0);
1650 }
1651 }
1652
1653 @keyframes shine {
1654 to {
1632 - background-position: -200% center;
1655 + background-position: -200% center;
1656 }
1657 }
1658
@@ -1672,8 +1695,8 @@ input:checked + .slider:before {
1695 /* New styles for mobile messages */
1696
1697 .message-followup {
1675 - margin-left: var(--spacing-md);
1676 - margin-bottom: var(--spacing-md);
1698 + margin-left: var(--spacing-md);
1699 + margin-bottom: var(--spacing-md);
1700 }
1701
1702 .msg-kvps {
@@ -1706,7 +1729,7 @@ input:checked + .slider:before {
1729 }
1730
1731 .kvps-val {
1709 - margin: 0 0 0.4rem 0;
1732 + margin: 0 0 0.4rem 0;
1733 }
1734 }
1735
@@ -1813,7 +1836,7 @@ input:checked + .slider:before {
1836 .msg-content span::after,
1837 .kvps-val::after,
1838 .message-text::after {
1816 - content: 'Copied!';
1839 + content: "Copied!";
1840 position: absolute;
1841 opacity: 0;
1842 font-family: "Rubik", Arial, Helvetica, sans-serif;
@@ -1880,20 +1903,20 @@ input:checked + .slider:before {
1903
1904 /* Link styling */
1905 a {
1883 - color: inherit;
1884 - /* text-decoration: none; */
1906 + color: inherit;
1907 + /* text-decoration: none; */
1908 }
1909
1910 a:visited {
1888 - color: inherit;
1911 + color: inherit;
1912 }
1913
1914 a:hover {
1892 - color: inherit;
1915 + color: inherit;
1916 }
1917
1918 a:active {
1896 - color: inherit;
1919 + color: inherit;
1920 }
1921
1922 /* Light mode class */
@@ -1912,7 +1935,7 @@ a:active {
1935 }
1936
1937 .light-mode .msg-kvps tr {
1915 - border-bottom: 1px solid rgb(192 192 192 / 50%)
1938 + border-bottom: 1px solid rgb(192 192 192 / 50%);
1939 }
1940
1941 .light-mode .message-default {
@@ -2034,7 +2057,7 @@ a:active {
2057
2058 .light-mode #logo-container img {
2059 -webkit-filter: invert(100%) grayscale(100%);
2037 - filter: invert(100%) grayscale(100%);
2060 + filter: invert(100%) grayscale(100%);
2061 }
2062
2063 .light-mode .extension {
@@ -2055,16 +2078,17 @@ a:active {
2078 }
2079 }
2080
2058 -@-webkit-keyframes fadeIn { /* Safari and older Chrome */
2081 +@-webkit-keyframes fadeIn {
2082 + /* Safari and older Chrome */
2083 from {
2084 opacity: 0;
2085 -webkit-transform: translateY(var(--spacing-sm));
2062 - transform: translateY(var(--spacing-sm));
2086 + transform: translateY(var(--spacing-sm));
2087 }
2088 to {
2089 opacity: 1;
2090 -webkit-transform: translateY(0);
2067 - transform: translateY(0);
2091 + transform: translateY(0);
2092 }
2093 }
2094
@@ -2096,77 +2120,77 @@ a:active {
2120 }
2121
2122 .dragdrop-overlay {
2099 - position: fixed;
2100 - top: 0;
2101 - left: 0;
2102 - width: 100%;
2103 - height: 100%;
2104 - background: rgba(0, 0, 0, 0.85);
2105 - z-index: 9999;
2106 - display: flex;
2107 - justify-content: center;
2108 - align-items: center;
2109 - flex-direction: column;
2110 - gap: 1rem;
2123 + position: fixed;
2124 + top: 0;
2125 + left: 0;
2126 + width: 100%;
2127 + height: 100%;
2128 + background: rgba(0, 0, 0, 0.85);
2129 + z-index: 9999;
2130 + display: flex;
2131 + justify-content: center;
2132 + align-items: center;
2133 + flex-direction: column;
2134 + gap: 1rem;
2135 }
2136
2137 .light-mode .dragdrop-overlay {
2114 - background: rgba(255, 255, 255, 0.92);
2138 + background: rgba(255, 255, 255, 0.92);
2139 }
2140
2141 /* Alpine transition classes */
2142 .transition {
2119 - transition-property: all;
2120 - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2143 + transition-property: all;
2144 + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2145 }
2146
2147 .duration-300 {
2124 - transition-duration: 300ms;
2148 + transition-duration: 300ms;
2149 }
2150
2151 .opacity-0 {
2128 - opacity: 0;
2152 + opacity: 0;
2153 }
2154
2155 .opacity-100 {
2132 - opacity: 1;
2156 + opacity: 1;
2157 }
2158
2159 /* Rest of dragdrop styles remain the same */
2160 .dragdrop-icon {
2137 - width: 128px;
2138 - height: 128px;
2139 - opacity: 0.85;
2140 - filter: invert(1);
2161 + width: 128px;
2162 + height: 128px;
2163 + opacity: 0.85;
2164 + filter: invert(1);
2165 }
2166
2167 .light-mode .dragdrop-icon {
2144 - filter: none;
2168 + filter: none;
2169 }
2170
2171 .dragdrop-text {
2148 - color: var(--color-text);
2149 - font-size: 1.2rem;
2150 - opacity: 0.85;
2151 - text-align: center;
2152 - max-width: 80%;
2153 - line-height: 1.5;
2172 + color: var(--color-text);
2173 + font-size: 1.2rem;
2174 + opacity: 0.85;
2175 + text-align: center;
2176 + max-width: 80%;
2177 + line-height: 1.5;
2178 }
2179
2180 .dragdrop-subtext {
2157 - color: var(--color-text);
2158 - font-size: 0.9rem;
2159 - opacity: 0.6;
2181 + color: var(--color-text);
2182 + font-size: 0.9rem;
2183 + opacity: 0.6;
2184 }
2185
2162 -.path-link{
2186 +.path-link {
2187 margin-left: 0.1em;
2188 margin-right: 0.1em;
2189 }
2190
2191 /* Alpine cloak to prevent FOUC */
2192 [x-cloak] {
2169 - display: none !important;
2193 + display: none !important;
2194 }
2195
2196 /* Remove unnecessary specific media query that was causing issues */
@@ -2179,89 +2203,88 @@ a:active {
2203
2204 /* Add to the existing .chat-actions class or create it */
2205 .chat-actions {
2182 - display: flex;
2183 - gap: 5px;
2184 - position: absolute;
2185 - right: 5px;
2186 - top: 50%;
2187 - transform: translateY(-50%);
2188 - z-index: 2; /* Ensure buttons are above the edit field */
2189 - min-width: 70px; /* Ensure minimum width for the buttons */
2190 - justify-content: flex-end;
2206 + display: flex;
2207 + gap: 5px;
2208 + position: absolute;
2209 + right: 5px;
2210 + top: 50%;
2211 + transform: translateY(-50%);
2212 + z-index: 2; /* Ensure buttons are above the edit field */
2213 + min-width: 70px; /* Ensure minimum width for the buttons */
2214 + justify-content: flex-end;
2215 }
2216
2193 -
2217 /* Tasks list container - similar to chats list */
2218 .tasks-list-container {
2196 - max-height: 300px;
2197 - overflow-y: auto;
2198 - margin-top: 10px;
2199 - padding-right: 5px;
2200 - border-radius: 5px;
2201 - position: relative;
2202 - scrollbar-width: thin;
2203 - -ms-overflow-style: auto;
2219 + max-height: 300px;
2220 + overflow-y: auto;
2221 + margin-top: 10px;
2222 + padding-right: 5px;
2223 + border-radius: 5px;
2224 + position: relative;
2225 + scrollbar-width: thin;
2226 + -ms-overflow-style: auto;
2227 }
2228
2229 .tasks-list-container::-webkit-scrollbar {
2207 - width: 5px;
2230 + width: 5px;
2231 }
2232
2233 .tasks-list-container::-webkit-scrollbar-track {
2211 - background: rgba(0, 0, 0, 0.2);
2212 - border-radius: 6px;
2234 + background: rgba(0, 0, 0, 0.2);
2235 + border-radius: 6px;
2236 }
2237
2238 .tasks-list-container::-webkit-scrollbar-thumb {
2216 - background-color: var(--color-border);
2217 - border-radius: 6px;
2239 + background-color: var(--color-border);
2240 + border-radius: 6px;
2241 }
2242
2243 .tasks-list-container::-webkit-scrollbar-thumb:hover {
2221 - background-color: var(--color-border);
2244 + background-color: var(--color-border);
2245 }
2246
2247 .task-name {
2225 - display: block;
2226 - width: 100%;
2227 - overflow: hidden;
2228 - text-overflow: ellipsis;
2229 - white-space: nowrap;
2230 - padding: 3px 0;
2231 - margin-left: 10px;
2232 - cursor: pointer;
2233 - border-radius: 4px;
2234 - transition: background-color 0.2s;
2235 - font-size: var(--font-size-small);
2236 - margin-bottom: 2px;
2248 + display: block;
2249 + width: 100%;
2250 + overflow: hidden;
2251 + text-overflow: ellipsis;
2252 + white-space: nowrap;
2253 + padding: 3px 0;
2254 + margin-left: 10px;
2255 + cursor: pointer;
2256 + border-radius: 4px;
2257 + transition: background-color 0.2s;
2258 + font-size: var(--font-size-small);
2259 + margin-bottom: 2px;
2260 }
2261
2262 .task-info-line {
2240 - display: flex;
2241 - justify-content: space-between;
2242 - align-items: center;
2243 - width: 100%;
2244 - margin-top: 2px;
2245 - margin-left: 5px;
2263 + display: flex;
2264 + justify-content: space-between;
2265 + align-items: center;
2266 + width: 100%;
2267 + margin-top: 2px;
2268 + margin-left: 5px;
2269 }
2270
2271 .task-name:hover {
2249 - background-color: rgba(255, 255, 255, 0.1);
2250 - text-decoration: none;
2272 + background-color: rgba(255, 255, 255, 0.1);
2273 + text-decoration: none;
2274 }
2275
2276 .light-mode .task-name:hover {
2254 - background-color: rgba(0, 0, 0, 0.05);
2277 + background-color: rgba(0, 0, 0, 0.05);
2278 }
2279
2280 /* Dark mode overrides */
2281 .light-mode .tab.active {
2259 - color: var(--highlight-pink);
2282 + color: var(--highlight-pink);
2283 }
2284
2285 .light-mode .tab.active::after {
2263 - background-color: var(--highlight-pink);
2264 - /* box-shadow: 0 0 8px var(--highlight-pink); */
2286 + background-color: var(--highlight-pink);
2287 + /* box-shadow: 0 0 8px var(--highlight-pink); */
2288 }
2289 /* Tabs styling */
2290 .tabs-container {
@@ -2311,10 +2334,8 @@ a:active {
2334 }
2335
2336 .light-mode .tab.active {
2314 - box-shadow:
2315 - 0 -4px 8px -2px var(--color-border),
2316 - 4px 0 8px -2px var(--color-border),
2317 - -4px 0 8px -2px var(--color-border);
2337 + box-shadow: 0 -4px 8px -2px var(--color-border),
2338 + 4px 0 8px -2px var(--color-border), -4px 0 8px -2px var(--color-border);
2339 }
2340
2341 .light-mode .tab:not(.active) {
@@ -2396,575 +2417,650 @@ a:active {
2417
2418 /* Add specific styling for selected chat items */
2419 .chat-list-button.font-bold {
2399 - position: relative;
2400 - background-color: var(--color-border) 0.05;
2420 + position: relative;
2421 + background-color: var(--color-border) 0.05;
2422 }
2423
2424 .chat-list-button.font-bold::before {
2404 - content: '';
2405 - position: absolute;
2406 - left: 0;
2407 - top: 0;
2408 - height: 100%;
2409 - width: 3px;
2410 - background-color: var(--color-border);
2411 - border-top-left-radius: 3px;
2412 - border-bottom-left-radius: 3px;
2425 + content: "";
2426 + position: absolute;
2427 + left: 0;
2428 + top: 0;
2429 + height: 100%;
2430 + width: 3px;
2431 + background-color: var(--color-border);
2432 + border-top-left-radius: 3px;
2433 + border-bottom-left-radius: 3px;
2434 }
2435
2436 .light-mode .chat-list-button.font-bold {
2416 - background-color: var(--color-border) 0.05;
2437 + background-color: var(--color-border) 0.05;
2438 }
2439
2440 .light-mode .chat-list-button.font-bold::before {
2420 - background-color: var(--color-border);
2441 + background-color: var(--color-border);
2442 }
2443
2444 /* Make sure the chat container has proper spacing */
2424 -.chat-container, .task-container {
2425 - display: flex;
2426 - align-items: center;
2427 - width: 100%;
2428 - justify-content: space-between;
2445 +.chat-container,
2446 +.task-container {
2447 + display: flex;
2448 + align-items: center;
2449 + width: 100%;
2450 + justify-content: space-between;
2451 }
2452
2453 /* Settings Modal Styles */
2454 .settings-modal {
2433 - position: fixed;
2434 - top: 0;
2435 - left: 0;
2436 - right: 0;
2437 - bottom: 0;
2438 - z-index: 1000;
2439 - background-color: rgba(0, 0, 0, 0.75);
2440 - display: flex;
2441 - justify-content: center;
2442 - align-items: center;
2443 - overflow: auto;
2444 - padding: 24px;
2455 + position: fixed;
2456 + top: 0;
2457 + left: 0;
2458 + right: 0;
2459 + bottom: 0;
2460 + z-index: 1000;
2461 + background-color: rgba(0, 0, 0, 0.75);
2462 + display: flex;
2463 + justify-content: center;
2464 + align-items: center;
2465 + overflow: auto;
2466 + padding: 24px;
2467 }
2468
2469 .settings-modal-close {
2448 - position: absolute;
2449 - top: 8px;
2450 - right: 16px;
2451 - border: none;
2452 - background: transparent;
2453 - font-size: 24px;
2454 - cursor: pointer;
2455 - color: var(--color-text-secondary);
2470 + position: absolute;
2471 + top: 8px;
2472 + right: 16px;
2473 + border: none;
2474 + background: transparent;
2475 + font-size: 24px;
2476 + cursor: pointer;
2477 + color: var(--color-text-secondary);
2478 }
2479
2480 .settings-modal-title {
2459 - margin-top: 0;
2460 - margin-bottom: 24px;
2461 - font-size: 1.8rem;
2462 - text-align: center;
2463 - border-bottom: 1px solid var(--color-border);
2464 - padding-bottom: 12px;
2481 + margin-top: 0;
2482 + margin-bottom: 24px;
2483 + font-size: 1.8rem;
2484 + text-align: center;
2485 + border-bottom: 1px solid var(--color-border);
2486 + padding-bottom: 12px;
2487 }
2488
2489 .settings-modal-content {
2468 - background-color: var(--color-panel);
2469 - color: var(--color-text);
2470 - width: 100%;
2471 - max-width: 800px;
2472 - height: auto;
2473 - max-height: 90vh;
2474 - border-radius: 8px;
2475 - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
2476 - padding: 24px;
2477 - position: relative;
2478 - overflow: auto;
2490 + background-color: var(--color-panel);
2491 + color: var(--color-text);
2492 + width: 100%;
2493 + max-width: 800px;
2494 + height: auto;
2495 + max-height: 90vh;
2496 + border-radius: 8px;
2497 + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
2498 + padding: 24px;
2499 + position: relative;
2500 + overflow: auto;
2501 }
2502
2503 /* Settings Sections */
2504 nav ul {
2483 - display: grid;
2484 - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
2485 - gap: 1rem;
2486 - padding: 0;
2487 - margin: 0 0 1rem 0;
2488 - list-style: none;
2505 + display: grid;
2506 + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
2507 + gap: 1rem;
2508 + padding: 0;
2509 + margin: 0 0 1rem 0;
2510 + list-style: none;
2511 }
2512
2513 /* Add specific styling for the nav element itself to ensure spacing */
2514 nav {
2493 -margin-bottom: 1rem;
2515 + margin-bottom: 1rem;
2516 }
2517
2518 nav ul li a {
2497 - display: flex;
2498 - flex-direction: column;
2499 - align-items: center;
2500 - padding: 1rem;
2501 - border-radius: 0.5rem;
2502 - background-color: var(--color-bg-secondary);
2503 - text-decoration: none;
2504 - color: var(--color-text);
2505 - transition: background-color 0.2s ease;
2519 + display: flex;
2520 + flex-direction: column;
2521 + align-items: center;
2522 + padding: 1rem;
2523 + border-radius: 0.5rem;
2524 + background-color: var(--color-bg-secondary);
2525 + text-decoration: none;
2526 + color: var(--color-text);
2527 + transition: background-color 0.2s ease;
2528 }
2529
2530 nav ul li a:hover {
2509 - background-color: var(--color-bg-tertiary);
2531 + background-color: var(--color-bg-tertiary);
2532 }
2533
2534 nav ul li a img {
2513 - width: 50px;
2514 - height: 50px;
2515 - margin-bottom: 0.5rem;
2516 - filter: var(--svg-filter);
2535 + width: 50px;
2536 + height: 50px;
2537 + margin-bottom: 0.5rem;
2538 + filter: var(--svg-filter);
2539 }
2540
2541 .section {
2520 - margin-bottom: 3rem;
2521 - animation: fadeIn 0.3s ease;
2522 - width: 100%;
2542 + margin-bottom: 3rem;
2543 + animation: fadeIn 0.3s ease;
2544 + width: 100%;
2545 }
2546
2547 .section-title {
2526 - margin-top: 0;
2527 - margin-bottom: 0.5rem;
2528 - font-size: 1.5rem;
2529 - font-weight: 500;
2530 - color: var(--color-primary);
2531 - border-bottom: 1px solid var(--color-border);
2532 - padding-bottom: 0.5rem;
2548 + margin-top: 0;
2549 + margin-bottom: 0.5rem;
2550 + font-size: 1.5rem;
2551 + font-weight: 500;
2552 + color: var(--color-primary);
2553 + border-bottom: 1px solid var(--color-border);
2554 + padding-bottom: 0.5rem;
2555 }
2556
2557 .section-description {
2536 - margin-bottom: 1.5rem;
2537 - color: var(--color-text-secondary);
2558 + margin-bottom: 1.5rem;
2559 + color: var(--color-text-secondary);
2560 }
2561
2562 .scheduler-header {
2541 - display: flex;
2542 - justify-content: space-between;
2543 - align-items: center;
2544 - margin-bottom: 20px;
2563 + display: flex;
2564 + justify-content: space-between;
2565 + align-items: center;
2566 + margin-bottom: 20px;
2567 }
2568
2569 .scheduler-header h2 {
2548 - margin: 0;
2549 - font-size: 1.2rem;
2550 - font-weight: 500;
2570 + margin: 0;
2571 + font-size: 1.2rem;
2572 + font-weight: 500;
2573 }
2574
2575 .scheduler-filters {
2554 - display: flex;
2555 - gap: 20px;
2556 - margin-bottom: 20px;
2557 - flex-wrap: wrap;
2576 + display: flex;
2577 + gap: 20px;
2578 + margin-bottom: 20px;
2579 + flex-wrap: wrap;
2580 }
2581
2582 .scheduler-filter-group {
2561 - display: flex;
2562 - align-items: center;
2563 - gap: 8px;
2583 + display: flex;
2584 + align-items: center;
2585 + gap: 8px;
2586 }
2587
2588 .scheduler-filter-label {
2567 - font-weight: 500;
2568 - color: var(--color-text-secondary);
2589 + font-weight: 500;
2590 + color: var(--color-text-secondary);
2591 }
2592
2593 .scheduler-filter-select {
2572 - padding: 6px 8px;
2573 - border-radius: 4px;
2574 - border: 1px solid var(--color-border);
2575 - background-color: var(--color-bg-secondary);
2576 - color: var(--color-text);
2594 + padding: 6px 8px;
2595 + border-radius: 4px;
2596 + border: 1px solid var(--color-border);
2597 + background-color: var(--color-bg-secondary);
2598 + color: var(--color-text);
2599 }
2600
2601 .scheduler-task-list {
2580 - width: 100%;
2581 - border-collapse: collapse;
2582 - margin: 1rem 0;
2583 - font-size: 0.9rem;
2584 - overflow-x: auto;
2585 - table-layout: fixed;
2602 + width: 100%;
2603 + border-collapse: collapse;
2604 + margin: 1rem 0;
2605 + font-size: 0.9rem;
2606 + overflow-x: auto;
2607 + table-layout: fixed;
2608 }
2609
2610 .scheduler-task-list th,
2611 .scheduler-task-list td {
2590 - padding: 0.75rem;
2591 - text-align: left;
2592 - border-bottom: 1px solid var(--color-border);
2593 - vertical-align: middle;
2594 - overflow: hidden;
2595 - text-overflow: ellipsis;
2596 - white-space: nowrap;
2612 + padding: 0.75rem;
2613 + text-align: left;
2614 + border-bottom: 1px solid var(--color-border);
2615 + vertical-align: middle;
2616 + overflow: hidden;
2617 + text-overflow: ellipsis;
2618 + white-space: nowrap;
2619 }
2620
2621 .scheduler-task-list th:nth-child(1), /* Name */
2622 .scheduler-task-list td:nth-child(1) {
2601 - width: 25%;
2623 + width: 25%;
2624 }
2625
2626 .scheduler-task-list th:nth-child(2), /* State */
2627 .scheduler-task-list td:nth-child(2) {
2606 - width: 10%;
2628 + width: 10%;
2629 }
2630
2631 .scheduler-task-list th:nth-child(3), /* Type */
2632 .scheduler-task-list td:nth-child(3) {
2611 - width: 10%;
2633 + width: 10%;
2634 }
2635
2636 .scheduler-task-list th:nth-child(4), /* Schedule */
2637 .scheduler-task-list td:nth-child(4) {
2616 - width: 20%;
2638 + width: 20%;
2639 }
2640
2641 .scheduler-task-list th:nth-child(5), /* Last Run */
2642 .scheduler-task-list td:nth-child(5) {
2621 - width: 20%;
2643 + width: 20%;
2644 }
2645
2646 .scheduler-task-list th:nth-child(6), /* Actions */
2647 .scheduler-task-list td:nth-child(6) {
2626 - width: 15%;
2627 - text-align: right;
2648 + width: 15%;
2649 + text-align: right;
2650 }
2651
2652 .scheduler-task-list th {
2631 - background-color: var(--color-bg-secondary);
2632 - font-weight: 500;
2633 - cursor: pointer;
2634 - user-select: none;
2653 + background-color: var(--color-bg-secondary);
2654 + font-weight: 500;
2655 + cursor: pointer;
2656 + user-select: none;
2657 }
2658
2659 .scheduler-task-list th:hover {
2638 - background-color: var(--color-bg-tertiary);
2660 + background-color: var(--color-bg-tertiary);
2661 }
2662
2663 .scheduler-task-actions {
2642 - display: flex;
2643 - gap: 8px;
2664 + display: flex;
2665 + gap: 8px;
2666 }
2667
2668 .scheduler-task-action {
2647 - padding: 4px;
2648 - background: none;
2649 - border: none;
2650 - color: var(--color-text-secondary);
2651 - cursor: pointer;
2652 - border-radius: 4px;
2669 + padding: 4px;
2670 + background: none;
2671 + border: none;
2672 + color: var(--color-text-secondary);
2673 + cursor: pointer;
2674 + border-radius: 4px;
2675 }
2676
2677 .scheduler-task-action:hover {
2656 - background-color: var(--color-bg-tertiary);
2657 - color: var(--color-text);
2678 + background-color: var(--color-bg-tertiary);
2679 + color: var(--color-text);
2680 }
2681
2682 .scheduler-status-badge {
2661 - display: inline-block;
2662 - padding: 4px 8px;
2663 - border-radius: 4px;
2664 - font-size: 12px;
2665 - font-weight: 500;
2666 - text-transform: capitalize;
2667 - white-space: nowrap;
2683 + display: inline-block;
2684 + padding: 4px 8px;
2685 + border-radius: 4px;
2686 + font-size: 12px;
2687 + font-weight: 500;
2688 + text-transform: capitalize;
2689 + white-space: nowrap;
2690 }
2691
2692 .scheduler-status-idle {
2671 - background-color: rgba(0, 100, 0, 0.2);
2672 - color: #2a9d8f; /* Dark green that works with both light and dark themes */
2673 - border: 1px solid rgba(42, 157, 143, 0.3);
2693 + background-color: rgba(0, 100, 0, 0.2);
2694 + color: #2a9d8f; /* Dark green that works with both light and dark themes */
2695 + border: 1px solid rgba(42, 157, 143, 0.3);
2696 }
2697
2698 .scheduler-status-running {
2677 - background-color: rgba(0, 60, 120, 0.2);
2678 - color: #4361ee; /* Dark blue that works with both light and dark themes */
2679 - border: 1px solid rgba(67, 97, 238, 0.3);
2699 + background-color: rgba(0, 60, 120, 0.2);
2700 + color: #4361ee; /* Dark blue that works with both light and dark themes */
2701 + border: 1px solid rgba(67, 97, 238, 0.3);
2702 }
2703
2704 .scheduler-status-disabled {
2683 - background-color: rgba(70, 70, 70, 0.2);
2684 - color: #6c757d; /* Dark grey that works with both light and dark themes */
2685 - border: 1px solid rgba(108, 117, 125, 0.3);
2705 + background-color: rgba(70, 70, 70, 0.2);
2706 + color: #6c757d; /* Dark grey that works with both light and dark themes */
2707 + border: 1px solid rgba(108, 117, 125, 0.3);
2708 }
2709
2710 .scheduler-status-error {
2689 - background-color: rgba(120, 0, 0, 0.2);
2690 - color: #e63946; /* Dark red that works with both light and dark themes */
2691 - border: 1px solid rgba(230, 57, 70, 0.3);
2711 + background-color: rgba(120, 0, 0, 0.2);
2712 + color: #e63946; /* Dark red that works with both light and dark themes */
2713 + border: 1px solid rgba(230, 57, 70, 0.3);
2714 }
2715
2716 /* Light mode adjustments */
2717 .light-mode .scheduler-status-idle {
2696 - background-color: rgba(42, 157, 143, 0.1);
2697 - color: #1a6f65; /* Darker green for light mode */
2718 + background-color: rgba(42, 157, 143, 0.1);
2719 + color: #1a6f65; /* Darker green for light mode */
2720 }
2721
2722 .light-mode .scheduler-status-running {
2701 - background-color: rgba(67, 97, 238, 0.1);
2702 - color: #2540b3; /* Darker blue for light mode */
2723 + background-color: rgba(67, 97, 238, 0.1);
2724 + color: #2540b3; /* Darker blue for light mode */
2725 }
2726
2727 .light-mode .scheduler-status-disabled {
2706 - background-color: rgba(108, 117, 125, 0.1);
2707 - color: #495057; /* Darker grey for light mode */
2728 + background-color: rgba(108, 117, 125, 0.1);
2729 + color: #495057; /* Darker grey for light mode */
2730 }
2731
2732 .light-mode .scheduler-status-error {
2711 - background-color: rgba(230, 57, 70, 0.1);
2712 - color: #c5283d; /* Darker red for light mode */
2733 + background-color: rgba(230, 57, 70, 0.1);
2734 + color: #c5283d; /* Darker red for light mode */
2735 }
2736
2737 .scheduler-empty {
2716 - text-align: center;
2717 - padding: 40px 0;
2718 - color: var(--color-text-secondary);
2738 + text-align: center;
2739 + padding: 40px 0;
2740 + color: var(--color-text-secondary);
2741 }
2742
2743 .scheduler-empty-icon {
2722 - font-size: 32px;
2723 - margin-bottom: 10px;
2744 + font-size: 32px;
2745 + margin-bottom: 10px;
2746 }
2747
2748 .scheduler-empty-text {
2727 - margin-bottom: 20px;
2749 + margin-bottom: 20px;
2750 }
2751
2752 .scheduler-loading {
2731 - text-align: center;
2732 - padding: 40px 0;
2733 - color: var(--color-text-secondary);
2753 + text-align: center;
2754 + padding: 40px 0;
2755 + color: var(--color-text-secondary);
2756 }
2757
2758 .scheduler-task-details {
2737 - padding: 16px;
2738 - background-color: var(--color-bg-secondary);
2739 - border-radius: 4px;
2759 + padding: 16px;
2760 + background-color: var(--color-bg-secondary);
2761 + border-radius: 4px;
2762 }
2763
2764 .scheduler-details-grid {
2743 - display: grid;
2744 - grid-template-columns: 120px 1fr;
2745 - gap: 8px 16px;
2746 - margin-bottom: 16px;
2765 + display: grid;
2766 + grid-template-columns: 120px 1fr;
2767 + gap: 8px 16px;
2768 + margin-bottom: 16px;
2769 }
2770
2771 .scheduler-details-label {
2750 - font-weight: 500;
2751 - color: var(--color-text-secondary);
2752 - display: flex;
2753 - align-items: center;
2772 + font-weight: 500;
2773 + color: var(--color-text-secondary);
2774 + display: flex;
2775 + align-items: center;
2776 }
2777
2778 .scheduler-details-value {
2757 - color: var(--color-text);
2758 - word-break: break-word;
2779 + color: var(--color-text);
2780 + word-break: break-word;
2781 }
2782
2783 .scheduler-details-actions {
2762 - display: flex;
2763 - justify-content: flex-end;
2784 + display: flex;
2785 + justify-content: flex-end;
2786 }
2787
2788 .scheduler-form {
2767 - background-color: var(--color-bg-secondary);
2768 - border-radius: 4px;
2769 - padding: 20px;
2770 - margin-bottom: 20px;
2789 + background-color: var(--color-bg-secondary);
2790 + border-radius: 4px;
2791 + padding: 20px;
2792 + margin-bottom: 20px;
2793 }
2794
2795 .scheduler-form-title {
2774 - font-size: 1.2rem;
2775 - font-weight: 500;
2776 - margin-bottom: 20px;
2777 - padding-bottom: 10px;
2778 - border-bottom: 1px solid var(--color-border);
2796 + font-size: 1.2rem;
2797 + font-weight: 500;
2798 + margin-bottom: 20px;
2799 + padding-bottom: 10px;
2800 + border-bottom: 1px solid var(--color-border);
2801 }
2802
2803 .scheduler-form-grid {
2782 - display: grid;
2783 - grid-template-columns: 1fr 1fr;
2784 - gap: 16px;
2785 - margin-bottom: 20px;
2804 + display: grid;
2805 + grid-template-columns: 1fr 1fr;
2806 + gap: 16px;
2807 + margin-bottom: 20px;
2808 }
2809
2810 .scheduler-form-field {
2789 - display: flex;
2790 - flex-direction: column;
2791 - gap: 6px;
2811 + display: flex;
2812 + flex-direction: column;
2813 + gap: 6px;
2814 }
2815
2816 .full-width {
2795 - grid-column: 1 / -1;
2817 + grid-column: 1 / -1;
2818 }
2819
2820 .scheduler-form-label {
2799 - font-weight: 500;
2821 + font-weight: 500;
2822 }
2823
2824 .scheduler-form-help {
2803 - font-size: 12px;
2804 - color: var(--color-text-secondary);
2825 + font-size: 12px;
2826 + color: var(--color-text-secondary);
2827 }
2828
2829 .scheduler-form-actions {
2808 - display: flex;
2809 - justify-content: flex-end;
2810 - gap: 12px;
2830 + display: flex;
2831 + justify-content: flex-end;
2832 + gap: 12px;
2833 }
2834
2835 .scheduler-schedule-builder {
2814 - display: grid;
2815 - grid-template-columns: repeat(5, 1fr);
2816 - gap: 12px;
2836 + display: grid;
2837 + grid-template-columns: repeat(5, 1fr);
2838 + gap: 12px;
2839 }
2840
2841 .scheduler-schedule-field {
2820 - display: flex;
2821 - flex-direction: column;
2822 - gap: 4px;
2823 - max-width: 70px; /* Limit width of schedule fields */
2842 + display: flex;
2843 + flex-direction: column;
2844 + gap: 4px;
2845 + max-width: 70px; /* Limit width of schedule fields */
2846 }
2847
2848 .scheduler-schedule-field input {
2827 - width: 100%;
2828 - min-width: 0; /* Allow shrinking below content size */
2829 - font-size: 0.9rem; /* Slightly reduce font size for better fit */
2849 + width: 100%;
2850 + min-width: 0; /* Allow shrinking below content size */
2851 + font-size: 0.9rem; /* Slightly reduce font size for better fit */
2852 }
2853
2854 .scheduler-schedule-label {
2833 - font-size: 12px;
2834 - color: var(--color-text-secondary);
2855 + font-size: 12px;
2856 + color: var(--color-text-secondary);
2857 }
2858
2859 .input-group {
2838 - display: flex;
2839 - gap: 8px;
2860 + display: flex;
2861 + gap: 8px;
2862 }
2863
2864 /* Sort indicators */
2865 .scheduler-sort-indicator {
2844 - display: inline-block;
2845 - margin-left: 4px;
2846 - transition: transform 0.2s ease;
2866 + display: inline-block;
2867 + margin-left: 4px;
2868 + transition: transform 0.2s ease;
2869 }
2870
2871 .scheduler-sort-desc {
2850 - transform: rotate(180deg);
2872 + transform: rotate(180deg);
2873 }
2874
2875 /* Responsive adjustments */
2876 @media (max-width: 768px) {
2855 - .scheduler-form-grid {
2856 - grid-template-columns: 1fr;
2857 - }
2877 + .scheduler-form-grid {
2878 + grid-template-columns: 1fr;
2879 + }
2880
2859 - .scheduler-schedule-builder {
2860 - grid-template-columns: 1fr 1fr;
2861 - }
2881 + .scheduler-schedule-builder {
2882 + grid-template-columns: 1fr 1fr;
2883 + }
2884
2863 - .scheduler-filters {
2864 - flex-direction: column;
2865 - gap: 12px;
2866 - }
2885 + .scheduler-filters {
2886 + flex-direction: column;
2887 + gap: 12px;
2888 + }
2889
2868 - .scheduler-task-actions {
2869 - flex-wrap: wrap;
2870 - }
2890 + .scheduler-task-actions {
2891 + flex-wrap: wrap;
2892 + }
2893 }
2894
2895 @media (max-width: 480px) {
2874 - nav ul li a {
2875 - flex-direction: row;
2876 - justify-content: flex-start;
2877 - gap: 1rem;
2878 - padding: 0.75rem 1rem;
2879 - }
2896 + nav ul li a {
2897 + flex-direction: row;
2898 + justify-content: flex-start;
2899 + gap: 1rem;
2900 + padding: 0.75rem 1rem;
2901 + }
2902
2881 - nav ul li a img {
2882 - margin-bottom: 0;
2883 - width: 30px;
2884 - height: 30px;
2885 - }
2903 + nav ul li a img {
2904 + margin-bottom: 0;
2905 + width: 30px;
2906 + height: 30px;
2907 + }
2908 }
2909
2910 /* Add row hover effect for task list rows matching left panel hover */
2911 .scheduler-task-list tbody tr {
2890 - cursor: pointer;
2891 - transition: background-color 0.2s ease;
2912 + cursor: pointer;
2913 + transition: background-color 0.2s ease;
2914 }
2915
2916 .scheduler-task-list tbody tr:hover {
2895 - background-color: rgba(255, 255, 255, 0.03);
2917 + background-color: rgba(255, 255, 255, 0.03);
2918 }
2919
2920 .light-mode .scheduler-task-list tbody tr:hover {
2899 - background-color: rgba(0, 0, 0, 0.02);
2921 + background-color: rgba(0, 0, 0, 0.02);
2922 }
2923
2924 .scheduler-task-list th {
2903 - background-color: var(--color-bg-secondary);
2904 - font-weight: 500;
2905 - cursor: pointer;
2906 - user-select: none;
2925 + background-color: var(--color-bg-secondary);
2926 + font-weight: 500;
2927 + cursor: pointer;
2928 + user-select: none;
2929 }
2930
2931 .scheduler-task-list th:hover {
2910 - background-color: var(--color-bg-tertiary);
2932 + background-color: var(--color-bg-tertiary);
2933 }
2934
2935 /* Task detail view styling */
2936 .scheduler-detail-view {
2915 - background-color: var(--color-bg-secondary);
2916 - border-radius: 4px;
2917 - padding: 20px;
2918 - margin-bottom: 20px;
2919 - animation: fadeIn 0.3s ease;
2937 + background-color: var(--color-bg-secondary);
2938 + border-radius: 4px;
2939 + padding: 20px;
2940 + margin-bottom: 20px;
2941 + animation: fadeIn 0.3s ease;
2942 }
2943
2944 .scheduler-detail-header {
2923 - display: flex;
2924 - justify-content: flex-start;
2925 - align-items: center;
2926 - margin-bottom: 20px;
2927 - padding-bottom: 10px;
2928 - border-bottom: 1px solid var(--color-border);
2929 - flex-wrap: wrap;
2930 - gap: 10px;
2945 + display: flex;
2946 + justify-content: flex-start;
2947 + align-items: center;
2948 + margin-bottom: 20px;
2949 + padding-bottom: 10px;
2950 + border-bottom: 1px solid var(--color-border);
2951 + flex-wrap: wrap;
2952 + gap: 10px;
2953 }
2954
2955 .scheduler-detail-header .scheduler-detail-title {
2934 - font-size: 1.4rem;
2935 - font-weight: 500;
2936 - margin: 0;
2937 - margin-right: auto;
2956 + font-size: 1.4rem;
2957 + font-weight: 500;
2958 + margin: 0;
2959 + margin-right: auto;
2960 }
2961
2962 .scheduler-detail-header .scheduler-status-badge {
2941 - margin-right: 10px;
2963 + margin-right: 10px;
2964 }
2965
2966 .scheduler-detail-content {
2945 - margin-bottom: 20px;
2967 + margin-bottom: 20px;
2968 }
2969
2970 /* Task Scheduler Styles */
2971
2972 .scheduler-no-schedule {
2951 - color: var(--color-text-secondary);
2952 - opacity: 0.7;
2953 - font-style: italic;
2973 + color: var(--color-text-secondary);
2974 + opacity: 0.7;
2975 + font-style: italic;
2976 }
2977
2978 .task-container-vertical {
2957 - display: flex;
2958 - flex-direction: column;
2959 - width: 100%;
2960 - gap: 6px;
2979 + display: flex;
2980 + flex-direction: column;
2981 + width: 100%;
2982 + gap: 6px;
2983 }
2984
2985 /* Smaller status badge for task list */
2986 .scheduler-status-badge-small {
2965 - font-size: 10px;
2966 - padding: 2px 6px;
2967 - margin-right: 5px;
2968 - min-width: 40px;
2969 - text-align: center;
2987 + font-size: 10px;
2988 + padding: 2px 6px;
2989 + margin-right: 5px;
2990 + min-width: 40px;
2991 + text-align: center;
2992 +}
2993 +
2994 +/* Markdown in messages */
2995 +.msg-content {
2996 + font-size: var(--font-size-small);
2997 +}
2998 +
2999 +.message-agent-response .msg-content {
3000 + font-size: var(--font-size-smaller);
3001 +}
3002 +
3003 +.msg-content h1 {
3004 + font-size: 1.25em;
3005 +}
3006 +
3007 +.msg-content h2 {
3008 + font-size: 1.2em;
3009 +}
3010 +
3011 +.msg-content h3 {
3012 + font-size: 1.15em;
3013 +}
3014 +
3015 +.msg-content h4 {
3016 + font-size: 1.1em;
3017 +}
3018 +
3019 +.msg-content h5 {
3020 + font-size: 1.05em;
3021 +}
3022 +
3023 +.msg-content h6 {
3024 + font-size: 1em;
3025 +}
3026 +
3027 +.msg-content table {
3028 + width: 100%;
3029 + border-collapse: collapse;
3030 + margin: 1em 0;
3031 + font-size: 0.98em;
3032 + background: transparent;
3033 + border-radius: 8px;
3034 + overflow: hidden;
3035 +}
3036 +
3037 +.msg-content th,
3038 +.msg-content td {
3039 + padding: 0.55em 1em;
3040 + border: 1px solid rgba(142, 142, 142, 0.1);
3041 + text-align: left;
3042 + background: transparent;
3043 +}
3044 +
3045 +.msg-content th {
3046 + font-weight: 600;
3047 + background: rgba(142, 142, 142, 0.1);
3048 +}
3049 +
3050 +.msg-content tr:nth-child(even) {
3051 + background: rgba(142, 142, 142, 0.1);
3052 +}
3053 +
3054 +.msg-content tr:nth-child(odd) {
3055 + background: transparent;
3056 +}
3057 +
3058 +.msg-content table {
3059 + box-shadow: none;
3060 +}
3061 +
3062 +.msg-content pre:has(code) {
3063 + padding: 0.5em;
3064 + border: 1px solid rgba(142, 142, 142, 0.1);
3065 + border-radius: 0.3em;
3066 }
webui/index.html
+2
@@ -118,6 +118,8 @@
118 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js"
119 crossorigin="anonymous"></script>
120
121 + <script src="https://rawgit.com/showdownjs/showdown/develop/dist/showdown.min.js"></script>
122 +
123 <!-- Non-module scripts after Alpine.js -->
124 <script type="text/javascript" src="js/settings.js"></script>
125 <script type="text/javascript" src="js/file_browser.js"></script>
webui/js/messages.js
+103 -42
@@ -2,6 +2,9 @@
2 import { openImageModal } from "./image_modal.js";
3 import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
4
5 +const katexDelimiterLeft = "<$LATEX>";
6 +const katexDelimiterRight = "</$LATEX>";
7 +
8 function createCopyButton() {
9 const button = document.createElement("button");
10 button.className = "copy-button";
@@ -92,6 +95,7 @@ export function _drawMessage(
95
96 if (heading) {
97 const headingElement = document.createElement("h4");
98 + headingElement.classList.add("msg-heading");
99 headingElement.textContent = heading;
100 messageDiv.appendChild(headingElement);
101 }
@@ -102,47 +106,62 @@ export function _drawMessage(
106 if (markdown) {
107 const contentDiv = document.createElement("div");
108 contentDiv.classList.add("msg-content", ...contentClasses);
105 -
109 +
110 const spanElement = document.createElement("span"); // Wrapper span
107 - let processedContent = convertPathsToLinks(content);
111 + let processedContent = content;
112 processedContent = convertImageTags(processedContent);
109 - spanElement.innerHTML = marked.parse(processedContent, { breaks: true });
113 + processedContent = convertImgFilePaths(processedContent);
114 + processedContent = marked.parse(processedContent, { breaks: true });
115 + processedContent = convertPathsToLinks(processedContent);
116 + spanElement.innerHTML = processedContent;
117 contentDiv.appendChild(spanElement);
111 -
118 +
119 addCopyButtonToElement(contentDiv);
120 messageDiv.appendChild(contentDiv);
121
122 // KaTeX rendering for markdown
123 if (window.renderMathInElement && latex) {
124 renderMathInElement(contentDiv, {
118 - delimiters: [{ left: "$", right: "$", display: true }],
125 + delimiters: [
126 + {
127 + left: katexDelimiterLeft,
128 + right: katexDelimiterRight,
129 + display: true,
130 + },
131 + ],
132 throwOnError: false,
133 });
134 }
135 } else {
123 - const preElement = document.createElement("pre");
124 - preElement.classList.add("msg-content", ...contentClasses);
125 - preElement.style.whiteSpace = "pre-wrap";
126 - preElement.style.wordBreak = "break-word";
136 + const preElement = document.createElement("pre");
137 + preElement.classList.add("msg-content", ...contentClasses);
138 + preElement.style.whiteSpace = "pre-wrap";
139 + preElement.style.wordBreak = "break-word";
140
128 - const spanElement = document.createElement("span");
129 - spanElement.innerHTML = convertHTML(content);
141 + const spanElement = document.createElement("span");
142 + spanElement.innerHTML = convertHTML(content);
143
131 - // Add click handler for small screens
132 - spanElement.addEventListener("click", () => {
133 - copyText(spanElement.textContent, spanElement);
134 - });
144 + // Add click handler for small screens
145 + spanElement.addEventListener("click", () => {
146 + copyText(spanElement.textContent, spanElement);
147 + });
148
136 - preElement.appendChild(spanElement);
137 - addCopyButtonToElement(preElement);
138 - messageDiv.appendChild(preElement);
149 + preElement.appendChild(spanElement);
150 + addCopyButtonToElement(preElement);
151 + messageDiv.appendChild(preElement);
152
140 - // Render LaTeX math within the span
141 - if (window.renderMathInElement && latex) {
142 - renderMathInElement(spanElement, {
143 - delimiters: [{ left: "$", right: "$", display: true }],
144 - throwOnError: false,
145 - });
153 + // Render LaTeX math within the span
154 + if (window.renderMathInElement && latex) {
155 + renderMathInElement(spanElement, {
156 + delimiters: [
157 + {
158 + left: katexDelimiterLeft,
159 + right: katexDelimiterRight,
160 + display: true,
161 + },
162 + ],
163 + throwOnError: false,
164 + });
165 }
166 }
167 }
@@ -268,6 +287,7 @@ export function drawMessageUser(
287 messageDiv.classList.add("message", "message-user");
288
289 const headingElement = document.createElement("h4");
290 + headingElement.classList.add("msg-heading");
291 headingElement.textContent = "User message";
292 messageDiv.appendChild(headingElement);
293
@@ -589,7 +609,7 @@ function drawKvps(container, kvps, latex) {
609
610 if (window.renderMathInElement && latex) {
611 renderMathInElement(span, {
592 - delimiters: [{ left: "$", right: "$", display: true }],
612 + delimiters: [{ left: katexDelimiterLeft, right: katexDelimiterRight, display: true }],
613 throwOnError: false,
614 });
615 }
@@ -651,11 +671,15 @@ function convertHTML(str) {
671 if (typeof str !== "string") str = JSON.stringify(str, null, 2);
672
673 let result = escapeHTML(str);
654 - result = convertPathsToLinks(result);
674 result = convertImageTags(result);
675 + result = convertPathsToLinks(result);
676 return result;
677 }
678
679 +function convertImgFilePaths(str) {
680 + return str.replace("img://", "/image_get?path=");
681 +}
682 +
683 function escapeHTML(str) {
684 const escapeChars = {
685 "&": "&amp;",
@@ -668,25 +692,62 @@ function escapeHTML(str) {
692 }
693
694 function convertPathsToLinks(str) {
671 - function generateLinks(match, ...args) {
672 - const parts = match.split("/");
673 -
674 - if (!parts[0]) parts.shift();
675 - let conc = "";
676 - let html = "";
677 - for (let part of parts) {
678 - conc += "/" + part;
679 - html += `/<a href="#" class="path-link" onclick="openFileLink('${conc}');">${part}</a>`;
680 - }
681 - return html;
695 + function generateLinks(match) {
696 + const parts = match.split("/");
697 + if (!parts[0]) parts.shift(); // drop empty element left of first “/”
698 + let conc = "";
699 + let html = "";
700 + for (const part of parts) {
701 + conc += "/" + part;
702 + html += `/<a href="#" class="path-link" onclick="openFileLink('${conc}');">${part}</a>`;
703 + }
704 + return html;
705 }
706
684 - const prefix = `(?:^|[ \`'"\\n]|&#39;|&quot;)`; // Use a non-capturing group for OR logic
685 - const folder = `[a-zA-Z0-9_\\/.\\-]`; // Characters allowed in folder chain
686 - const file = `[a-zA-Z0-9_\\-\\/]`; // Characters allowed in file names
707 + const prefix = `(?:^|[> \`'"\\n]|&#39;|&quot;)`;
708 + const folder = `[a-zA-Z0-9_\\/.\\-]`;
709 + const file = `[a-zA-Z0-9_\\-\\/]`;
710 const suffix = `(?<!\\.)`;
711 + const pathRegex = new RegExp(`(?<=${prefix})\\/${folder}*${file}${suffix}`, "g");
712
689 - const regex = new RegExp(`(?<=${prefix})\\/${folder}*${file}${suffix}`, "g");
713 + // skip paths inside html tags, like <img src="/path/to/image">
714 + const tagRegex = /(<(?:[^<>"']+|"[^"]*"|'[^']*')*>)/g;
715
691 - return str.replace(regex, generateLinks);
716 + return str
717 + .split(tagRegex) // keep tags & text separate
718 + .map(chunk => {
719 + // if it *starts* with '<', it’s a tag -> leave untouched
720 + if (chunk.startsWith("<")) return chunk;
721 + // otherwise run your link-generation
722 + return chunk.replace(pathRegex, generateLinks);
723 + })
724 + .join("");
725 }
726 +
727 +// function convertPathsToLinksInHtml(htmlString) {
728 +// // 1. Parse the input safely
729 +// const wrapper = document.createElement("div");
730 +// wrapper.innerHTML = htmlString;
731 +
732 +// // 2. Depth-first walk
733 +// function walk(node) {
734 +// // Skip <script> and <style> blocks entirely
735 +// if (node.nodeName === "SCRIPT" || node.nodeName === "STYLE") return;
736 +
737 +// if (node.nodeType === Node.TEXT_NODE) {
738 +// const original = node.nodeValue;
739 +// const replaced = convertPathsToLinks(original);
740 +// if (replaced !== original) {
741 +// // Turn the replacement HTML string into real nodes
742 +// const frag = document.createRange().createContextualFragment(replaced);
743 +// node.replaceWith(frag);
744 +// }
745 +// } else {
746 +// // Recurse into children
747 +// for (const child of Array.from(node.childNodes)) walk(child);
748 +// }
749 +// }
750 +
751 +// walk(wrapper);
752 +// return wrapper.innerHTML;
753 +// }