Spaces:
Running
Running
| 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 = ` | |
| <span>${stat}: <strong>${val}</strong></span> | |
| <div> | |
| <button class="stat-btn btn-down" data-stat="${stat}">-</button> | |
| <button class="stat-btn btn-up" data-stat="${stat}">+</button> | |
| </div> | |
| `; | |
| 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 = `<span class="lore-tag">${item.tag}</span><p>${item.content}</p>`; | |
| 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 += `<div class="msg user"><strong>Player:</strong> ${userAction}</div>`; | |
| 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 += `<div class="msg gm"><strong>Game Master:</strong> ${gmReply}</div>`; | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } catch (err) { | |
| chatArea.innerHTML += `<div class="msg gm"><strong>System:</strong> Engine waking up or timed out. Please try sending again in a few seconds.</div>`; | |
| } finally { | |
| document.getElementById('gm-status').innerText = 'GM: Ready'; | |
| } | |
| }; | |
| // Initial Setup | |
| renderStats(); | |
| renderLoreList(); |