// FileSource — the control behind a file/image/record/video param. The bot runs // on the SERVER, so a browser-local path is meaningless to it; this offers three // honest sources: a URL the bot fetches, a path that already exists on the // server, or a browser upload that we stream to a server temp path (and then // feed that path to the action). Upload progress flows through the app-level // task registry so it survives tab switches. import { useId, useState } from 'react'; import { FileUp, Link2, Loader2, ServerCog, X } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { useApi } from '@/lib/api'; import { useDebugTasks } from '@/contexts/DebugTaskContext'; import { cn } from '@/lib/utils'; import type { FieldRole } from '@/types'; type Mode = 'url' | 'server' | 'upload'; const ACCEPT: Partial> = { image: 'image/*,.png,.jpg,.jpeg,.gif,.webp,.bmp,.heic,.heif', record: 'audio/*,.silk,.amr,.mp3,.wav,.m4a,.ogg', video: 'video/*,.mp4,.mov,.mkv,.webm', }; function Seg({ active, onClick, icon, label, }: { active: boolean; onClick: () => void; icon: React.ReactNode; label: string; }) { return ( ); } export function FileSource({ value, onChange, role, placeholder, }: { value: string; onChange: (v: string) => void; role?: FieldRole; placeholder?: string; }) { const api = useApi(); const { start, update, finish } = useDebugTasks(); const [mode, setMode] = useState('url'); const [uploading, setUploading] = useState(false); const [uploadName, setUploadName] = useState(null); const [uploadErr, setUploadErr] = useState(null); const inputId = useId(); const accept = role ? ACCEPT[role] : undefined; const doUpload = async (file: File) => { setUploadErr(null); setUploadName(file.name); setUploading(true); const ac = new AbortController(); const taskId = start({ kind: 'upload', label: `上传 ${file.name}`, progress: 0, cancel: () => ac.abort(), }); try { const res = await api.debug.upload(file, { signal: ac.signal, onProgress: (f) => update(taskId, { progress: f }), }); if (!res.path) { throw new Error(res.message || '上传失败'); } onChange(res.path); finish(taskId, 'done', res.path); } catch (e) { const msg = e instanceof Error ? e.message : '上传失败'; setUploadErr(msg); finish(taskId, ac.signal.aborted ? 'canceled' : 'failed', msg); } finally { setUploading(false); } }; return (
setMode('url')} icon={} label="URL" /> setMode('server')} icon={} label="服务器路径" /> setMode('upload')} icon={} label="上传" />
{mode === 'url' && ( onChange(e.target.value)} placeholder={placeholder || 'https://…'} /> )} {mode === 'server' && (
onChange(e.target.value)} placeholder="/path/on/server/file.jpg" /> 使用服务器上的文件路径
)} {mode === 'upload' && (
{ const f = e.target.files?.[0]; if (f) { void doUpload(f); } e.target.value = ''; }} /> {value && uploadName && !uploading && (
已上传:{uploadName} {value}
)} {uploadErr && {uploadErr}}
)}
); }