feat: enhance turn display with time and diagnostics styling

This commit is contained in:
2026-04-26 14:15:46 -04:00
parent fc10e46ccc
commit fca69d3cb5
2 changed files with 94 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useMemo, useState } from "react";
type Entity = { type Entity = {
id: string; id: string;
@@ -28,7 +28,22 @@ type Turn = {
validation: ValidationResult[]; validation: ValidationResult[];
createdAt: number; createdAt: number;
interpreter?: { interpreter?: {
interpreterVersion: string;
resolutionSource: "deterministic" | "llm" | "hybrid";
minConfidence: number;
status: "resolved" | "needs_clarification" | "rejected"; 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; worldState: WorldState;
interpreter: { interpreter: {
interpreterVersion: string; interpreterVersion: string;
resolutionSource: "deterministic" | "llm" | "hybrid";
minConfidence: number;
status: "resolved" | "needs_clarification" | "rejected"; status: "resolved" | "needs_clarification" | "rejected";
selectedConfidence?: number; selectedConfidence?: number;
diagnostics: string[]; diagnostics: string[];
clarification?: { clarification?: {
reasonCode: string; reasonCode: string;
question: string; question: string;
field?: string;
options?: Array<{ options?: Array<{
id: string; id: string;
label: string; label: string;
@@ -94,11 +112,27 @@ type SceneRulebook = {
const starterPrompts = [ const starterPrompts = [
"look around", "look around",
"take key", "take key",
"take lantern",
"give key to groundskeeper", "give key to groundskeeper",
"introduce jeff",
"describe groundskeeper as patient",
"open door", "open door",
"move to exit", "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<T>(input: RequestInfo, init?: RequestInit): Promise<T> { async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
const response = await fetch(input, init); const response = await fetch(input, init);
if (!response.ok) { if (!response.ok) {
@@ -198,6 +232,9 @@ function RulebookEditor() {
<div className="rulebook-header"> <div className="rulebook-header">
<div> <div>
<p className="rulebook-name">{rulebook.name}</p> <p className="rulebook-name">{rulebook.name}</p>
<p className="rulebook-desc">
Version {rulebook.version} | Updated {formatTurnTime(rulebook.updatedAt)}
</p>
{rulebook.description ? ( {rulebook.description ? (
<p className="rulebook-desc">{rulebook.description}</p> <p className="rulebook-desc">{rulebook.description}</p>
) : null} ) : null}
@@ -340,6 +377,15 @@ export default function App() {
} }
const entities = snapshot ? Object.values(snapshot.worldState.entities) : []; 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 ( return (
<main className="page-shell"> <main className="page-shell">
@@ -382,20 +428,32 @@ export default function App() {
<p><strong>Input:</strong> {latest.rawText}</p> <p><strong>Input:</strong> {latest.rawText}</p>
<p> <p>
<strong>Interpreter:</strong> {latest.interpreter.status} <strong>Interpreter:</strong> {latest.interpreter.status}
{typeof latest.interpreter.selectedConfidence === "number" {` via ${latest.interpreter.resolutionSource}`}
? ` (${Math.round(latest.interpreter.selectedConfidence * 100)}% confidence)` {` | model threshold ${formatConfidence(latest.interpreter.minConfidence)}`}
: ""} {` | selected ${formatConfidence(latest.interpreter.selectedConfidence)}`}
</p>
<p>
<strong>Interpreter version:</strong> {latest.interpreter.interpreterVersion}
</p> </p>
{latest.interpreter.clarification ? ( {latest.interpreter.clarification ? (
<p> <p>
<strong>Clarification:</strong> {latest.interpreter.clarification.question} <strong>Clarification ({latest.interpreter.clarification.reasonCode}):</strong>{" "}
{latest.interpreter.clarification.question}
</p> </p>
) : null} ) : null}
{latest.interpreter.clarification?.options?.length ? ( {latest.interpreter.clarification?.options?.length ? (
<p> <div className="chips">
<strong>Options:</strong>{" "} {latest.interpreter.clarification.options.map((o) => (
{latest.interpreter.clarification.options.map((o) => o.label).join(", ")} <button
</p> key={o.id}
type="button"
className="chip"
onClick={() => applyClarificationOption(o.value)}
>
clarify: {o.label}
</button>
))}
</div>
) : null} ) : null}
<ul className="timeline-list compact"> <ul className="timeline-list compact">
{latest.validation.map((v) => ( {latest.validation.map((v) => (
@@ -459,11 +517,25 @@ export default function App() {
<article className="panel"> <article className="panel">
<h2>Turn log</h2> <h2>Turn log</h2>
<ul className="timeline-list"> <ul className="timeline-list">
{snapshot?.turns.slice().reverse().map((turn) => ( {turns.map((turn) => (
<li key={turn.id}> <li key={turn.id}>
<strong>{turn.rawText}</strong> <strong>{turn.rawText}</strong>
<span className="turn-time"> at {formatTurnTime(turn.createdAt)}</span>
{turn.interpreter ? ( {turn.interpreter ? (
<span> [interp:{turn.interpreter.status}]</span> <span>
{" "}[interp:{turn.interpreter.status} via {turn.interpreter.resolutionSource};
conf {formatConfidence(turn.interpreter.selectedConfidence)}]
</span>
) : null}
{turn.interpreter?.clarification ? (
<p className="parser-hint">
Clarification ({turn.interpreter.clarification.reasonCode}): {turn.interpreter.clarification.question}
</p>
) : null}
{turn.interpreter?.diagnostics?.length ? (
<p className="turn-diagnostics">
Diagnostics: {turn.interpreter.diagnostics.join(" | ")}
</p>
) : null} ) : null}
{turn.validation.map((v) => ( {turn.validation.map((v) => (
<span key={v.actionIndex}> [{v.success ? "ok" : v.reason}]</span> <span key={v.actionIndex}> [{v.success ? "ok" : v.reason}]</span>

View File

@@ -376,3 +376,14 @@ pre {
padding: 14px; 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;
}