/* static/script.js */ function chatApp() { return { // ------------------------------ // State // ------------------------------ messages: [], // {id, role, content} userInput: '', loading: false, dark: false, // dark‑mode flag // ------------------------------ // Init – runs once when component mounts // ------------------------------ init() { // Detect system dark‑mode preference this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches; this.applyDarkMode(); // Listen for changes (e.g., user switches OS theme) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { this.dark = e.matches; this.applyDarkMode(); }); }, // ------------------------------ // Dark‑mode helper // ------------------------------ toggleDark() { this.dark = !this.dark; this.applyDarkMode(); }, applyDarkMode() { if (this.dark) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }, // ------------------------------ // Auto‑grow textarea // ------------------------------ autoResize(event) { const el = event.target; el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }, // ------------------------------ // Send a message to the back‑end // ------------------------------ async sendMessage() { const content = this.userInput.trim(); if (!content) return; // 1️⃣ Add user bubble const userMsg = { id: Date.now(), role: 'user', content: this.escapeHtml(content).replace(/\n/g, '
') }; this.messages.push(userMsg); this.userInput = ''; this.scrollToBottom(); // 2️⃣ Show loading indicator this.loading = true; // 3️⃣ Call the OpenAI‑compatible endpoint try { const response = await fetch('/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'qwen2.5-1.5b-instruct', messages: [ // Keep the whole conversation – the model can handle a few turns ...this.messages.map(m => ({ role: m.role, content: this.stripHtml(m.content) // plain text for the model })) ], temperature: 0.7, max_tokens: 512, top_p: 0.9, stream: false // set true later if you implement SSE streaming }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.detail || 'API error'); } const data = await response.json(); const assistant = data.choices[0].message.content; // 4️⃣ Add assistant bubble const assistantMsg = { id: Date.now() + 1, role: 'assistant', content: this.escapeHtml(assistant).replace(/\n/g, '
') }; this.messages.push(assistantMsg); this.scrollToBottom(); } catch (e) { console.error(e); const errMsg = { id: Date.now() + 1, role: 'assistant', content: `⚠️ ${this.escapeHtml(e.message)}` }; this.messages.push(errMsg); this.scrollToBottom(); } finally { this.loading = false; } }, // ------------------------------ // Helpers // ------------------------------ scrollToBottom() { this.$nextTick(() => { const chat = document.getElementById('chat-window'); chat.scrollTop = chat.scrollHeight; }); }, // Escape HTML to avoid XSS when we later render raw strings escapeHtml(str) { const div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; }, // Strip HTML tags – used when sending the prompt to the model stripHtml(html) { const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; } }; }