feat: 新增模型管理和供应商配置功能

### v1.1.0
- #### Added
  - 新增 AI 笔记风格选择
  - 新增 AI 笔记返回格式选择
  - 添加 AI 自定义笔记备注 Prompt
  - 添加任务失败重试
  - 添加全局设置页,可在设置页进行模型设置

- #### Optimize
  - 优化前端样式,优化用户体验
  - 增加生成中间产物,可用于失败后加快生成速度
- #### Fix
  - 修复视频截图视频过早删除错误
This commit is contained in:
思诺特
2025-04-26 23:40:17 +08:00
parent 1323cfd1ec
commit 171dea5e0d
51 changed files with 2511 additions and 414 deletions

View File

@@ -7,10 +7,19 @@ import { Route } from 'react-router-dom'
import Index from '@/pages/Index.tsx'
import NotFoundPage from '@/pages/NotFoundPage' //
import Model from '@/pages/SettingPage/Model.tsx'
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
import ProviderForm from '@/components/Form/modelForm/Form.tsx'
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
import Downloading from '@/components/Lottie/download.tsx'
function App() {
useTaskPolling(3000) // 每 3 秒轮询一次
const steps = [
{ label: '解析链接', key: 'PARSING', icon: <Downloading /> },
{ label: '下载音频', key: 'DOWNLOADING' },
{ label: '转写文字', key: 'TRANSCRIBING' },
{ label: '总结内容', key: 'SUMMARIZING' },
{ label: '保存完成', key: 'SUCCESS' },
]
return (
<>
<BrowserRouter>
@@ -20,9 +29,11 @@ function App() {
<Route path="settings" element={<SettingPage />}>
<Route index element={<Navigate to="model" replace />} />
<Route path="model" element={<Model />}>
<Route index element={<Navigate to="openai" replace />} />
<Route path="new" element={<ProviderForm isCreate />} />
{/*<Route index element={<Navigate to="openai" replace />} />*/}
<Route path=":id" element={<ProviderForm />} />
</Route>
<Route path="transcriber" elment={<Transcriber />}></Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -1,153 +1,281 @@
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { useParams } from 'react-router-dom';
import { useProviderStore } from '@/store/providerStore';
import {useEffect, useState} from 'react';
FormDescription,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { useParams, useNavigate } from 'react-router-dom'
import { useProviderStore } from '@/store/providerStore'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { testConnection, fetchModels } from '@/services/model.ts'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select.tsx' // ⚡新增 fetchModels
import { ModelSelector } from '@/components/Form/modelForm/ModelSelector.tsx'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert.tsx'
// ✅ 表单校验 schema
// ✅ Provider表单schema
const ProviderSchema = z.object({
name: z.string().min(2, '名称不能少于 2 个字符'),
apiKey: z.string().optional(),
baseUrl: z.string().url('必须是合法 URL'),
type: z.string(), // 只展示,不可改
});
type: z.string(),
})
type ProviderFormValues = z.infer<typeof ProviderSchema>;
type ProviderFormValues = z.infer<typeof ProviderSchema>
const ProviderForm = () => {
const rawId= useParams();
console.log('rawId',rawId)
// @ts-ignore
const [providerName, idPart] = rawId.id.split('&');
const [id,setId ]= useState(Number(idPart?.split('=')[1])) // => "1"
const getProviderById = useProviderStore((state) => state.getProviderById);
const provider = getProviderById(id);
// ✅ Model表单schema
const ModelSchema = z.object({
modelName: z.string().min(1, '请选择或填写模型名称'),
})
const form = useForm<ProviderFormValues>({
type ModelFormValues = z.infer<typeof ModelSchema>
interface IModel {
id: string
created: number
object: string
owned_by: string
permission: string
root: string
}
const ProviderForm = ({ isCreate = false }: { isCreate?: boolean }) => {
const { id } = useParams()
const navigate = useNavigate()
const isEditMode = !isCreate
const getProviderById = useProviderStore(state => state.getProviderById)
const loadProviderById = useProviderStore(state => state.loadProviderById)
const updateProvider = useProviderStore(state => state.updateProvider)
const addNewProvider = useProviderStore(state => state.addNewProvider)
const [loading, setLoading] = useState(true)
const [testing, setTesting] = useState(false)
const [isBuiltIn, setIsBuiltIn] = useState(false)
const [modelOptions, setModelOptions] = useState<IModel[]>([]) // ⚡新增,保存模型列表
const [modelLoading, setModelLoading] = useState(false)
const [search, setSearch] = useState('')
const providerForm = useForm<ProviderFormValues>({
resolver: zodResolver(ProviderSchema),
defaultValues: {
name: '',
apiKey: '',
baseUrl: '',
type: '',
type: 'custom',
},
});
})
const filteredModelOptions = modelOptions.filter(model => {
const keywords = search.trim().toLowerCase().split(/\s+/) // 支持多个关键词
const target = model.id.toLowerCase()
return keywords.every(kw => target.includes(kw))
})
const modelForm = useForm<ModelFormValues>({
resolver: zodResolver(ModelSchema),
defaultValues: {
modelName: '',
},
})
useEffect(() => {
console.log(provider)
// if (provider) {
// form.reset({
// name: provider.name,
// apiKey: provider.apiKey,
// baseUrl: provider.baseUrl,
// type: provider.type,
// });
// }
}, [id,provider, form]);
const load = async () => {
if (isEditMode) {
const data = await loadProviderById(id!)
providerForm.reset(data)
setIsBuiltIn(data.type === 'built-in')
} else {
providerForm.reset({
name: '',
apiKey: '',
baseUrl: '',
type: 'custom',
})
setIsBuiltIn(false)
}
setLoading(false)
}
load()
}, [id])
const isBuiltIn = provider?.type === 'built-in';
// 测试连通性
const handleTest = async () => {
const values = providerForm.getValues()
if (!values.apiKey || !values.baseUrl) {
toast.error('请填写 API Key 和 Base URL')
return
}
try {
setTesting(true)
const data = await testConnection({
api_key: values.apiKey,
base_url: values.baseUrl,
})
if (data.data.code === 0) {
toast.success('测试连通性成功 🎉')
} else {
toast.error(`连接失败: ${data.data.msg || '未知错误'}`)
}
} catch (error) {
toast.error('测试连通性异常')
} finally {
setTesting(false)
}
}
const onSubmit = (values: ProviderFormValues) => {
console.log('📝 提交表单数据:', values);
// TODO: 提交接口 /update_provider
};
// 加载模型列表
const handleModelLoad = async () => {
const values = providerForm.getValues()
if (!values.apiKey || !values.baseUrl) {
toast.error('请先填写 API Key 和 Base URL')
return
}
try {
setModelLoading(true) // ✅ 开始 loading
const res = await fetchModels(id!, { noCache: true }) // 这里稍后解释
if (res.data.code === 0 && res.data.data.models.data.length > 0) {
setModelOptions(res.data.data.models.data)
console.log('🔧 模型列表:', res.data.data)
toast.success('模型列表加载成功 🎉')
} else {
toast.error('未获取到模型列表')
}
} catch (error) {
toast.error('加载模型列表失败')
} finally {
setModelLoading(false) // ✅ 结束 loading
}
}
// if (!provider) return <div className="p-4">加载中...</div>;
// 保存Provider信息
const onProviderSubmit = async (values: ProviderFormValues) => {
if (isEditMode) {
updateProvider({ ...values, id: id! })
toast.success('更新供应商成功')
} else {
addNewProvider({ ...values })
toast.success('新增供应商成功')
}
}
// 保存Model信息
const onModelSubmit = async (values: ModelFormValues) => {
console.log('🔧 选择的模型:', values.modelName)
toast.success(`保存模型: ${values.modelName}`)
}
if (loading) return <div className="p-4">...</div>
return (
<Form {...form}>
<div className="flex flex-col gap-8 p-4">
{/* Provider信息表单 */}
<Form {...providerForm}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-full max-w-xl p-4 flex flex-col gap-4"
onSubmit={providerForm.handleSubmit(onProviderSubmit)}
className="flex max-w-xl flex-col gap-4"
>
<div className="text-lg font-bold"></div>
{/* 名称 */}
<div className="text-lg font-bold">
{isEditMode ? '编辑模型供应商' : '新增模型供应商'}
</div>
{!isBuiltIn && (
<div className="text-sm text-red-500 italic">
OpenAI SDK
</div>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled={isBuiltIn} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
control={providerForm.control}
name="name"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled={isBuiltIn} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* API Key */}
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API Key</FormLabel>
<FormControl>
<Input placeholder={'sk-xxx'} {...field} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
control={providerForm.control}
name="apiKey"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API Key</FormLabel>
<FormControl>
<Input {...field} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Base URL */}
<FormField
control={form.control}
name="baseUrl"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API </FormLabel>
<FormControl>
<Input {...field} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
control={providerForm.control}
name="baseUrl"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API</FormLabel>
<FormControl>
<Input {...field} className="flex-1" />
</FormControl>
<Button type="button" onClick={handleTest} variant="ghost" disabled={testing}>
{testing ? '测试中...' : '测试连通性'}
</Button>
<FormMessage />
</FormItem>
)}
/>
{/* 类型 */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled className="flex-1" />
</FormControl>
</FormItem>
)}
control={providerForm.control}
name="type"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="pt-2">
<Button type="submit" disabled={!form.formState.isDirty}>
<Button type="submit" disabled={!providerForm.formState.isDirty}>
{isEditMode ? '保存修改' : '保存创建'}
</Button>
</div>
</form>
</Form>
);
};
export default ProviderForm;
{/* 模型信息表单 */}
<div className="flex max-w-xl flex-col gap-4">
<div className="flex flex-col gap-2">
<span className="font-bold"></span>
<div className={'flex flex-col gap-2 rounded bg-[#FEF0F0] p-2.5'}>
<h2 className={'font-bold'}>!</h2>
<span>,.</span>
</div>
<ModelSelector providerId={id!} />
{/*<datalist id="model-options">*/}
{/* {modelOptions.map(model => (*/}
{/* <option key={model.id + '1'} value={model.id} />*/}
{/* ))}*/}
{/*</datalist>*/}
</div>
</div>
</div>
)
}
export default ProviderForm

