268 lines
7.6 KiB
HTML
268 lines
7.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Local RAG Chat</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: "SF Mono", "Consolas", "Monaco", monospace;
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background: #0f0f12;
|
|
color: #e4e4e7;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
header {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid #27272a;
|
|
background: #18181b;
|
|
}
|
|
header h1 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
header p {
|
|
margin: 0.25rem 0 0;
|
|
font-size: 0.75rem;
|
|
color: #71717a;
|
|
}
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
.msg {
|
|
max-width: 85%;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
.msg.user, .msg.error {
|
|
white-space: pre-wrap;
|
|
}
|
|
.msg.assistant .markdown-body {
|
|
white-space: normal;
|
|
}
|
|
.msg.assistant .markdown-body h1, .msg.assistant .markdown-body h2, .msg.assistant .markdown-body h3 {
|
|
margin: 0.75em 0 0.35em;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
}
|
|
.msg.assistant .markdown-body h1:first-child, .msg.assistant .markdown-body h2:first-child, .msg.assistant .markdown-body h3:first-child { margin-top: 0; }
|
|
.msg.assistant .markdown-body p { margin: 0.5em 0; }
|
|
.msg.assistant .markdown-body p:first-child { margin-top: 0; }
|
|
.msg.assistant .markdown-body p:last-child { margin-bottom: 0; }
|
|
.msg.assistant .markdown-body pre {
|
|
margin: 0.5em 0;
|
|
padding: 0.6rem;
|
|
background: #18181b;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
font-size: 0.85em;
|
|
}
|
|
.msg.assistant .markdown-body code {
|
|
background: #18181b;
|
|
padding: 0.15em 0.35em;
|
|
border-radius: 4px;
|
|
font-size: 0.9em;
|
|
}
|
|
.msg.assistant .markdown-body pre code {
|
|
padding: 0;
|
|
background: none;
|
|
}
|
|
.msg.assistant .markdown-body ul, .msg.assistant .markdown-body ol { margin: 0.5em 0; padding-left: 1.4em; }
|
|
.msg.assistant .markdown-body li { margin: 0.25em 0; }
|
|
.msg.assistant .markdown-body a { color: #60a5fa; text-decoration: none; }
|
|
.msg.assistant .markdown-body a:hover { text-decoration: underline; }
|
|
.msg.user {
|
|
align-self: flex-end;
|
|
background: #3f3f46;
|
|
color: #fafafa;
|
|
}
|
|
.msg.assistant {
|
|
align-self: flex-start;
|
|
background: #27272a;
|
|
border: 1px solid #3f3f46;
|
|
}
|
|
.msg.error {
|
|
background: #451a1a;
|
|
border: 1px solid #7f1d1d;
|
|
color: #fecaca;
|
|
}
|
|
.msg .label {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #71717a;
|
|
margin-bottom: 0.5rem;
|
|
display: block;
|
|
}
|
|
#input-area {
|
|
padding: 1rem 1.5rem 1.5rem;
|
|
border-top: 1px solid #27272a;
|
|
background: #18181b;
|
|
}
|
|
#input-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: flex-end;
|
|
}
|
|
#input {
|
|
flex: 1;
|
|
min-height: 44px;
|
|
max-height: 160px;
|
|
padding: 0.6rem 1rem;
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
color: #e4e4e7;
|
|
background: #27272a;
|
|
border: 1px solid #3f3f46;
|
|
border-radius: 8px;
|
|
resize: none;
|
|
outline: none;
|
|
}
|
|
#input::placeholder { color: #71717a; }
|
|
#input:focus { border-color: #52525b; }
|
|
#send {
|
|
padding: 0.6rem 1.2rem;
|
|
font: inherit;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
color: #0f0f12;
|
|
background: #e4e4e7;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
#send:hover { background: #fafafa; }
|
|
#send:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
.loading {
|
|
align-self: flex-start;
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.8rem;
|
|
color: #71717a;
|
|
}
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Local RAG Chat</h1>
|
|
<p>Ask questions about your documents. Answers are generated from the vector store + Ollama / OpenAI.</p>
|
|
</header>
|
|
|
|
<div id="messages"></div>
|
|
|
|
<div id="input-area">
|
|
<div id="input-row">
|
|
<textarea id="input" rows="1" placeholder="Ask a question…" autofocus></textarea>
|
|
<button type="button" id="send">Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const messagesEl = document.getElementById('messages');
|
|
const inputEl = document.getElementById('input');
|
|
const sendBtn = document.getElementById('send');
|
|
const chatHistory = [];
|
|
|
|
function appendMessage(role, text, isError = false) {
|
|
text = text ?? '';
|
|
const div = document.createElement('div');
|
|
div.className = 'msg ' + (isError ? 'error' : role);
|
|
const label = role === 'user' ? 'You' : 'RAG';
|
|
let body;
|
|
if (role === 'assistant' && !isError) {
|
|
const rawHtml = marked.parse(text, { gfm: true, breaks: true });
|
|
body = '<div class="markdown-body">' + DOMPurify.sanitize(rawHtml) + '</div>';
|
|
} else {
|
|
body = escapeHtml(text);
|
|
}
|
|
div.innerHTML = '<span class="label">' + label + '</span>' + body;
|
|
messagesEl.appendChild(div);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
chatHistory.push({ role: role, content: text });
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
const div = document.createElement('div');
|
|
div.textContent = s;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function setLoading(show) {
|
|
if (show) {
|
|
const el = document.createElement('div');
|
|
el.className = 'loading';
|
|
el.id = 'loading-dot';
|
|
el.textContent = 'Thinking…';
|
|
messagesEl.appendChild(el);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
} else {
|
|
const el = document.getElementById('loading-dot');
|
|
if (el) el.remove();
|
|
}
|
|
sendBtn.disabled = show;
|
|
}
|
|
|
|
async function send() {
|
|
const text = inputEl.value.trim();
|
|
if (!text) return;
|
|
inputEl.value = '';
|
|
appendMessage('user', text);
|
|
|
|
setLoading(true);
|
|
try {
|
|
const history = chatHistory.slice(0, -1);
|
|
const res = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message: text, history: history })
|
|
});
|
|
const data = await res.json();
|
|
setLoading(false);
|
|
if (data.retrieved && data.retrieved.length > 0) {
|
|
console.groupCollapsed('[RAG] Retrieved ' + data.retrieved.length + ' chunk(s)');
|
|
data.retrieved.forEach(function(chunk, i) {
|
|
console.group('Chunk ' + (i + 1) + ' | ' + (chunk.source || '') + (chunk.page != null ? ' p.' + chunk.page : ''));
|
|
console.log(chunk.content);
|
|
console.groupEnd();
|
|
});
|
|
console.groupEnd();
|
|
}
|
|
if (data.error) {
|
|
appendMessage('assistant', data.error, true);
|
|
} else {
|
|
appendMessage('assistant', (data.answer || '(No response)').trim());
|
|
}
|
|
} catch (err) {
|
|
setLoading(false);
|
|
appendMessage('assistant', 'Request failed: ' + err.message, true);
|
|
}
|
|
}
|
|
|
|
sendBtn.addEventListener('click', send);
|
|
inputEl.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
send();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|