localRAG/templates/chat.html

222 lines
5.8 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;
white-space: pre-wrap;
word-break: break-word;
}
.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>
</head>
<body>
<header>
<h1>Local RAG Chat</h1>
<p>Ask questions about your documents. Answers are generated from the vector store + Ollama.</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) {
const div = document.createElement('div');
div.className = 'msg ' + (isError ? 'error' : role);
const label = role === 'user' ? 'You' : 'RAG';
div.innerHTML = '<span class="label">' + label + '</span>' + escapeHtml(text);
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>