feat: unify phase one creation flow
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Mic,
|
||||
Pause,
|
||||
Plus,
|
||||
Send,
|
||||
X,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
WoodenFishAudioAsset,
|
||||
@@ -21,6 +18,7 @@ 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 = {
|
||||
@@ -68,182 +66,6 @@ function normalizeFloatingWords(words: string[]) {
|
||||
return normalized.length > 0 ? normalized : [...DEFAULT_FLOATING_WORDS];
|
||||
}
|
||||
|
||||
function readAudioFileAsAsset(file: File, source: 'uploaded' | 'recorded') {
|
||||
return new Promise<WoodenFishAudioAsset>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(new Error('音频读取失败,请重试。'));
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result !== 'string') {
|
||||
reject(new Error('音频读取失败,请重试。'));
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
assetId: `local-${source}-${Date.now()}`,
|
||||
audioSrc: reader.result,
|
||||
audioObjectKey: '',
|
||||
assetObjectId: '',
|
||||
source,
|
||||
prompt: file.name,
|
||||
durationMs: null,
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function WoodenFishAudioInputPanel({
|
||||
disabled,
|
||||
asset,
|
||||
onAssetChange,
|
||||
onError,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
asset: WoodenFishAudioAsset | null;
|
||||
onAssetChange: (asset: WoodenFishAudioAsset | null) => void;
|
||||
onError: (message: string | null) => void;
|
||||
}) {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const recorderRef = useRef<MediaRecorder | null>(null);
|
||||
const chunksRef = useRef<BlobPart[]>([]);
|
||||
|
||||
const startRecording = async () => {
|
||||
if (disabled || isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof navigator === 'undefined' ||
|
||||
!navigator.mediaDevices?.getUserMedia ||
|
||||
typeof MediaRecorder === 'undefined'
|
||||
) {
|
||||
throw new Error('当前浏览器不支持录音。');
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const recorder = new MediaRecorder(stream);
|
||||
chunksRef.current = [];
|
||||
recorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
chunksRef.current.push(event.data);
|
||||
}
|
||||
};
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(chunksRef.current, {
|
||||
type: recorder.mimeType || 'audio/webm',
|
||||
});
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
const file = new File([blob], `wooden-fish-hit-${Date.now()}.webm`, {
|
||||
type: blob.type,
|
||||
});
|
||||
void readAudioFileAsAsset(file, 'recorded')
|
||||
.then(onAssetChange)
|
||||
.catch((caughtError) => {
|
||||
onError(
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: '录音保存失败。',
|
||||
);
|
||||
});
|
||||
};
|
||||
recorderRef.current = recorder;
|
||||
recorder.start();
|
||||
setIsRecording(true);
|
||||
onError(null);
|
||||
} catch (caughtError) {
|
||||
onError(
|
||||
caughtError instanceof Error ? caughtError.message : '录音启动失败。',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
recorderRef.current?.stop();
|
||||
recorderRef.current = null;
|
||||
setIsRecording(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="platform-subpanel rounded-[1.25rem] p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
敲击音效
|
||||
</div>
|
||||
{asset ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAssetChange(null)}
|
||||
disabled={disabled}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-xs"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<label
|
||||
className={`platform-button platform-button--secondary min-h-10 cursor-pointer gap-2 px-3 py-2 text-sm ${
|
||||
disabled ? 'pointer-events-none opacity-55' : ''
|
||||
}`}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
上传
|
||||
<input
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
disabled={disabled}
|
||||
className="sr-only"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.[0] ?? null;
|
||||
event.currentTarget.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
void readAudioFileAsAsset(file, 'uploaded')
|
||||
.then((nextAsset) => {
|
||||
onError(null);
|
||||
onAssetChange(nextAsset);
|
||||
})
|
||||
.catch((caughtError) => {
|
||||
onError(
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: '音频读取失败。',
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
return;
|
||||
}
|
||||
void startRecording();
|
||||
}}
|
||||
className="platform-button platform-button--ghost min-h-10 gap-2 px-3 py-2 text-sm"
|
||||
>
|
||||
{isRecording ? (
|
||||
<Pause className="h-4 w-4" />
|
||||
) : (
|
||||
<Mic className="h-4 w-4" />
|
||||
)}
|
||||
{isRecording ? '停止' : '录音'}
|
||||
</button>
|
||||
{asset?.audioSrc ? (
|
||||
<audio controls src={asset.audioSrc} className="h-10 max-w-full" />
|
||||
) : (
|
||||
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
{asset ? '音效已选择' : '默认木鱼音'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function WoodenFishWorkspace({
|
||||
isBusy = false,
|
||||
error = null,
|
||||
@@ -410,9 +232,12 @@ export function WoodenFishWorkspace({
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col gap-3 overflow-y-auto pr-0 lg:pr-1">
|
||||
<WoodenFishAudioInputPanel
|
||||
<CreativeAudioInputPanel<WoodenFishAudioAsset>
|
||||
disabled={isBusy || isSubmitting}
|
||||
title="敲击音效"
|
||||
defaultLabel="默认木鱼音"
|
||||
asset={formState.hitSoundAsset}
|
||||
buildRecordedFileName={() => `wooden-fish-hit-${Date.now()}.webm`}
|
||||
onAssetChange={(asset) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
|
||||
Reference in New Issue
Block a user