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