import { ArrowLeft, Loader2, Plus, Send, X, } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { WoodenFishAudioAsset, WoodenFishSessionResponse, WoodenFishWorkspaceCreateRequest, } from '../../../packages/shared/src/contracts/woodenFish'; import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage'; import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient'; import { WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, WOODEN_FISH_DEFAULT_HIT_SOUND_ASSET, } from '../../services/wooden-fish/woodenFishDefaults'; import { CreativeAudioInputPanel } from '../common/CreativeAudioInputPanel'; import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel'; type WoodenFishWorkspaceProps = { isBusy?: boolean; error?: string | null; onBack: () => void; onSubmitted: ( result: WoodenFishSessionResponse, payload: WoodenFishWorkspaceCreateRequest, ) => void; }; type WoodenFishWorkspaceFormState = { hitObjectPrompt: string; hitObjectReferenceImageSrc: string; hitSoundAsset: WoodenFishAudioAsset | null; floatingWords: string[]; }; const DEFAULT_THEME_TAGS = ['敲木鱼', '解压']; const DEFAULT_FLOATING_WORDS = ['幸运']; const MAX_FLOATING_WORD_COUNT = 8; const DEFAULT_FORM_STATE: WoodenFishWorkspaceFormState = { hitObjectPrompt: '', hitObjectReferenceImageSrc: '', hitSoundAsset: null, floatingWords: DEFAULT_FLOATING_WORDS, }; function normalizeFloatingWords(words: string[]) { const seen = new Set(); const normalized: string[] = []; for (const word of words) { const trimmed = word.trim().replace(/[++]\s*1$/u, '').trim(); if (!trimmed || seen.has(trimmed)) { continue; } seen.add(trimmed); normalized.push(trimmed); if (normalized.length >= 8) { break; } } return normalized.length > 0 ? normalized : [...DEFAULT_FLOATING_WORDS]; } export function WoodenFishWorkspace({ isBusy = false, error = null, onBack, onSubmitted, }: WoodenFishWorkspaceProps) { const [formState, setFormState] = useState(DEFAULT_FORM_STATE); const [localError, setLocalError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [aiRedraw, setAiRedraw] = useState(true); const normalizedFloatingWords = useMemo( () => normalizeFloatingWords(formState.floatingWords), [formState.floatingWords], ); const canSubmit = normalizedFloatingWords.length > 0; const updateFloatingWord = (index: number, value: string) => { setFormState((current) => { const nextWords = [...current.floatingWords]; nextWords[index] = value; return { ...current, floatingWords: nextWords.slice(0, MAX_FLOATING_WORD_COUNT), }; }); }; const addFloatingWord = () => { setFormState((current) => { if (current.floatingWords.length >= MAX_FLOATING_WORD_COUNT) { return current; } return { ...current, floatingWords: [...current.floatingWords, ''], }; }); }; const removeFloatingWord = (index: number) => { if (index <= 0) { return; } setFormState((current) => ({ ...current, floatingWords: current.floatingWords.filter( (_word, currentIndex) => currentIndex !== index, ), })); }; const handleSubmit = async () => { if (!canSubmit || isSubmitting || isBusy) { setLocalError('请先补全输入。'); return; } setIsSubmitting(true); setLocalError(null); try { const payload: WoodenFishWorkspaceCreateRequest = { templateId: 'wooden-fish', workTitle: '', workDescription: formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, themeTags: DEFAULT_THEME_TAGS, hitObjectPrompt: formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, hitObjectReferenceImageSrc: formState.hitObjectReferenceImageSrc.trim() || null, hitSoundPrompt: null, hitSoundAsset: formState.hitSoundAsset ?? WOODEN_FISH_DEFAULT_HIT_SOUND_ASSET, floatingWords: normalizedFloatingWords, }; const response = await woodenFishClient.createSession(payload); onSubmitted(response, payload); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '创建草稿失败。', ); } finally { setIsSubmitting(false); } }; return (
{ void readPuzzleReferenceImageAsDataUrl(file) .then((dataUrl) => { setLocalError(null); setFormState((current) => ({ ...current, hitObjectReferenceImageSrc: dataUrl, })); setAiRedraw(true); }) .catch((caughtError) => { setLocalError( caughtError instanceof Error ? caughtError.message : '参考图读取失败。', ); }); }} onMainImageRemove={() => { setFormState((current) => ({ ...current, hitObjectReferenceImageSrc: '', })); }} onAiRedrawChange={setAiRedraw} onPromptChange={(value) => setFormState((current) => ({ ...current, hitObjectPrompt: value, })) } onSubmit={handleSubmit} />
disabled={isBusy || isSubmitting} title="敲击音效" defaultLabel="默认木鱼音" asset={formState.hitSoundAsset} buildRecordedFileName={() => `wooden-fish-hit-${Date.now()}.webm`} onAssetChange={(asset) => setFormState((current) => ({ ...current, hitSoundAsset: asset, })) } onError={setLocalError} />
功德有什么
{formState.floatingWords.map((word, index) => (
updateFloatingWord(index, event.target.value) } className="h-12 w-full rounded-[0.95rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 pr-10 text-sm font-semibold text-[var(--platform-text-strong)] outline-none" /> {index > 0 ? ( ) : null}
))} {formState.floatingWords.length < MAX_FLOATING_WORD_COUNT ? ( ) : null}
{localError || error ? (
{localError ?? error}
) : null}
); } export default WoodenFishWorkspace;