Files
BiliNote/BillNote_frontend/src/pages/HomePage/components/NoteHistory.tsx
huangjianwu c105342ded fix: 性能优化、前端转写器配置、任务进度丢失及 MLX Whisper 回退问题修复
### 性能优化
- 后端任务执行从串行锁改为 ThreadPoolExecutor 并发执行(默认3线程)
- 添加 GZipMiddleware 响应压缩 + Nginx gzip 配置
- 数据库连接池参数优化(pool_size=10, max_overflow=20)
- 视频帧提取并行化(ThreadPoolExecutor)
- LLM 重试配置缓存到实例,避免每次请求读 env var
- 前端路由级代码拆分(React.lazy + Suspense)
- Vite manualChunks 拆分 markdown/markmap/vendor
- MarkdownViewer 用 React.memo + useMemo 减少不必要渲染
- NoteHistory Fuse.js 实例 useMemo 缓存
- useTaskPolling 无待处理任务时跳过轮询
- 移除 antd 依赖(NoteForm Alert、modelForm Tag),改用 shadcn/ui

### 前端转写器配置(新功能)
- 新增 TranscriberConfigManager(JSON 文件存储,替代环境变量)
- 新增 GET/POST /transcriber_config API 端点
- 新增 GET /transcriber_models_status 模型下载状态查询
- 新增 POST /transcriber_download 后台模型下载触发
- 前端转写器设置页面:引擎选择、模型大小选择、模型下载管理
- deploy_status 端点同步从配置文件读取

### Bug 修复
- 修复任务进行中切换页面后进度丢失:Home.tsx status 派生逻辑补全中间状态
- 修复 MLX Whisper 静默回退 fast-whisper:移除环境变量门控,macOS 下自动尝试导入
- MLX Whisper 不可用时抛出 RuntimeError 而非静默回退
- 前端展示 MLX Whisper 可用性状态,不可用时禁用保存

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 14:09:34 +08:00

187 lines
6.5 KiB
TypeScript

import { useTaskStore } from '@/store/taskStore'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
import { Badge } from '@/components/ui/badge.tsx'
import { cn } from '@/lib/utils.ts'
import { Trash } from 'lucide-react'
import { Button } from '@/components/ui/button.tsx'
import PinyinMatch from 'pinyin-match'
import Fuse from 'fuse.js'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx'
import LazyImage from "@/components/LazyImage.tsx";
import {FC, useState, useEffect, useMemo} from 'react'
interface NoteHistoryProps {
onSelect: (taskId: string) => void
selectedId: string | null
}
const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
const tasks = useTaskStore(state => state.tasks)
const removeTask = useTaskStore(state => state.removeTask)
// 确保baseURL没有尾部斜杠
const baseURL = (String(import.meta.env.VITE_API_BASE_URL || 'api')).replace(/\/$/, '')
const [rawSearch, setRawSearch] = useState('')
const [search, setSearch] = useState('')
const fuse = useMemo(() => new Fuse(tasks, {
keys: ['audioMeta.title'],
threshold: 0.4 // 匹配精度(越低越严格)
}), [tasks])
useEffect(() => {
const timer = setTimeout(() => {
if (rawSearch === '') return
setSearch(rawSearch)
}, 300) // 300ms 防抖
return () => clearTimeout(timer)
}, [rawSearch])
const filteredTasks = search.trim()
? fuse.search(search).map(result => result.item)
: tasks
if (filteredTasks.length === 0) {
return (
<>
<div className="mb-2">
<input
type="text"
placeholder="搜索笔记标题..."
className="w-full rounded border border-neutral-300 px-3 py-1 text-sm outline-none focus:border-primary"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className="rounded-md border border-neutral-200 bg-neutral-50 py-6 text-center">
<p className="text-sm text-neutral-500"></p>
</div>
</>
)
}
return (
<>
<div className="mb-2">
<input
type="text"
placeholder="搜索笔记标题..."
className="w-full rounded border border-neutral-300 px-3 py-1 text-sm outline-none focus:border-primary"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2 overflow-hidden">
{filteredTasks.map(task => (
<div
key={task.id}
onClick={() => onSelect(task.id)}
className={cn(
'flex cursor-pointer flex-col rounded-md border border-neutral-200 p-3',
selectedId === task.id && 'border-primary bg-primary-light'
)}
>
<div
className={cn('flex items-center gap-4')}
>
{/* 封面图 */}
{task.platform === 'local' ? (
<img
src={
task.audioMeta.cover_url ? `${task.audioMeta.cover_url}` : '/placeholder.png'
}
alt="封面"
className="h-10 w-12 rounded-md object-cover"
/>
) : (
<LazyImage
src={
task.audioMeta.cover_url
? `${baseURL}/image_proxy?url=${encodeURIComponent(task.audioMeta.cover_url)}`
: '/placeholder.png'
}
alt="封面"
/>
)}
{/* 标题 + 状态 */}
<div className="flex w-full items-center justify-between gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="line-clamp-2 max-w-[180px] flex-1 overflow-hidden text-sm text-ellipsis">
{task.audioMeta.title || '未命名笔记'}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{task.audioMeta.title || '未命名笔记'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className={'mt-2 flex items-center justify-between text-[10px]'}>
<div className="shrink-0">
{task.status === 'SUCCESS' && (
<div className={'bg-primary w-10 rounded p-0.5 text-center text-white'}>
</div>
)}
{task.status !== 'SUCCESS' && task.status !== 'FAILED' ? (
<div className={'w-10 rounded bg-green-500 p-0.5 text-center text-white'}>
</div>
) : (
<></>
)}
{task.status === 'FAILED' && (
<div className={'w-10 rounded bg-red-500 p-0.5 text-center text-white'}></div>
)}
</div>
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="small"
variant="ghost"
onClick={e => {
e.stopPropagation()
removeTask(task.id)
}}
className="shrink-0"
>
<Trash className="text-muted-foreground h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/*<div className="shrink-0">*/}
{/* {task.status === 'SUCCESS' && <Badge variant="default">已完成</Badge>}*/}
{/* {task.status !== 'SUCCESS' && task.status === 'FAILED' && (*/}
{/* <Badge variant="outline">等待中</Badge>*/}
{/* )}*/}
{/* {task.status === 'FAILED' && <Badge variant="destructive">失败</Badge>}*/}
{/*</div>*/}
</div>
</div>
))}
</div>
</>
)
}
export default NoteHistory