mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-03 14:50:46 +08:00
feat: 新增模型管理和供应商配置功能
### v1.1.0 - #### Added - 新增 AI 笔记风格选择 - 新增 AI 笔记返回格式选择 - 添加 AI 自定义笔记备注 Prompt - 添加任务失败重试 - 添加全局设置页,可在设置页进行模型设置 - #### Optimize - 优化前端样式,优化用户体验 - 增加生成中间产物,可用于失败后加快生成速度 - #### Fix - 修复视频截图视频过早删除错误
This commit is contained in:
@@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import { Copy, Download, FileText, ArrowRight } from 'lucide-react'
|
||||
import { toast } from 'sonner' // 你可以换成自己的通知组件
|
||||
|
||||
import Error from '@/components/Lottie/error.tsx'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { solarizedlight as codeStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||
import 'github-markdown-css/github-markdown-light.css'
|
||||
@@ -11,14 +11,26 @@ import { FC } from 'react'
|
||||
import Loading from '@/components/Lottie/Loading.tsx'
|
||||
import Idle from '@/components/Lottie/Idle.tsx'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
|
||||
interface MarkdownViewerProps {
|
||||
content: string
|
||||
status: 'idle' | 'loading' | 'success'
|
||||
status: 'idle' | 'loading' | 'success' | 'failed'
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{ label: '解析链接', key: 'PARSING' },
|
||||
{ label: '下载音频', key: 'DOWNLOADING' },
|
||||
{ label: '转写文字', key: 'TRANSCRIBING' },
|
||||
{ label: '总结内容', key: 'SUMMARIZING' },
|
||||
{ label: '保存完成', key: 'SUCCESS' },
|
||||
]
|
||||
|
||||
const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const getCurrentTask = useTaskStore.getState().getCurrentTask
|
||||
const currentTask = useTaskStore(state => state.getCurrentTask())
|
||||
const taskStatus = currentTask?.status || 'PENDING'
|
||||
const retryTask = useTaskStore.getState().retryTask
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
@@ -34,6 +46,7 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
const handleDownload = () => {
|
||||
const currentTask = getCurrentTask()
|
||||
const currentTaskName = currentTask?.audioMeta.title
|
||||
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
@@ -45,6 +58,7 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center space-y-4 text-neutral-500">
|
||||
<StepBar steps={steps} currentStep={taskStatus} />
|
||||
<Loading className="h-5 w-5" />
|
||||
<div className="text-center text-sm">
|
||||
<p className="text-lg font-bold">正在生成笔记,请稍候…</p>
|
||||
@@ -63,6 +77,24 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else if (status === 'failed') {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-4 space-y-3">
|
||||
<Error /> {/* 你可以换成 Failed 动画 */}
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-red-500">笔记生成失败</p>
|
||||
<p className="mt-2 mb-2 text-xs text-red-400">请检查后台或稍后再试</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
retryTask(currentTask.id)
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form.tsx'
|
||||
import { useEffect } from 'react'
|
||||
import { Input } from '@/components/ui/input.tsx'
|
||||
import {
|
||||
Select,
|
||||
@@ -30,7 +31,9 @@ import {
|
||||
import { generateNote } from '@/services/note.ts'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx'
|
||||
|
||||
import { useModelStore } from '@/store/modelStore'
|
||||
import { Alert } from 'antd'
|
||||
import { Textarea } from '@/components/ui/textarea.tsx'
|
||||
// ✅ 定义表单 schema
|
||||
const formSchema = z.object({
|
||||
video_url: z.string().url('请输入正确的视频链接'),
|
||||
@@ -40,15 +43,70 @@ const formSchema = z.object({
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
|
||||
type NoteFormValues = z.infer<typeof formSchema>
|
||||
const noteFormats = [
|
||||
{
|
||||
label: '目录',
|
||||
value: 'toc',
|
||||
},
|
||||
{ label: '原片跳转', value: 'link' },
|
||||
{ label: '原片截图', value: 'screenshot' },
|
||||
{ label: 'AI总结', value: 'summary' },
|
||||
]
|
||||
const noteStyles = [
|
||||
{
|
||||
label: '精简',
|
||||
value: 'minimal', // 简洁、快速呈现要点
|
||||
},
|
||||
{
|
||||
label: '详细',
|
||||
value: 'detailed', // 详细记录,包含时间戳、关键点
|
||||
},
|
||||
{
|
||||
label: '教程',
|
||||
value: 'tutorial', // 详细记录,包含时间戳、关键点
|
||||
},
|
||||
{
|
||||
label: '学术',
|
||||
value: 'academic', // 适合学术报告,正式且结构化
|
||||
},
|
||||
{
|
||||
label: '小红书',
|
||||
value: 'xiaohongshu', // 适合社交平台分享,亲切、口语化
|
||||
},
|
||||
{
|
||||
label: '生活向',
|
||||
value: 'life_journal', // 记录个人生活感悟,情感化表达
|
||||
},
|
||||
{
|
||||
label: '任务导向',
|
||||
value: 'task_oriented', // 强调任务、目标,适合工作和待办事项
|
||||
},
|
||||
{
|
||||
label: '商业风格',
|
||||
value: 'business', // 适合商业报告、会议纪要,正式且精准
|
||||
},
|
||||
{
|
||||
label: '会议纪要',
|
||||
value: 'meeting_minutes', // 适合商业报告、会议纪要,正式且精准
|
||||
},
|
||||
]
|
||||
|
||||
const NoteForm = () => {
|
||||
useTaskStore(state => state.tasks)
|
||||
const setCurrentTask = useTaskStore(state => state.setCurrentTask)
|
||||
const currentTaskId = useTaskStore(state => state.currentTaskId)
|
||||
const getCurrentTask = useTaskStore(state => state.getCurrentTask)
|
||||
const loadEnabledModels = useModelStore(state => state.loadEnabledModels)
|
||||
const modelList = useModelStore(state => state.modelList)
|
||||
const showFeatureHint = useModelStore(state => state.showFeatureHint)
|
||||
const setShowFeatureHint = useModelStore(state => state.setShowFeatureHint)
|
||||
const form = useForm<NoteFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -56,9 +114,16 @@ const NoteForm = () => {
|
||||
platform: 'bilibili',
|
||||
quality: 'medium', // 默认中等质量
|
||||
screenshot: false,
|
||||
model_name: modelList[0]?.model_name || '', // 确保有值
|
||||
format: [], // 初始化为空数组
|
||||
style: 'minimal', // 默认选择精简风格
|
||||
extras: '', // 初始化为空字符串
|
||||
},
|
||||
})
|
||||
|
||||
const onClose = () => {
|
||||
setShowFeatureHint(false)
|
||||
}
|
||||
const isGenerating = () => {
|
||||
console.log('🚀 isGenerating', getCurrentTask()?.status)
|
||||
return getCurrentTask()?.status === 'PENDING'
|
||||
@@ -66,14 +131,23 @@ const NoteForm = () => {
|
||||
|
||||
const onSubmit = async (data: NoteFormValues) => {
|
||||
console.log('🎯 提交内容:', data)
|
||||
await generateNote({
|
||||
const payload = {
|
||||
video_url: data.video_url,
|
||||
platform: data.platform,
|
||||
quality: data.quality,
|
||||
screenshot: data.screenshot,
|
||||
link: data.link,
|
||||
})
|
||||
model_name: data.model_name,
|
||||
provider_id: modelList.find(model => model.model_name === data.model_name).provider_id,
|
||||
format: data.format,
|
||||
style: data.style,
|
||||
extras: data.extras,
|
||||
}
|
||||
const res = await generateNote(payload)
|
||||
const taskId = res.data.task_id
|
||||
useTaskStore.getState().addPendingTask(taskId, data.platform, payload)
|
||||
}
|
||||
useEffect(() => {
|
||||
loadEnabledModels()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -173,48 +247,157 @@ const NoteForm = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">模型选择</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[200px] text-xs">不同模型返回质量不同,可自行测试</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择配置好的模型" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{modelList.map(item => {
|
||||
return <SelectItem value={item.model_name}>{item.model_name}</SelectItem>
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/*<FormDescription className="text-xs text-neutral-500">*/}
|
||||
{/* 质量越高,下载体积越大,速度越慢*/}
|
||||
{/*</FormDescription>*/}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 是否需要原片位置 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="link"
|
||||
name="style"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
<FormItem>
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">笔记风格</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[200px] text-xs">选择你希望生成的笔记风格</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} id="link" />
|
||||
</FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择笔记风格" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{noteStyles.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormLabel htmlFor="link" className="text-sm leading-none font-medium">
|
||||
是否插入内容跳转链接
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 是否需要下载 */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="screenshot"
|
||||
name="format"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
<FormItem>
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">笔记格式</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">选择要包含的笔记元素,比如时间戳、截图提示或总结</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id="screenshot"
|
||||
/>
|
||||
<div className="flex space-x-1.5">
|
||||
{noteFormats.map(item => (
|
||||
<label key={item.value} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.value)}
|
||||
onCheckedChange={checked => {
|
||||
const currentValue = field.value ?? [] // ✨ 保底是数组
|
||||
if (checked) {
|
||||
field.onChange([...currentValue, item.value])
|
||||
} else {
|
||||
field.onChange(currentValue.filter(v => v !== item.value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extras"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">备注</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">会把这段加入到Prompt最后 可自行测试</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Textarea placeholder={'笔记需要罗列出 xxx 关键点'} />
|
||||
|
||||
<FormLabel htmlFor="screenshot" className="text-sm leading-none font-medium">
|
||||
是否插入视频截图
|
||||
</FormLabel>
|
||||
{/*<FormDescription className="text-xs text-neutral-500">*/}
|
||||
{/* 质量越高,下载体积越大,速度越慢*/}
|
||||
{/*</FormDescription>*/}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={'flex w-full items-center gap-2 py-1.5'}>
|
||||
{/* 提交按钮 */}
|
||||
<Button type="submit" className="bg-primary w-full" disabled={isGenerating()}>
|
||||
@@ -235,27 +418,35 @@ const NoteForm = () => {
|
||||
</div>
|
||||
|
||||
{/* 添加一些额外的说明或功能介绍 */}
|
||||
<div className="bg-primary-light mt-6 rounded-lg p-4">
|
||||
<h3 className="text-primary mb-2 font-medium">功能介绍</h3>
|
||||
<ul className="space-y-2 text-sm text-neutral-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>自动提取视频内容,生成结构化笔记</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>支持多个视频平台,包括哔哩哔哩、YouTube等</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>一键复制笔记,支持Markdown格式</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>可选择是否插入图片</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{showFeatureHint && (
|
||||
<Alert
|
||||
message="功能介绍 v2.0.0"
|
||||
description={
|
||||
<ul className="space-y-2 text-sm text-neutral-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>自动提取视频内容,生成结构化笔记</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>支持多个视频平台,包括哔哩哔哩、YouTube等</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>一键复制笔记,支持Markdown格式</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>可选择是否插入图片</span>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
type="info"
|
||||
onClose={onClose}
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
{/*<div className="bg-primary-light mt-6 rounded-lg p-4"></div>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
54
BillNote_frontend/src/pages/HomePage/components/StepBar.tsx
Normal file
54
BillNote_frontend/src/pages/HomePage/components/StepBar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Step {
|
||||
label: string
|
||||
key: string
|
||||
Icon?: React.ReactNode // 加一个可选的 Lottie 动画
|
||||
}
|
||||
|
||||
interface StepBarProps {
|
||||
steps: Step[]
|
||||
currentStep: string
|
||||
}
|
||||
|
||||
const StepBar: FC<StepBarProps> = ({ steps, currentStep }) => {
|
||||
const currentIndex = steps.findIndex(step => step.key === currentStep)
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const isActive = index <= currentIndex
|
||||
const isCurrent = index === currentIndex
|
||||
const isLast = index === steps.length - 1
|
||||
return (
|
||||
<div key={step.key} className="relative flex flex-1 flex-col items-center">
|
||||
{/* 圆圈或者Lottie */}
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold ${
|
||||
isActive ? 'bg-primary text-white' : 'bg-gray-300 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
{/* 当前步骤显示动画 */}
|
||||
{isCurrent && step.Icon && (
|
||||
<div className="absolute top-10 h-16 w-16">{step.Icon}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 步骤名称 */}
|
||||
<div className="mt-4 text-center text-xs text-gray-700">{step.label}</div>
|
||||
|
||||
{/* 连接线 */}
|
||||
{!isLast && (
|
||||
<div className={`h-1 w-full ${isActive ? 'bg-primary' : 'bg-gray-300'}`}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StepBar
|
||||
Reference in New Issue
Block a user