myrpg / app.js
Dippergl231's picture
Update app.js
2c9817c verified
Raw
History Blame Contribute Delete
7.72 kB
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();