// ZovOS AI — Frontend Application (function() { 'use strict'; // --- State --- let ws = null; let sessionId = 'default'; let isStreaming = false; let currentAssistantMsg = null; // --- DOM Elements --- const chatContainer = document.getElementById('chatContainer'); const messagesEl = document.getElementById('messages'); const welcomeScreen = document.getElementById('welcomeScreen'); const messageInput = document.getElementById('messageInput'); const btnSend = document.getElementById('btnSend'); const btnNewChat = document.getElementById('btnNewChat'); const providerSelect = document.getElementById('providerSelect'); const statusIndicator = document.getElementById('statusIndicator'); const statusText = statusIndicator.querySelector('span'); // --- WebSocket --- function connectWebSocket() { const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = `${protocol}//${location.host}/ws`; ws = new WebSocket(url); ws.onopen = () => { setStatus('ready', 'Подключён'); console.log('[WS] Connected'); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); handleWSMessage(msg); }; ws.onclose = () => { setStatus('error', 'Отключён'); console.log('[WS] Disconnected, reconnecting in 3s...'); setTimeout(connectWebSocket, 3000); }; ws.onerror = (err) => { console.error('[WS] Error:', err); setStatus('error', 'Ошибка соединения'); }; } function handleWSMessage(msg) { switch (msg.type) { case 'stream': hideWelcome(); if (!currentAssistantMsg) { currentAssistantMsg = addMessage('assistant', ''); } appendToMessage(currentAssistantMsg, msg.content); scrollToBottom(); break; case 'tool_call': hideWelcome(); if (!currentAssistantMsg) { currentAssistantMsg = addMessage('assistant', ''); } addToolCall(currentAssistantMsg, msg.tool_name, msg.tool_args, msg.tool_id); scrollToBottom(); break; case 'tool_result': if (currentAssistantMsg) { addToolResult(currentAssistantMsg, msg.tool_name, msg.content, msg.tool_id); scrollToBottom(); } break; case 'done': isStreaming = false; if (currentAssistantMsg) { renderMarkdown(currentAssistantMsg); } currentAssistantMsg = null; setStatus('ready', 'Готов'); btnSend.disabled = false; messageInput.focus(); break; case 'error': isStreaming = false; currentAssistantMsg = null; addMessage('assistant', `⚠️ Ошибка: ${msg.content}`); setStatus('error', 'Ошибка'); btnSend.disabled = false; setTimeout(() => setStatus('ready', 'Готов'), 3000); break; } } // --- Messages --- function addMessage(role, content) { hideWelcome(); const msgEl = document.createElement('div'); msgEl.className = `message ${role}`; const avatar = document.createElement('div'); avatar.className = 'message-avatar'; avatar.textContent = role === 'user' ? '👤' : '🤖'; const body = document.createElement('div'); body.className = 'message-body'; const contentEl = document.createElement('div'); contentEl.className = 'message-content'; contentEl.textContent = content; body.appendChild(contentEl); msgEl.appendChild(avatar); msgEl.appendChild(body); messagesEl.appendChild(msgEl); scrollToBottom(); return contentEl; } function appendToMessage(contentEl, text) { contentEl.textContent += text; } function renderMarkdown(contentEl) { const raw = contentEl.textContent; if (typeof marked !== 'undefined') { contentEl.innerHTML = marked.parse(raw); } } function addToolCall(contentEl, name, args, id) { const parent = contentEl.closest('.message-body'); const el = document.createElement('div'); el.className = 'tool-call'; el.id = `tool-${id}`; let argsDisplay = args; try { argsDisplay = JSON.stringify(JSON.parse(args), null, 2); } catch(e) {} el.innerHTML = `
${escapeHtml(name)}
${escapeHtml(argsDisplay)}
`; parent.appendChild(el); } function addToolResult(contentEl, name, result, id) { const parent = contentEl.closest('.message-body'); const el = document.createElement('div'); el.className = 'tool-result'; const truncated = result.length > 500 ? result.substring(0, 500) + '\n... (truncated)' : result; el.textContent = truncated; parent.appendChild(el); } // --- Actions --- function sendMessage() { const content = messageInput.value.trim(); if (!content || isStreaming || !ws || ws.readyState !== WebSocket.OPEN) return; addMessage('user', content); messageInput.value = ''; autoResize(); isStreaming = true; currentAssistantMsg = null; btnSend.disabled = true; setStatus('busy', 'Думаю...'); ws.send(JSON.stringify({ type: 'message', content: content, session_id: sessionId, })); } function newChat() { sessionId = 'chat-' + Date.now(); messagesEl.innerHTML = ''; welcomeScreen.style.display = 'flex'; currentAssistantMsg = null; isStreaming = false; btnSend.disabled = false; // Clear on server if (ws && ws.readyState === WebSocket.OPEN) { fetch('/api/session/clear', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({session_id: sessionId}), }); } } // --- Provider --- async function loadProviders() { try { const resp = await fetch('/api/providers'); const data = await resp.json(); providerSelect.innerHTML = ''; const icons = {ollama: '🦙', openai: '🧠', anthropic: '🟣', google: '🔵'}; data.providers.forEach(p => { const opt = document.createElement('option'); opt.value = p; opt.textContent = `${icons[p] || '🤖'} ${p.charAt(0).toUpperCase() + p.slice(1)}`; if (p === data.active) opt.selected = true; providerSelect.appendChild(opt); }); } catch(e) { console.error('Failed to load providers:', e); } } async function switchProvider(name) { try { await fetch('/api/provider', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({provider: name}), }); } catch(e) { console.error('Failed to switch provider:', e); } } // --- Helpers --- function hideWelcome() { if (welcomeScreen) welcomeScreen.style.display = 'none'; } function scrollToBottom() { requestAnimationFrame(() => { chatContainer.scrollTop = chatContainer.scrollHeight; }); } function setStatus(type, text) { statusIndicator.className = 'status-indicator ' + type; statusText.textContent = text; } function autoResize() { messageInput.style.height = 'auto'; messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px'; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // --- Events --- btnSend.addEventListener('click', sendMessage); btnNewChat.addEventListener('click', newChat); providerSelect.addEventListener('change', (e) => switchProvider(e.target.value)); messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); messageInput.addEventListener('input', autoResize); // --- Init --- connectWebSocket(); loadProviders(); messageInput.focus(); })();