msg redesign wip
frdel committed
Jul 1, 2025 at 16:38 UTC
e15b959eee71275a8727bb8ea3c44963f33a6493
8 files changed
+811
-619
prompts/default/fw.msg_truncated.md
+3
-1
@@ -1 +1,3 @@
1
-<< {{length}} CHARACTERS REMOVED TO SAVE SPACE >>
\ No newline at end of file
1
+<<
2
+{{length}} CHARACTERS REMOVED TO SAVE SPACE
3
+>>
\ No newline at end of file
python/helpers/strings.py
+11
-1
@@ -119,4 +119,14 @@ def dict_to_text(d: dict) -> str:
119
parts.append(f"{value}")
120
parts.append("") # Add empty line between entries
121
122
- return "\n".join(parts).rstrip() # rstrip to remove trailing newline
\ No newline at end of file
122
+ return "\n".join(parts).rstrip() # rstrip to remove trailing newline
123
+
124
+def truncate_text(text: str, length: int, at_end: bool = True, replacement: str = "...") -> str:
125
+ orig_length = len(text)
126
+ if orig_length <= length:
127
+ return text
128
+ if at_end:
129
+ return text[:length] + replacement
130
+ else:
131
+ return replacement + text[-length:]
132
+
\ No newline at end of file
python/tools/code_execution_tool.py
+52
-10
@@ -8,7 +8,8 @@ from python.helpers.print_style import PrintStyle
8
from python.helpers.shell_local import LocalInteractiveSession
9
from python.helpers.shell_ssh import SSHInteractiveSession
10
from python.helpers.docker import DockerContainerManager
11
-from python.helpers.messages import truncate_text
11
+from python.helpers.strings import truncate_text as truncate_text_string
12
+from python.helpers.messages import truncate_text as truncate_text_agent
13
import re
14
15
@@ -63,11 +64,17 @@ class CodeExecution(Tool):
64
def get_log_object(self):
65
return self.agent.context.log.log(
66
type="code_exe",
66
- heading=f"{self.agent.agent_name}: Using tool '{self.name}'",
67
+ heading=self.get_heading(),
68
content="",
69
kvps=self.args,
70
)
71
72
+ def get_heading(self, text: str = ""):
73
+ if not text:
74
+ text = f"{self.name} - {self.args['runtime']}"
75
+ text = truncate_text_string(text, 60)
76
+ return f"icon://terminal {text}"
77
+
78
async def after_execution(self, response, **kwargs):
79
self.agent.hist_add_tool_result(self.name, response.message)
80
@@ -229,10 +236,9 @@ class CodeExecution(Tool):
236
if partial_output:
237
PrintStyle(font_color="#85C1E9").stream(partial_output)
238
# full_output += partial_output # Append new output
232
- truncated_output = truncate_text(
233
- agent=self.agent, output=full_output, threshold=10000
234
- )
235
- self.log.update(content=truncated_output)
239
+ truncated_output = self.fix_full_output(full_output)
240
+ heading = self.get_heading_from_output(truncated_output, 0)
241
+ self.log.update(content=truncated_output, heading=heading)
242
last_output_time = now
243
got_output = True
244
@@ -240,12 +246,18 @@ class CodeExecution(Tool):
246
last_lines = (
247
truncated_output.splitlines()[-3:] if truncated_output else []
248
)
243
- for line in last_lines:
249
+ last_lines.reverse()
250
+ for idx, line in enumerate(last_lines):
251
for pat in prompt_patterns:
252
if pat.search(line.strip()):
253
PrintStyle.info(
254
"Detected shell prompt, returning output early."
255
)
256
+ last_lines.reverse()
257
+ heading = self.get_heading_from_output(
258
+ "\n".join(last_lines), idx + 1, True
259
+ )
260
+ self.log.update(heading=heading)
261
return truncated_output
262
263
# Check for max execution time
@@ -257,7 +269,8 @@ class CodeExecution(Tool):
269
if truncated_output:
270
response = truncated_output + "\n\n" + response
271
PrintStyle.warning(sysinfo)
260
- self.log.update(content=response)
272
+ heading = self.get_heading_from_output(truncated_output, 0)
273
+ self.log.update(content=response, heading=heading)
274
return response
275
276
# Waiting for first output
@@ -280,7 +293,8 @@ class CodeExecution(Tool):
293
if truncated_output:
294
response = truncated_output + "\n\n" + response
295
PrintStyle.warning(sysinfo)
283
- self.log.update(content=response)
296
+ heading = self.get_heading_from_output(truncated_output, 0)
297
+ self.log.update(content=response, heading=heading)
298
return response
299
300
# potential dialog detection
@@ -305,7 +319,10 @@ class CodeExecution(Tool):
319
if truncated_output:
320
response = truncated_output + "\n\n" + response
321
PrintStyle.warning(sysinfo)
308
- self.log.update(content=response)
322
+ heading = self.get_heading_from_output(
323
+ truncated_output, 0
324
+ )
325
+ self.log.update(content=response, heading=heading)
326
return response
327
328
async def reset_terminal(self, session=0, reason: str | None = None):
@@ -326,3 +343,28 @@ class CodeExecution(Tool):
343
)
344
self.log.update(content=response)
345
return response
346
+
347
+ def get_heading_from_output(self, output: str, skip_lines=0, done=False):
348
+ done_icon = " icon://done_all" if done else ""
349
+
350
+ if not output:
351
+ return self.get_heading() + done_icon
352
+
353
+ # find last non-empty line with skip
354
+ lines = output.splitlines()
355
+ # Start from len(lines) - skip_lines - 1 down to 0
356
+ for i in range(len(lines) - skip_lines - 1, -1, -1):
357
+ line = lines[i].strip()
358
+ if not line:
359
+ continue
360
+ return self.get_heading(line) + done_icon
361
+
362
+ return self.get_heading() + done_icon
363
+
364
+ def fix_full_output(self, output: str):
365
+ # remove any single byte \xXX escapes
366
+ output = re.sub(r'(?<!\\)\\x[0-9A-Fa-f]{2}', '', output)
367
+ # Strip every line of output before truncation
368
+ output = "\n".join(line.strip() for line in output.splitlines())
369
+ output = truncate_text_agent(agent=self.agent, output=output, threshold=10000)
370
+ return output
webui/css/messages.css
new
+656
@@ -0,0 +1,656 @@
1
+/* Chat History */
2
+#chat-history {
3
+ display: -webkit-flex;
4
+ display: flex;
5
+ flex-direction: column;
6
+ flex-grow: 1;
7
+ width: 100%;
8
+ overflow-y: scroll;
9
+ overflow-x: hidden;
10
+ scroll-behavior: auto !important; /* avoid infinite scrolling! */
11
+ padding: var(--spacing-md) var(--spacing-sm) 0;
12
+ -webkit-transition: all 0.3s ease;
13
+ transition: all 0.3s ease;
14
+ scrollbar-width: thin;
15
+ scrollbar-color: #555 transparent;
16
+ }
17
+
18
+ #chat-history > *:first-child {
19
+ margin-top: 4.4em;
20
+ }
21
+
22
+ /* Scrollbar styling for Firefox */
23
+ #chat-history::-webkit-scrollbar {
24
+ width: 5px;
25
+ }
26
+
27
+ #chat-history::-webkit-scrollbar-track {
28
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
29
+ border-radius: 3px;
30
+ }
31
+
32
+ #chat-history::-webkit-scrollbar-thumb {
33
+ border-radius: 3px;
34
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
35
+ background-color: #555;
36
+ -webkit-transition: background-color var(--transition-speed) ease-in-out;
37
+ transition: background-color var(--transition-speed) ease-in-out;
38
+ }
39
+
40
+ #chat-history::-webkit-scrollbar-thumb:hover {
41
+ background-color: #666;
42
+ }
43
+
44
+ #chat-history::-webkit-scrollbar-thumb:active {
45
+ background-color: #888;
46
+ }
47
+
48
+
49
+/* Message Styles */
50
+
51
+
52
+ .user-container {
53
+ align-self: flex-end;
54
+ /* margin: var(--spacing-sm) var(--spacing-md); */
55
+ /* margin-bottom: var(--spacing-lg); */
56
+ /* margin-top: var(--spacing-sm); */
57
+ display: flex;
58
+ justify-content: flex-end;
59
+ width: 100%;
60
+ }
61
+
62
+ .ai-container {
63
+ align-self: flex-start;
64
+ }
65
+
66
+ .center-container {
67
+ align-self: center;
68
+ max-width: 80%;
69
+ margin: 0;
70
+ }
71
+
72
+ .center-container .message {
73
+ margin-bottom: var(--spacing-sm);
74
+ }
75
+
76
+ .message-user {
77
+ background-color: #4a4a4a;
78
+ /* border-bottom-right-radius: var(--spacing-xxs); */
79
+ min-width: 195px;
80
+ text-align: end;
81
+ }
82
+
83
+ .message-user > div {
84
+ padding-top: var(--spacing-xs);
85
+ font-family: "Roboto Mono", monospace;
86
+ font-optical-sizing: auto;
87
+ -webkit-font-optical-sizing: auto;
88
+ font-size: var(--font-size-small);
89
+ }
90
+
91
+ .message-ai {
92
+ /* border-bottom-left-radius: var(--spacing-xxs); */
93
+ }
94
+
95
+ .message-center {
96
+ align-self: center;
97
+ /* border-bottom-left-radius: unset; */
98
+ }
99
+
100
+ .message-followup {
101
+ /* margin-left: var(--spacing-lg); */
102
+ /* margin-bottom: var(--spacing-lg); */
103
+ }
104
+
105
+ .message-followup .message {
106
+ border-radius: 1.125em; /* 18px */
107
+ /* border-top-left-radius: var(--spacing-xxs); */
108
+ }
109
+
110
+ .message-followup + .message-followup {
111
+ margin-bottom: 0;
112
+ }
113
+
114
+ /* Update message types for dark mode */
115
+ .message-default,
116
+ .message-agent,
117
+ .message-agent-response,
118
+ .message-agent-delegation,
119
+ .message-tool,
120
+ .message-code-exe,
121
+ .message-browser,
122
+ .message-info,
123
+ .message-util,
124
+ .message-warning,
125
+ .message-error {
126
+ color: #e0e0e0;
127
+ }
128
+
129
+ .message-default {
130
+ background-color: #1a242f;
131
+ }
132
+
133
+ .message-agent {
134
+ background-color: #34506b;
135
+ }
136
+
137
+ .message-agent-response {
138
+ min-width: 255px;
139
+ background-color: #1f3c1e;
140
+ }
141
+
142
+ .message-agent-delegation {
143
+ background-color: #12685e;
144
+ }
145
+
146
+ .message-tool {
147
+ background-color: #2a4170;
148
+ }
149
+
150
+ .message-code-exe {
151
+ background-color: #4b3a69;
152
+ }
153
+
154
+ .message-code-exe .message-body {
155
+ min-height: 5em;
156
+ width: 100%;
157
+ background-color: var(--color-panel);
158
+ border-radius: 0.5em;
159
+ margin-top: 0.5em;
160
+ padding: 0.3em;
161
+ }
162
+
163
+ .light-mode .message-code-exe .message-body {
164
+ border: 1px solid var(--color-border);
165
+ }
166
+
167
+ .message-browser {
168
+ background-color: #4b3a69;
169
+ }
170
+
171
+ .message-info {
172
+ background-color: var(--color-panel);
173
+ }
174
+
175
+ .message-util {
176
+ background-color: #23211a;
177
+ display: none;
178
+ }
179
+
180
+ .message-warning {
181
+ background-color: #bc8036;
182
+ }
183
+
184
+ .message-error {
185
+ background-color: #af2222;
186
+ }
187
+
188
+ /* Agent and AI Info */
189
+ .agent-start {
190
+ color: var(--color-text);
191
+ font-size: var(--font-size-small);
192
+ margin-bottom: var(--spacing-xs);
193
+ opacity: 0.7;
194
+ }
195
+
196
+ .msg-kvps {
197
+ font-size: 0.9em;
198
+ margin: 0.5rem 0 0.55rem 0;
199
+ border-collapse: collapse;
200
+ width: 100%;
201
+ }
202
+
203
+ .kvps-val pre {
204
+ white-space: pre-wrap; /* keep \n, collapse no spaces, allow wrapping */
205
+ word-break: break-word; /* optional – forces really long “words” to break */
206
+ }
207
+
208
+ .msg-kvps th,
209
+ .msg-kvps td {
210
+ align-content: center;
211
+ padding: 0.25rem;
212
+ padding-left: 0;
213
+ text-align: left;
214
+ }
215
+
216
+ .msg-kvps th {
217
+ color: var(--color-primary);
218
+ width: 40%;
219
+ }
220
+
221
+ .msg-kvps tr {
222
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
223
+ }
224
+
225
+ .msg-heading {
226
+ margin: 0;
227
+ position: relative;
228
+ display: block;
229
+ white-space: nowrap;
230
+ }
231
+
232
+ .msg-heading h4 {
233
+ margin: 0;
234
+ /* width: calc(100% - 4em); */
235
+ margin-right: 4em;
236
+ overflow-y: hidden;
237
+ text-overflow: ellipsis;
238
+ }
239
+
240
+ /* Message Actions */
241
+ .message-actions {
242
+ color: var(--color-text);
243
+ font-size: var(--font-size-small);
244
+ margin-top: var(--spacing-xs);
245
+ }
246
+
247
+ .message-action {
248
+ cursor: pointer;
249
+ opacity: 0.7;
250
+ -webkit-transition: opacity var(--transition-speed) ease-in-out;
251
+ transition: opacity var(--transition-speed) ease-in-out;
252
+ }
253
+
254
+ .message-action:hover {
255
+ opacity: 1;
256
+ }
257
+
258
+
259
+/* Copy button styles */
260
+.copy-button {
261
+ position: absolute;
262
+ right: 0;
263
+ top: var(--spacing-sm);
264
+ background: none;
265
+ border: none;
266
+ padding: var(--spacing-xs) var(--spacing-sm);
267
+ padding-right: 0;
268
+ cursor: pointer;
269
+ text-decoration: underline;
270
+ text-wrap: nowrap;
271
+ opacity: 0;
272
+ -webkit-transition: opacity var(--transition-speed) ease-in-out;
273
+ transition: opacity var(--transition-speed) ease-in-out;
274
+ color: inherit;
275
+ font-size: 12px;
276
+ font-family: "Rubik", Arial, Helvetica, sans-serif;
277
+ }
278
+
279
+ .copy-button:hover {
280
+ opacity: 0.8 !important;
281
+ }
282
+
283
+ .msg-content:hover .copy-button,
284
+ .kvps-row:hover .copy-button,
285
+ .message-text:hover .copy-button {
286
+ opacity: 0.6;
287
+ }
288
+
289
+ .copy-button.copied {
290
+ font-family: "Rubik", Arial, Helvetica, sans-serif !important;
291
+ opacity: 1 !important;
292
+ }
293
+
294
+ .msg-thoughts .copy-button {
295
+ top: -12px !important;
296
+ }
297
+
298
+ .message-user .copy-button {
299
+ top: -15px !important;
300
+ left: -13px !important;
301
+ right: 99% !important;
302
+ }
303
+
304
+ .message-agent-response .copy-button {
305
+ top: -22px !important;
306
+ right: 0 !important;
307
+ padding-right: 0px !important;
308
+ }
309
+
310
+ .message-info .copy-button {
311
+ top: -22px !important;
312
+ right: 0 !important;
313
+ padding-right: 0px !important;
314
+ }
315
+
316
+ .message-tool .copy-button {
317
+ top: -12px !important;
318
+ right: 0 !important;
319
+ }
320
+
321
+ .msg-output .copy-button {
322
+ top: -6px !important;
323
+ right: 0 !important;
324
+ }
325
+
326
+ /* Make message containers relative for absolute positioning of copy buttons */
327
+ .msg-content,
328
+ .kvps-row,
329
+ .message-text {
330
+ position: relative;
331
+ }
332
+
333
+ .message-text pre {
334
+ margin: 0;
335
+ }
336
+
337
+/* Utility Classes */
338
+.kvps-key {
339
+ font-weight: 500;
340
+ font-size: var(--font-size-small);
341
+ }
342
+
343
+ .kvps-val {
344
+ margin: 0.65rem 0 0.65rem 0.4rem;
345
+ white-space: pre-wrap;
346
+ }
347
+
348
+ .kvps-img {
349
+ width: 8em;
350
+ height: 8em;
351
+ object-fit: cover;
352
+ object-position: top left;
353
+ border-radius: 10%;
354
+ border: 1px solid rgba(255, 255, 255, 0.15);
355
+ }
356
+
357
+ .image-viewer-img {
358
+ width: 100%;
359
+ }
360
+
361
+ .msg-json {
362
+ display: none;
363
+ }
364
+
365
+ .msg-thoughts {
366
+ display: auto;
367
+ }
368
+
369
+ .msg-thoughts .kvps-val {
370
+ max-height: 20em;
371
+ overflow: auto;
372
+ }
373
+
374
+ .msg-content {
375
+ margin-bottom: 0;
376
+ }
377
+
378
+ .message-temp {
379
+ display: none;
380
+ }
381
+
382
+ .message-temp:not([style*="display: none"]):last-of-type {
383
+ display: block; /* or any style you want for visibility */
384
+ }
385
+
386
+/* Math (KaTeX) */
387
+.katex {
388
+ line-height: 1.2 !important;
389
+ font-size: 1.1em;
390
+ }
391
+
392
+/* Media Queries */
393
+@media (max-width: 640px) {
394
+ /* New styles for mobile messages */
395
+
396
+ .message-followup {
397
+ /* margin-left: var(--spacing-md); */
398
+ margin-bottom: var(--spacing-md);
399
+ }
400
+
401
+ .msg-kvps {
402
+ display: flex;
403
+ flex-direction: column;
404
+ border-collapse: separate;
405
+ border-spacing: 0 0.5rem;
406
+ }
407
+
408
+ .msg-kvps tr {
409
+ display: flex;
410
+ flex-direction: column;
411
+ margin-top: 0.3rem;
412
+ padding-bottom: 0;
413
+ }
414
+
415
+ .msg-kvps th,
416
+ .msg-kvps td {
417
+ display: block;
418
+ width: 100%;
419
+ text-align: left;
420
+ border-bottom: none;
421
+ padding: 0.25rem 0;
422
+ padding-left: 0 !important;
423
+ }
424
+
425
+ .msg-kvps th {
426
+ color: var(--color-primary);
427
+ margin-bottom: 0.25rem;
428
+ }
429
+
430
+ .kvps-val {
431
+ margin: 0 0 0.4rem 0;
432
+ white-space: pre-wrap;
433
+ word-break: break-word;
434
+ overflow-wrap: anywhere;
435
+ }
436
+ }
437
+
438
+.light-mode .msg-kvps tr {
439
+ border-bottom: 1px solid rgb(192 192 192 / 50%);
440
+ }
441
+
442
+ .light-mode .message-default {
443
+ background-color: #f3f3f3;
444
+ color: #1a242f;
445
+ }
446
+
447
+ .light-mode .message-agent {
448
+ background-color: #f3f3f3;
449
+ color: #356ca3;
450
+ }
451
+
452
+ .light-mode .message-agent-response {
453
+ background-color: #f3f3f3;
454
+ color: #188216;
455
+ }
456
+
457
+ .light-mode .message-agent-delegation {
458
+ background-color: #f3f3f3;
459
+ color: #12685e;
460
+ }
461
+
462
+ .light-mode .message-tool {
463
+ background-color: #f3f3f3;
464
+ color: #1c3c88;
465
+ }
466
+
467
+ .light-mode .message-code-exe {
468
+ background-color: #f3f3f3;
469
+ color: #6c43b0;
470
+ }
471
+
472
+ .light-mode .message-browser {
473
+ background-color: #ffffff;
474
+ color: #6c43b0;
475
+ }
476
+
477
+ .light-mode .message-info {
478
+ background-color: #f3f3f3;
479
+ color: #3f3f3f;
480
+ }
481
+
482
+ .light-mode .message-util {
483
+ background-color: #f3f3f3;
484
+ color: #5b5540;
485
+ }
486
+
487
+ .light-mode .message-warning {
488
+ background-color: #f3f3f3;
489
+ color: #8f4800;
490
+ }
491
+
492
+ .light-mode .message-error {
493
+ background-color: #f3f3f3;
494
+ color: #8f1010;
495
+ }
496
+
497
+ .light-mode .message-user {
498
+ background-color: #f3f3f3;
499
+ color: #4e4e4e;
500
+ }
501
+
502
+/* Markdown in messages */
503
+.msg-content {
504
+ font-size: var(--font-size-small);
505
+ }
506
+
507
+ .message-agent-response .msg-content {
508
+ font-size: var(--font-size-smaller);
509
+ }
510
+
511
+ .msg-content h1 {
512
+ font-size: 1.25em;
513
+ font-weight: 800;
514
+ margin-bottom: 0.2em;
515
+ }
516
+
517
+ .msg-content h2 {
518
+ font-size: 1.2em;
519
+ font-weight: 700;
520
+ margin-bottom: 0.2em;
521
+ }
522
+
523
+ .msg-content h3 {
524
+ font-size: 1.15em;
525
+ font-weight: 600;
526
+ margin-bottom: 0.2em;
527
+ }
528
+
529
+ .msg-content h4 {
530
+ font-size: 1.1em;
531
+ font-weight: 500;
532
+ margin-bottom: 0.2em;
533
+ }
534
+
535
+ .msg-content h5 {
536
+ font-size: 1.05em;
537
+ font-weight: 500;
538
+ margin-bottom: 0.2em;
539
+ }
540
+
541
+ .msg-content h6 {
542
+ font-size: 1em;
543
+ font-weight: 500;
544
+ margin-bottom: 0.2em;
545
+ }
546
+
547
+ .msg-content table {
548
+ width: 100%;
549
+ border-collapse: collapse;
550
+ margin: 1em 0;
551
+ font-size: 0.98em;
552
+ background: transparent;
553
+ border-radius: 8px;
554
+ overflow: hidden;
555
+ }
556
+
557
+ .msg-content th,
558
+ .msg-content td {
559
+ padding: 0.55em 1em;
560
+ border: 1px solid rgba(142, 142, 142, 0.1);
561
+ text-align: left;
562
+ background: transparent;
563
+ }
564
+
565
+ .msg-content th {
566
+ font-weight: 600;
567
+ background: rgba(142, 142, 142, 0.1);
568
+ }
569
+
570
+ .msg-content tr:nth-child(even) {
571
+ background: rgba(142, 142, 142, 0.1);
572
+ }
573
+
574
+ .msg-content tr:nth-child(odd) {
575
+ background: transparent;
576
+ }
577
+
578
+ .msg-content table {
579
+ box-shadow: none;
580
+ }
581
+
582
+ .msg-content pre:has(code) {
583
+ padding: 0.5em;
584
+ border: 1px solid rgba(142, 142, 142, 0.1);
585
+ border-radius: 0.3em;
586
+ }
587
+
588
+ .msg-min-max-btns {
589
+ opacity: 40%;
590
+ position: absolute;
591
+ top: -0.2em;
592
+ right: -0.2em;
593
+ display: flex;
594
+ gap: 0.3em;
595
+ z-index: 1;
596
+ }
597
+
598
+.message-group {
599
+ margin-top: 1em;
600
+ margin-bottom: 1em;
601
+ display: inline-grid;
602
+ grid-template-columns: minmax(0, max-content);
603
+ grid-auto-rows: auto;
604
+ max-width: 100%;
605
+ width: fit-content;
606
+ }
607
+
608
+ .message-group > * {
609
+ grid-column: 1;
610
+ } /* both children sit in the same column */
611
+
612
+ .message-group-right {
613
+ justify-content: end;
614
+ }
615
+
616
+ .message-group-mid {
617
+ justify-content: center;
618
+ }
619
+
620
+ /* 1. FIRST child’s .message – clear ONLY bottom corners */
621
+ .message-group > *:first-child:not(:last-child) > .message {
622
+ border-bottom-left-radius: var(--spacing-xxs);;
623
+ border-bottom-right-radius: var(--spacing-xxs);;
624
+ }
625
+
626
+ /* 2. MIDDLE children’s .message – clear ALL corners */
627
+ .message-group > *:not(:first-child):not(:last-child) > .message {
628
+ border-radius: var(--spacing-xxs);
629
+ }
630
+
631
+ /* 3. LAST child’s .message – clear ONLY top corners */
632
+ .message-group > *:last-child:not(:first-child) > .message {
633
+ border-top-left-radius: var(--spacing-xxs);;
634
+ border-top-right-radius: var(--spacing-xxs);;
635
+ }
636
+
637
+ .message-container {
638
+ animation: fadeIn 0.5s;
639
+ -webkit-animation: fadeIn 0.5s;
640
+ margin-bottom: var(--spacing-sm);
641
+ width: 100%;
642
+ max-width: 100%;
643
+ }
644
+
645
+ .message {
646
+ /* background-color: var(--color-message-bg); */
647
+ border-radius: var(--border-radius);
648
+ padding: 0.9rem var(--spacing-sm) 0.7rem var(--spacing-sm);
649
+ overflow-x: auto;
650
+ width: auto;
651
+ max-width: 100%;
652
+ box-sizing: border-box;
653
+ display: block;
654
+ word-break: break-word;
655
+ overflow-wrap: anywhere;
656
+ }
\ No newline at end of file
webui/index.css
+8
-576
@@ -48,6 +48,7 @@
48
--color-input-focus: var(--color-input-focus-dark);
49
50
/* Spacing variables */
51
+ --spacing-xxs: 0.15rem;
52
--spacing-xs: 0.3125rem;
53
--spacing-sm: 0.625rem;
54
--spacing-md: 1.25rem;
@@ -90,6 +91,10 @@ body {
91
-webkit-overscroll-behavior: none;
92
}
93
94
+p:last-child {
95
+ margin-bottom: 0;
96
+}
97
+
98
body,
99
#left-panel,
100
#chat-input,
@@ -441,51 +446,6 @@ pre {
446
font-size: 0.75rem;
447
}
448
444
-/* Chat History */
445
-#chat-history {
446
- display: -webkit-flex;
447
- display: flex;
448
- flex-direction: column;
449
- flex-grow: 1;
450
- overflow-y: scroll;
451
- overflow-x: hidden;
452
- scroll-behavior: auto !important; /* avoid infinite scrolling! */
453
- padding: var(--spacing-md) var(--spacing-md) 0;
454
- -webkit-transition: all 0.3s ease;
455
- transition: all 0.3s ease;
456
- scrollbar-width: thin;
457
- scrollbar-color: #555 transparent;
458
-}
459
-
460
-#chat-history > *:first-child {
461
- margin-top: 4.4em;
462
-}
463
-
464
-/* Scrollbar styling for Firefox */
465
-#chat-history::-webkit-scrollbar {
466
- width: 5px;
467
-}
468
-
469
-#chat-history::-webkit-scrollbar-track {
470
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
471
- border-radius: 3px;
472
-}
473
-
474
-#chat-history::-webkit-scrollbar-thumb {
475
- border-radius: 3px;
476
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
477
- background-color: #555;
478
- -webkit-transition: background-color var(--transition-speed) ease-in-out;
479
- transition: background-color var(--transition-speed) ease-in-out;
480
-}
481
-
482
-#chat-history::-webkit-scrollbar-thumb:hover {
483
- background-color: #666;
484
-}
485
-
486
-#chat-history::-webkit-scrollbar-thumb:active {
487
- background-color: #888;
488
-}
449
450
/* Logo Container */
451
#logo-container {
@@ -569,220 +529,7 @@ pre {
529
margin-left: 4.6rem;
530
}
531
572
-/* Message Styles */
573
-.message-container {
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 {
601
- align-self: flex-start;
602
-}
603
-
604
-.center-container {
605
- align-self: center;
606
- max-width: 80%;
607
- margin: 0;
608
-}
609
-
610
-.center-container .message {
611
- margin-bottom: var(--spacing-sm);
612
-}
613
-
614
-.message-user {
615
- background-color: #4a4a4a;
616
- border-bottom-right-radius: var(--spacing-xs);
617
- min-width: 195px;
618
- text-align: end;
619
-}
620
-
621
-.message-user > div {
622
- padding-top: var(--spacing-xs);
623
- font-family: "Roboto Mono", monospace;
624
- font-optical-sizing: auto;
625
- -webkit-font-optical-sizing: auto;
626
- font-size: var(--font-size-small);
627
-}
628
-
629
-.message-ai {
630
- border-bottom-left-radius: var(--spacing-xs);
631
-}
632
-
633
-.message-center {
634
- align-self: center;
635
- border-bottom-left-radius: unset;
636
-}
637
-
638
-.message-followup {
639
- margin-left: var(--spacing-lg);
640
- margin-bottom: var(--spacing-lg);
641
-}
642
-
643
-.message-followup .message {
644
- border-radius: 1.125em; /* 18px */
645
- border-top-left-radius: 0.3125em; /* 5px */
646
-}
647
-
648
-.message-followup + .message-followup {
649
- margin-bottom: 0;
650
-}
651
-
652
-/* Update message types for dark mode */
653
-.message-default,
654
-.message-agent,
655
-.message-agent-response,
656
-.message-agent-delegation,
657
-.message-tool,
658
-.message-code-exe,
659
-.message-browser,
660
-.message-info,
661
-.message-util,
662
-.message-warning,
663
-.message-error {
664
- color: #e0e0e0;
665
-}
666
-
667
-.message-default {
668
- background-color: #1a242f;
669
-}
670
-
671
-.message-agent {
672
- background-color: #34506b;
673
-}
674
-
675
-.message-agent-response {
676
- min-width: 255px;
677
- background-color: #1f3c1e;
678
-}
679
-
680
-.message-agent-delegation {
681
- background-color: #12685e;
682
-}
683
-
684
-.message-tool {
685
- background-color: #2a4170;
686
-}
687
-
688
-.message-code-exe {
689
- background-color: #4b3a69;
690
-}
691
-
692
-.message-code-exe .message-body {
693
- min-height: 5em;
694
- background-color: var(--color-panel);
695
- border-radius: 0.5em;
696
- margin: 0.3em;
697
- padding: 0.3em;
698
-}
699
-
700
-.light-mode .message-code-exe .message-body {
701
- border: 1px solid var(--color-border);
702
-}
703
-
704
-.message-browser {
705
- background-color: #4b3a69;
706
-}
707
-
708
-.message-info {
709
- background-color: var(--color-panel);
710
-}
711
-
712
-.message-util {
713
- background-color: #23211a;
714
- display: none;
715
-}
716
-
717
-.message-warning {
718
- background-color: #bc8036;
719
-}
720
-
721
-.message-error {
722
- background-color: #af2222;
723
-}
724
-
725
-/* Agent and AI Info */
726
-.agent-start {
727
- color: var(--color-text);
728
- font-size: var(--font-size-small);
729
- margin-bottom: var(--spacing-xs);
730
- opacity: 0.7;
731
-}
732
-
733
-.msg-kvps {
734
- font-size: 0.9em;
735
- margin: 0.5rem 0 0.55rem 0;
736
- border-collapse: collapse;
737
- width: 100%;
738
-}
739
-
740
-.kvps-val pre {
741
- white-space: pre-wrap; /* keep \n, collapse no spaces, allow wrapping */
742
- word-break: break-word; /* optional – forces really long “words” to break */
743
-}
744
-
745
-.msg-kvps th,
746
-.msg-kvps td {
747
- align-content: center;
748
- padding: 0.25rem;
749
- padding-left: 0;
750
- text-align: left;
751
-}
532
753
-.msg-kvps th {
754
- color: var(--color-primary);
755
- width: 40%;
756
-}
757
-
758
-.msg-kvps tr {
759
- border-bottom: 1px solid rgba(255, 255, 255, 0.15);
760
-}
761
-
762
-.msg-heading {
763
- margin: 0;
764
- padding-right: 5.5em; /* space for buttons */
765
- position: relative;
766
- display: block;
767
-}
768
-
769
-/* Message Actions */
770
-.message-actions {
771
- color: var(--color-text);
772
- font-size: var(--font-size-small);
773
- margin-top: var(--spacing-xs);
774
-}
775
-
776
-.message-action {
777
- cursor: pointer;
778
- opacity: 0.7;
779
- -webkit-transition: opacity var(--transition-speed) ease-in-out;
780
- transition: opacity var(--transition-speed) ease-in-out;
781
-}
782
-
783
-.message-action:hover {
784
- opacity: 1;
785
-}
533
534
/* Input Section */
535
#input-section {
@@ -1486,128 +1233,9 @@ input:checked + .slider:before {
1233
/* Already defined above */
1234
}
1235
1489
-/* Copy button styles */
1490
-.copy-button {
1491
- position: absolute;
1492
- right: 0;
1493
- top: var(--spacing-sm);
1494
- background: none;
1495
- border: none;
1496
- padding: var(--spacing-xs) var(--spacing-sm);
1497
- padding-right: 0;
1498
- cursor: pointer;
1499
- text-decoration: underline;
1500
- text-wrap: nowrap;
1501
- opacity: 0;
1502
- -webkit-transition: opacity var(--transition-speed) ease-in-out;
1503
- transition: opacity var(--transition-speed) ease-in-out;
1504
- color: inherit;
1505
- font-size: 12px;
1506
- font-family: "Rubik", Arial, Helvetica, sans-serif;
1507
-}
1508
-
1509
-.copy-button:hover {
1510
- opacity: 0.8 !important;
1511
-}
1512
-
1513
-.msg-content:hover .copy-button,
1514
-.kvps-row:hover .copy-button,
1515
-.message-text:hover .copy-button {
1516
- opacity: 0.6;
1517
-}
1518
-
1519
-.copy-button.copied {
1520
- font-family: "Rubik", Arial, Helvetica, sans-serif !important;
1521
- opacity: 1 !important;
1522
-}
1523
-
1524
-.msg-thoughts .copy-button {
1525
- top: -12px !important;
1526
-}
1527
-
1528
-.message-user .copy-button {
1529
- top: -15px !important;
1530
- left: -13px !important;
1531
- right: 99% !important;
1532
-}
1533
-
1534
-.message-agent-response .copy-button {
1535
- top: -22px !important;
1536
- right: 0 !important;
1537
- padding-right: 0px !important;
1538
-}
1539
-
1540
-.message-info .copy-button {
1541
- top: -22px !important;
1542
- right: 0 !important;
1543
- padding-right: 0px !important;
1544
-}
1545
-
1546
-.message-tool .copy-button {
1547
- top: -12px !important;
1548
- right: 0 !important;
1549
-}
1550
-
1551
-.msg-output .copy-button {
1552
- top: -6px !important;
1553
- right: 0 !important;
1554
-}
1555
-
1556
-/* Make message containers relative for absolute positioning of copy buttons */
1557
-.msg-content,
1558
-.kvps-row,
1559
-.message-text {
1560
- position: relative;
1561
-}
1562
-
1563
-/* Utility Classes */
1564
-.kvps-key {
1565
- font-weight: 500;
1566
- font-size: var(--font-size-small);
1567
-}
1568
-
1569
-.kvps-val {
1570
- margin: 0.65rem 0 0.65rem 0.4rem;
1571
- white-space: pre-wrap;
1572
-}
1573
-
1574
-.kvps-img {
1575
- width: 8em;
1576
- height: 8em;
1577
- object-fit: cover;
1578
- object-position: top left;
1579
- border-radius: 10%;
1580
- border: 1px solid rgba(255, 255, 255, 0.15);
1581
-}
1582
-
1583
-.image-viewer-img {
1584
- width: 100%;
1585
-}
1586
-
1587
-.msg-json {
1588
- display: none;
1589
-}
1590
-
1591
-.msg-thoughts {
1592
- display: auto;
1593
-}
1594
-
1595
-.msg-thoughts .kvps-val {
1596
- max-height: 20em;
1597
- overflow: auto;
1598
-}
1236
1600
-.msg-content {
1601
- margin-bottom: 0;
1602
-}
1237
1604
-.message-temp {
1605
- display: none;
1606
-}
1238
1608
-.message-temp:not([style*="display: none"]):last-of-type {
1609
- display: block; /* or any style you want for visibility */
1610
-}
1239
1240
.status-icon {
1241
display: flex;
@@ -1643,11 +1271,7 @@ input:checked + .slider:before {
1271
font-weight: bold;
1272
}
1273
1646
-/* Math (KaTeX) */
1647
-.katex {
1648
- line-height: 1.2 !important;
1649
- font-size: 1.1em;
1650
-}
1274
+
1275
1276
/* Animations */
1277
@keyframes fadeIn {
@@ -1717,48 +1341,6 @@ input:checked + .slider:before {
1341
display: none;
1342
}
1343
1720
- /* New styles for mobile messages */
1721
-
1722
- .message-followup {
1723
- margin-left: var(--spacing-md);
1724
- margin-bottom: var(--spacing-md);
1725
- }
1726
-
1727
- .msg-kvps {
1728
- display: flex;
1729
- flex-direction: column;
1730
- border-collapse: separate;
1731
- border-spacing: 0 0.5rem;
1732
- }
1733
-
1734
- .msg-kvps tr {
1735
- display: flex;
1736
- flex-direction: column;
1737
- margin-top: 0.3rem;
1738
- padding-bottom: 0;
1739
- }
1740
-
1741
- .msg-kvps th,
1742
- .msg-kvps td {
1743
- display: block;
1744
- width: 100%;
1745
- text-align: left;
1746
- border-bottom: none;
1747
- padding: 0.25rem 0;
1748
- padding-left: 0 !important;
1749
- }
1750
-
1751
- .msg-kvps th {
1752
- color: var(--color-primary);
1753
- margin-bottom: 0.25rem;
1754
- }
1755
-
1756
- .kvps-val {
1757
- margin: 0 0 0.4rem 0;
1758
- white-space: pre-wrap;
1759
- word-break: break-word;
1760
- overflow-wrap: anywhere;
1761
- }
1344
}
1345
1346
@media (max-width: 640px) {
@@ -1962,69 +1544,7 @@ a:active {
1544
--color-input-focus: var(--color-input-focus-light);
1545
}
1546
1965
-.light-mode .msg-kvps tr {
1966
- border-bottom: 1px solid rgb(192 192 192 / 50%);
1967
-}
1968
-
1969
-.light-mode .message-default {
1970
- background-color: #f3f3f3;
1971
- color: #1a242f;
1972
-}
1973
-
1974
-.light-mode .message-agent {
1975
- background-color: #f3f3f3;
1976
- color: #356ca3;
1977
-}
1978
-
1979
-.light-mode .message-agent-response {
1980
- background-color: #f3f3f3;
1981
- color: #188216;
1982
-}
1983
-
1984
-.light-mode .message-agent-delegation {
1985
- background-color: #f3f3f3;
1986
- color: #12685e;
1987
-}
1547
1989
-.light-mode .message-tool {
1990
- background-color: #f3f3f3;
1991
- color: #1c3c88;
1992
-}
1993
-
1994
-.light-mode .message-code-exe {
1995
- background-color: #f3f3f3;
1996
- color: #6c43b0;
1997
-}
1998
-
1999
-.light-mode .message-browser {
2000
- background-color: #ffffff;
2001
- color: #6c43b0;
2002
-}
2003
-
2004
-.light-mode .message-info {
2005
- background-color: #f3f3f3;
2006
- color: #3f3f3f;
2007
-}
2008
-
2009
-.light-mode .message-util {
2010
- background-color: #f3f3f3;
2011
- color: #5b5540;
2012
-}
2013
-
2014
-.light-mode .message-warning {
2015
- background-color: #f3f3f3;
2016
- color: #8f4800;
2017
-}
2018
-
2019
-.light-mode .message-error {
2020
- background-color: #f3f3f3;
2021
- color: #8f1010;
2022
-}
2023
-
2024
-.light-mode .message-user {
2025
- background-color: #f3f3f3;
2026
- color: #4e4e4e;
2027
-}
1548
1549
.light-mode .connected {
1550
color: #4caf50;
@@ -3019,97 +2539,9 @@ nav ul li a img {
2539
text-align: center;
2540
}
2541
3022
-/* Markdown in messages */
3023
-.msg-content {
3024
- font-size: var(--font-size-small);
3025
-}
3026
-
3027
-.message-agent-response .msg-content {
3028
- font-size: var(--font-size-smaller);
3029
-}
3030
-
3031
-.msg-content h1 {
3032
- font-size: 1.25em;
3033
- font-weight: 800;
3034
- margin-bottom: 0.2em;
3035
-}
3036
-
3037
-.msg-content h2 {
3038
- font-size: 1.2em;
3039
- font-weight: 700;
3040
- margin-bottom: 0.2em;
3041
-}
3042
-
3043
-.msg-content h3 {
3044
- font-size: 1.15em;
3045
- font-weight: 600;
3046
- margin-bottom: 0.2em;
3047
-}
3048
-
3049
-.msg-content h4 {
3050
- font-size: 1.1em;
3051
- font-weight: 500;
3052
- margin-bottom: 0.2em;
3053
-}
3054
-
3055
-.msg-content h5 {
3056
- font-size: 1.05em;
3057
- font-weight: 500;
3058
- margin-bottom: 0.2em;
3059
-}
3060
-
3061
-.msg-content h6 {
3062
- font-size: 1em;
3063
- font-weight: 500;
3064
- margin-bottom: 0.2em;
3065
-}
2542
3067
-.msg-content table {
3068
- width: 100%;
3069
- border-collapse: collapse;
3070
- margin: 1em 0;
3071
- font-size: 0.98em;
3072
- background: transparent;
3073
- border-radius: 8px;
3074
- overflow: hidden;
3075
-}
2543
3077
-.msg-content th,
3078
-.msg-content td {
3079
- padding: 0.55em 1em;
3080
- border: 1px solid rgba(142, 142, 142, 0.1);
3081
- text-align: left;
3082
- background: transparent;
3083
-}
3084
-
3085
-.msg-content th {
3086
- font-weight: 600;
3087
- background: rgba(142, 142, 142, 0.1);
3088
-}
3089
-
3090
-.msg-content tr:nth-child(even) {
3091
- background: rgba(142, 142, 142, 0.1);
3092
-}
3093
-
3094
-.msg-content tr:nth-child(odd) {
3095
- background: transparent;
3096
-}
3097
-
3098
-.msg-content table {
3099
- box-shadow: none;
3100
-}
3101
-
3102
-.msg-content pre:has(code) {
3103
- padding: 0.5em;
3104
- border: 1px solid rgba(142, 142, 142, 0.1);
3105
- border-radius: 0.3em;
2544
+.icon {
2545
+ vertical-align: middle;
2546
}
2547
3108
-.msg-min-max-btns {
3109
- position: absolute;
3110
- top: -0.2em;
3111
- right: -0.5em;
3112
- display: flex;
3113
- gap: 0.3em;
3114
- z-index: 1;
3115
-}
webui/index.html
+1
@@ -7,6 +7,7 @@
7
<title>Agent Zero</title>
8
<link rel="icon" type="image/svg+xml" href="public/favicon.svg">
9
<link rel="stylesheet" href="index.css">
10
+ <link rel="stylesheet" href="css/messages.css">
11
<link rel="stylesheet" href="css/toast.css">
12
<link rel="stylesheet" href="css/settings.css">
13
<link rel="stylesheet" href="css/file_browser.css">
webui/index.js
+2
-27
@@ -235,34 +235,9 @@ updateUserTime();
235
setInterval(updateUserTime, 1000);
236
237
function setMessage(id, type, heading, content, temp, kvps = null) {
238
- // Search for the existing message container by id
239
- let messageContainer = document.getElementById(`message-${id}`);
240
-
241
- if (messageContainer) {
242
- // Don't re-render user messages
243
- if (type === "user") {
244
- return; // Skip re-rendering
245
- }
246
- // For other types, update the message
247
- messageContainer.innerHTML = "";
248
- } else {
249
- // Create a new container if not found
250
- const sender = type === "user" ? "user" : "ai";
251
- messageContainer = document.createElement("div");
252
- messageContainer.id = `message-${id}`;
253
- messageContainer.classList.add("message-container", `${sender}-container`);
254
- if (temp) messageContainer.classList.add("message-temp");
255
- }
256
-
257
- const handler = msgs.getHandler(type);
258
- handler(messageContainer, id, type, heading, content, temp, kvps);
259
-
260
- // If the container was found, it was already in the DOM, no need to append again
261
- if (!document.getElementById(`message-${id}`)) {
262
- chatHistory.appendChild(messageContainer);
263
- }
264
-
238
+ const result = msgs.setMessage(id, type, heading, content, temp, kvps);
239
if (autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
240
+ return result;
241
}
242
243
window.loadKnowledge = async function () {
webui/js/messages.js
+78
-4
@@ -3,6 +3,71 @@ import { openImageModal } from "./image_modal.js";
3
import { marked } from "../vendor/marked/marked.esm.js";
4
import { store as messageResizeStore } from "/components/messages/resize/message-resize-store.js";
5
6
+const chatHistory = document.getElementById("chat-history");
7
+
8
+let messageGroup = null;
9
+
10
+export function setMessage(id, type, heading, content, temp, kvps = null) {
11
+ // Search for the existing message container by id
12
+ let messageContainer = document.getElementById(`message-${id}`);
13
+
14
+ if (messageContainer) {
15
+ // Don't re-render user messages
16
+ if (type === "user") {
17
+ return; // Skip re-rendering
18
+ }
19
+ // For other types, update the message
20
+ messageContainer.innerHTML = "";
21
+ } else {
22
+ // Create a new container if not found
23
+ const sender = type === "user" ? "user" : "ai";
24
+ messageContainer = document.createElement("div");
25
+ messageContainer.id = `message-${id}`;
26
+ messageContainer.classList.add("message-container", `${sender}-container`);
27
+ // if (temp) messageContainer.classList.add("message-temp");
28
+ }
29
+
30
+ const handler = getHandler(type);
31
+ handler(messageContainer, id, type, heading, content, temp, kvps);
32
+
33
+ // If the container was found, it was already in the DOM, no need to append again
34
+ if (!document.getElementById(`message-${id}`)) {
35
+ // message type visual grouping
36
+ const groupTypeMap = {
37
+ user: "right",
38
+ info: "mid",
39
+ warning: "mid",
40
+ error: "mid",
41
+ rate_limit: "mid",
42
+ util: "mid",
43
+ hint: "mid",
44
+ // anything else is "left"
45
+ };
46
+
47
+ //force new group on these types
48
+ const groupStart = {
49
+ agent: true,
50
+ // anything else is false
51
+ };
52
+
53
+ const groupType = groupTypeMap[type] || "left";
54
+
55
+ if (
56
+ !messageGroup || // no group yet exists
57
+ groupStart[type] || // message type forces new group
58
+ groupType != messageGroup.getAttribute("data-group-type") // message type changes group
59
+ ) {
60
+ messageGroup = document.createElement("div");
61
+ messageGroup.id = `message-group-${id}`;
62
+ messageGroup.classList.add(`message-group`, `message-group-${groupType}`);
63
+ messageGroup.setAttribute("data-group-type", groupType);
64
+ }
65
+
66
+ messageGroup.appendChild(messageContainer);
67
+ chatHistory.appendChild(messageGroup);
68
+ }
69
+}
70
+
71
function createCopyButton() {
72
const button = document.createElement("button");
73
button.className = "copy-button";
@@ -94,9 +159,11 @@ export function _drawMessage(
159
messageDiv.classList.add("message", mainClass, ...messageClasses);
160
161
if (heading) {
97
- const headingElement = document.createElement("h4");
162
+ const headingElement = document.createElement("div");
163
headingElement.classList.add("msg-heading");
99
- headingElement.textContent = heading;
164
+ const headingH4 = document.createElement("h4");
165
+ headingH4.innerHTML = convertIcons(escapeHTML(heading));
166
+ headingElement.appendChild(headingH4);
167
messageDiv.appendChild(headingElement);
168
169
if (resizeBtns) {
@@ -301,8 +368,8 @@ export function drawMessageUser(
368
textDiv.classList.add("message-text");
369
370
// Create a span for the content
304
- const spanElement = document.createElement("span");
305
- spanElement.innerHTML = convertHTML(content);
371
+ const spanElement = document.createElement("pre");
372
+ spanElement.innerHTML = escapeHTML(content);
373
textDiv.appendChild(spanElement);
374
375
// Add click handler
@@ -699,6 +766,13 @@ function convertImgFilePaths(str) {
766
return str.replace("img://", "/image_get?path=");
767
}
768
769
+function convertIcons(str) {
770
+ return str.replace(
771
+ /icon:\/\/([a-zA-Z0-9_]+)/g,
772
+ '<span class="icon material-symbols-outlined">$1</span>'
773
+ );
774
+}
775
+
776
function escapeHTML(str) {
777
const escapeChars = {
778
"&": "&",