diff --git a/static/app.js b/static/app.js
index 3be4b3a..1bb5c16 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,5 +1,14 @@
const API = '';
let currentSession = null;
+let sendTime = 0;
+
+// Configure marked
+marked.setOptions({
+ breaks: true,
+ gfm: true,
+ headerIds: false,
+ mangle: false,
+});
// DOM
const sessionList = document.getElementById('session-list');
@@ -97,14 +106,17 @@ async function sendMessage() {
scrollToBottom();
btnSend.disabled = true;
+ sendTime = performance.now();
+
try {
const data = await api(`/api/sessions/${currentSession.id}/messages`, {
method: 'POST',
body: JSON.stringify({ content: text }),
});
+ const elapsed = ((performance.now() - sendTime) / 1000).toFixed(1);
typingEl.remove();
- appendMessage('assistant', data.content, data.sources);
+ appendMessage('assistant', data.content, data.sources, elapsed);
scrollToBottom();
loadSessions();
} catch (e) {
@@ -117,7 +129,15 @@ async function sendMessage() {
}
}
-function appendMessage(role, content, sources = null) {
+function renderMarkdown(text) {
+ try {
+ return marked.parse(text || '');
+ } catch {
+ return esc(text);
+ }
+}
+
+function appendMessage(role, content, sources = null, elapsed = null) {
// Remove empty state if present
const empty = messages.querySelector('.empty-state');
if (empty) empty.remove();
@@ -125,14 +145,27 @@ function appendMessage(role, content, sources = null) {
const div = document.createElement('div');
div.className = `message ${role}`;
- let html = `
${esc(content)}
`;
+ let html = '';
+ if (role === 'assistant') {
+ html += `${renderMarkdown(content)}
`;
+ } else {
+ html += `${esc(content)}
`;
+ }
+
+ // Sources + timing
+ const metaParts = [];
if (sources && sources.length > 0) {
- html += ``;
+ const chips = sources.map(s =>
+ `${esc(s.title.substring(0, 40))}`
+ ).join('');
+ metaParts.push(`${chips}
`);
+ }
+ if (elapsed !== null && role === 'assistant') {
+ metaParts.push(`⚡ ${elapsed}s
`);
+ }
+ if (metaParts.length) {
+ html += metaParts.join('');
}
div.innerHTML = html;
diff --git a/static/style.css b/static/style.css
index 5e476a6..6129103 100644
--- a/static/style.css
+++ b/static/style.css
@@ -190,10 +190,98 @@ body {
padding: 12px 16px;
font-size: 14px;
line-height: 1.7;
- white-space: pre-wrap;
word-break: break-word;
}
+/* Markdown content inside assistant bubble */
+.bubble h1, .bubble h2, .bubble h3, .bubble h4 {
+ margin: 12px 0 6px;
+ font-weight: 600;
+ line-height: 1.3;
+}
+.bubble h1 { font-size: 18px; }
+.bubble h2 { font-size: 16px; }
+.bubble h3 { font-size: 15px; }
+
+.bubble p { margin: 0 0 8px; }
+.bubble p:last-child { margin-bottom: 0; }
+
+.bubble ul, .bubble ol {
+ margin: 6px 0;
+ padding-left: 20px;
+}
+.bubble li { margin: 2px 0; }
+
+.bubble code {
+ background: #ffffff10;
+ padding: 1px 5px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-family: 'SF Mono', 'Consolas', 'Menlo', monospace;
+}
+
+.bubble pre {
+ background: #0a0a0a;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 10px 12px;
+ margin: 8px 0;
+ overflow-x: auto;
+}
+.bubble pre code {
+ background: none;
+ padding: 0;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.bubble blockquote {
+ border-left: 3px solid var(--accent);
+ margin: 8px 0;
+ padding: 4px 12px;
+ color: var(--text-dim);
+}
+
+.bubble a {
+ color: #60a5fa;
+ text-decoration: none;
+}
+.bubble a:hover { text-decoration: underline; }
+
+.bubble table {
+ border-collapse: collapse;
+ margin: 8px 0;
+ width: 100%;
+ font-size: 13px;
+}
+.bubble th, .bubble td {
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+ text-align: left;
+}
+.bubble th { background: #ffffff08; font-weight: 600; }
+
+.bubble strong { font-weight: 600; }
+.bubble em { font-style: italic; }
+
+.message.user .bubble {
+ white-space: pre-wrap;
+}
+
+.msg-meta {
+ font-size: 11px;
+ color: var(--text-muted);
+ padding: 0 4px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.msg-meta .timing {
+ color: var(--accent);
+ font-weight: 500;
+}
+
.sources {
display: flex;
flex-wrap: wrap;
diff --git a/templates/index.html b/templates/index.html
index 3683f67..1b2ae72 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -5,6 +5,7 @@
Blog RAG Chat
+