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:
+28
-4
@@ -61,9 +61,11 @@ export function CutsceneHost({ componentKey, label, onComplete }: { componentKey
|
||||
|
||||
// Walk a dialogue node's utterance tree: play NPC lines, present player options at a
|
||||
// branch, follow a chosen option to the next line or out through its exit terminal.
|
||||
export function DialoguePlayer({ node, onExit }: { node: { utterances: RuntimeUtterance[]; rootId: string | null }; onExit: (terminalKey?: string) => void }) {
|
||||
export function DialoguePlayer({ node, onExit, inline, startId }: { node: { utterances: RuntimeUtterance[]; rootId: string | null }; onExit: (terminalKey?: string) => void; inline?: boolean; startId?: string | null }) {
|
||||
const byId = useMemo(() => new Map(node.utterances.map(u => [u.id, u])), [node.utterances])
|
||||
const [currentId, setCurrentId] = useState<string | null>(node.rootId)
|
||||
const [currentId, setCurrentId] = useState<string | null>(startId ?? node.rootId)
|
||||
// In preview, clicking an utterance card jumps the walk to that line.
|
||||
useEffect(() => { if (startId !== undefined) setCurrentId(startId ?? node.rootId) }, [startId, node.rootId])
|
||||
const [charCount, setCharCount] = useState(0)
|
||||
const reduced = usePrefersReducedMotion()
|
||||
const current = currentId ? byId.get(currentId) ?? null : null
|
||||
@@ -94,15 +96,16 @@ export function DialoguePlayer({ node, onExit }: { node: { utterances: RuntimeUt
|
||||
setCurrentId(children[0].id) // linear next line
|
||||
}
|
||||
useEffect(() => {
|
||||
if (inline) return // preview advances by click only, so it never steals the editor's keys
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (!showChoices && (event.key === ' ' || event.key === 'Enter' || event.key === 'ArrowRight')) { event.preventDefault(); proceedRef.current() }
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [showChoices])
|
||||
}, [showChoices, inline])
|
||||
|
||||
if (!current) return null
|
||||
return <div className="dialogue" role="dialog" aria-label="Dialogue" onClick={() => { if (!showChoices) proceedRef.current() }}>
|
||||
return <div className={`dialogue${inline ? ' inline' : ''}`} role="dialog" aria-label="Dialogue" onClick={() => { if (!showChoices) proceedRef.current() }}>
|
||||
<div className="dialogue-portrait">{current.poseUrl && <img src={current.poseUrl} alt={current.speaker.name} />}</div>
|
||||
<div className="dialogue-scrim" aria-hidden />
|
||||
<div className="dialogue-box">
|
||||
@@ -116,3 +119,24 @@ export function DialoguePlayer({ node, onExit }: { node: { utterances: RuntimeUt
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// A live, scaled-down mini player for the editor — runs the real DialoguePlayer
|
||||
// against the same server resolver, so it reflects the current authored dialogue.
|
||||
export function DialoguePreview({ nodeId, startId, revision, onClose }: { nodeId: string; startId?: string | null; revision?: number; onClose?: () => void }) {
|
||||
const [tree, setTree] = useState<{ utterances: RuntimeUtterance[]; rootId: string | null } | null>(null)
|
||||
const [playKey, setPlayKey] = useState(0)
|
||||
useEffect(() => {
|
||||
fetch(`/api/admin/story-nodes/${nodeId}/dialogue`).then(response => response.ok ? response.json() : null).then(setTree).catch(() => setTree(null))
|
||||
}, [nodeId, revision])
|
||||
return <div className="dialogue-preview" onPointerDown={event => event.stopPropagation()}>
|
||||
<div className="dialogue-preview-bar"><span>preview</span>
|
||||
<button title="Restart" onClick={event => { event.stopPropagation(); setPlayKey(key => key + 1) }}>↻</button>
|
||||
{onClose && <button title="Close" onClick={event => { event.stopPropagation(); onClose() }}>×</button>}
|
||||
</div>
|
||||
<div className="dialogue-preview-stage">
|
||||
{tree?.rootId
|
||||
? <div className="dialogue-preview-scale"><DialoguePlayer key={playKey} inline node={tree} startId={startId} onExit={() => setPlayKey(key => key + 1)} /></div>
|
||||
: <div className="dialogue-preview-empty">no utterances yet</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user