update frontend
This commit is contained in:
+41
-8
@@ -1,5 +1,14 @@
|
|||||||
const API = '';
|
const API = '';
|
||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
|
let sendTime = 0;
|
||||||
|
|
||||||
|
// Configure marked
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
headerIds: false,
|
||||||
|
mangle: false,
|
||||||
|
});
|
||||||
|
|
||||||
// DOM
|
// DOM
|
||||||
const sessionList = document.getElementById('session-list');
|
const sessionList = document.getElementById('session-list');
|
||||||
@@ -97,14 +106,17 @@ async function sendMessage() {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
btnSend.disabled = true;
|
btnSend.disabled = true;
|
||||||
|
|
||||||
|
sendTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api(`/api/sessions/${currentSession.id}/messages`, {
|
const data = await api(`/api/sessions/${currentSession.id}/messages`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ content: text }),
|
body: JSON.stringify({ content: text }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const elapsed = ((performance.now() - sendTime) / 1000).toFixed(1);
|
||||||
typingEl.remove();
|
typingEl.remove();
|
||||||
appendMessage('assistant', data.content, data.sources);
|
appendMessage('assistant', data.content, data.sources, elapsed);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
loadSessions();
|
loadSessions();
|
||||||
} catch (e) {
|
} 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
|
// Remove empty state if present
|
||||||
const empty = messages.querySelector('.empty-state');
|
const empty = messages.querySelector('.empty-state');
|
||||||
if (empty) empty.remove();
|
if (empty) empty.remove();
|
||||||
@@ -125,14 +145,27 @@ function appendMessage(role, content, sources = null) {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `message ${role}`;
|
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) {
|
if (sources && sources.length > 0) {
|
||||||
html += `<div class="sources">`;
|
const chips = sources.map(s =>
|
||||||
sources.forEach(s => {
|
`<a class="source-chip" href="${esc(s.url)}" target="_blank" title="${esc(s.title)}">${esc(s.title.substring(0, 40))}</a>`
|
||||||
html += `<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>`);
|
||||||
html += `</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;
|
div.innerHTML = html;
|
||||||
|
|||||||
+89
-1
@@ -190,10 +190,98 @@ body {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
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 {
|
.sources {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Blog RAG Chat</title>
|
<title>Blog RAG Chat</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
|
|||||||
Reference in New Issue
Block a user