diff --git a/package-lock.json b/package-lock.json index 500da39..3ef9727 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,9 @@ "ajv": "^8.12.0", "async-cache-dedupe": "^1.12.0", "classnames": "^2.3.2", + "dompurify": "^3.4.12", + "highlight.js": "^11.11.1", + "marked": "^12.0.2", "prom-client": "^15.1.3" }, "devDependencies": { @@ -2516,7 +2519,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT", "optional": true }, @@ -3900,10 +3902,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", - "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", - "dev": true, + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5094,6 +5095,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -5905,6 +5915,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index c902482..465cf45 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,9 @@ "ajv": "^8.12.0", "async-cache-dedupe": "^1.12.0", "classnames": "^2.3.2", + "dompurify": "^3.4.12", + "highlight.js": "^11.11.1", + "marked": "^12.0.2", "prom-client": "^15.1.3" } } diff --git a/src/app.postcss b/src/app.postcss index 44b538a..575eadf 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -26,3 +26,150 @@ color: #a7a7ef !important; opacity: 1 !important; } + +/* Rendered markdown in agent chat replies. Tailwind base strips list/heading + styles, so restore just what model output needs. */ +.chat-md > :first-child { + margin-top: 0; +} +.chat-md p { + margin: 0.4rem 0; +} +.chat-md ul, +.chat-md ol { + margin: 0.4rem 0; + padding-left: 1.25rem; +} +.chat-md ul { + list-style: disc; +} +.chat-md ol { + list-style: decimal; +} +.chat-md li { + margin: 0.15rem 0; +} +.chat-md h1, +.chat-md h2, +.chat-md h3, +.chat-md h4 { + margin: 0.6rem 0 0.3rem; + font-weight: 600; + line-height: 1.25; +} +.chat-md h1 { + font-size: 1.1rem; +} +.chat-md h2 { + font-size: 1.05rem; +} +.chat-md h3, +.chat-md h4 { + font-size: 1rem; +} +.chat-md a { + color: #2563eb; + text-decoration: underline; +} +:is(.dark .chat-md) a { + color: #93c5fd; +} +.chat-md blockquote { + margin: 0.4rem 0; + padding-left: 0.75rem; + border-left: 3px solid #d1d5db; + color: #6b7280; +} +:is(.dark .chat-md) blockquote { + border-left-color: #4b5563; + color: #9ca3af; +} +/* Inline code */ +.chat-md code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.85em; + background: var(--hl-bg); + padding: 0.1rem 0.3rem; + border-radius: 0.25rem; +} +/* Fenced code blocks: the
owns the surface; the inner is bare. */
+.chat-md pre {
+ margin: 0.5rem 0;
+ padding: 0.65rem 0.75rem;
+ overflow-x: auto;
+ border-radius: 0.375rem;
+ background: var(--hl-bg);
+}
+.chat-md pre code {
+ padding: 0;
+ background: none;
+ font-size: 0.8rem;
+ line-height: 1.45;
+}
+
+/* highlight.js palette as CSS variables so a single set of scope rules serves
+ both themes (github light / github-dark). */
+.chat-md {
+ --hl-bg: #f6f8fa;
+ --hl-fg: #24292e;
+ --hl-comment: #6a737d;
+ --hl-keyword: #d73a49;
+ --hl-string: #032f62;
+ --hl-number: #005cc5;
+ --hl-title: #6f42c1;
+ --hl-type: #e36209;
+ --hl-attr: #005cc5;
+}
+:is(.dark .chat-md) {
+ --hl-bg: #161b22;
+ --hl-fg: #c9d1d9;
+ --hl-comment: #8b949e;
+ --hl-keyword: #ff7b72;
+ --hl-string: #a5d6ff;
+ --hl-number: #79c0ff;
+ --hl-title: #d2a8ff;
+ --hl-type: #ffa657;
+ --hl-attr: #79c0ff;
+}
+.chat-md .hljs {
+ color: var(--hl-fg);
+ background: none;
+}
+.chat-md .hljs-comment,
+.chat-md .hljs-quote {
+ color: var(--hl-comment);
+ font-style: italic;
+}
+.chat-md .hljs-keyword,
+.chat-md .hljs-selector-tag,
+.chat-md .hljs-literal,
+.chat-md .hljs-doctag {
+ color: var(--hl-keyword);
+}
+.chat-md .hljs-string,
+.chat-md .hljs-regexp,
+.chat-md .hljs-addition {
+ color: var(--hl-string);
+}
+.chat-md .hljs-number,
+.chat-md .hljs-built_in,
+.chat-md .hljs-bullet {
+ color: var(--hl-number);
+}
+.chat-md .hljs-title,
+.chat-md .hljs-section,
+.chat-md .hljs-function .hljs-title {
+ color: var(--hl-title);
+}
+.chat-md .hljs-type,
+.chat-md .hljs-class .hljs-title,
+.chat-md .hljs-tag,
+.chat-md .hljs-symbol {
+ color: var(--hl-type);
+}
+.chat-md .hljs-attr,
+.chat-md .hljs-attribute,
+.chat-md .hljs-variable,
+.chat-md .hljs-name {
+ color: var(--hl-attr);
+}
diff --git a/src/lib/components/agentChat.svelte b/src/lib/components/agentChat.svelte
index b743821..dec6f51 100644
--- a/src/lib/components/agentChat.svelte
+++ b/src/lib/components/agentChat.svelte
@@ -1,6 +1,8 @@
');
+ expect(html).not.toContain('onerror');
+ expect(html).not.toContain('