From fca69d3cb55ab9f898ab6a56ab0c0881824aa700 Mon Sep 17 00:00:00 2001 From: spencer Date: Sun, 26 Apr 2026 14:15:46 -0400 Subject: [PATCH] feat: enhance turn display with time and diagnostics styling --- charactergarden/frontend/src/App.tsx | 94 ++++++++++++++++++++++--- charactergarden/frontend/src/styles.css | 11 +++ 2 files changed, 94 insertions(+), 11 deletions(-) diff --git a/charactergarden/frontend/src/App.tsx b/charactergarden/frontend/src/App.tsx index e6c4ff6..eef1cdb 100644 --- a/charactergarden/frontend/src/App.tsx +++ b/charactergarden/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useEffect, useState } from "react"; +import { FormEvent, useEffect, useMemo, useState } from "react"; type Entity = { id: string; @@ -28,7 +28,22 @@ type Turn = { validation: ValidationResult[]; createdAt: number; interpreter?: { + interpreterVersion: string; + resolutionSource: "deterministic" | "llm" | "hybrid"; + minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; + selectedConfidence?: number; + diagnostics: string[]; + clarification?: { + reasonCode: string; + question: string; + field?: string; + options?: Array<{ + id: string; + label: string; + value: string; + }>; + }; }; }; @@ -51,12 +66,15 @@ type ProcessTurnResponse = { worldState: WorldState; interpreter: { interpreterVersion: string; + resolutionSource: "deterministic" | "llm" | "hybrid"; + minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; selectedConfidence?: number; diagnostics: string[]; clarification?: { reasonCode: string; question: string; + field?: string; options?: Array<{ id: string; label: string; @@ -94,11 +112,27 @@ type SceneRulebook = { const starterPrompts = [ "look around", "take key", + "take lantern", "give key to groundskeeper", + "introduce jeff", + "describe groundskeeper as patient", "open door", "move to exit", ]; +function formatConfidence(value: number | undefined): string { + if (typeof value !== "number") return "n/a"; + return `${Math.round(value * 100)}%`; +} + +function formatTurnTime(epochMs: number): string { + try { + return new Date(epochMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + } catch { + return String(epochMs); + } +} + async function fetchJson(input: RequestInfo, init?: RequestInit): Promise { const response = await fetch(input, init); if (!response.ok) { @@ -198,6 +232,9 @@ function RulebookEditor() {

{rulebook.name}

+

+ Version {rulebook.version} | Updated {formatTurnTime(rulebook.updatedAt)} +

{rulebook.description ? (

{rulebook.description}

) : null} @@ -340,6 +377,15 @@ export default function App() { } const entities = snapshot ? Object.values(snapshot.worldState.entities) : []; + const turns = useMemo(() => snapshot?.turns.slice().reverse() ?? [], [snapshot]); + + function applyClarificationOption(optionValue: string) { + setInput((prev) => { + const trimmed = prev.trim(); + if (!trimmed) return optionValue; + return `${trimmed} ${optionValue}`; + }); + } return (
@@ -382,20 +428,32 @@ export default function App() {

Input: {latest.rawText}

Interpreter: {latest.interpreter.status} - {typeof latest.interpreter.selectedConfidence === "number" - ? ` (${Math.round(latest.interpreter.selectedConfidence * 100)}% confidence)` - : ""} + {` via ${latest.interpreter.resolutionSource}`} + {` | model threshold ${formatConfidence(latest.interpreter.minConfidence)}`} + {` | selected ${formatConfidence(latest.interpreter.selectedConfidence)}`} +

+

+ Interpreter version: {latest.interpreter.interpreterVersion}

{latest.interpreter.clarification ? (

- Clarification: {latest.interpreter.clarification.question} + Clarification ({latest.interpreter.clarification.reasonCode}):{" "} + {latest.interpreter.clarification.question}

) : null} {latest.interpreter.clarification?.options?.length ? ( -

- Options:{" "} - {latest.interpreter.clarification.options.map((o) => o.label).join(", ")} -

+
+ {latest.interpreter.clarification.options.map((o) => ( + + ))} +
) : null}
    {latest.validation.map((v) => ( @@ -459,11 +517,25 @@ export default function App() {

    Turn log

      - {snapshot?.turns.slice().reverse().map((turn) => ( + {turns.map((turn) => (
    • {turn.rawText} + at {formatTurnTime(turn.createdAt)} {turn.interpreter ? ( - [interp:{turn.interpreter.status}] + + {" "}[interp:{turn.interpreter.status} via {turn.interpreter.resolutionSource}; + conf {formatConfidence(turn.interpreter.selectedConfidence)}] + + ) : null} + {turn.interpreter?.clarification ? ( +

      + Clarification ({turn.interpreter.clarification.reasonCode}): {turn.interpreter.clarification.question} +

      + ) : null} + {turn.interpreter?.diagnostics?.length ? ( +

      + Diagnostics: {turn.interpreter.diagnostics.join(" | ")} +

      ) : null} {turn.validation.map((v) => ( [{v.success ? "ok" : v.reason}] diff --git a/charactergarden/frontend/src/styles.css b/charactergarden/frontend/src/styles.css index 0c26e5b..aa45bf0 100644 --- a/charactergarden/frontend/src/styles.css +++ b/charactergarden/frontend/src/styles.css @@ -375,4 +375,15 @@ pre { .panel { padding: 14px; } +} + +.turn-time { + opacity: 0.7; + font-size: 0.82rem; +} + +.turn-diagnostics { + margin: 6px 0 0; + color: rgba(244, 239, 228, 0.74); + font-size: 0.82rem; } \ No newline at end of file