import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm"; import { getAll, putItem } from "./db.js"; import { SensoryDiceRoller } from "./dice.js"; import { searchChunks } from "./rag.js"; async function callHuggingFaceGM(systemPrompt, userAction) { // MUST use the direct subdomain host (NOT huggingface.co/spaces/...) const SPACE_URL = "https://dippergl231-myrpg.hf.space"; // 1. Send action to Gradio queue const res = await fetch(`${SPACE_URL}/gradio_api/call/predict`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: [systemPrompt, userAction] }) }); if (!res.ok) { throw new Error(`HF Space returned status ${res.status}`); } const { event_id } = await res.json(); // 2. Receive stream response return new Promise((resolve, reject) => { const eventSource = new EventSource(`${SPACE_URL}/gradio_api/call/predict/${event_id}`); eventSource.onmessage = (e) => { const data = JSON.parse(e.data); if (data[0]) { eventSource.close(); resolve(data[0]); } }; eventSource.onerror = (err) => { eventSource.close(); reject(err); }; }); } // Global App State let hfClient = null; let rulebookChunks = []; let currentPointsMax = 27; const defaultStats = { "D&D 5e": { STR: 8, DEX: 8, CON: 8, INT: 8, WIS: 8, CHA: 8 }, "Pathfinder 2e": { STR: 10, DEX: 10, CON: 10, INT: 10, WIS: 10, CHA: 10 }, "GURPS": { ST: 10, DX: 10, IQ: 10, HT: 10 } }; let activeCharacter = { id: 'char_default', name: 'Hero', system: 'D&D 5e', stats: { ...defaultStats["D&D 5e"] } }; let activePrefab = { id: 'prefab_default', title: 'Dark Fantasy Campaign', tone: 'Gritty, tactical, immersive', supplements: ['Xanathar\'s Guide'] }; // Connect to HF Space API async function getClient() { if (!hfClient) { document.getElementById('gm-status').innerText = 'GM: Connecting...'; hfClient = await Client.connect("Dippergl231/MYRPG"); document.getElementById('gm-status').innerText = 'GM: Ready'; } return hfClient; } // Tab Navigation Setup document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); btn.classList.add('active'); document.getElementById(btn.dataset.tab).classList.add('active'); }); }); // Character Point Buy Calculator & UI function renderStats() { const container = document.getElementById('stats-container'); container.innerHTML = ''; let currentCost = 0; Object.entries(activeCharacter.stats).forEach(([stat, val]) => { if (activeCharacter.system === 'GURPS') { currentCost += (val - 10) * 10; } else { currentCost += (val - 8); } const box = document.createElement('div'); box.className = 'stat-box'; box.innerHTML = ` ${stat}: ${val}
`; container.appendChild(box); }); const remaining = currentPointsMax - currentCost; document.getElementById('point-buy-counter').innerText = `Remaining: ${remaining} pts`; container.querySelectorAll('.btn-up').forEach(b => { b.onclick = () => { activeCharacter.stats[b.dataset.stat] += 1; renderStats(); }; }); container.querySelectorAll('.btn-down').forEach(b => { b.onclick = () => { activeCharacter.stats[b.dataset.stat] -= 1; renderStats(); }; }); } document.getElementById('char-system').addEventListener('change', (e) => { activeCharacter.system = e.target.value; currentPointsMax = activeCharacter.system === 'GURPS' ? 100 : 27; activeCharacter.stats = { ...defaultStats[activeCharacter.system] }; renderStats(); }); // Dice Roller Integration const diceRoller = new SensoryDiceRoller((val) => { document.getElementById('dice-result-display').innerText = `d20: ${val}`; const input = document.getElementById('user-input'); input.value += ` [Test result: ${val}]`; document.getElementById('btn-roll-dice').innerText = '🎲 Shake/Move Mouse to Roll d20'; }); document.getElementById('btn-roll-dice').onclick = () => { document.getElementById('btn-roll-dice').innerText = 'Sampling motion... Shake now!'; diceRoller.startSequence(); }; // Lorebook Management document.getElementById('btn-add-lore').onclick = async () => { const tag = document.getElementById('lore-tag').value; const content = document.getElementById('lore-content').value; if (!content) return; const item = { id: 'lore_' + Date.now(), tag, content }; await putItem('lorebook', item); renderLoreList(); document.getElementById('lore-content').value = ''; }; async function renderLoreList() { const list = await getAll('lorebook'); const container = document.getElementById('lore-list'); container.innerHTML = ''; list.forEach(item => { const card = document.createElement('div'); card.className = 'lore-card'; card.innerHTML = `${item.tag}

${item.content}

`; container.appendChild(card); }); } // Rulebook Upload (.txt RAG) document.getElementById('file-rulebook').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { const text = evt.target.result; rulebookChunks = text.split('\n\n').filter(c => c.trim().length > 20); alert(`Rulebook loaded! Split into ${rulebookChunks.length} search chunks.`); }; reader.readAsText(file); }); // Main Action Dispatch to HF Space Engine document.getElementById('btn-send').onclick = async () => { const userInput = document.getElementById('user-input'); const userAction = userInput.value.trim(); if (!userAction) return; // Add message to chat display const chatArea = document.getElementById('chat-history'); chatArea.innerHTML += `
Player: ${userAction}
`; userInput.value = ''; chatArea.scrollTop = chatArea.scrollHeight; // Build System Prompt Context const loreList = await getAll('lorebook'); const recentLore = loreList.slice(-5).map(l => `[${l.tag}]: ${l.content}`).join('\n'); const matchedRules = searchChunks(userAction, rulebookChunks, 2).join('\n---\n'); const systemPrompt = ` You are an expert RPG Game Master running a game in ${activeCharacter.system}. Campaign Prefab: ${activePrefab.title} DM Tone: ${activePrefab.tone} Active Supplements: ${activePrefab.supplements.join(', ')} ACTIVE CHARACTER: Name: ${activeCharacter.name} Stats: ${JSON.stringify(activeCharacter.stats)} RECENT LOREBOOK MEMORY: ${recentLore} RELEVANT RULE EXCERPTS: ${matchedRules} Follow rules strictly. Describe outcomes vividly. `.trim(); document.getElementById('gm-status').innerText = 'GM: Thinking...'; try { const client = await getClient(); const result = await client.predict("/predict", [systemPrompt, userAction]); const gmReply = result.data[0]; chatArea.innerHTML += `
Game Master: ${gmReply}
`; chatArea.scrollTop = chatArea.scrollHeight; } catch (err) { chatArea.innerHTML += `
System: Engine waking up or timed out. Please try sending again in a few seconds.
`; } finally { document.getElementById('gm-status').innerText = 'GM: Ready'; } }; // Initial Setup renderStats(); renderLoreList();