Files
notes/static/js/chat.js
SimolZimol 939cc13689 new file: .dockerignore
new file:   .env.example
	new file:   Dockerfile
	new file:   app.py
	new file:   blueprints/__init__.py
	new file:   blueprints/auth.py
	new file:   blueprints/chat.py
	new file:   blueprints/context.py
	new file:   blueprints/documents.py
	new file:   blueprints/main.py
	new file:   config.py
	new file:   docker-compose.yml
	new file:   models/__init__.py
	new file:   models/chat_session.py
	new file:   models/document.py
	new file:   models/user.py
	new file:   requirements.txt
	new file:   services/__init__.py
	new file:   services/document_parser.py
	new file:   services/llm_service.py
	new file:   services/rag_service.py
	new file:   services/url_scraper.py
	new file:   static/css/style.css
	new file:   static/js/chat.js
	new file:   static/js/inline_chat.js
	new file:   static/js/main.js
	new file:   templates/base.html
	new file:   templates/document_view.html
	new file:   templates/index.html
	new file:   templates/login.html
	new file:   templates/register.html
2026-05-22 16:03:50 +02:00

116 lines
4.0 KiB
JavaScript

// ── Chat UI controller ────────────────────────────────────────────────────────
const $ = (sel) => document.querySelector(sel);
export class Chat {
constructor() {
this._container = $('#messages');
this._emptyState = $('#empty-state');
}
clear() {
this._container.innerHTML = '';
this._emptyState?.classList.remove('hidden');
}
renderHistory(messages) {
this._container.innerHTML = '';
this._emptyState?.classList.add('hidden');
messages.forEach((m) => this._appendBubble(m.role, m.content));
this._scrollBottom();
}
async send(sessionId, message, contextIds = []) {
// Immediately render user bubble
this._appendBubble('user', message);
this._emptyState?.classList.add('hidden');
// Typing indicator
const typingId = this._appendTyping();
const res = await fetch(`/api/chat/sessions/${sessionId}/ask`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, context_ids: contextIds }),
});
this._removeTyping(typingId);
const data = await res.json();
if (!res.ok) {
this._appendBubble('assistant', `Error: ${data.error || 'Unknown error'}`, true);
} else {
this._appendBubble('assistant', data.reply);
}
this._scrollBottom();
}
_appendBubble(role, content, isError = false) {
const wrap = document.createElement('div');
wrap.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} gap-3`;
if (role === 'assistant') {
wrap.innerHTML = `
<div class="w-7 h-7 rounded-full bg-copilot-accent flex items-center justify-center shrink-0 mt-0.5">
<svg class="w-4 h-4 text-copilot-bg" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
</svg>
</div>
<div class="max-w-[75%] rounded-2xl rounded-tl-sm px-4 py-3 text-sm leading-relaxed
${isError ? 'bg-copilot-danger text-copilot-bg' : 'bg-copilot-panel text-copilot-text'}
shadow-sm whitespace-pre-wrap">${escapeHtml(content)}</div>`;
} else {
wrap.innerHTML = `
<div class="max-w-[75%] rounded-2xl rounded-tr-sm px-4 py-3 text-sm leading-relaxed
bg-copilot-user text-copilot-text shadow-sm whitespace-pre-wrap">
${escapeHtml(content)}
</div>
<div class="w-7 h-7 rounded-full bg-copilot-muted flex items-center justify-center shrink-0 mt-0.5 text-xs font-bold text-copilot-bg">
U
</div>`;
}
this._container.appendChild(wrap);
return wrap;
}
_appendTyping() {
const id = `typing-${Date.now()}`;
const div = document.createElement('div');
div.id = id;
div.className = 'flex justify-start gap-3';
div.innerHTML = `
<div class="w-7 h-7 rounded-full bg-copilot-accent flex items-center justify-center shrink-0 mt-0.5">
<svg class="w-4 h-4 text-copilot-bg" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
</svg>
</div>
<div class="bg-copilot-panel rounded-2xl rounded-tl-sm px-4 py-3 shadow-sm flex gap-1 items-center">
<span class="w-2 h-2 rounded-full bg-copilot-muted animate-bounce" style="animation-delay:0ms"></span>
<span class="w-2 h-2 rounded-full bg-copilot-muted animate-bounce" style="animation-delay:150ms"></span>
<span class="w-2 h-2 rounded-full bg-copilot-muted animate-bounce" style="animation-delay:300ms"></span>
</div>`;
this._container.appendChild(div);
this._scrollBottom();
return id;
}
_removeTyping(id) {
document.getElementById(id)?.remove();
}
_scrollBottom() {
this._container.scrollTop = this._container.scrollHeight;
}
}
function escapeHtml(str = '') {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}