1
This commit is contained in:
112
src/components/visual-novel-runtime/VisualNovelHistoryPanel.tsx
Normal file
112
src/components/visual-novel-runtime/VisualNovelHistoryPanel.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user