/* NoteForm.tsx ---------------------------------------------------- */ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form.tsx' import { useEffect } 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} from "@/constant/note.ts"; /* -------------------- 校验 Schema -------------------- */ const formSchema = z.object({ video_url: z.string(), 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' || platform === 'douyin') { 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 { 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 /* ---- 副作用 ---- */ useEffect(() => { loadEnabledModels() return}, []) useEffect(() => { const currentTask = getCurrentTask() const { formData } = currentTask || {} if (!currentTask) return form.reset( { ...formData, extras: formData?.extras || '', } ) }, [currentTaskId]) /* ---- 帮助函数 ---- */ 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) try { const { data } = await uploadFile(formData) if (data.code === 0) cb(data.data.url) } catch (err) { console.error('上传失败:', err) message.error('上传失败,请重试') } } const onSubmit = async (values: NoteFormValues) => { 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 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() }} >

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

)}
)} />
{/* 模型选择 */} ( )} /> {/* 笔记风格 */} ( )} />
{/* 视频理解 */}
(
启用 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" />
{/* 笔记格式 */} ( )} /> {/* 备注 */} (