const BACKEND_URL = "https://text-adventure.spacek.blue"; const MODEL = 'gemma4:26b'; // Default model for generation and analysis const chatContainer = document.getElementById('chat-container'); const userInput = document.getElementById('user-input'); const loreArea = document.getElementById('lore'); const charactersArea = document.getElementById('characters'); const plotArea = document.getElementById('plot'); const statusElement = document.getElementById('status-message'); const worldGenArea = document.getElementById('world-gen'); async function pollStatus() { try { const response = await fetch(`${BACKEND_URL}/api/status`); const data = await response.json(); statusElement.textContent = `Status: ${data.status}`; statusElement.className = `status-message status-${data.status}`; } catch (err) { console.error("Failed to fetch status:", err); } } setInterval(pollStatus, 1000); function formatStateForDisplay(state) { return { lore: state.lore || "", characters: (state.characters || []).map(c => `${c.name}: ${c.description}, possessions: ${(c.possessions || []).join(', ')}`).join("\n"), plots: (state.plot_plans || []).map(p => `${p.description} (relevance: ${p.relevance_timer})`).join("\n") }; } async function loadState() { try { const response = await fetch(`${BACKEND_URL}/api/state`); const state = await response.json(); const formatted = formatStateForDisplay(state); loreArea.value = formatted.lore; charactersArea.value = formatted.characters; plotArea.value = formatted.plots; chatContainer.innerHTML = ""; if (state.history) { state.history.forEach(msg => { const div = document.createElement('div'); div.className = 'message'; if (msg.startsWith("User:")) { div.className += ' user-message'; div.textContent = msg; } else if (msg.startsWith("AI:")) { div.className += ' ai-message'; const content = msg.substring(4).trim(); div.innerHTML = `AI: ${marked.parse(content)}`; } else { div.textContent = msg; } chatContainer.appendChild(div); }); } // Generate suggestions after loading state to ensure they are ready for the user generateSuggestions(); } catch (err) { console.error("Failed to load state from backend:", err); } } function appendMessage(msg) { const div = document.createElement('div'); div.className = 'message'; if (msg.startsWith("User:")) { div.className += ' user-message'; } else if (msg.startsWith("AI:")) { div.className += ' ai-message'; } if (msg.startsWith("AI:")) { const content = msg.substring(4).trim(); div.innerHTML = `AI: ${marked.parse(content)}`; } else { div.textContent = msg; } chatContainer.appendChild(div); chatContainer.scrollTop = chatContainer.scrollHeight; } async function sendTurn() { const text = userInput.value.trim(); if (!text) return; userInput.value = ""; appendMessage(`User: ${text}`); try { const response = await fetch(`${BACKEND_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: text, model: MODEL, stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let aiResponseText = ""; appendMessage(`AI: ${aiResponseText}`); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const json = JSON.parse(line); if (json.response) { const content = json.response; aiResponseText += content; // Find the message element that corresponds to this specific AI response stream. // We look for the latest message that has not been fully updated yet, // or we create a new one if none exist. const messages = chatContainer.querySelectorAll('.message'); let targetMessage = null; // Iterate backwards to find the most recent AI message that we are currently streaming into for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].classList.contains('ai-message')) { targetMessage = messages[i]; break; } } if (targetMessage) { targetMessage.innerHTML = `AI: ${marked.parse(aiResponseText)}`; } else { appendMessage(`AI: ${aiResponseText}`); } } } catch (e) { // Partial JSON or other issues } } } // Refresh state after generation and background update // Increased delay to ensure backend has finished writing to disk setTimeout(loadState, 3000); } catch (err) { console.error("Error during generation:", err); } } async function generateSuggestions() { const suggestionsContainer = document.getElementById('suggestions-container'); suggestionsContainer.innerHTML = "Generating suggestions..."; try { const stateResponse = await fetch(`${BACKEND_URL}/api/state`); const state = await stateResponse.json(); const history = state.history || []; const response = await fetch(`${BACKEND_URL}/api/suggest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ history: history, model: MODEL }) }); const data = await response.json(); const suggestions = data.suggestions; suggestionsContainer.innerHTML = ""; suggestions.forEach(suggestion => { const btn = document.createElement('button'); btn.textContent = suggestion; btn.className = 'suggestion-button'; btn.onclick = () => { userInput.value = suggestion; suggestionsContainer.innerHTML = ""; }; suggestionsContainer.appendChild(btn); }); } catch (err) { console.error("Error generating suggestions:", err); suggestionsContainer.innerHTML = "Failed to load suggestions."; } } async function updateState() { const parseCharacters = (text) => { return text.split('\n').map(line => { const match = line.match(/^(.+?):\s*(.+?),\s*possessions:\s*(.+)$/); if (match) { const [, name, description, possessionsStr] = match; const possessions = possessionsStr.split(',').map(p => p.trim()).filter(p => p); return { name: name.trim(), description: description.trim(), possessions }; } return null; }).filter(Boolean); }; const parsePlotPlans = (text) => { return text.split('\n').map(line => { const match = line.match(/^(.+?)\s*\(relevance:\s*(\d+)\)$/); if (match) { const [, description, relevance_timer] = match; return { description: description.trim(), relevance_timer: parseInt(relevance_timer) }; } return null; }).filter(Boolean); }; const newState = { lore: loreArea.value, characters: parseCharacters(charactersArea.value), plot_plans: parsePlotPlans(plotArea.value), history: [] }; try { const response = await fetch(`${BACKEND_URL}/api/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newState) }); if (response.ok) { alert("World state updated successfully!"); loadState(); } else { alert("Failed to update world state."); } } catch (err) { console.error("Error updating state:", err); alert("Error updating state."); } } async function generateWorld() { const suggestions = worldGenArea.value.trim(); if (!suggestions) { alert("Please enter some ideas for the world first!"); return; } if (!confirm("This will overwrite the current world state. Are you sure?")) { return; } statusElement.textContent = "Status: Generating World..."; try { const response = await fetch(`${BACKEND_URL}/api/generate-world`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suggestions: suggestions, model: MODEL }) }); if (!response.ok) { throw new Error('World generation failed'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let aiResponseText = ""; // We create a placeholder for the streaming intro const div = document.createElement('div'); div.className = 'message ai-message'; div.innerHTML = `AI: `; chatContainer.appendChild(div); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const json = JSON.parse(line); if (json.response) { aiResponseText += json.response; div.innerHTML = `AI: ${marked.parse(aiResponseText)}`; chatContainer.scrollTop = chatContainer.scrollHeight; } } catch (e) { // Partial JSON } } } // Refresh state after generation setTimeout(loadState, 3000); } catch (err) { console.error("Error during world generation:", err); alert("Error during world generation. Check console for details."); } } // Initial load loadState(); // Periodic sync of text areas from backend setInterval(async () => { try { const response = await fetch(`${BACKEND_URL}/api/state`); const state = await response.json(); const formatted = formatStateForDisplay(state); if (loreArea.value !== formatted.lore) { loreArea.value = formatted.lore; } if (charactersArea.value !== formatted.characters) { charactersArea.value = formatted.characters; } if (plotArea.value !== formatted.plots) { plotArea.value = formatted.plots; } } catch (err) { // Silently fail } }, 2000);