import { sessionId, currentPersona, dom } from './state.js'; import { parseImagePromptFromContent, copyToClipboard } from './utils.js'; export async function initChat() { if (!sessionId || !currentPersona) return; const res = await fetch('/chat/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: '', session_id: sessionId, persona_id: currentPersona }), }); if (!res.ok) return; const data = await res.json(); if (data.first_mes) addMessage('assistant', data.first_mes); } export function updateEmptyState() { const hasMessages = dom.messagesEl.querySelector('.message'); dom.emptyState?.classList.toggle('hidden', !!hasMessages); } export function createImagePromptBlock(promptText) { const block = document.createElement('div'); block.className = 'image-prompt-block'; const header = document.createElement('div'); header.className = 'image-prompt-header'; header.innerHTML = '🎨 SD prompt'; const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'copy-prompt-btn'; copyBtn.textContent = 'Копировать'; copyBtn.addEventListener('click', async () => { const ok = await copyToClipboard(promptText); copyBtn.textContent = ok ? 'Скопировано' : 'Ошибка'; setTimeout(() => { copyBtn.textContent = 'Копировать'; }, 1500); }); header.appendChild(copyBtn); const genBtn = document.createElement('button'); genBtn.type = 'button'; genBtn.className = 'gen-image-btn'; genBtn.textContent = '🖼 Генерировать'; genBtn.addEventListener('click', () => generateImageViaA1111(promptText, block)); header.appendChild(genBtn); const textEl = document.createElement('span'); textEl.className = 'prompt-text'; textEl.textContent = promptText; block.appendChild(header); block.appendChild(textEl); return block; } function renderChoices(wrapper, choices) { if (!choices || !choices.length) return; const row = document.createElement('div'); row.className = 'choice-row'; for (const c of choices) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'choice-btn'; btn.textContent = c.label; btn.addEventListener('click', () => { dom.inputEl.value = c.label; dom.inputEl.focus(); }); row.appendChild(btn); } wrapper.appendChild(row); } function renderResolution(wrapper, resolution) { if (!resolution?.text) return; const block = document.createElement('div'); block.className = 'resolution-block'; block.innerHTML = `
Resolution (d20=${resolution.roll}, ${resolution.outcome})
`; block.querySelector('.resolution-text').textContent = resolution.text; wrapper.appendChild(block); } function renderDebugBlocks(wrapper, blocks) { if (!blocks || !blocks.length) return; for (const b of blocks) { if (!b?.text) continue; if (b.type === 'global_plot') { addMessage('assistant', `--- Global plot ---\n${b.text}\n---`); } else if (b.type === 'facts') { addMessage('assistant', b.text); } else if (b.type === 'status_quo') { addMessage('assistant', b.text); } else { addMessage('assistant', b.text); } } } async function generateImageViaA1111(promptText, block) { block.parentElement.querySelector('.chat-image')?.remove(); block.parentElement.querySelector('.image-error')?.remove(); try { const res = await fetch('/images/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, prompt: promptText }), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || res.statusText); const img = document.createElement('img'); img.className = 'chat-image'; img.src = data.image_path; block.parentElement.appendChild(img); } catch (e) { const err = document.createElement('div'); err.className = 'image-error'; err.textContent = '🖼 ' + e.message; block.parentElement.appendChild(err); } } export function appendChatImage(wrapper, imagePath) { if (!imagePath) return; const img = document.createElement('img'); img.className = 'chat-image'; img.src = imagePath; wrapper.appendChild(img); } export function addMessage(role, content = '', imagePrompt = null, imagePath = null) { updateEmptyState(); const wrapper = document.createElement('div'); wrapper.className = `message ${role}`; const label = document.createElement('div'); label.className = 'label'; label.textContent = role === 'user' ? 'Вы' : 'AI'; wrapper.appendChild(label); let displayContent = content; let prompt = imagePrompt; if (role === 'assistant' && !prompt) { const parsed = parseImagePromptFromContent(content); displayContent = parsed.text; prompt = parsed.prompt; } const bubble = document.createElement('div'); bubble.className = 'bubble'; bubble.textContent = displayContent; wrapper.appendChild(bubble); if (role === 'assistant') { const translateBtn = document.createElement('button'); translateBtn.type = 'button'; translateBtn.className = 'translate-btn'; translateBtn.textContent = '🌐 RU'; let originalText = null; translateBtn.addEventListener('click', async () => { if (originalText !== null) { bubble.textContent = originalText; originalText = null; translateBtn.textContent = '🌐 RU'; return; } originalText = bubble.textContent; translateBtn.disabled = true; translateBtn.textContent = '…'; try { const res = await fetch('/translate/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: originalText }), }); if (!res.ok) throw new Error(res.statusText); const data = await res.json(); bubble.textContent = data.translated; translateBtn.textContent = '↩ Оригинал'; } catch { originalText = null; translateBtn.textContent = '⚠️'; setTimeout(() => { translateBtn.textContent = '🌐 RU'; }, 2000); } translateBtn.disabled = false; }); wrapper.appendChild(translateBtn); } if (prompt) wrapper.appendChild(createImagePromptBlock(prompt)); if (imagePath) appendChatImage(wrapper, imagePath); dom.messagesEl.appendChild(wrapper); dom.messagesEl.scrollTop = dom.messagesEl.scrollHeight; return bubble; } export function showTyping() { const typing = document.createElement('div'); typing.className = 'typing'; typing.id = 'typing'; typing.innerHTML = ''; dom.messagesEl.appendChild(typing); dom.messagesEl.scrollTop = dom.messagesEl.scrollHeight; } export function removeTyping() { document.getElementById('typing')?.remove(); } export function clearMessages() { dom.messagesEl.innerHTML = ''; if (dom.emptyState) { dom.messagesEl.appendChild(dom.emptyState); dom.emptyState.classList.remove('hidden'); } } export async function sendMessage() { const text = dom.inputEl.value.trim(); if (!text || !sessionId) return; dom.inputEl.value = ''; dom.inputEl.style.height = 'auto'; dom.sendBtn.disabled = true; addMessage('user', text); showTyping(); try { const res = await fetch('/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, session_id: sessionId, persona_id: currentPersona }), }); if (!res.ok) throw new Error('Ошибка сервера: ' + res.status); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let bubble = null; removeTyping(); while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const data = JSON.parse(line.slice(6)); if (data.chunk !== undefined) { if (!bubble) { bubble = addMessage('assistant', ''); bubble.classList.add('typing-active'); } bubble.textContent += data.chunk; bubble.textContent = bubble.textContent.replace(/\[IMAGE_PROMPT:.*?\]/gs, '').trim(); dom.messagesEl.scrollTop = dom.messagesEl.scrollHeight; } if (data.done) { bubble?.classList.remove('typing-active'); if (data.image_prompt && bubble) { bubble.parentElement.appendChild(createImagePromptBlock(data.image_prompt)); } if (data.image_path && bubble) { appendChatImage(bubble.parentElement, data.image_path); } if (data.image_error && bubble) { const err = document.createElement('div'); err.className = 'image-error'; err.textContent = '🖼 ' + data.image_error; bubble.parentElement.appendChild(err); } if (data.choices && bubble) { renderChoices(bubble.parentElement, data.choices); } if (data.resolution && bubble) { // show resolution under the last user message (best-effort: attach near assistant response) renderResolution(bubble.parentElement, data.resolution); } if (data.debug) { renderDebugBlocks(bubble?.parentElement || dom.messagesEl, data.debug); } const { loadSessions } = await import('./sessions.js'); loadSessions(); } } catch { /* skip */ } } } } catch (err) { removeTyping(); addMessage('assistant', '⚠️ Ошибка: ' + err.message); } finally { dom.sendBtn.disabled = false; dom.inputEl.focus(); } } export async function clearHistory() { if (!sessionId) return; await fetch(`/chat/${sessionId}`, { method: 'DELETE' }); clearMessages(); }