Files
Genarrative/src/components/visual-novel-runtime/VisualNovelHistoryPanel.tsx
2026-05-08 11:44:42 +08:00

113 lines
3.3 KiB
TypeScript

import { RotateCcw } from 'lucide-react';
import type {
VisualNovelHistoryEntry,
VisualNovelRunSnapshot,
VisualNovelRuntimeStep,
} from '../../../packages/shared/src/contracts/visualNovel';
type VisualNovelHistoryPanelProps = {
run: VisualNovelRunSnapshot;
allowRegeneration?: boolean;
isBusy?: boolean;
onRegenerateHistoryEntry?: (entryId: string) => void;
};
function renderStepText(step: VisualNovelRuntimeStep) {
if (step.type === 'narration') {
return step.text;
}
if (step.type === 'dialogue') {
return `${step.characterName}: ${step.text}`;
}
if (step.type === 'transition') {
return step.text ?? '';
}
if (step.type === 'choice') {
return step.choices.map((choice) => choice.text).join(' / ');
}
return '';
}
function VisualNovelHistoryEntryCard({
entry,
allowRegeneration,
isBusy,
onRegenerateHistoryEntry,
}: {
entry: VisualNovelHistoryEntry;
allowRegeneration: boolean;
isBusy: boolean;
onRegenerateHistoryEntry?: (entryId: string) => void;
}) {
const visibleStepTexts = entry.steps.map(renderStepText).filter(Boolean);
const canRegenerate =
allowRegeneration && entry.source === 'assistant' && onRegenerateHistoryEntry;
return (
<article className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/78 p-3">
<div className="mb-2 flex items-center justify-between gap-3 text-xs font-bold text-[var(--platform-text-soft)]">
<span>#{entry.turnIndex}</span>
<span>{entry.source === 'player' ? '玩家' : '故事'}</span>
</div>
{entry.actionText ? (
<p className="m-0 break-words text-sm leading-6 text-[var(--platform-text-strong)]">
{entry.actionText}
</p>
) : null}
{visibleStepTexts.map((text, index) => (
<p
key={`${entry.entryId}-${index}`}
className="m-0 mt-2 break-words text-sm leading-6 text-[var(--platform-text-base)]"
>
{text}
</p>
))}
{canRegenerate ? (
<button
type="button"
disabled={isBusy}
onClick={() => onRegenerateHistoryEntry(entry.entryId)}
className="mt-3 inline-flex min-h-9 items-center gap-2 rounded-full border border-[var(--platform-subpanel-border)] bg-white/86 px-3 text-xs font-black text-[var(--platform-text-strong)] transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-55"
>
<RotateCcw className="h-3.5 w-3.5" />
<span></span>
</button>
) : null}
</article>
);
}
export function VisualNovelHistoryPanel({
run,
allowRegeneration = false,
isBusy = false,
onRegenerateHistoryEntry,
}: VisualNovelHistoryPanelProps) {
return (
<div className="space-y-3">
{run.history.length > 0 ? (
run.history.map((entry) => (
<VisualNovelHistoryEntryCard
key={entry.entryId}
entry={entry}
allowRegeneration={allowRegeneration}
isBusy={isBusy}
onRegenerateHistoryEntry={onRegenerateHistoryEntry}
/>
))
) : (
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74 px-4 py-5 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
</div>
)}
</div>
);
}
export default VisualNovelHistoryPanel;