:feat 新增模型配置页面和相关功能

- 新增模型配置页面组件和路由
- 实现模型配置表单和相关逻辑- 添加全局配置入口和功能- 优化首页布局和样式- 新增 404 页面组件
- 更新部分组件样式和结构
This commit is contained in:
Jefferyhcool
2025-04-22 17:01:02 +08:00
parent 2aad103a77
commit bb974b0b89
95 changed files with 7723 additions and 1697 deletions

View File

@@ -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} />}
/>
)
}

View 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} />}
/>
)
}

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -0,0 +1,10 @@
import { Outlet } from 'react-router-dom'
const Index = () => {
return (
<>
<Outlet />
</>
)
}
export default Index

View 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

View 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

View 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

View File

@@ -0,0 +1,7 @@
.menuBar {
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.menuBar:hover {
background-color: #f7f7f7;
}

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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