/* NoteForm.tsx ---------------------------------------------------- */ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form.tsx' import { useEffect,useState } from 'react' import { useForm, useWatch } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { Info, Loader2, Plus } from 'lucide-react' import { message, Alert } from 'antd' import { generateNote } from '@/services/note.ts' import { uploadFile } from '@/services/upload.ts' import { useTaskStore } from '@/store/taskStore' import { useModelStore } from '@/store/modelStore' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip.tsx' import { Checkbox } from '@/components/ui/checkbox.tsx' import { ScrollArea } from '@/components/ui/scroll-area.tsx' import { Button } from '@/components/ui/button.tsx' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select.tsx' import { Input } from '@/components/ui/input.tsx' import { Textarea } from '@/components/ui/textarea.tsx' import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts' import { fetchModels } from '@/services/model.ts' import { useNavigate } from 'react-router-dom' /* -------------------- 校验 Schema -------------------- */ const formSchema = z .object({ video_url: z.string().optional(), platform: z.string().nonempty('请选择平台'), quality: z.enum(['fast', 'medium', 'slow']), screenshot: z.boolean().optional(), link: z.boolean().optional(), model_name: z.string().nonempty('请选择模型'), format: z.array(z.string()).default([]), style: z.string().nonempty('请选择笔记生成风格'), extras: z.string().optional(), video_understanding: z.boolean().optional(), video_interval: z.coerce.number().min(1).max(30).default(4).optional(), grid_size: z .tuple([z.coerce.number().min(1).max(10), z.coerce.number().min(1).max(10)]) .default([3, 3]) .optional(), }) .superRefine(({ video_url, platform }, ctx) => { if (platform === 'local' && !video_url) { ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] }) } else if (!video_url) { ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] }) } else { try { const url = new URL(video_url) if (!['http:', 'https:'].includes(url.protocol)) throw new Error() } catch { ctx.addIssue({ code: 'custom', message: '请输入正确的视频链接', path: ['video_url'] }) } } }) type NoteFormValues = z.infer /* -------------------- 可复用子组件 -------------------- */ const SectionHeader = ({ title, tip }: { title: string; tip?: string }) => (

{title}

{tip && ( {tip} )}
) const CheckboxGroup = ({ value = [], onChange, disabledMap, }: { value?: string[] onChange: (v: string[]) => void disabledMap: Record }) => (
{noteFormats.map(({ label, value: v }) => ( ))}
) /* -------------------- 主组件 -------------------- */ const NoteForm = () => { const navigate = useNavigate(); const [isUploading, setIsUploading] = useState(false) const [uploadSuccess, setUploadSuccess] = useState(false) /* ---- 全局状态 ---- */ const { addPendingTask, currentTaskId, setCurrentTask, getCurrentTask, retryTask } = useTaskStore() const { loadEnabledModels, modelList, showFeatureHint, setShowFeatureHint } = useModelStore() /* ---- 表单 ---- */ const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { platform: 'bilibili', quality: 'medium', model_name: modelList[0]?.model_name || '', style: 'minimal', video_interval: 4, grid_size: [3, 3], format: [], }, }) const currentTask = getCurrentTask() /* ---- 派生状态(只 watch 一次,提高性能) ---- */ const platform = useWatch({ control: form.control, name: 'platform' }) as string const videoUnderstandingEnabled = useWatch({ control: form.control, name: 'video_understanding' }) const editing = currentTask && currentTask.id const goModelAdd = () => { navigate("/settings/model"); }; /* ---- 副作用 ---- */ useEffect(() => { loadEnabledModels() return }, []) useEffect(() => { if (!currentTask) return const { formData } = currentTask console.log('currentTask.formData.platform:', formData.platform) form.reset({ platform: formData.platform || 'bilibili', video_url: formData.video_url || '', model_name: formData.model_name || modelList[0]?.model_name || '', style: formData.style || 'minimal', quality: formData.quality || 'medium', extras: formData.extras || '', screenshot: formData.screenshot ?? false, link: formData.link ?? false, video_understanding: formData.video_understanding ?? false, video_interval: formData.video_interval ?? 4, grid_size: formData.grid_size ?? [3, 3], format: formData.format ?? [], }) }, [ // 当下面任意一个变了,就重新 reset currentTaskId, // modelList 用来兜底 model_name modelList.length, // 还要加上 formData 的各字段,或者直接 currentTask currentTask?.formData, ]) /* ---- 帮助函数 ---- */ const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status) const generating = isGenerating() const handleFileUpload = async (file: File, cb: (url: string) => void) => { const formData = new FormData() formData.append('file', file) setIsUploading(true) setUploadSuccess(false) try { const data = await uploadFile(formData) cb(data.url) setUploadSuccess(true) } catch (err) { console.error('上传失败:', err) // message.error('上传失败,请重试') } finally { setIsUploading(false) } } const onSubmit = async (values: NoteFormValues) => { console.log('Not even go here') const payload: NoteFormValues = { ...values, provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id, task_id: currentTaskId || '', } if (currentTaskId) { retryTask(currentTaskId, payload) return } // message.success('已提交任务') const data = await generateNote(payload) addPendingTask(data.task_id, values.platform, payload) } const onInvalid = (errors: FieldErrors) => { console.warn('表单校验失败:', errors) // message.error('请完善所有必填项后再提交') } const handleCreateNew = () => { // 🔁 这里清空当前任务状态 // 比如调用 resetCurrentTask() 或者 navigate 到一个新页面 setCurrentTask(null) } const FormButton = () => { const label = generating ? '正在生成…' : editing ? '重新生成' : '生成笔记' return (
{editing && ( )}
) } /* -------------------- 渲染 -------------------- */ return (
{/* 顶部按钮 */} {/* 视频链接 & 平台 */}
{/* 平台选择 */} ( )} /> {/* 链接输入 / 上传框 */} ( {platform === 'local' ? ( <> ) : ( )} )} />
( {platform === 'local' && ( <>
{ e.preventDefault() e.stopPropagation() }} onDrop={e => { e.preventDefault() const file = e.dataTransfer.files?.[0] if (file) handleFileUpload(file, field.onChange) }} onClick={() => { const input = document.createElement('input') input.type = 'file' input.accept = 'video/*' input.onchange = e => { const file = (e.target as HTMLInputElement).files?.[0] if (file) handleFileUpload(file, field.onChange) } input.click() }} > {isUploading ? (

上传中,请稍候…

) : uploadSuccess ? (

上传成功!

) : (

拖拽文件到这里上传
或点击选择文件

)}
)}
)} />
{/* 模型选择 */} { modelList.length>0?( ( )} />): ( ) } {/* 笔记风格 */} ( )} />
{/* 视频理解 */}
(
启用 form.setValue('video_understanding', v)} />
)} />
{/* 采样间隔 */} ( 采样间隔(秒) )} /> {/* 拼图大小 */} ( 拼图尺寸(列 × 行)
field.onChange([+e.target.value, field.value?.[1] || 3])} className="w-16" /> x field.onChange([field.value?.[0] || 3, +e.target.value])} className="w-16" />
)} />
提示:

视频理解功能必须使用多模态模型。

} className="text-sm" />
{/* 笔记格式 */} ( )} /> {/* 备注 */} (