mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-17 15:47:37 +08:00
:feat 新增模型配置页面和相关功能
- 新增模型配置页面组件和路由 - 实现模型配置表单和相关逻辑- 添加全局配置入口和功能- 优化首页布局和样式- 新增 404 页面组件 - 更新部分组件样式和结构
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import React,{FC,useEffect,useState} from "react";
|
||||
import HomeLayout from "@/layouts/HomeLayout.tsx";
|
||||
import NoteForm from '@/pages/components/NoteForm'
|
||||
import MarkdownViewer from '@/pages/components/MarkdownViewer'
|
||||
import NoteFormWrapper from "@/pages/components/NoteFormWrapper.tsx";
|
||||
import {get_task_status} from "@/services/note.ts";
|
||||
import {useTaskStore} from "@/store/taskStore";
|
||||
type ViewStatus = 'idle' | 'loading' | 'success'
|
||||
export const HomePage:FC =()=>{
|
||||
const tasks = useTaskStore((state) => state.tasks)
|
||||
const currentTaskId = useTaskStore((state) => state.currentTaskId)
|
||||
|
||||
const currentTask = tasks.find((t) => t.id === currentTaskId)
|
||||
|
||||
const [status, setStatus] = useState<ViewStatus>('idle')
|
||||
|
||||
const content = currentTask?.markdown || ''
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentTask) {
|
||||
setStatus('idle')
|
||||
} else if (currentTask.status === 'PENDING') {
|
||||
setStatus('loading')
|
||||
} else if (currentTask.status === 'SUCCESS') {
|
||||
setStatus('success')
|
||||
}
|
||||
}, [currentTask])
|
||||
|
||||
// useEffect( () => {
|
||||
// get_task_status('d4e87938-c066-48a0-bbd5-9bec40d53354').then(res=>{
|
||||
// console.log('res1',res)
|
||||
// setContent(res.data.result.markdown)
|
||||
// })
|
||||
// }, [tasks]);
|
||||
return (
|
||||
<HomeLayout
|
||||
form={<NoteForm/>}
|
||||
preview={<MarkdownViewer status={status} content={content} />}
|
||||
|
||||
/>
|
||||
)
|
||||
}
|
||||
39
BillNote_frontend/src/pages/HomePage/Home.tsx
Normal file
39
BillNote_frontend/src/pages/HomePage/Home.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import HomeLayout from '@/layouts/HomeLayout.tsx'
|
||||
import NoteForm from '@/pages/HomePage/components/NoteForm.tsx'
|
||||
import MarkdownViewer from '@/pages/HomePage/components/MarkdownViewer.tsx'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
type ViewStatus = 'idle' | 'loading' | 'success'
|
||||
export const HomePage: FC = () => {
|
||||
const tasks = useTaskStore(state => state.tasks)
|
||||
const currentTaskId = useTaskStore(state => state.currentTaskId)
|
||||
|
||||
const currentTask = tasks.find(t => t.id === currentTaskId)
|
||||
|
||||
const [status, setStatus] = useState<ViewStatus>('idle')
|
||||
|
||||
const content = currentTask?.markdown || ''
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentTask) {
|
||||
setStatus('idle')
|
||||
} else if (currentTask.status === 'PENDING') {
|
||||
setStatus('loading')
|
||||
} else if (currentTask.status === 'SUCCESS') {
|
||||
setStatus('success')
|
||||
}
|
||||
}, [currentTask])
|
||||
|
||||
// useEffect( () => {
|
||||
// get_task_status('d4e87938-c066-48a0-bbd5-9bec40d53354').then(res=>{
|
||||
// console.log('res1',res)
|
||||
// setContent(res.data.result.markdown)
|
||||
// })
|
||||
// }, [tasks]);
|
||||
return (
|
||||
<HomeLayout
|
||||
NoteForm={<NoteForm />}
|
||||
Preview={<MarkdownViewer status={status} content={content} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useState } from 'react'
|
||||
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 { 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'
|
||||
import { FC } from 'react'
|
||||
import Loading from '@/components/Lottie/Loading.tsx'
|
||||
import Idle from '@/components/Lottie/Idle.tsx'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
interface MarkdownViewerProps {
|
||||
content: string
|
||||
status: 'idle' | 'loading' | 'success'
|
||||
}
|
||||
|
||||
const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const getCurrentTask = useTaskStore.getState().getCurrentTask
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
setCopied(true)
|
||||
toast.success('已复制到剪贴板')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (e) {
|
||||
toast.error(`复制失败${e}`)
|
||||
toast.error('复制失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
link.download = `${currentTaskName}.md`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center space-y-4 text-neutral-500">
|
||||
<Loading className="h-5 w-5" />
|
||||
<div className="text-center text-sm">
|
||||
<p className="text-lg font-bold">正在生成笔记,请稍候…</p>
|
||||
<p className="mt-2 text-xs text-neutral-500">这可能需要几秒钟时间,取决于视频长度</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else if (status === 'idle') {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center space-y-3 text-neutral-500">
|
||||
<Idle></Idle>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold">输入视频链接并点击“生成笔记”</p>
|
||||
<p className="mt-2 text-xs text-neutral-500">支持哔哩哔哩、YouTube 等视频平台</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="flex items-center gap-2 text-xl font-semibold text-neutral-900">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
笔记内容
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleCopy} variant="outline" size="sm">
|
||||
<Copy className="mr-1 h-4 w-4" />
|
||||
{copied ? '已复制' : '复制'}
|
||||
</Button>
|
||||
<Button onClick={handleDownload} variant="outline" size="sm">
|
||||
<Download className="mr-1 h-4 w-4" />
|
||||
导出 Markdown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 滚动容器 */}
|
||||
|
||||
<div className="overflow-y-auto">
|
||||
{(content && content != 'loading') || content != 'empty' ? (
|
||||
<div className="markdown-body flex-1 bg-white">
|
||||
{' '}
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const codeContent = String(children).replace(/\n$/, '')
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<div className="group relative">
|
||||
<SyntaxHighlighter
|
||||
style={codeStyle}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(codeContent)
|
||||
toast.success('代码已复制')
|
||||
}}
|
||||
className="absolute top-2 right-2 hidden items-center gap-1 rounded border border-gray-300 bg-white/70 px-2 py-1 text-xs shadow-sm transition group-hover:flex hover:bg-white"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<code className="rounded bg-gray-100 px-1 py-0.5 text-sm" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<div className="w-[300px] flex-col justify-items-center">
|
||||
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<ArrowRight className="text-primary h-8 w-8" />
|
||||
</div>
|
||||
<p className="mb-2 text-neutral-600">输入视频链接并点击"生成笔记"按钮</p>
|
||||
<p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube等视频网站</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/*<div className="markdown-body flex-1 overflow-y-auto bg-white">*/}
|
||||
{/* {content ? (*/}
|
||||
{/* */}
|
||||
{/* ) : (*/}
|
||||
{/* <>*/}
|
||||
{/* <div className="w-16 h-16 bg-primary-light rounded-full flex items-center justify-center mb-4">*/}
|
||||
{/* <ArrowRight className="h-8 w-8 text-primary" />*/}
|
||||
{/* </div>*/}
|
||||
{/* <p className="text-neutral-600 mb-2">输入视频链接并点击"生成笔记"按钮</p>*/}
|
||||
{/* <p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube、腾讯视频和爱奇艺</p>*/}
|
||||
{/* </>*/}
|
||||
{/* )}*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownViewer
|
||||
263
BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx
Normal file
263
BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form.tsx'
|
||||
import { Input } from '@/components/ui/input.tsx'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select.tsx'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import { Checkbox } from '@/components/ui/checkbox.tsx'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Info, Clock, Loader2 } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip.tsx'
|
||||
import { generateNote } from '@/services/note.ts'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx'
|
||||
|
||||
// ✅ 定义表单 schema
|
||||
const formSchema = z.object({
|
||||
video_url: z.string().url('请输入正确的视频链接'),
|
||||
platform: z.string().nonempty('请选择平台'),
|
||||
quality: z.enum(['fast', 'medium', 'slow'], {
|
||||
required_error: '请选择音频质量',
|
||||
}),
|
||||
screenshot: z.boolean().optional(),
|
||||
link: z.boolean().optional(),
|
||||
})
|
||||
|
||||
type NoteFormValues = z.infer<typeof formSchema>
|
||||
|
||||
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 form = useForm<NoteFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
video_url: '',
|
||||
platform: 'bilibili',
|
||||
quality: 'medium', // 默认中等质量
|
||||
screenshot: false,
|
||||
},
|
||||
})
|
||||
|
||||
const isGenerating = () => {
|
||||
console.log('🚀 isGenerating', getCurrentTask()?.status)
|
||||
return getCurrentTask()?.status === 'PENDING'
|
||||
}
|
||||
|
||||
const onSubmit = async (data: NoteFormValues) => {
|
||||
console.log('🎯 提交内容:', data)
|
||||
await generateNote({
|
||||
video_url: data.video_url,
|
||||
platform: data.platform,
|
||||
quality: data.quality,
|
||||
screenshot: data.screenshot,
|
||||
link: data.link,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<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">输入视频链接,支持哔哩哔哩、YouTube等平台</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* 平台选择 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="platform"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="选择平台" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="bilibili">哔哩哔哩</SelectItem>
|
||||
<SelectItem value="youtube">Youtube</SelectItem>
|
||||
{/*<SelectItem value="local">本地视频</SelectItem>*/}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 视频地址 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input placeholder="视频链接" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/*<p className="text-xs text-neutral-500">*/}
|
||||
{/* 支持哔哩哔哩视频链接,例如:*/}
|
||||
{/* https://www.bilibili.com/video/BV1vc25YQE9X/*/}
|
||||
{/*</p>*/}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quality"
|
||||
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>
|
||||
<SelectItem value="fast">快速(压缩)</SelectItem>
|
||||
<SelectItem value="medium">中等(推荐)</SelectItem>
|
||||
<SelectItem value="slow">高质量(清晰)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/*<FormDescription className="text-xs text-neutral-500">*/}
|
||||
{/* 质量越高,下载体积越大,速度越慢*/}
|
||||
{/*</FormDescription>*/}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 是否需要原片位置 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="link"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} id="link" />
|
||||
</FormControl>
|
||||
|
||||
<FormLabel htmlFor="link" className="text-sm leading-none font-medium">
|
||||
是否插入内容跳转链接
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 是否需要下载 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="screenshot"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id="screenshot"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel htmlFor="screenshot" className="text-sm leading-none font-medium">
|
||||
是否插入视频截图
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className={'flex w-full items-center gap-2 py-1.5'}>
|
||||
{/* 提交按钮 */}
|
||||
<Button type="submit" className="bg-primary w-full" disabled={isGenerating()}>
|
||||
{isGenerating() && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isGenerating() ? '正在生成…' : '生成笔记'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/*生成历史 */}
|
||||
<div className="my-4 flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-neutral-500" />
|
||||
<h2 className="text-base font-medium text-neutral-900">生成历史</h2>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<NoteHistory onSelect={setCurrentTask} selectedId={currentTaskId} />
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteForm
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Form } from '@/components/ui/form.tsx'
|
||||
import NoteForm from './NoteForm.tsx'
|
||||
|
||||
const NoteFormWrapper = () => {
|
||||
const form = useForm()
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<NoteForm />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteFormWrapper
|
||||
106
BillNote_frontend/src/pages/HomePage/components/NoteHistory.tsx
Normal file
106
BillNote_frontend/src/pages/HomePage/components/NoteHistory.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import { FC } from 'react'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
|
||||
import { Badge } from '@/components/ui/badge.tsx'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { Trash } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip.tsx'
|
||||
|
||||
interface NoteHistoryProps {
|
||||
onSelect: (taskId: string) => void
|
||||
selectedId: string | null
|
||||
}
|
||||
|
||||
const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
|
||||
const tasks = useTaskStore(state => state.tasks)
|
||||
const removeTask = useTaskStore(state => state.removeTask)
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="rounded-md border border-neutral-200 bg-neutral-50 py-6 text-center">
|
||||
<p className="text-sm text-neutral-500">暂无历史记录</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-auto max-h-[20vh] sm:max-h-[10vh]">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{tasks.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-4 rounded-md border p-3 transition hover:bg-neutral-50',
|
||||
selectedId === task.id && 'border-primary bg-primary-light'
|
||||
)}
|
||||
onClick={() => onSelect(task.id)}
|
||||
>
|
||||
{/* 封面图 */}
|
||||
<img
|
||||
src={
|
||||
task.audioMeta.cover_url
|
||||
? `/api/image_proxy?url=${encodeURIComponent(task.audioMeta.cover_url)}`
|
||||
: '/placeholder.png'
|
||||
}
|
||||
alt="封面"
|
||||
className="h-10 w-16 rounded-md object-cover"
|
||||
/>
|
||||
|
||||
{/* 标题 + 状态 */}
|
||||
|
||||
<div className="flex w-full min-w-0 items-center justify-between gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="max-w-[120px] flex-1 truncate font-medium">
|
||||
{task.audioMeta.title || '未命名笔记'}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{task.audioMeta.title || '未命名笔记'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="shrink-0">
|
||||
{task.status === 'SUCCESS' && <Badge variant="default">已完成</Badge>}
|
||||
{task.status === 'PENDING' && <Badge variant="outline">等待中</Badge>}
|
||||
{task.status === 'FAILED' && <Badge variant="destructive">失败</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
removeTask(task.id)
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Trash className="text-muted-foreground h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>删除</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteHistory
|
||||
10
BillNote_frontend/src/pages/Index.tsx
Normal file
10
BillNote_frontend/src/pages/Index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Index
|
||||
25
BillNote_frontend/src/pages/NotFoundPage/index.tsx
Normal file
25
BillNote_frontend/src/pages/NotFoundPage/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
// src/pages/NotFoundPage.tsx
|
||||
import NotFound from '@/components/Lottie/404.tsx'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const NotFoundPage = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col items-center justify-center text-gray-500">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold">你好像走丢了哦!~~</h1>
|
||||
<p className="mb-4 text-lg">请检查你的网址是否正确,或者点击下面的按钮返回首页。</p>
|
||||
<Button onClick={() => navigate('/')} className="hover:underline">
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<NotFound />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
||||
48
BillNote_frontend/src/pages/SettingPage/Menu.tsx
Normal file
48
BillNote_frontend/src/pages/SettingPage/Menu.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { BotMessageSquare, Captions, HardDriveDownload, Wrench } from 'lucide-react'
|
||||
import MenuBar, { IMenuProps } from '@/pages/SettingPage/components/menuBar.tsx'
|
||||
|
||||
const Menu = () => {
|
||||
const menuList: IMenuProps[] = [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'AI 模型设置',
|
||||
icon: <BotMessageSquare />,
|
||||
path: '/settings/model',
|
||||
},
|
||||
{
|
||||
id: ' transcriber',
|
||||
name: '音频转译配置',
|
||||
icon: <Captions />,
|
||||
path: '/settings/transcriber',
|
||||
},
|
||||
//下载配置
|
||||
{
|
||||
id: 'download',
|
||||
name: '下载配置',
|
||||
icon: <HardDriveDownload />,
|
||||
path: '/settings/download',
|
||||
},
|
||||
//其他配置
|
||||
{
|
||||
id: 'other',
|
||||
name: '其他配置',
|
||||
icon: <Wrench />,
|
||||
path: '/settings/other',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className={'flex w-full flex-col gap-2'}>
|
||||
<div className="text-2xl font-medium">设置</div>
|
||||
<div className="text-sm font-light text-gray-800">全局配置与模型设置</div>
|
||||
</div>
|
||||
<div className="mt-6 flex-1">
|
||||
{menuList &&
|
||||
menuList.map(item => {
|
||||
return <MenuBar key={item.id} menuItem={item} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Menu
|
||||
16
BillNote_frontend/src/pages/SettingPage/Model.tsx
Normal file
16
BillNote_frontend/src/pages/SettingPage/Model.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Provider from '@/components/Form/modelForm/Provider.tsx'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Model = () => {
|
||||
return (
|
||||
<div className={'flex h-full bg-white'}>
|
||||
<div className={'flex-1/5 border-r border-neutral-200 p-2'}>
|
||||
<Provider></Provider>
|
||||
</div>
|
||||
<div className={'flex-4/5'}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Model
|
||||
@@ -0,0 +1,7 @@
|
||||
.menuBar {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.menuBar:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import styles from './index.module.css'
|
||||
import { FC, JSX } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
export interface IMenuProps {
|
||||
id: string
|
||||
name: string
|
||||
icon: JSX.Element
|
||||
path: string
|
||||
}
|
||||
|
||||
interface IMenuItem {
|
||||
menuItem: IMenuProps
|
||||
}
|
||||
|
||||
const MenuBar: FC<IMenuItem> = ({ menuItem }) => {
|
||||
const location = useLocation()
|
||||
const isActive = location.pathname.startsWith(menuItem.path + '/')
|
||||
|| location.pathname === menuItem.path
|
||||
|
||||
return (
|
||||
<Link to={menuItem.path} className="w-full">
|
||||
<div
|
||||
className={
|
||||
styles.menuBar +
|
||||
' flex h-12 w-full items-center gap-1 rounded px-2' +
|
||||
(isActive ? ' bg-[#F0F0F0] font-semibold text-blue-600' : '')
|
||||
}
|
||||
>
|
||||
<div className="h-6 w-6">{menuItem.icon}</div>
|
||||
<div className="text-[16px]">{menuItem.name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuBar
|
||||
17
BillNote_frontend/src/pages/SettingPage/index.tsx
Normal file
17
BillNote_frontend/src/pages/SettingPage/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import SettingLayout from '@/layouts/SettingLayout.tsx'
|
||||
import Menu from '@/pages/SettingPage/Menu'
|
||||
import { useProviderStore } from '@/store/providerStore'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const SettingPage = () => {
|
||||
const fetchProviderList = useProviderStore(state => state.fetchProviderList)
|
||||
useEffect(() => {
|
||||
fetchProviderList()
|
||||
}, [])
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<SettingLayout Menu={<Menu />} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SettingPage
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useState } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Copy, Download, FileText,ArrowRight } from "lucide-react"
|
||||
import { toast } from "sonner" // 你可以换成自己的通知组件
|
||||
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'
|
||||
import {FC} from 'react'
|
||||
import Loading from "@/components/Lottie/Loading.tsx";
|
||||
import Idle from "@/components/Lottie/Idle.tsx";
|
||||
import {useTaskStore} from "@/store/taskStore";
|
||||
interface MarkdownViewerProps {
|
||||
content: string
|
||||
status: 'idle' | 'loading' | 'success'
|
||||
}
|
||||
|
||||
const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const getCurrentTask =useTaskStore.getState().getCurrentTask
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
setCopied(true)
|
||||
toast.success("已复制到剪贴板")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (e) {
|
||||
toast.error(`复制失败${e}`)
|
||||
toast.error("复制失败",e)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
link.download = `${currentTaskName}.md`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col justify-center items-center text-neutral-500 space-y-4">
|
||||
<Loading className='h-5 w-5' />
|
||||
<div className="text-center text-sm">
|
||||
<p className="text-lg font-bold">正在生成笔记,请稍候…</p>
|
||||
<p className="mt-2 text-xs text-neutral-500">这可能需要几秒钟时间,取决于视频长度</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
else if (status === 'idle'){
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col justify-center items-center text-neutral-500 space-y-3">
|
||||
|
||||
<Idle ></Idle>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold">输入视频链接并点击“生成笔记”</p>
|
||||
<p className="mt-2 text-xs text-neutral-500">支持哔哩哔哩、YouTube 等视频平台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-neutral-900 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-primary" />
|
||||
笔记内容
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleCopy} variant="outline" size="sm">
|
||||
<Copy className="w-4 h-4 mr-1" />
|
||||
{copied ? "已复制" : "复制"}
|
||||
</Button>
|
||||
<Button onClick={handleDownload} variant="outline" size="sm">
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
导出 Markdown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 滚动容器 */}
|
||||
|
||||
<div className='overflow-y-auto'>
|
||||
{
|
||||
content && content!='loading' || content!='empty'?(
|
||||
<div className="markdown-body flex-1 bg-white"> <ReactMarkdown
|
||||
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const codeContent = String(children).replace(/\n$/, '')
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<SyntaxHighlighter
|
||||
style={codeStyle}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(codeContent)
|
||||
toast.success("代码已复制")
|
||||
}}
|
||||
className="absolute top-2 right-2 hidden group-hover:flex items-center gap-1 text-xs px-2 py-1 bg-white/70 border border-gray-300 rounded hover:bg-white shadow-sm transition"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<code className="bg-gray-100 px-1 py-0.5 rounded text-sm" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown></div>
|
||||
):(
|
||||
<div className='w-full h-screen flex justify-center items-center'>
|
||||
<div className='w-[300px] flex-col justify-items-center '>
|
||||
<div className="w-16 h-16 bg-primary-light rounded-full flex items-center justify-center mb-4">
|
||||
<ArrowRight className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-neutral-600 mb-2">输入视频链接并点击"生成笔记"按钮</p>
|
||||
<p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube等视频网站</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{/*<div className="markdown-body flex-1 overflow-y-auto bg-white">*/}
|
||||
{/* {content ? (*/}
|
||||
{/* */}
|
||||
{/* ) : (*/}
|
||||
{/* <>*/}
|
||||
{/* <div className="w-16 h-16 bg-primary-light rounded-full flex items-center justify-center mb-4">*/}
|
||||
{/* <ArrowRight className="h-8 w-8 text-primary" />*/}
|
||||
{/* </div>*/}
|
||||
{/* <p className="text-neutral-600 mb-2">输入视频链接并点击"生成笔记"按钮</p>*/}
|
||||
{/* <p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube、腾讯视频和爱奇艺</p>*/}
|
||||
{/* </>*/}
|
||||
{/* )}*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownViewer
|
||||
@@ -1,287 +0,0 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Info,Clock } from "lucide-react"
|
||||
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip.tsx";
|
||||
import {generateNote} from "@/services/note.ts";
|
||||
import {useTaskStore} from "@/store/taskStore";
|
||||
import { useState } from "react"
|
||||
import NoteHistory from "@/pages/components/NoteHistory.tsx";
|
||||
|
||||
// ✅ 定义表单 schema
|
||||
const formSchema = z.object({
|
||||
video_url: z.string().url("请输入正确的视频链接"),
|
||||
platform: z.string().nonempty("请选择平台"),
|
||||
quality: z.enum(["fast", "medium", "slow"], {
|
||||
required_error: "请选择音频质量",
|
||||
}),
|
||||
screenshot: z.boolean().optional(),
|
||||
link:z.boolean().optional(),
|
||||
})
|
||||
|
||||
|
||||
type NoteFormValues = z.infer<typeof formSchema>
|
||||
|
||||
const NoteForm = () => {
|
||||
const [selectedTaskId] = useState<string | null>(null)
|
||||
|
||||
const tasks = useTaskStore((state) => state.tasks)
|
||||
const setCurrentTask=useTaskStore((state)=>state.setCurrentTask)
|
||||
const currentTaskId=useTaskStore(state=>state.currentTaskId )
|
||||
tasks.find((t) => t.id === selectedTaskId);
|
||||
const form = useForm<NoteFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
video_url: "",
|
||||
platform: "bilibili",
|
||||
quality: "medium", // 默认中等质量
|
||||
screenshot: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const isGenerating = false
|
||||
|
||||
const onSubmit = async (data: NoteFormValues) => {
|
||||
console.log("🎯 提交内容:", data)
|
||||
await generateNote({
|
||||
video_url: data.video_url,
|
||||
platform: data.platform,
|
||||
quality: data.quality,
|
||||
screenshot:data.screenshot,
|
||||
link:data.link
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between my-3">
|
||||
<h2 className="block ">视频链接</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-neutral-400 hover:text-primary cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs ">输入视频链接,支持哔哩哔哩、YouTube等平台</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* 平台选择 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="platform"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="选择平台" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="bilibili">哔哩哔哩</SelectItem>
|
||||
<SelectItem value="youtube">Youtube</SelectItem>
|
||||
{/*<SelectItem value="local">本地视频</SelectItem>*/}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 视频地址 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="视频链接"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
{/*<p className="text-xs text-neutral-500">*/}
|
||||
{/* 支持哔哩哔哩视频链接,例如:*/}
|
||||
{/* https://www.bilibili.com/video/BV1vc25YQE9X/*/}
|
||||
{/*</p>*/}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quality"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between my-3">
|
||||
<h2 className="block ">音频质量</h2>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-neutral-400 hover:text-primary cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs max-w-[200px]">质量越高,下载体积越大,速度越慢</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择质量" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="fast">快速(压缩)</SelectItem>
|
||||
<SelectItem value="medium">中等(推荐)</SelectItem>
|
||||
<SelectItem value="slow">高质量(清晰)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/*<FormDescription className="text-xs text-neutral-500">*/}
|
||||
{/* 质量越高,下载体积越大,速度越慢*/}
|
||||
{/*</FormDescription>*/}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 是否需要原片位置 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="link"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
|
||||
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id="link"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel
|
||||
htmlFor="link"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
是否插入内容跳转链接
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 是否需要下载 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="screenshot"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
{/* Tooltip 部分 */}
|
||||
|
||||
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id="screenshot"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel
|
||||
htmlFor="screenshot"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
是否插入视频截图
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-primary cursor-pointer"
|
||||
>
|
||||
{isGenerating ? "正在生成…" : "生成笔记"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
|
||||
{/*生成历史 */}
|
||||
<div className="flex items-center gap-2 my-4">
|
||||
<Clock className="h-4 w-4 text-neutral-500" />
|
||||
<h2 className="text-base font-medium text-neutral-900">生成历史</h2>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
<NoteHistory onSelect={setCurrentTask} selectedId={currentTaskId} />
|
||||
|
||||
</div>
|
||||
|
||||
{/* 添加一些额外的说明或功能介绍 */}
|
||||
<div className="mt-6 p-4 bg-primary-light rounded-lg">
|
||||
<h3 className="font-medium text-primary mb-2">功能介绍</h3>
|
||||
<ul className="text-sm space-y-2 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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteForm
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Form } from "@/components/ui/form"
|
||||
import NoteForm from "./NoteForm"
|
||||
|
||||
const NoteFormWrapper = () => {
|
||||
const form = useForm()
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<NoteForm />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteFormWrapper
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useTaskStore } from "@/store/taskStore"
|
||||
import { FC } from "react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Trash ,Clock} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
|
||||
interface NoteHistoryProps {
|
||||
onSelect: (taskId: string) => void
|
||||
selectedId: string | null
|
||||
}
|
||||
|
||||
const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
|
||||
const tasks = useTaskStore((state) => state.tasks)
|
||||
const removeTask = useTaskStore((state) => state.removeTask)
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-6 bg-neutral-50 rounded-md border border-neutral-200">
|
||||
<p className="text-sm text-neutral-500">暂无历史记录</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-auto max-h-[20vh] sm:max-h-[10vh]">
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className={cn(
|
||||
"flex items-center gap-4 p-3 cursor-pointer transition hover:bg-neutral-50 rounded-md border",
|
||||
selectedId === task.id && "border-primary bg-primary-light"
|
||||
)}
|
||||
onClick={() => onSelect(task.id)}
|
||||
>
|
||||
{/* 封面图 */}
|
||||
<img
|
||||
src={task.audioMeta.cover_url
|
||||
? `/api/image_proxy?url=${encodeURIComponent(task.audioMeta.cover_url)}`
|
||||
: "/placeholder.png"}
|
||||
alt="封面"
|
||||
className="w-16 h-10 object-cover rounded-md"
|
||||
/>
|
||||
|
||||
{/* 标题 + 状态 */}
|
||||
|
||||
<div className="flex items-center justify-between gap-2 min-w-0 w-full">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="font-medium max-w-[120px] truncate flex-1">{task.audioMeta.title || "未命名笔记"}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{task.audioMeta.title || "未命名笔记"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="shrink-0">
|
||||
{task.status === "SUCCESS" && <Badge variant="default">已完成</Badge>}
|
||||
{task.status === "PENDING" && <Badge variant="outline">等待中</Badge>}
|
||||
{task.status === "FAILED" && <Badge variant="destructive">失败</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeTask(task.id)
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Trash className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>删除</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteHistory
|
||||
Reference in New Issue
Block a user