feat(play): add history dialog

Signed-off-by: baizhi958216 <1475289190@qq.com>
This commit is contained in:
baizhi958216
2026-06-06 20:52:10 +08:00
parent aef4771d2e
commit 5a7daa8452
5 changed files with 290 additions and 3 deletions
+36
View File
@@ -52,6 +52,42 @@
text-transform: uppercase;
letter-spacing: 0.32em;
}
.vn-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(195, 155, 75, 0.58) rgba(20, 14, 8, 0.32);
}
.vn-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.vn-scrollbar::-webkit-scrollbar-track {
background:
linear-gradient(
to right,
transparent 0,
transparent 3px,
rgba(20, 14, 8, 0.46) 3px,
rgba(20, 14, 8, 0.46) 5px,
transparent 5px
);
}
.vn-scrollbar::-webkit-scrollbar-thumb {
background: rgba(195, 155, 75, 0.52);
border: 2px solid rgba(14, 10, 6, 0.88);
border-radius: 999px;
}
.vn-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(220, 180, 95, 0.76);
}
.vn-scrollbar::-webkit-scrollbar-corner {
background: transparent;
}
}
@keyframes infiplot-ripple {
+74 -1
View File
@@ -11,7 +11,11 @@ import {
useRef,
useState,
} from "react";
import { PlayCanvas, type Phase } from "@/components/PlayCanvas";
import {
PlayCanvas,
type Phase,
} from "@/components/PlayCanvas";
import type { DialogueHistoryItem } from "@/components/DialogueHistoryModal";
import { TtsKeyModal } from "@/components/TtsKeyModal";
import { annotateClick } from "@/lib/annotateClient";
import { loadClientTtsConfig } from "@/lib/clientTtsConfig";
@@ -262,6 +266,63 @@ type ScenePathStep = {
exit: { choiceId: string; label: string; nextSceneSeed: string };
};
function buildDialogueHistory(
session: Session | null,
currentSceneId: string | undefined,
currentVisitedBeatIds: string[],
): DialogueHistoryItem[] {
if (!session) return [];
return session.history.flatMap((entry, sceneIndex) => {
const beatsById = new Map(entry.scene.beats.map((b) => [b.id, b]));
const visitedBeatIds =
entry.scene.id === currentSceneId
? currentVisitedBeatIds
: entry.visitedBeatIds;
return visitedBeatIds.flatMap((beatId, beatIndex) => {
const beat = beatsById.get(beatId);
if (!beat) return [];
const nextVisitedBeatId = visitedBeatIds[beatIndex + 1];
const choice =
beat.next.type === "choice"
? beat.next.choices.find((c) => {
if (c.effect.kind === "advance-beat") {
return c.effect.targetBeatId === nextVisitedBeatId;
}
return (
beatIndex === visitedBeatIds.length - 1 &&
entry.exit?.kind === "choice" &&
c.id === entry.exit.choiceId
);
})
: undefined;
const freeformAction =
beatIndex === visitedBeatIds.length - 1 &&
entry.exit?.kind === "freeform"
? entry.exit.action
: undefined;
const body = beat.speaker ? beat.line : beat.narration;
const narration = beat.speaker ? beat.narration : undefined;
if (!body && !narration && !choice && !freeformAction) return [];
return [
{
id: `${sceneIndex}:${beatId}:${beatIndex}`,
sceneIndex: sceneIndex + 1,
speaker: beat.speaker,
body,
narration,
selectedChoice: choice?.label,
freeformAction,
},
];
});
});
}
function pathKey(steps: ScenePathStep[]): string {
return steps.map((s) => s.exit.choiceId).join("/");
}
@@ -549,6 +610,16 @@ function PlayInner() {
return currentScene.beats.find((b) => b.id === currentBeatId) ?? null;
}, [currentScene, currentBeatId]);
const dialogueHistory = useMemo<DialogueHistoryItem[]>(
() =>
buildDialogueHistory(
session,
currentScene?.id,
visitedBeatsRef.current,
),
[session, currentScene?.id, currentBeatId],
);
const audioSrc = (currentBeat ? beatAudioMap[currentBeat.id] : undefined) ?? null;
useEffect(() => {
@@ -1369,6 +1440,7 @@ function PlayInner() {
onSelectChoice={onSelectChoice}
orientation={orientation}
fullViewport
dialogueHistory={dialogueHistory}
/>
{orientation === "portrait" && (
<div
@@ -1442,6 +1514,7 @@ function PlayInner() {
onAdvance={onAdvance}
onSelectChoice={onSelectChoice}
orientation={orientation}
dialogueHistory={dialogueHistory}
aboveCanvas={
<button
type="button"