diff --git a/charactergarden/app/src/db.ts b/charactergarden/app/src/db.ts index 24cf1fd..a7bcfdc 100644 --- a/charactergarden/app/src/db.ts +++ b/charactergarden/app/src/db.ts @@ -262,6 +262,7 @@ export function createDatabase(config: DatabaseConfig): CharacterGardenDatabase DELETE FROM world_states; DELETE FROM turns; DELETE FROM entities; + DELETE FROM rulebooks; `); }, diff --git a/charactergarden/app/src/tests/sqliteProbe.ts b/charactergarden/app/src/tests/sqliteProbe.ts new file mode 100644 index 0000000..e42f247 --- /dev/null +++ b/charactergarden/app/src/tests/sqliteProbe.ts @@ -0,0 +1,6 @@ +import Database from "better-sqlite3"; + +const db = new Database(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db"); +db.prepare("CREATE TABLE IF NOT EXISTS probe(value TEXT)").run(); +db.close(); +console.log("sqlite_probe_ok"); diff --git a/charactergarden/app/src/tests/verifyDefaultSeed.ts b/charactergarden/app/src/tests/verifyDefaultSeed.ts new file mode 100644 index 0000000..6dcceb8 --- /dev/null +++ b/charactergarden/app/src/tests/verifyDefaultSeed.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; + +import { createCharacterGardenApp } from "../app"; + +function run(): void { + const app = createCharacterGardenApp(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db"); + const snapshot = app.getSnapshot(); + const rulebook = app.getRulebook(); + + assert.equal(snapshot.turns.length, 0, "Expected no persisted turns after database reset."); + assert.ok(snapshot.worldState.entities.player, "Expected player in default world state."); + assert.ok(snapshot.worldState.entities.room_start, "Expected room_start in default world state."); + assert.equal(rulebook.id, "rulebook_default", "Expected default rulebook to be active."); + assert.equal(rulebook.version, 1, "Expected default rulebook version to be 1."); + + console.log( + JSON.stringify( + { + ok: true, + entityCount: Object.keys(snapshot.worldState.entities).length, + turnCount: snapshot.turns.length, + rulebookId: rulebook.id, + rulebookVersion: rulebook.version, + ruleCount: rulebook.rules.length, + }, + null, + 2 + ) + ); + + app.db.close(); +} + +run(); diff --git a/charactergarden/data/sqlite/test_write_probe.txt b/charactergarden/data/sqlite/test_write_probe.txt new file mode 100644 index 0000000..e69de29 diff --git a/charactergarden/docker-compose.yml b/charactergarden/docker-compose.yml index 211d3f3..b56a01b 100644 --- a/charactergarden/docker-compose.yml +++ b/charactergarden/docker-compose.yml @@ -7,10 +7,10 @@ services: environment: - APP_PORT=3000 - NODE_ENV=${NODE_ENV:-development} - - DB_PATH=/data/sqlite/app.db + - DB_PATH=/var/lib/charactergarden/app.db - OLLAMA_URL=${OLLAMA_URL:-http://ollama:11434} volumes: - - ./data:/data + - sqlite_data:/var/lib/charactergarden - ./app/src:/app/src frontend: @@ -38,3 +38,4 @@ services: volumes: ollama_data: + sqlite_data: diff --git a/charactergarden/frontend/src/App.tsx b/charactergarden/frontend/src/App.tsx index eef1cdb..e6b32b2 100644 --- a/charactergarden/frontend/src/App.tsx +++ b/charactergarden/frontend/src/App.tsx @@ -33,6 +33,11 @@ type Turn = { minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; selectedConfidence?: number; + candidates?: Array<{ + action: Action; + confidence: number; + rationale?: string; + }>; diagnostics: string[]; clarification?: { reasonCode: string; @@ -70,6 +75,11 @@ type ProcessTurnResponse = { minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; selectedConfidence?: number; + candidates: Array<{ + action: Action; + confidence: number; + rationale?: string; + }>; diagnostics: string[]; clarification?: { reasonCode: string; @@ -109,6 +119,13 @@ type SceneRulebook = { updatedAt: number; }; +type RulebookListItem = { + id: string; + name: string; + version: number; + updatedAt: number; +}; + const starterPrompts = [ "look around", "take key", @@ -145,7 +162,7 @@ async function fetchJson(input: RequestInfo, init?: RequestInit): Promise // Rulebook editor component // --------------------------------------------------------------------------- -function RulebookEditor() { +function RulebookEditor(props: { onSaved?: (rulebook: SceneRulebook) => void }) { const [rulebook, setRulebook] = useState(null); const [drafts, setDrafts] = useState>({}); const [parseErrors, setParseErrors] = useState>({}); @@ -216,6 +233,7 @@ function RulebookEditor() { body: JSON.stringify({ ...rulebook, rules: updatedRules }), }); setRulebook(updated); + props.onSaved?.(updated); setSaved(true); setTimeout(() => setSaved(false), 2500); } catch (err) { @@ -327,10 +345,21 @@ export default function App() { const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [tab, setTab] = useState<"world" | "rulebook">("world"); + const [activeRulebook, setActiveRulebook] = useState(null); + const [rulebooks, setRulebooks] = useState([]); + + async function refreshRulebookState() { + const [active, list] = await Promise.all([ + fetchJson("/api/rulebook"), + fetchJson("/api/rulebooks"), + ]); + setActiveRulebook(active); + setRulebooks(list); + } useEffect(() => { - void fetchJson("/api/state") - .then((data) => { + void Promise.all([fetchJson("/api/state"), refreshRulebookState()]) + .then(([data]) => { setSnapshot(data); setLoading(false); }) @@ -352,7 +381,10 @@ export default function App() { body: JSON.stringify({ input }), }); setLatest(result); - const nextSnapshot = await fetchJson("/api/state"); + const [nextSnapshot] = await Promise.all([ + fetchJson("/api/state"), + refreshRulebookState(), + ]); setSnapshot(nextSnapshot); } catch (submitError) { setError(submitError instanceof Error ? submitError.message : "Unknown error"); @@ -371,6 +403,7 @@ export default function App() { }); setSnapshot(result); setLatest(null); + await refreshRulebookState(); } catch (resetError) { setError(resetError instanceof Error ? resetError.message : "Unknown error"); } @@ -475,6 +508,25 @@ export default function App() { Diagnostics: {latest.interpreter.diagnostics.join(" | ")}

) : null} + {latest.actions.length > 0 ? ( +
+ Selected actions ({latest.actions.length}) +
{JSON.stringify(latest.actions, null, 2)}
+
+ ) : null} + {latest.interpreter.candidates.length > 0 ? ( +
+ Interpreter candidates ({latest.interpreter.candidates.length}) +
    + {latest.interpreter.candidates.map((candidate, index) => ( +
  • + {candidate.action.type} at {formatConfidence(candidate.confidence)} + {candidate.rationale ? ` - ${candidate.rationale}` : ""} +
  • + ))} +
+
+ ) : null} ) : null} @@ -504,6 +556,14 @@ export default function App() {

World state

{loading && !snapshot ?

Loading...

: null} + {snapshot ? ( +
+

World ID: {snapshot.worldState.id}

+

Created: {formatTurnTime(snapshot.worldState.createdAt)}

+

Domain: {String(snapshot.worldState.metadata?.domain ?? "unknown")}

+

World schema: {String(snapshot.worldState.metadata?.version ?? "unknown")}

+
+ ) : null}
    {entities.map((entity) => (
  • @@ -544,11 +604,35 @@ export default function App() { ))}
+ +
+

System

+ {activeRulebook ? ( +
+

Active rulebook: {activeRulebook.name}

+

Rulebook ID: {activeRulebook.id}

+

Rulebook version: {activeRulebook.version}

+

Updated: {formatTurnTime(activeRulebook.updatedAt)}

+

Saved rulebooks: {rulebooks.length}

+
+ ) : ( +

Loading rulebook info...

+ )} + {rulebooks.length > 0 ? ( +
    + {rulebooks.map((rb) => ( +
  • + {rb.name} ({rb.id}) v{rb.version} - updated {formatTurnTime(rb.updatedAt)} +
  • + ))} +
+ ) : null} +
) : (

Rulebook editor

- + setActiveRulebook(rulebook)} />
)} diff --git a/charactergarden/frontend/src/styles.css b/charactergarden/frontend/src/styles.css index aa45bf0..8116846 100644 --- a/charactergarden/frontend/src/styles.css +++ b/charactergarden/frontend/src/styles.css @@ -161,6 +161,19 @@ pre { padding: 8px 10px; } +.meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px 12px; + margin-bottom: 10px; +} + +.meta-kv { + margin: 0; + font-size: 0.86rem; + color: rgba(244, 239, 228, 0.86); +} + .error-banner { margin-top: 16px; color: #ffd2b8; @@ -375,6 +388,10 @@ pre { .panel { padding: 14px; } + + .meta-grid { + grid-template-columns: 1fr; + } } .turn-time {