142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
import type { CustomWorldDraftCardDetail } from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
|
|
type CustomWorldDraftEditPanelProps = {
|
|
detail: CustomWorldDraftCardDetail;
|
|
disabled?: boolean;
|
|
onSave: (
|
|
sections: Array<{
|
|
sectionId: string;
|
|
value: string;
|
|
}>,
|
|
) => void;
|
|
onCancel: () => void;
|
|
};
|
|
|
|
function shouldUseTextarea(sectionId: string, value: string) {
|
|
const sceneActField = sectionId.match(/^act:[^:]+:(.+)$/u)?.[1] ?? null;
|
|
return (
|
|
value.length > 28 ||
|
|
value.includes('\n') ||
|
|
sectionId === 'summary' ||
|
|
sectionId === 'tone' ||
|
|
sectionId === 'coreConflicts' ||
|
|
sectionId === 'hiddenHook' ||
|
|
sectionId === 'secret' ||
|
|
sectionId === 'stakes' ||
|
|
sectionId === 'openingEvent' ||
|
|
sectionId === 'understandingShift' ||
|
|
sectionId === 'description' ||
|
|
sceneActField === 'summary' ||
|
|
sceneActField === 'encounterNpcIds' ||
|
|
sceneActField === 'actGoal' ||
|
|
sceneActField === 'transitionHook'
|
|
);
|
|
}
|
|
|
|
export function CustomWorldDraftEditPanel({
|
|
detail,
|
|
disabled = false,
|
|
onSave,
|
|
onCancel,
|
|
}: CustomWorldDraftEditPanelProps) {
|
|
const editableSections = useMemo(
|
|
() =>
|
|
detail.sections.filter((section) =>
|
|
detail.editableSectionIds.includes(section.id),
|
|
),
|
|
[detail],
|
|
);
|
|
const [draftValues, setDraftValues] = useState<Record<string, string>>(() =>
|
|
Object.fromEntries(
|
|
editableSections.map((section) => [section.id, section.value]),
|
|
),
|
|
);
|
|
|
|
useEffect(() => {
|
|
setDraftValues(
|
|
Object.fromEntries(editableSections.map((section) => [section.id, section.value])),
|
|
);
|
|
}, [editableSections]);
|
|
|
|
if (editableSections.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{editableSections.map((section) => {
|
|
const value = draftValues[section.id] ?? '';
|
|
const multiline = shouldUseTextarea(section.id, value);
|
|
|
|
return (
|
|
<label
|
|
key={section.id}
|
|
className="block rounded-[1.1rem] border border-white/8 bg-white/5 px-3 py-3"
|
|
>
|
|
<div className="text-[11px] tracking-[0.16em] text-zinc-400">
|
|
{section.label}
|
|
</div>
|
|
{multiline ? (
|
|
<textarea
|
|
value={value}
|
|
onChange={(event) => {
|
|
const nextValue = event.target.value;
|
|
setDraftValues((current) => ({
|
|
...current,
|
|
[section.id]: nextValue,
|
|
}));
|
|
}}
|
|
rows={4}
|
|
disabled={disabled}
|
|
className="mt-2 w-full resize-none rounded-[0.9rem] border border-white/10 bg-black/26 px-3 py-3 text-sm leading-7 text-white outline-none transition focus:border-sky-400/40 disabled:cursor-not-allowed disabled:opacity-60"
|
|
/>
|
|
) : (
|
|
<input
|
|
type="text"
|
|
value={value}
|
|
onChange={(event) => {
|
|
const nextValue = event.target.value;
|
|
setDraftValues((current) => ({
|
|
...current,
|
|
[section.id]: nextValue,
|
|
}));
|
|
}}
|
|
disabled={disabled}
|
|
className="mt-2 h-11 w-full rounded-[0.9rem] border border-white/10 bg-black/26 px-3 text-sm text-white outline-none transition focus:border-sky-400/40 disabled:cursor-not-allowed disabled:opacity-60"
|
|
/>
|
|
)}
|
|
</label>
|
|
);
|
|
})}
|
|
|
|
<div className="flex items-center justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={disabled}
|
|
className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-zinc-200 transition hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
|
>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onSave(
|
|
editableSections.map((section) => ({
|
|
sectionId: section.id,
|
|
value: draftValues[section.id] ?? '',
|
|
})),
|
|
);
|
|
}}
|
|
disabled={disabled}
|
|
className="rounded-full border border-sky-300/20 bg-sky-500/10 px-4 py-2 text-sm font-medium text-sky-100 transition hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
|
>
|
|
保存
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|