From be5b156abcd9f785da46e499338c4a1decb0217a Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Sun, 14 Jun 2026 17:04:17 +0700 Subject: [PATCH] update frontend --- static/app.js | 49 ++++++++++++++++++++---- static/style.css | 90 +++++++++++++++++++++++++++++++++++++++++++- templates/index.html | 1 + 3 files changed, 131 insertions(+), 9 deletions(-) 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 += `
`; - sources.forEach(s => { - html += `${esc(s.title.substring(0, 40))}`; - }); - 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 +