Live dialogue preview in the editors (reuses the real DialoguePlayer)

- DialoguePlayer gains an `inline` mode (fills its container, no window key
  capture) and a `startId` to jump the walk to a given utterance.
- New DialoguePreview: a scaled-down mini player that fetches the resolved tree
  from GET /api/admin/story-nodes/:id/dialogue (same resolver as playback).
- Mystery graph: preview appears next to a selected dialogue node.
- Utterance editor: docked preview that jumps to the clicked/selected utterance
  and refreshes after each edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 19:24:03 +02:00
co-authored by Claude Opus 4.8
parent f10d7584d2
commit 0b1e5e7fc7
6 changed files with 59 additions and 6 deletions
+6 -1
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'
import { DialoguePreview } from './narrative'
type Utterer = 'npc' | 'player'
type Utterance = {
@@ -34,8 +35,9 @@ export function UtteranceCanvas({ nodeId, nodeLabel, terminals, onClose, setStat
const cardRefs = useRef(new Map<string, HTMLDivElement>())
const [heights, setHeights] = useState<Record<string, number>>({})
const [revision, setRevision] = useState(0)
const reload = useCallback(async () => {
try { setUtterances(await api<Utterance[]>(`/api/admin/story-nodes/${nodeId}/utterances`, 'GET')) }
try { setUtterances(await api<Utterance[]>(`/api/admin/story-nodes/${nodeId}/utterances`, 'GET')); setRevision(r => r + 1) }
catch (error) { setStatus(String((error as Error).message || error)) }
}, [nodeId, setStatus])
useEffect(() => { void reload(); api<Npc[]>('/api/admin/npcs', 'GET').then(setNpcs).catch(() => {}) }, [reload])
@@ -205,6 +207,9 @@ export function UtteranceCanvas({ nodeId, nodeLabel, terminals, onClose, setStat
</div>)}
</div>
{utterances.length === 0 && <div className="graph-empty">No utterances yet add an NPC line or player choice.</div>}
<div className="utterance-preview-dock" onPointerDown={event => event.stopPropagation()}>
<DialoguePreview nodeId={nodeId} revision={revision} startId={selected ? (selected.utterer === 'player' ? selected.parentUtteranceId : selected.id) : null} />
</div>
</div>
{selected && <aside className="graph-inspector">