View File

@@ -0,0 +1,4 @@
// iconMap.ts
import * as Icons from '@lobehub/icons'
export const IconMap = Icons;

View File

@@ -0,0 +1,29 @@
import * as Icons from '@lobehub/icons'
import CustomLogo from '@/assets/customAI.png'
interface AILogoProps {
name: string // 图标名称(区分大小写!如 OpenAI、DeepSeek
style?: 'Color' | 'Text' | 'Outlined' | 'Glyph'
size?: number
}
const AILogo = ({ name, style = 'Color', size = 24 }: AILogoProps) => {
const Icon = Icons[name as keyof typeof Icons]
if (!Icon) {
console.error(`❌ 图标组件不存在: ${name}`)
return (
<span style={{ fontSize: size }}>
<img src={CustomLogo} alt="CustomLogo" style={{ width: size, height: size }} />
</span>
)
}
const Variant = Icon[style as keyof typeof Icon]
if (!Variant) {
return <Icon size={size} />
}
return <Variant size={size} />
}
export default AILogo

View File

@@ -0,0 +1,92 @@
import { useState, useEffect } from 'react'
import { useModelStore } from '@/store/modelStore'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import toast from 'react-hot-toast'
interface ModelSelectorProps {
providerId: string
}
export function ModelSelector({ providerId }: ModelSelectorProps) {
const { models, loading, selectedModel, loadModels, setSelectedModel, addNewModel } =
useModelStore()
const [search, setSearch] = useState('')
const [submitting, setSubmitting] = useState(false)
const filteredModels = models.filter(model => {
const keywords = search.trim().toLowerCase().split(/\s+/)
const target = model.id.toLowerCase()
return keywords.every(kw => target.includes(kw))
})
useEffect(() => {
if (providerId) {
loadModels(providerId)
}
}, [providerId])
const handleSubmit = async () => {
if (!selectedModel) {
toast.error('请选择一个模型')
return
}
try {
setSubmitting(true)
await addNewModel(providerId, selectedModel)
toast.success('保存模型成功 🎉')
} catch (error) {
toast.error('保存失败')
} finally {
setSubmitting(false)
}
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2 font-bold">
<span></span>
<Button
variant="ghost"
type="button"
onClick={() => loadModels(providerId)}
disabled={loading}
>
{loading ? '加载中...' : '刷新模型'}
</Button>
</div>
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="w-[300px]">
<SelectValue placeholder="请选择模型" />
</SelectTrigger>
<SelectContent>
<div className="p-2">
<Input
placeholder="搜索模型..."
value={search}
onChange={e => setSearch(e.target.value)}
className="h-8"
/>
</div>
{filteredModels.map(model => (
<SelectItem key={model.id} value={model.id}>
{model.id}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleSubmit} disabled={submitting || !selectedModel}>
{submitting ? '保存中...' : '保存模型'}
</Button>
</div>
)
}

View File

@@ -1,28 +1,39 @@
import ProviderCard from '@/components/Form/modelForm/components/providerCard.tsx'
import { Button } from '@/components/ui/button.tsx'
import { useProviderStore } from '@/store/providerStore'
import { useNavigate } from 'react-router-dom'
const Provider = () => {
const providers = useProviderStore(state => state.provider)
const providers = useProviderStore(state => state.provider)
const navigate = useNavigate()
const handleClick = () => {
navigate(`/settings/model/new`)
}
return (
<div className="flex flex-col gap-2">
<div className={'search flex gap-1 py-1.5'}>
<Button type={'button'} className="w-full">
<Button
type={'button'}
onClick={() => {
handleClick()
}}
className="w-full"
>
</Button>
</div>
<div className="text-sm font-light"></div>
<div>
{providers &&
providers.map((provider, index) => {
providers.map((provider, index) => {
return (
<ProviderCard
key={index}
providerName={provider.name}
Icon={provider.logo}
id={provider.id}
enable={provider.enabled}
/>
)
})}

View File

@@ -1,22 +1,37 @@
import { Switch } from '@/components/ui/switch'
import { FC } from 'react'
import styles from './index.module.css'
import {useNavigate, useParams} from 'react-router-dom'
import AILogo from "@/components/Icons";
import { useNavigate, useParams } from 'react-router-dom'
import AILogo from '@/components/Form/modelForm/Icons'
import { useProviderStore } from '@/store/providerStore'
export interface IProviderCardProps {
id: string
providerName: string
Icon: string
enable: number
}
const ProviderCard: FC<IProviderCardProps> = ({ providerName, Icon, id }: IProviderCardProps) => {
const ProviderCard: FC<IProviderCardProps> = ({
providerName,
Icon,
id,
enable,
}: IProviderCardProps) => {
const navigate = useNavigate()
const updateProvider = useProviderStore(state => state.updateProvider)
const handleClick = () => {
navigate(`/settings/model/${providerName}&id=${id}`)
navigate(`/settings/model/${id}`)
}
const rawId= useParams();
console.log('rawId',rawId)
const handleEnable = () => {
console.log('enable', enable)
updateProvider({
id,
enabled: enable == 1 ? 0 : 1,
})
}
const rawId = useParams()
console.log('rawId', rawId)
// @ts-ignore
const { id: currentId } = useParams();
const { id: currentId } = useParams()
const isActive = currentId === id
return (
<div
@@ -24,18 +39,26 @@ const ProviderCard: FC<IProviderCardProps> = ({ providerName, Icon, id }: IProvi
handleClick()
}}
className={
styles.card + ' flex h-14 items-center justify-between rounded border border-[#f3f3f3] p-2'
+(isActive ? ' bg-[#F0F0F0] font-semibold text-blue-600' : '')
styles.card +
' flex h-14 items-center justify-between rounded border border-[#f3f3f3] p-2' +
(isActive ? ' bg-[#F0F0F0] font-semibold text-blue-600' : '')
}
>
<div className="flex items-center text-lg">
<div className="h-9 w-9 flex items-center">
<AILogo name={Icon} />
<div className="flex h-9 w-9 items-center">
<AILogo name={Icon} />
</div>
<div className="font-semibold">{providerName}</div>
</div>
<div>
<Switch />
<Switch
onClick={e => {
e.preventDefault()
handleEnable()
}}
checked={enable == 1}
/>
</div>
</div>
)

View File

@@ -0,0 +1,40 @@
import { FC, useRef, useEffect } from 'react'
import Lottie, { LottieRefCurrentProps } from 'lottie-react'
import download from '@/assets/Lottie/download.json'
interface LoadingProps {
play?: boolean // 是否播放
color?: string // 控制主色,比如 "#00BFFF"
}
const Downloading: FC<LoadingProps> = ({ play = true, color = '#00BFFF' }) => {
const lottieRef = useRef<LottieRefCurrentProps>(null)
useEffect(() => {
if (!lottieRef.current) return
if (play) {
lottieRef.current.play()
} else {
lottieRef.current.pause()
}
}, [play])
return (
<div className="flex items-center justify-center">
<Lottie
lottieRef={lottieRef}
animationData={download}
loop
autoplay={play}
style={{
width: 150,
height: 150,
filter: `drop-shadow(0 0 4px ${color}) saturate(2) brightness(1.2)`,
}}
/>
</div>
)
}
export default Downloading

View File

@@ -0,0 +1,21 @@
import { FC } from 'react'
import Lottie from 'lottie-react'
import error from '@/assets/Lottie/error.json'
const Error: FC = () => {
return (
<div className="flex items-center justify-center">
<Lottie
animationData={error}
loop
autoplay
style={{
width: 450,
height: 450,
}}
/>
</div>
)
}
export default Error

View File

@@ -0,0 +1,64 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
{
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
success:
'text-success bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-success/90',
warning:
'text-[#303133] bg-[#FEF0F0] [&>svg]:text-current *:data-[slot=alert-description]:text-warning/90',
},
},
defaultVariants: {
variant: 'default',
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-title"
className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
{...props}
/>
)
}
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-description"
className={cn(
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -1,45 +1,59 @@
// hooks/useTaskPolling.ts
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { useTaskStore } from '@/store/taskStore'
import { get_task_status } from '@/services/note.ts'
import toast from 'react-hot-toast'
export const useTaskPolling = (interval = 3000) => {
const tasks = useTaskStore(state => state.tasks)
const updateTaskContent = useTaskStore(state => state.updateTaskContent)
const updateTaskStatus = useTaskStore(state => state.updateTaskStatus)
const removeTask = useTaskStore(state => state.removeTask)
const tasksRef = useRef(tasks)
// 每次 tasks 更新,把最新的 tasks 同步进去
useEffect(() => {
tasksRef.current = tasks
}, [tasks])
useEffect(() => {
const timer = setInterval(async () => {
const pendingTasks = tasks.filter(
task => task.status === 'PENDING' || task.status === 'running'
const pendingTasks = tasksRef.current.filter(
task => task.status != 'SUCCESS' && task.status != 'FAILED'
)
for (const task of pendingTasks) {
try {
console.log(task)
console.log('🔄 正在轮询任务:', task.id)
const res = await get_task_status(task.id)
const { status } = res.data
if (status && status !== task.status) {
if (status === 'SUCCESS') {
const { markdown, transcript, audio_meta } = res.data.result
toast.success('笔记生成成功')
updateTaskContent(task.id, {
status,
markdown,
transcript,
audioMeta: audio_meta,
})
} else if (status === 'FAILED') {
updateTaskContent(task.id, { status })
console.warn(`⚠️ 任务 ${task.id} 失败`)
} else {
updateTaskStatus(task.id, status)
updateTaskContent(task.id, { status })
}
}
} catch (e) {
console.error('❌ 任务轮询失败:', e)
removeTask(task.id)
toast.error(`生成失败 ${e.message || e}`)
updateTaskContent(task.id, { status: 'FAILED' })
// removeTask(task.id)
}
}
}, interval)
return () => clearInterval(timer)
}, [interval, tasks])
}, [interval])
}

View File

@@ -3,7 +3,7 @@ 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'
type ViewStatus = 'idle' | 'loading' | 'success' | 'failed'
export const HomePage: FC = () => {
const tasks = useTaskStore(state => state.tasks)
const currentTaskId = useTaskStore(state => state.currentTaskId)
@@ -21,6 +21,8 @@ export const HomePage: FC = () => {
setStatus('loading')
} else if (currentTask.status === 'SUCCESS') {
setStatus('success')
} else if (currentTask.status === 'FAILED') {
setStatus('failed')
}
}, [currentTask])

View File

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

View File

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

View 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

View File

@@ -9,26 +9,27 @@ const Menu = () => {
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',
},
// TODO :下一版本升级优化
// {
// 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">

View File

@@ -0,0 +1,8 @@
const Transcriber = () => {
return (
<div className="flex h-screen w-full flex-col items-center justify-center">
<h1 className="text-center text-4xl font-bold">Transcriber is under development</h1>
</div>
)
}
export default Transcriber

View File

@@ -3,3 +3,29 @@ import request from '@/utils/request.ts'
export const getProviderList = async () => {
return await request.get('/get_all_providers')
}
export const getProviderById = async (id: string) => {
return await request.get(`/get_provider_by_id/${id}`)
}
export const updateProviderById = async (data: any) => {
return await request.post('/update_provider', data)
}
export const addProvider = async (data: any) => {
return await request.post('/add_provider', data)
}
export const testConnection = async (data: any) => {
return await request.post('/connect_test', data)
}
export const fetchModels = async (providerId: any) => {
return await request.get('/model_list/' + providerId)
}
export async function addModel(data: { provider_id: string; model_name: string }) {
return request.post('/models', data)
}
export const fetchEnableModels = async () => {
return await request.get('/model_list')
}

View File

@@ -4,10 +4,14 @@ import { useTaskStore } from '@/store/taskStore'
import request from '@/utils/request'
export const generateNote = async (data: {
video_url: string
link: undefined | boolean
screenshot: undefined | boolean
platform: string
quality: string
model_name: string
provider_id: string
task_id?: string
format: Array<string>
style: string
extras?: string
}) => {
try {
const response = await request.post('/generate_note', data)
@@ -20,11 +24,8 @@ export const generateNote = async (data: {
}
toast.success('笔记生成任务已提交!')
const taskId = response.data.data.task_id
console.log('res', response)
// 成功提示
useTaskStore.getState().addPendingTask(taskId, data.platform)
return response.data
} catch (e: any) {

View File

@@ -0,0 +1,25 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface SystemState {
showFeatureHint: boolean // ✅ 是否显示功能提示
setShowFeatureHint: (value: boolean) => void
// 后续如果有其他全局状态,可以继续加
sidebarCollapsed: boolean // ✅ 侧边栏是否收起
setSidebarCollapsed: (value: boolean) => void
}
// 暂不启用
export const useSystemStore = create<SystemState>()(
persist(
set => ({
showFeatureHint: true,
setShowFeatureHint: value => set({ showFeatureHint: value }),
sidebarCollapsed: false,
setSidebarCollapsed: value => set({ sidebarCollapsed: value }),
}),
{
name: 'system-store', // 本地存储的 key
}
)
)

View File

@@ -0,0 +1,101 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchModels, addModel, fetchEnableModels } from '@/services/model.ts'
interface IModel {
id: string
created: number
object: string
owned_by: string
permission: string
root: string
}
interface ModelStore {
models: IModel[]
modelList: []
loading: boolean
selectedModel: string
loadModels: (providerId: string) => Promise<void>
loadEnabledModels: () => Promise<void>
addNewModel: (providerId: string, modelId: string) => Promise<void>
setSelectedModel: (modelId: string) => void
clearModels: () => void
}
export const useModelStore = create<ModelStore>()(
devtools(set => ({
models: [],
loading: false,
selectedModel: '',
modelList: [],
loadEnabledModels: async () => {
try {
set({ loading: true })
const res = await fetchEnableModels()
if (res.data.code === 0 && res.data.data.length > 0) {
set({ modelList: res.data.data })
} else {
set({ modelList: [] })
console.error('模型列表加载失败')
}
} catch (error) {
set({ modelList: [] })
console.error('加载模型出错', error)
}
},
// 加载模型列表
loadModels: async (providerId: string) => {
try {
set({ loading: true })
const res = await fetchModels(providerId)
if (res.data.code === 0 && res.data.data.models.data.length > 0) {
set({ models: res.data.data.models.data })
} else {
set({ models: [] })
console.error('模型列表加载失败')
}
} catch (error) {
set({ models: [] })
console.error('加载模型出错', error)
} finally {
set({ loading: false })
}
},
// 新增模型
addNewModel: async (providerId: string, modelId: string) => {
try {
const res = await addModel({ provider_id: providerId, model_name: modelId })
if (res.code === 0) {
console.log('新增模型成功:', modelId)
// ✅ 新增成功以后,前端直接追加一条到 models 列表
set(state => ({
models: [
...state.models,
{
id: modelId,
created: Date.now(),
object: 'model',
owned_by: '',
permission: '',
root: '',
},
],
}))
} else {
console.error('新增模型失败')
}
} catch (error) {
console.error('添加模型出错', error)
}
},
// 设置选中的模型
setSelectedModel: modelId => set({ selectedModel: modelId }),
// 清空
clearModels: () => set({ models: [], selectedModel: '' }),
}))
)

View File

@@ -1,6 +1,11 @@
import { create } from 'zustand'
import { IProvider } from '@/types'
import { getProviderList } from '@/services/model.ts'
import {
addProvider,
getProviderById,
getProviderList,
updateProviderById,
} from '@/services/model.ts'
interface ProviderStore {
provider: IProvider[]
@@ -9,12 +14,14 @@ interface ProviderStore {
getProviderById: (id: number) => IProvider | undefined
getProviderList: () => IProvider[]
fetchProviderList: () => Promise<void>
loadProviderById: (id: string) => Promise<void>
addNewProvider: (provider: IProvider) => Promise<void>
updateProvider: (provider: IProvider) => Promise<void>
}
export const useProviderStore = create<ProviderStore>((set, get) => ({
provider: [],
// 添加或更新一个 provider
setProvider: newProvider =>
set(state => {
@@ -30,10 +37,60 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
// 设置整个 provider 列表
setAllProviders: providers => set({ provider: providers }),
loadProviderById: async (id: string) => {
const res = await getProviderById(id)
if (res.data.code === 0) {
const item = res.data.data
console.log('Provider ', item)
return {
id: item.id,
name: item.name,
logo: item.logo,
apiKey: item.api_key,
baseUrl: item.base_url,
type: item.type,
enabled: item.enabled,
}
} else {
console.log('Provider not found')
}
},
addNewProvider: async (provider: IProvider) => {
const payload = {
...provider,
api_key: provider.apiKey,
base_url: provider.baseUrl,
}
try {
const res = await addProvider(payload)
if (res.data.code === 0) {
const item = res.data.data
console.log('Provider ', item)
await get().fetchProviderList()
}
} catch (error) {
console.error('Error fetching provider:', error)
}
},
// 按 id 获取单个 provider
getProviderById: id => get().provider.find(p => p.id === id),
updateProvider: async (provider: IProvider) => {
try {
const data = {
...provider,
api_key: provider.apiKey,
base_url: provider.baseUrl,
}
const res = await updateProviderById(data)
if (res.data.code === 0) {
const item = res.data.data
console.log('Provider ', item)
await get().fetchProviderList()
}
} catch (error) {
console.error('Error fetching provider:', error)
}
},
getProviderList: () => get().provider,
fetchProviderList: async () => {
try {
@@ -55,6 +112,7 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
apiKey: item.api_key,
baseUrl: item.base_url,
type: item.type,
enabled: item.enabled,
}
}
),

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { delete_task } from '@/services/note.ts'
import { delete_task, generateNote } from '@/services/note.ts'
export type TaskStatus = 'PENDING' | 'RUNNING' | 'SUCCESS' | 'FAILD'
@@ -34,6 +34,15 @@ export interface Task {
status: TaskStatus
audioMeta: AudioMeta
createdAt: string
formData: {
video_url: string
link: undefined | boolean
screenshot: undefined | boolean
platform: string
quality: string
model_name: string
provider_id: string
}
}
interface TaskStore {
@@ -45,6 +54,7 @@ interface TaskStore {
clearTasks: () => void
setCurrentTask: (taskId: string | null) => void
getCurrentTask: () => Task | null
retryTask: (id: string) => void
}
export const useTaskStore = create<TaskStore>()(
@@ -53,10 +63,11 @@ export const useTaskStore = create<TaskStore>()(
tasks: [],
currentTaskId: null,
addPendingTask: (taskId: string, platform: string) =>
addPendingTask: (taskId: string, platform: string, formData: any) =>
set(state => ({
tasks: [
{
formData: formData,
id: taskId,
status: 'PENDING',
markdown: '',
@@ -91,6 +102,17 @@ export const useTaskStore = create<TaskStore>()(
const currentTaskId = get().currentTaskId
return get().tasks.find(task => task.id === currentTaskId) || null
},
retryTask: async (id: string) => {
const task = get().tasks.find(task => task.id === id).formData
await generateNote({
task_id: id,
...task,
})
set(state => ({
tasks: state.tasks.map(task => (task.id === id ? { ...task, status: 'PENDING' } : task)),
}))
},
removeTask: async id => {
const task = get().tasks.find(t => t.id === id)

View File

@@ -5,4 +5,5 @@ export interface IProvider {
type: string
apiKey: string
baseUrl: string
enabled: number
}