update frontend
This commit is contained in:
+41
-8
@@ -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 = `<div class="bubble">${esc(content)}</div>`;
|
||||
let html = '';
|
||||
|
||||
if (role === 'assistant') {
|
||||
html += `<div class="bubble">${renderMarkdown(content)}</div>`;
|
||||
} else {
|
||||
html += `<div class="bubble">${esc(content)}</div>`;
|
||||
}
|
||||
|
||||
// Sources + timing
|
||||
const metaParts = [];
|
||||
if (sources && sources.length > 0) {
|
||||
html += `<div class="sources">`;
|
||||
sources.forEach(s => {
|
||||
html += `<a class="source-chip" href="${esc(s.url)}" target="_blank" title="${esc(s.title)}">${esc(s.title.substring(0, 40))}</a>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
const chips = sources.map(s =>
|
||||
`<a class="source-chip" href="${esc(s.url)}" target="_blank" title="${esc(s.title)}">${esc(s.title.substring(0, 40))}</a>`
|
||||
).join('');
|
||||
metaParts.push(`<div class="sources">${chips}</div>`);
|
||||
}
|
||||
if (elapsed !== null && role === 'assistant') {
|
||||
metaParts.push(`<div class="msg-meta"><span class="timing">⚡ ${elapsed}s</span></div>`);
|
||||
}
|
||||
if (metaParts.length) {
|
||||
html += metaParts.join('');
|
||||
}
|
||||
|
||||
div.innerHTML = html;
|
||||
|
||||
+89
-1
@@ -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;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blog RAG Chat</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
|
||||
Reference in New Issue
Block a user