You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
12 KiB

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);

Powered by TurnKey Linux.