feat: enhance chat widget with markdown rendering and style updates

- Integrated dynamic loading of markdown rendering for chat responses.
- Implemented a fallback for markdown rendering to ensure consistent display.
- Updated CSS variables for improved theming and visual consistency.
- Enhanced chat bubble and input styles for better user experience.
- Added new styles for markdown content in chat bubbles, including code blocks and lists.
This commit is contained in:
Buns Enchantress
2026-02-03 02:30:56 -06:00
parent 332d9a2ad1
commit 09d32e629e

View File

@@ -3,26 +3,51 @@
const apiBase = window.DOCS_CHAT_API_URL || "http://localhost:3001";
// Load @create-markdown/preview for markdown rendering
let markdownToHTML = null;
import("https://esm.sh/@create-markdown/preview@0.1.0")
.then((mod) => {
markdownToHTML = mod.markdownToHTML;
})
.catch((err) => console.warn("Failed to load create-markdown:", err));
// Markdown renderer with fallback before module loads
const renderMarkdown = (text) => {
if (markdownToHTML) {
return markdownToHTML(text, { sanitize: true, linkTarget: "_blank" });
}
// Fallback: escape HTML and preserve newlines
return text
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "<br>");
};
const style = document.createElement("style");
style.textContent = `
#docs-chat-root { position: fixed; right: 20px; bottom: 20px; z-index: 9999; font-family: inherit; }
#docs-chat-root { position: fixed; right: 20px; bottom: 20px; z-index: 9999; font-family: var(--font-body, system-ui, -apple-system, sans-serif); }
#docs-chat-root.docs-chat-expanded { right: 0; bottom: 0; }
:root {
--docs-chat-accent: #FF5A36;
--docs-chat-text: #121212;
--docs-chat-muted: #4b4b4b;
--docs-chat-panel: rgba(255, 255, 255, 0.78);
--docs-chat-panel-border: rgba(17, 17, 17, 0.08);
--docs-chat-surface: rgba(255, 255, 255, 0.6);
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.18);
--docs-chat-accent: var(--accent, #FF5A36);
--docs-chat-text: #1a1a1a;
--docs-chat-muted: #555;
--docs-chat-panel: rgba(255, 255, 255, 0.92);
--docs-chat-panel-border: rgba(0, 0, 0, 0.1);
--docs-chat-surface: rgba(250, 250, 250, 0.95);
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.15);
--docs-chat-code-bg: rgba(0, 0, 0, 0.05);
--docs-chat-assistant-bg: #f5f5f5;
}
html[data-theme="dark"] {
--docs-chat-text: #ececec;
--docs-chat-muted: #b7b7b7;
--docs-chat-panel: rgba(20, 20, 20, 0.78);
--docs-chat-panel-border: rgba(255, 255, 255, 0.1);
--docs-chat-surface: rgba(24, 24, 24, 0.65);
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.45);
--docs-chat-text: #e8e8e8;
--docs-chat-muted: #aaa;
--docs-chat-panel: rgba(28, 28, 30, 0.95);
--docs-chat-panel-border: rgba(255, 255, 255, 0.12);
--docs-chat-surface: rgba(38, 38, 40, 0.95);
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.5);
--docs-chat-code-bg: rgba(255, 255, 255, 0.08);
--docs-chat-assistant-bg: #2a2a2c;
}
#docs-chat-button {
display: inline-flex;
@@ -37,8 +62,9 @@ html[data-theme="dark"] {
box-shadow: 0 8px 30px rgba(255,90,54, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
font-family: var(--font-pixel, var(--font-body, system-ui, sans-serif));
}
#docs-chat-button span { font-weight: 600; letter-spacing: 0.2px; }
#docs-chat-button span { font-weight: 600; letter-spacing: 0.04em; font-size: 14px; }
.docs-chat-logo { width: 20px; height: 20px; }
#docs-chat-panel {
width: 360px;
@@ -62,13 +88,15 @@ html[data-theme="dark"] {
#docs-chat-header {
padding: 12px 14px;
font-weight: 600;
font-family: var(--font-pixel, var(--font-body, system-ui, sans-serif));
letter-spacing: 0.03em;
border-bottom: 1px solid var(--docs-chat-panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
#docs-chat-header-title { display: inline-flex; align-items: center; gap: 8px; }
#docs-chat-header-title span { color: var(--docs-chat-text); }
#docs-chat-header-title span { color: var(--docs-chat-text); font-size: 15px; }
#docs-chat-header-actions { display: inline-flex; align-items: center; gap: 6px; }
.docs-chat-icon-button {
border: 1px solid var(--docs-chat-panel-border);
@@ -97,40 +125,117 @@ html[data-theme="dark"] {
border: 1px solid var(--docs-chat-panel-border);
border-radius: 10px;
padding: 9px 10px;
font-size: 13px;
font-size: 14px;
line-height: 1.5;
font-family: inherit;
color: var(--docs-chat-text);
background: rgba(255,255,255,0.7);
background: var(--docs-chat-surface);
min-height: 42px;
max-height: 120px;
overflow-y: auto;
}
html[data-theme="dark"] #docs-chat-input textarea { background: rgba(15,15,15,0.7); }
#docs-chat-input textarea::placeholder { color: var(--docs-chat-muted); }
#docs-chat-send {
background: var(--docs-chat-accent);
color: #fff;
border: none;
border-radius: 10px;
padding: 8px 12px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
font-family: inherit;
font-size: 14px;
transition: opacity 0.15s ease;
}
#docs-chat-send:hover { opacity: 0.9; }
#docs-chat-send:active { opacity: 0.8; }
.docs-chat-bubble {
margin-bottom: 10px;
padding: 9px 12px;
padding: 10px 14px;
border-radius: 12px;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
max-width: 90%;
font-size: 14px;
line-height: 1.6;
max-width: 92%;
}
.docs-chat-user {
background: var(--docs-chat-accent);
color: #fff;
align-self: flex-end;
white-space: pre-wrap;
margin-left: auto;
}
.docs-chat-assistant {
background: rgba(255, 255, 255, 0.7);
background: var(--docs-chat-assistant-bg);
color: var(--docs-chat-text);
border: 1px solid var(--docs-chat-panel-border);
}
html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7); }
/* Markdown content styling for chat bubbles */
.docs-chat-assistant p { margin: 0 0 10px 0; }
.docs-chat-assistant p:last-child { margin-bottom: 0; }
.docs-chat-assistant code {
background: var(--docs-chat-code-bg);
padding: 2px 6px;
border-radius: 5px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.9em;
}
.docs-chat-assistant pre {
background: var(--docs-chat-code-bg);
padding: 12px 14px;
border-radius: 8px;
overflow-x: auto;
margin: 10px 0;
font-size: 0.9em;
}
.docs-chat-assistant pre code {
background: transparent;
padding: 0;
font-size: inherit;
}
.docs-chat-assistant a {
color: var(--docs-chat-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.docs-chat-assistant a:hover { opacity: 0.8; }
.docs-chat-assistant ul {
margin: 8px 0;
padding-left: 22px;
list-style-type: disc;
}
.docs-chat-assistant ol {
margin: 8px 0;
padding-left: 22px;
list-style-type: decimal;
}
.docs-chat-assistant li {
margin: 4px 0;
display: list-item;
}
.docs-chat-assistant strong { font-weight: 600; }
.docs-chat-assistant em { font-style: italic; }
.docs-chat-assistant h1, .docs-chat-assistant h2, .docs-chat-assistant h3 {
font-weight: 600;
margin: 12px 0 6px 0;
line-height: 1.3;
}
.docs-chat-assistant h1 { font-size: 1.2em; }
.docs-chat-assistant h2 { font-size: 1.1em; }
.docs-chat-assistant h3 { font-size: 1.05em; }
.docs-chat-assistant blockquote {
border-left: 3px solid var(--docs-chat-accent);
margin: 10px 0;
padding: 4px 12px;
color: var(--docs-chat-muted);
background: var(--docs-chat-code-bg);
border-radius: 0 6px 6px 0;
}
.docs-chat-assistant hr {
border: none;
height: 1px;
background: var(--docs-chat-panel-border);
margin: 12px 0;
}
`;
document.head.appendChild(style);
@@ -182,8 +287,16 @@ html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7)
const inputWrap = document.createElement("div");
inputWrap.id = "docs-chat-input";
const textarea = document.createElement("textarea");
textarea.rows = 2;
textarea.rows = 1;
textarea.placeholder = "Ask about OpenClaw Docs...";
// Auto-expand textarea as user types (up to max-height set in CSS)
const autoExpand = () => {
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
};
textarea.addEventListener("input", autoExpand);
const send = document.createElement("button");
send.id = "docs-chat-send";
send.type = "button";
@@ -200,12 +313,16 @@ html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7)
root.appendChild(panel);
document.body.appendChild(root);
const addBubble = (text, role) => {
const addBubble = (text, role, isMarkdown = false) => {
const bubble = document.createElement("div");
bubble.className =
"docs-chat-bubble " +
(role === "user" ? "docs-chat-user" : "docs-chat-assistant");
bubble.textContent = text;
if (isMarkdown && role === "assistant") {
bubble.innerHTML = renderMarkdown(text);
} else {
bubble.textContent = text;
}
messages.appendChild(bubble);
messages.scrollTop = messages.scrollHeight;
return bubble;
@@ -242,9 +359,10 @@ html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7)
const text = textarea.value.trim();
if (!text) return;
textarea.value = "";
textarea.style.height = "auto"; // Reset height after sending
addBubble(text, "user");
const assistantBubble = addBubble("...", "assistant");
assistantBubble.textContent = "";
assistantBubble.innerHTML = "";
try {
const response = await fetch(`${apiBase}/chat`, {
@@ -253,7 +371,8 @@ html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7)
body: JSON.stringify({ message: text }),
});
if (!response.body) {
assistantBubble.textContent = await response.text();
const respText = await response.text();
assistantBubble.innerHTML = renderMarkdown(respText);
return;
}
const reader = response.body.getReader();
@@ -263,11 +382,12 @@ html[data-theme="dark"] .docs-chat-assistant { background: rgba(20, 20, 20, 0.7)
const { value, done } = await reader.read();
if (done) break;
fullText += decoder.decode(value, { stream: true });
assistantBubble.textContent = fullText;
// Re-render markdown on each chunk for live preview
assistantBubble.innerHTML = renderMarkdown(fullText);
messages.scrollTop = messages.scrollHeight;
}
} catch (err) {
assistantBubble.textContent = "Failed to reach docs chat API.";
assistantBubble.innerHTML = renderMarkdown("Failed to reach docs chat API.");
}
};