222 Commits

Author SHA1 Message Date
Jianwu Huang
e487b5382b Merge pull request #158 from JefferyHcool/feature/1.8.1
fix:windows 日志格式问题
2025-06-23 09:21:20 +08:00
JefferyHcool
b20725cb00 fix:修复windows 日志格式问题 2025-06-23 09:20:36 +08:00
JefferyHcool
e40c97b3fd fix:修复windows 日志格式问题 2025-06-23 09:18:31 +08:00
Jianwu Huang
ebb6d174ad Merge pull request #153 from JefferyHcool/feature/1.8.0
build:完成打包功能
2025-06-20 14:46:57 +08:00
JefferyHcool
ef4e67eda6 build:完成打包功能 2025-06-20 14:45:49 +08:00
Jianwu Huang
d5c3fe472a Merge pull request #152 from JefferyHcool/feature/1.8.0
refactor(backend): 修改系统初始化和健康检查相关逻辑
2025-06-20 13:46:36 +08:00
JefferyHcool
50bf467341 refactor(backend): 修改系统初始化和健康检查相关逻辑
- 更新 BackendInitDialog 组件中的提示信息,增加报错提示
- 在 config 路由中添加 sys_check 接口,用于系统检查
- 修改 useCheckBackend钩子,使用新的 sys_check接口进行系统检查
2025-06-20 13:44:48 +08:00
Jianwu Huang
caad44414f Merge pull request #151 from JefferyHcool/feature/1.8.0
feat(system): 添加后端初始化和健康检查功能
2025-06-20 13:06:45 +08:00
JefferyHcool
f23ed6ec6c feat(system): 添加后端初始化和健康检查功能
- 新增后端初始化对话框组件
- 实现后端健康检查和初始化逻辑
- 在 App 组件中集成后端初始化和健康检查
- 新增系统健康检查 API 和相关服务
2025-06-20 13:05:42 +08:00
Jianwu Huang
0919e65ad7 Merge pull request #150 from JefferyHcool/feature/1.8.0
docs: 更新 README 中微信交流群图片链接
2025-06-20 12:08:21 +08:00
JefferyHcool
7f8d4faa44 docs: 更新 README 中微信交流群图片链接
- 将微信交流群图片链接从外部 URL 更改为本地文件路径
- 从腾讯云存储地址改为相对路径的图片文件
2025-06-20 12:07:38 +08:00
Jianwu Huang
7687e898bc Merge pull request #149 from JefferyHcool/feature/1.8.0
build:完成打包功能
2025-06-20 12:03:41 +08:00
JefferyHcool
467deefd28 build:完成打包功能 2025-06-20 12:03:10 +08:00
Jianwu Huang
d3f42f967b Merge pull request #147 from JefferyHcool/feature/1.8.0 2025-06-20 09:47:01 +08:00
JefferyHcool
601bd7c4e3 refactor: 修复 build.bat脚本中的路径问题
- 修改了 .env 文件复制和删除操作的路径,使用反斜杠 (\) 替代斜杠 (/)
- 这个改动解决了 Windows 系统上路径解析的问题,确保脚本能够正确执行
2025-06-20 09:46:12 +08:00
JefferyHcool
6d06cb662d build(tauri): 更新后端端口并优化打包流程
- 将后端端口从8000 修改为 8483
- 更新前端请求基础 URL 以匹配新的后端端口
- 优化后端打包脚本,确保 .env 文件正确复制和清理
- 修改后端主程序和请求工具中的端口配置
2025-06-20 09:39:45 +08:00
Jianwu Huang
63e0345812 Merge pull request #146 from JefferyHcool/feature/1.8.0
build(tauri): 更新后端端口并优化打包流程
2025-06-20 09:36:22 +08:00
JefferyHcool
c24fcc6d7d build(tauri): 更新后端端口并优化打包流程
- 将后端端口从8000 修改为 8483
- 更新前端请求基础 URL 以匹配新的后端端口
- 优化后端打包脚本,确保 .env 文件正确复制和清理
- 修改后端主程序和请求工具中的端口配置
2025-06-20 09:35:34 +08:00
Jianwu Huang
2b0b1d2a85 Merge pull request #144 from JefferyHcool/feature/1.8.0
ci/cd:修复win打包
2025-06-19 18:22:05 +08:00
JefferyHcool
29372bab6b ci/cd:修复win打包 2025-06-19 18:21:13 +08:00
Jianwu Huang
03cb670bfa Update main.yml 2025-06-19 18:13:01 +08:00
Jianwu Huang
8ad1ea6d38 Merge pull request #143 from JefferyHcool/feature/1.8.0
chore:win打包
2025-06-19 18:10:11 +08:00
JefferyHcool
cdcbfc89bc ci/cd:修复win打包 2025-06-19 18:09:01 +08:00
JefferyHcool
2a510e8059 fix:修复bugs 2025-06-19 18:04:37 +08:00
Jianwu Huang
f8808737a3 Merge pull request #142 from JefferyHcool/feature/1.8.0
fix:修复bugs
2025-06-19 17:48:33 +08:00
JefferyHcool
0aaec4a53f fix:修复bugs 2025-06-19 17:44:48 +08:00
Jianwu Huang
1ab91965f3 Update main.yml 2025-06-19 17:35:06 +08:00
Jianwu Huang
689a6d99b0 Update main.yml 2025-06-19 17:26:49 +08:00
Jianwu Huang
2a164828a2 Update main.yml 2025-06-19 17:23:18 +08:00
Jianwu Huang
abbda6848a Update main.yml 2025-06-19 17:17:41 +08:00
Jianwu Huang
3afc0c1166 Update main.yml 2025-06-19 17:11:56 +08:00
Jianwu Huang
e9ac6e499f Merge pull request #141 from JefferyHcool/feature/1.8.0
fix:修复bugs
2025-06-19 17:01:31 +08:00
JefferyHcool
02d2b6d983 fix:修复bugs 2025-06-19 17:00:42 +08:00
Jianwu Huang
2683569a0b Update main.yml 2025-06-19 16:57:41 +08:00
Jianwu Huang
5876b88a8a Update main.yml 2025-06-19 16:28:25 +08:00
Jianwu Huang
58648399a2 Update main.yml 2025-06-19 16:26:58 +08:00
Jianwu Huang
4981e09ede Merge pull request #140 from JefferyHcool/feature/1.8.0
chore:打包测试
2025-06-19 16:24:36 +08:00
Jianwu Huang
29c4926306 Merge branch 'master' into feature/1.8.0 2025-06-19 16:24:26 +08:00
Jianwu Huang
7d9d47d7b7 Create workflow.yml 2025-06-19 16:21:14 +08:00
JefferyHcool
3b3e6b86f3 chore:打包测试 2025-06-19 16:20:32 +08:00
JefferyHcool
d92cc4a977 feat(NoteForm): 增加文件上传状态反馈 2025-06-19 14:54:51 +08:00
Jianwu Huang
4a0f483224 Update README.md 2025-06-08 18:12:54 +08:00
Jianwu Huang
5e63630033 Update README.md 2025-06-08 11:12:03 +08:00
Jianwu Huang
80f1b6b48b Merge pull request #133 from JefferyHcool/feature/1.7.5
fix:修复bugs
2025-06-06 22:16:35 +08:00
JefferyHcool
032446d5eb fix:修复bugs 2025-06-06 22:15:31 +08:00
Jianwu Huang
35ef60b956 Merge pull request #132 from JefferyHcool/feature/1.7.5
Feature/1.7.5
2025-06-06 22:03:53 +08:00
JefferyHcool
cf512e226f feat:优化上传体验 2025-06-06 22:03:12 +08:00
JefferyHcool
2dfc1c068f feat(NoteForm): 增加文件上传状态反馈
- 添加上传中和上传成功状态的显示- 优化上传逻辑,增加状态控制
- 提升用户体验,明确上传过程
2025-06-06 22:02:02 +08:00
Jianwu Huang
2b0fb8f4ad Merge pull request #131 from JefferyHcool/feature/v1.7.4
fix:修复bugs
2025-06-06 21:50:05 +08:00
JefferyHcool
f1cc79aab4 fix:修复bugs 2025-06-06 21:49:07 +08:00
Jianwu Huang
fff4fdc9c9 Merge pull request #126 from JefferyHcool/codex/查找并修复错误
Fix duplicate handler registration
2025-06-06 21:31:45 +08:00
Jianwu Huang
1945586b55 Merge pull request #130 from JefferyHcool/feature/v1.7.4
refactor(backend): 重构后端异常处理和模型管理
2025-06-06 21:31:22 +08:00
JefferyHcool
8b1bc54f2d refactor(backend): 重构后端异常处理和模型管理
- 新增自定义异常类 BizException、NoteError 和 ProviderError
- 优化了模型管理相关的逻辑,包括加载、删除和测试连接等功能
- 改进了 Douyin 下载器的错误处理
- 调整了任务重试逻辑和笔记生成的异常处理- 更新了相关组件和页面以适应新的异常处理机制
2025-06-06 21:30:23 +08:00
Jianwu Huang
707241bf6b Update README.md 2025-06-04 20:26:21 +08:00
Jianwu Huang
b965020491 Fix startup and GPT initialization issues 2025-06-04 09:37:21 +08:00
JefferyHcool
df5c0f771a feat(model): 增加模型管理和测试功能
- 新增模型删除功能
- 实现模型测试连接功能
- 优化模型选择器组件
- 更新模型相关API和数据库操作
2025-05-27 08:53:24 +08:00
JefferyHcool
31f42aa26e feat(model): 增加模型管理和测试功能
- 新增模型删除功能
- 实现模型测试连接功能
- 优化模型选择器组件
- 更新模型相关API和数据库操作
2025-05-27 08:52:38 +08:00
JefferyHcool
be3db5faaf feat(model): 增加模型管理和测试功能
- 新增模型删除功能
- 实现模型测试连接功能
- 优化模型选择器组件
- 更新模型相关API和数据库操作
2025-05-26 23:16:49 +08:00
JefferyHcool
9b298d3094 feat(model): 增加模型管理和测试功能
- 新增模型删除功能
- 实现模型测试连接功能
- 优化模型选择器组件
- 更新模型相关API和数据库操作
2025-05-26 23:16:19 +08:00
黄建武
ee9f6ed80c UPDATE:更新微信二维码 2025-05-25 20:10:57 +08:00
Jianwu Huang
0a43bca0c0 更新 .env.example 2025-05-23 19:05:58 +08:00
Jianwu Huang
9063026d45 Update README.md 2025-05-21 09:10:02 +08:00
Jianwu Huang
32c57b61b5 Update default.conf
修复文件上传大小限制问题
2025-05-21 09:06:48 +08:00
Jianwu Huang
3d91a4f29e Update README.md 2025-05-15 11:35:27 +08:00
Jianwu Huang
44203c5382 Merge pull request #108 from JefferyHcool/feature/kuaishou
feat(nginx): 调整客户端请求体大小限制
2025-05-15 11:35:13 +08:00
黄建武
44d89e3b73 feat(nginx): 调整客户端请求体大小限制
- 设置 client_max_body_size 为 10G,允许上传大文件- 设置 client_body_buffer_size 为 128k,优化请求体缓冲
2025-05-15 11:34:39 +08:00
Jianwu Huang
539d9f3868 Update README.md 2025-05-15 10:10:10 +08:00
Jianwu Huang
d6e7a6e394 Update README.md 2025-05-14 21:25:20 +08:00
Jianwu Huang
d4f18feaf9 Update README.md 2025-05-14 21:21:05 +08:00
Jianwu Huang
f365dfe5de Update .env.example 2025-05-14 21:00:57 +08:00
Jianwu Huang
8c4d59918e Update README.md 2025-05-14 15:30:59 +08:00
Jianwu Huang
53be1f341c Merge pull request #106 from JefferyHcool/feature/kuaishou
feat(db): 添加 Ollama本地离线模型支持
2025-05-14 15:30:05 +08:00
黄建武
aeae3410a0 feat(db): 添加 Ollama本地离线模型支持
- 在 builtin_providers.json 中添加 Ollama 提供商配置
- 修改 OpenAI_compatible_provider.py,优化与 Ollama 的兼容性
2025-05-14 15:28:57 +08:00
Jianwu Huang
41b067305e Merge pull request #103 from JefferyHcool/feature/kuaishou
Feature/kuaishou
2025-05-12 16:15:16 +08:00
黄建武
3862c657b7 build(docker): 更新 docker-compose配置
- 将 Nginx 容器的端口映射改为使用环境变量 APP_PORT- 移除 frontend 服务的依赖
2025-05-12 16:13:05 +08:00
黄建武
1c848c727f docs: 更新 .env.example 文件中的 TRANSCRIBER_TYPE 配置选项
- 调整配置选项的描述,使 groq 成为独立的配置项
-优化注释内容,提高可读性和准确性
2025-05-12 16:04:42 +08:00
Jianwu Huang
4a0bff9919 Merge pull request #102 from JefferyHcool/feature/kuaishou
feat(.env.example): 添加 groq 语音识别模型配置选项
2025-05-12 16:01:27 +08:00
黄建武
a5a523f918 feat(.env.example): 添加 groq 语音识别模型配置选项
- 在 TRANSCRIBER_TYPE 中添加 groq 作为可选的 Apple 平台专用模型
- 新增 GROQ_TRANSCRIBER_MODEL 变量,用于指定 groq 提供的 faster-whisper模型,默认为 whisper-large-v3-turbo
2025-05-12 15:59:04 +08:00
Jianwu Huang
1b78fb6417 Merge pull request #101 from JefferyHcool/feature/kuaishou
feat(backend): 添加 Groq供应商支持并优化笔记生成流程- 在 builtin_providers.json 中添加 Groq 供应商信息
- 实现 GroqTranscriber 类以支持 Groq 语音转录服务
- 新增异常处理中间件以提高系统稳定性
- 优化笔记生成流程,增加错误处理和日志记录
- 添加思维导图功能和相关组件
-重构 Markdown 查看器以支持切换视图模式
2025-05-12 15:39:27 +08:00
Jianwu Huang
c1ef98f6d9 Merge branch 'master' into feature/kuaishou 2025-05-12 15:38:54 +08:00
黄建武
fbb292d0e3 docs(README): 更新 BiliNote 版本号
将 README.md 中的 BiliNote 版本号从 v1.6.0 修改为 v1.7.0
2025-05-12 15:03:10 +08:00
黄建武
6ff8b4d90f feat(backend): 添加 Groq供应商支持并优化笔记生成流程- 在 builtin_providers.json 中添加 Groq 供应商信息
- 实现 GroqTranscriber 类以支持 Groq 语音转录服务
- 新增异常处理中间件以提高系统稳定性
- 优化笔记生成流程,增加错误处理和日志记录
- 添加思维导图功能和相关组件
-重构 Markdown 查看器以支持切换视图模式
2025-05-12 14:59:06 +08:00
Jianwu Huang
490ee11a85 Update README.md 2025-05-12 14:24:28 +08:00
Jianwu Huang
591f0d5ddd Merge pull request #100 from JefferyHcool/feature/kuaishou
feat(db): 更新内置 AI 服务提供商配置
2025-05-12 09:06:41 +08:00
黄建武
b2034c0865 feat(db): 更新内置 AI 服务提供商配置
- 移除Doubao 服务商配置- 添加 Gemini 服务商配置
- 更新 Claude 服务商的 base_url
2025-05-12 09:05:53 +08:00
Jianwu Huang
3239675e69 Merge pull request #98 from JefferyHcool/feature/kuaishou
fix(markdown): 修复 Markdown 组件以提高可读性和维护性
2025-05-09 16:09:04 +08:00
黄建武
137cf81d29 fix(markdown): 修复 Markdown 组件以提高可读性和维护性
- 格式化代码以提高可读性
-优化组件结构以提高维护性- 调整样式和布局以提升用户体验
2025-05-09 16:08:18 +08:00
Jianwu Huang
c6aa6603ef Update README.md 2025-05-09 15:39:21 +08:00
Jianwu Huang
235a044a1a Merge pull request #97 from JefferyHcool/feature/kuaishou
fix:修复视频生成错误
2025-05-09 15:39:05 +08:00
黄建武
1888849270 fix:修复视频生成错误 2025-05-09 15:38:31 +08:00
Jianwu Huang
8c0f637ab1 Merge pull request #96 from JefferyHcool/feature/kuaishou
refactor(env): 优化环境变量配置和请求基地址设置
2025-05-09 13:24:52 +08:00
Jianwu Huang
00ca7e891c Merge pull request #95 from JefferyHcool/feature/kuaishou
refactor(env): 优化环境变量配置和请求基地址设置
2025-05-09 13:24:21 +08:00
黄建武
5e7c381e07 refactor(env): 优化环境变量配置和请求基地址设置
- 移除 request.ts 中的重复 baseURL 配置
- 在 vite.config.ts 中添加默认的 API基地址和前端端口- 更新请求基地址为相对路径,提高可维护性
2025-05-09 13:24:12 +08:00
黄建武
da645291a2 refactor(env): 优化环境变量配置和请求基地址设置
- 移除 request.ts 中的重复 baseURL 配置
- 在 vite.config.ts 中添加默认的 API基地址和前端端口- 更新请求基地址为相对路径,提高可维护性
2025-05-09 13:23:29 +08:00
Jianwu Huang
c7656e5609 Update README.md 2025-05-09 12:45:21 +08:00
Jianwu Huang
faad0fd4d4 Merge pull request #94 from JefferyHcool/feature/kuaishou
refactor(app/utils): 更新 VideoReader 类的目录设置
2025-05-09 12:41:28 +08:00
黄建武
048a3b70df refactor(app/utils): 更新 VideoReader 类的目录设置
- 引入 get_app_dir 函数用于获取应用目录路径
- 修改 frame_dir 和 grid_dir 参数默认值为 None
- 在构造函数中使用 get_app_dir 设置默认目录路径
2025-05-09 12:40:36 +08:00
Jianwu Huang
bab8e3af65 Merge pull request #93 from JefferyHcool/feature/kuaishou
Feature/kuaishou
2025-05-09 11:58:40 +08:00
黄建武
b75caaea0e Merge remote-tracking branch 'origin/master' into feature/kuaishou 2025-05-09 11:58:03 +08:00
黄建武
140c9b1d88 refactor(path_helper): 重构路径获取方法,支持打包运行
- 修改 get_data_dir 函数,以支持打包后可写的运行目录- 新增 get_app_dir 函数,提供更灵活的路径获取方式
- 优化路径处理逻辑,确保在不同环境下都能正确获取路径
2025-05-09 11:57:41 +08:00
黄建武
668785ebe5 refactor(path_helper): 重构路径获取方法,支持打包运行
- 修改 get_data_dir 函数,以支持打包后可写的运行目录- 新增 get_app_dir 函数,提供更灵活的路径获取方式
- 优化路径处理逻辑,确保在不同环境下都能正确获取路径
2025-05-09 11:57:11 +08:00
Jianwu Huang
883b112fc2 Merge pull request #92 from JefferyHcool/release/v1.5.0
refactor(utils): 优化 request.ts 中的 API 地址配置- 提取 base URL 到单独的常量中
2025-05-09 11:13:22 +08:00
黄建武
8e917ee15e refactor(utils): 优化 request.ts 中的 API 地址配置- 提取 base URL 到单独的常量中
- 使用 import.meta.env.VITE_API_BASE_URL 或本地地址作为默认值
-简化了代码结构,提高了可读性和可维护性
2025-05-09 11:12:49 +08:00
Jianwu Huang
5298e6adb3 Merge pull request #91 from JefferyHcool/feature/kuaishou
feat(utils): 添加 API 路径到请求基础 URL
2025-05-09 10:54:44 +08:00
黄建武
17216534cb feat(utils): 添加 API 路径到请求基础 URL
- 在请求基础 URL 中加入 '/api' 路径
-确保无论是生产环境还是本地环境,请求 URL 都包含 API路径
2025-05-09 10:53:51 +08:00
Jianwu Huang
1071da2bed Merge pull request #90 from JefferyHcool/feature/kuaishou
refactor(backend): 更新默认提供商路径获取方法并配置前端请求基础 URL
2025-05-09 10:42:53 +08:00
黄建武
2dfcb600ae refactor(backend): 更新默认提供商路径获取方法并配置前端请求基础 URL
- 新增 get_builtin_providers_path 函数以动态获取内置提供商 JSON 文件路径
- 修改 seed_default_providers 函数,使用新的路径获取方法
- 更新前端请求工具,配置 API 基础 URL 以适应不同环境
2025-05-09 10:41:46 +08:00
Jianwu Huang
274d7b9677 Merge pull request #89 from JefferyHcool/feature/kuaishou
refactor(utils): 更新模型目录获取逻辑以支持打包运行
2025-05-09 10:28:27 +08:00
黄建武
0a5196a475 refactor(utils): 更新模型目录获取逻辑以支持打包运行
-增加对打包状态的判断,使用不同的目录路径
-打包时将模型目录设置为 APPDATA 或 ~/.cache 下的 BiliNote/models
- 开发时仍使用项目根目录下的 models目录
- 确保兼容性和可移植性
2025-05-09 10:27:16 +08:00
Jianwu Huang
892ccd9ee4 Merge pull request #88 from JefferyHcool/release/v1.5.0
refactor(layout): 优化网页布局和路由设置
2025-05-09 09:59:57 +08:00
黄建武
732ea0ba2b refactor(layout): 优化网页布局和路由设置
- 更新 logo显示方式,使用 import 代替直接引用
- 将 BrowserRouter 替换为 HashRouter,以适应前端路由
- 在项目中添加 logo.svg 文件,统一 logo 资源
- 调整 vite.config.ts,设置 base 为 './' 以优化构建
2025-05-09 09:57:13 +08:00
Jianwu Huang
8ed50ba662 Merge pull request #87 from JefferyHcool/feature/kuaishou
refactor(.gitignore and vite.config): 更新忽略文件配置和前端端口设置
2025-05-09 09:07:24 +08:00
黄建武
d4d5e063d0 refactor(.gitignore and vite.config): 更新忽略文件配置和前端端口设置
- 在 .gitignore 文件中添加 backend/config/* 和 BiliNo 到忽略列表
- 移除 BiliNote_frontend/.idea/* 以便于前端项目的重构
- 在 vite.config.ts 中添加前端端口配置,使用环境变量或默认值 3015
- 修改服务器端口配置,使用新设置的前端端口
2025-05-09 09:05:46 +08:00
Jianwu Huang
8e1ab5373f Update README.md 2025-05-08 18:16:54 +08:00
Jianwu Huang
61ca6d2fe6 Merge pull request #83 from JefferyHcool/feature/kuaishou
feat(download): 添加快手下载器并优化下载配置功能
2025-05-08 18:16:37 +08:00
黄建武
21c9d47495 feat(download): 添加快手下载器并优化下载配置功能
- 新增快手下载器,支持快手视频下载
- 添加下载配置页面,可设置各平台Cookies
- 优化后端接口,增加获取和更新Cookies的功能
- 前端新增Downloader组件和相关表单组件
- 更新路由配置,增加下载配置相关路由
2025-05-08 18:15:59 +08:00
Jianwu Huang
321d22271a Merge pull request #82 from JefferyHcool/deploy/docker
chore: 添加 .dockerignore 文件
2025-05-08 15:14:45 +08:00
黄建武
1af6cde68f chore: 添加 .dockerignore 文件
- 新增 .dockerignore 文件,用于配置 Docker 构建过程中需要忽略的文件和目录
- 忽略日志、缓存、临时文件等,以减少 Docker 镜像大小
-排除敏感信息文件,如 .env 等环境变量配置- 忽略开发相关的文件和目录,如 .idea、.pytest_cache 等
2025-05-08 15:13:18 +08:00
Jianwu Huang
5fe78c2a68 Update README.md 2025-05-08 14:46:02 +08:00
Jianwu Huang
17f5bad16d Merge pull request #81 from JefferyHcool/feature/regenerate
feat(transcriber): 使用 ModelScope 替代 Hugging Face 下载模型
2025-05-08 14:45:46 +08:00
黄建武
51fb59e3e1 feat(transcriber): 使用 ModelScope 替代 Hugging Face 下载模型
- 在 requirements.txt 中添加 modelscope 依赖
- 修改 whisper.py 中的模型下载逻辑,使用 ModelScope 的 snapshot_download 函数- 更新 MODEL_MAP 字典,映射不同大小的模型到对应的 ModelScope 仓库
- 调整模型路径,直接使用 ModelScope 下载的路径
2025-05-08 14:42:43 +08:00
Jianwu Huang
3d9cb1aaa9 Update README.md 2025-05-08 11:25:12 +08:00
Jianwu Huang
ae92ec190a Update README.md 2025-05-08 09:18:37 +08:00
Jianwu Huang
832c0fe437 Update README.md 2025-05-06 15:32:42 +08:00
Jianwu Huang
894e34b28d Update README.md 2025-05-06 15:29:28 +08:00
Jianwu Huang
b31588e00d Update README.md 2025-05-06 14:18:02 +08:00
Jianwu Huang
d1f108041b Merge pull request #79 from JefferyHcool/deploy/docker
docs(README): 更新快速开始指南
2025-05-06 14:01:16 +08:00
黄建武
e2757a18b9 docs(README): 更新快速开始指南
- 移除了 .env 文件重命名步骤的单独命令,将其合并到一行中
-简化了端口说明,指出了默认端口为 80
- 更新了访问地址示例,使用默认端口 80
2025-05-06 14:00:29 +08:00
Jianwu Huang
051a099d5f Merge pull request #78 from JefferyHcool/deploy/docker
feat(deploy): 重构部署方案并添加 nginx 代理
2025-05-06 13:57:23 +08:00
黄建武
be4c3313d4 feat(deploy): 重构部署方案并添加 nginx 代理
- 新增 nginx 服务作为前端和后端的代理
- 重新配置前端和后端服务,不再直接暴露端口
- 更新前端 Dockerfile,简化为静态文件服务器- 在 MarkdownViewer 组件中添加 ExternalLink 图标
2025-05-06 13:56:43 +08:00
Jianwu Huang
bab61d8462 Update README.md 2025-05-06 13:16:52 +08:00
Jianwu Huang
41a79d60a5 Merge pull request #77 from JefferyHcool/feature/regenerate
feat: 更新图片路径生成逻辑
2025-05-06 13:16:38 +08:00
黄建武
0bedd7ff6f feat: 更新图片路径生成逻辑- 修改了生成截图 URL 的方式,使用相对路径替代绝对路径- 在前端 Vite 配置中添加了对 /static路径的代理设置 2025-05-06 13:13:31 +08:00
Jianwu Huang
be2a749905 Update README.md 2025-05-06 10:26:47 +08:00
Jianwu Huang
c1b1439510 Merge pull request #75 from JefferyHcool/feature/regenerate
chore:更新版本号
2025-05-04 22:19:08 +08:00
黄建武
03c950eb63 chore:更新版本号 2025-05-04 22:18:18 +08:00
Jianwu Huang
3d8981f970 Merge pull request #74 from JefferyHcool/feature/regenerate
fix(provider): 重新启用通过 ID 获取供应商信息的接口并增强安全性
2025-05-04 17:48:54 +08:00
黄建武
cbc94fafce fix(provider): 重新启用通过 ID 获取供应商信息的接口并增强安全性
- 重新启用了 /get_provider_by_id/{id}接口
- 新增了 get_provider_by_id_safe 方法,用于安全地获取供应商信息
- 将原有的 get_provider_by_id 方法重命名为 get_provider_by_id_safe
2025-05-04 17:48:15 +08:00
Jianwu Huang
d8cec22f54 Merge pull request #73 from JefferyHcool/feature/regenerate
docs(README): 更新版本号至 v1.5.0 并添加新功能说明
2025-05-04 11:04:45 +08:00
黄建武
0f40a99f70 docs(README): 更新版本号至 v1.5.0 并添加新功能说明
- 将版本号从 v1.4.0 更新为 v1.5.0
- 新增功能:支持多版本记录保留
- 修正前端目录名称拼写错误
2025-05-04 11:03:58 +08:00
Jianwu Huang
b9b0e581e7 Merge pull request #72 from JefferyHcool/feature/regenerate
Feature/regenerate: 新增多版本笔记功能,并做了向下兼容。
2025-05-04 11:03:39 +08:00
黄建武
d6b50773b9 Merge remote-tracking branch 'origin/master' into feature/regenerate 2025-05-04 11:01:16 +08:00
黄建武
97f153646f feat(frontend): 新增多版本笔记功能,并做了向下兼容。
- 新增关于页面组件,介绍项目背景、功能和使用方法
- 重构笔记生成逻辑,支持多版本笔记
- 新增笔记版本选择、复制和导出功能
-优化笔记界面布局和交互
- 调整部分组件样式,提升用户体验
2025-05-04 11:00:54 +08:00
Jianwu Huang
6ea9023558 Merge pull request #71 from scdotbox/master
Update video_reader.py
2025-05-03 15:22:44 +08:00
scdotbox
c0746aab57 Update video_reader.py
增加本地视频下载文件的检查
2025-05-03 15:20:04 +08:00
Jianwu Huang
c492f0780b Delete .idea directory 2025-05-03 12:10:28 +08:00
Jianwu Huang
bf9098db3c Merge pull request #68 from JefferyHcool/ui/markdown_perf
feat(MarkdownViewer):增强 Markdown 解析和渲染能力
2025-05-03 02:25:54 +08:00
黄建武
0e055b34ca feat(MarkdownViewer):增强 Markdown 解析和渲染能力
- 添加对 GFM (GitHub Flavored Markdown) 的支持
- 增加数学公式渲染功能
- 实现加粗编号标题的特殊处理
- 优化代码块样式
- 添加图片缩放功能
2025-05-03 02:24:56 +08:00
Jianwu Huang
5fbf84fc36 Merge pull request #67 from JefferyHcool/feature/video_read
feat(note): 添加视频理解功能- 在 GPT 模型中增加 video_img_urls 字段用于存储视频截图
- 在笔记生成请求中添加视频理解相关参数
- 实现视频截图功能,支持按指定间隔生成截图
- 更新笔记生成逻辑,支持视频理解功能- 在前端服务中添加视频理解相
2025-05-03 00:03:44 +08:00
黄建武
bb64936a38 docs(README): 更新项目版本和功能说明
- 将 BiliNote 版本号从 v1.3.0 更新为 v1.4.0
- 新增多模态视频理解功能说明- 添加代码参考部分,说明抖音下载功能的代码来源
-增加 Star History 图标和相关说明
2025-05-03 00:01:02 +08:00
黄建武
749635e156 Merge remote-tracking branch 'origin/master' into feature/video_read
# Conflicts:
#	backend/requirements.txt
2025-05-02 23:54:05 +08:00
黄建武
6e084f720d feat(note): 添加视频理解功能- 在 GPT 模型中增加 video_img_urls 字段用于存储视频截图
- 在笔记生成请求中添加视频理解相关参数
- 实现视频截图功能,支持按指定间隔生成截图
- 更新笔记生成逻辑,支持视频理解功能- 在前端服务中添加视频理解相关参数
2025-05-02 23:47:15 +08:00
Jianwu Huang
23e7104f5a Merge pull request #65 from JefferyHcool/feature/douyin
build: 添加 gmssl 和 pycryptodomex 依赖
2025-05-02 16:11:02 +08:00
黄建武
bbba401637 build: 添加 gmssl 和 pycryptodomex 依赖
- 新增 gmssl 依赖,版本为 3.2.2
- 新增 pycryptodomex依赖,版本为 3.22.0
2025-05-02 16:10:30 +08:00
Jianwu Huang
72daeda465 Merge pull request #63 from JefferyHcool/feature/douyin
Feature/douyin
2025-05-02 15:11:35 +08:00
黄建武
fd3b105821 chore: 忽略 .idea 目录
- 在 .gitignore 文件中添加 .idea/目录,以忽略 IDE 相关的配置文件
2025-05-02 15:10:57 +08:00
黄建武
94220c8b97 Merge remote-tracking branch 'origin/master' into feature/douyin 2025-05-02 14:58:08 +08:00
Jianwu Huang
e4c1c0f7d1 Update README.md 2025-05-02 14:07:34 +08:00
黄建武
58402c6554 feat(env): 添加抖音 Cookie 设置项
- 在 .env.example 文件中添加了 DOUYIN_COOKIES 变量,用于设置抖音 Cookie
2025-05-02 14:04:54 +08:00
Jianwu Huang
244bf73260 Update README.md 2025-05-02 14:03:17 +08:00
Jianwu Huang
f766966802 Update README.md 2025-05-02 14:02:15 +08:00
Jianwu Huang
6496fd097b Merge pull request #62 from JefferyHcool/feature/douyin
feat(downloaders): 添加抖音视频识别功能
2025-05-02 14:01:19 +08:00
黄建武
04dad3b72a feat(downloaders): 添加抖音视频识别功能
- 新增 abogus.py 文件,实现 a_bogus 参数的生成逻辑
- 代码源自 JoeanAmier/TikTokDownloader 项目,并进行了适配和优化
- 功能包括生成用户代理字符串、加密 URL 参数和生成最终的 a_bogus值
- 提供了详细的注释和函数说明,便于理解和维护
2025-05-02 14:00:29 +08:00
Jianwu Huang
7066b4288a Merge pull request #61 from JefferyHcool/fix/dependence
fix(noteForm):修复按钮点击无效
2025-05-01 16:50:51 +08:00
思诺特
1e2a2d33a8 fix(noteForm):修复按钮点击无效 2025-05-01 16:50:16 +08:00
Jianwu Huang
d04c7f50ef Update README.md 2025-05-01 11:36:26 +08:00
Jianwu Huang
0a13a22d1d Update README.md 2025-04-29 22:13:26 +08:00
Jianwu Huang
f3839951bd Merge pull request #60 from JefferyHcool/fix/dependence
refactor(HomePage): 优化笔记表单提交状态显示
2025-04-29 22:12:58 +08:00
黄建武
b66c366a08 refactor(HomePage): 优化笔记表单提交状态显示
- 修改 isGenerating 函数逻辑,增加对任务状态的判断
- 在 onSubmit 函数中添加成功提交任务的提示信息
2025-04-29 22:11:44 +08:00
Jianwu Huang
f3c8deb367 更新 README.md 2025-04-29 00:24:14 +08:00
Jianwu Huang
44e991a9d0 Update README.md 2025-04-28 13:47:38 +08:00
Jianwu Huang
ad730bd52d Update README.md 2025-04-28 13:47:04 +08:00
Jianwu Huang
1309a592df Update README.md 2025-04-28 13:38:08 +08:00
Jianwu Huang
eea22fb1c5 Merge pull request #58 from JefferyHcool/feature/local_video
feat(local): 添加本地视频处理功能
2025-04-28 13:36:21 +08:00
思诺特
c65de4654f feat(local): 添加本地视频处理功能
- 实现本地视频上传和处理功能
- 新增 LocalDownloader 类处理本地视频
- 更新前端界面支持本地视频选择
- 添加视频封面提取和保存功能
- 优化后端路由支持本地视频上传
2025-04-28 13:34:09 +08:00
Jianwu Huang
eb0a46183d Update issue templates 2025-04-28 09:54:29 +08:00
Jianwu Huang
75541f3d34 Update issue templates 2025-04-27 23:26:36 +08:00
Jianwu Huang
c037c4b385 Merge pull request #55 from JefferyHcool/fix/dependence
fix(modelStore): 修复模型列表加载逻辑,兼容通义模型
2025-04-27 23:20:15 +08:00
黄建武
06e0eb2ce3 fix(modelStore): 修复模型列表加载逻辑,兼容通义模型
- 增加了对 res.data.data.models.length > 0 的判断条件
- 当模型列表存在时,更新 models状态
- 优化了错误处理逻辑
2025-04-27 23:19:33 +08:00
Jianwu Huang
02688f1600 Merge pull request #54 from JefferyHcool/fix/dependence
fix(HomePage): 调整生成笔记按钮的位置,解决按钮失效问题
2025-04-27 23:15:32 +08:00
黄建武
bd68ba35b9 fix(HomePage): 调整生成笔记按钮的位置,解决按钮失效问题 2025-04-27 23:14:50 +08:00
Jianwu Huang
885083e8e6 Merge pull request #53 from JefferyHcool/fix/dependence
fix(frontend): 修正 Error 组件的动画文件引用
2025-04-27 22:46:24 +08:00
黄建武
e24979f6f4 fix(frontend): 修正 Error 组件的动画文件引用
- 将 error.json 文件名首字母大写,统一为 Error.json- 更新 Lottie 组件的动画文件引用
- 后端更新依赖文件
2025-04-27 22:45:49 +08:00
Jianwu Huang
6fcffb635e Merge pull request #51 from JefferyHcool/fix/ui
fix(layout): 优化首页布局并添加可调整面板 fixes #50
2025-04-27 21:56:37 +08:00
思诺特
246e8a1406 fix(layout): 优化首页布局并添加可调整面板 fixes #123
- 使用 react-resizable-panels 实现可调整大小的面板
- 重新布局首页结构,分为左、中、右三个可调整区域
- 更新 NoteForm 和 NoteHistory 组件以适应新布局
- 调整 History 组件样式,优化滚动体验
- 更新项目依赖,添加 react-resizable-panels
2025-04-27 21:55:38 +08:00
Jianwu Huang
508a0efd92 Update README.md 2025-04-27 19:02:50 +08:00
Jianwu Huang
89ceef60d0 Merge pull request #48 from JefferyHcool/dev
feat(frontend): 重构首页布局并添加生成历史组件
2025-04-27 17:00:43 +08:00
思诺特
84cd345b9f feat(frontend): 重构首页布局并添加生成历史组件
- 新增 History 组件用于展示生成历史记录
- 调整 HomeLayout 布局,增加 History 侧边栏
- 优化 NoteHistory 组件样式和布局- 更新首页样式,调整各个组件的位置和样式
2025-04-27 16:57:03 +08:00
Jianwu Huang
446e2a60c7 Merge pull request #47 from JefferyHcool/dev
fix(env): 修复 Youtube 转义失败问题
2025-04-27 14:46:43 +08:00
思诺特
489fa78946 fix(env): 修复 Youtube 转义失败问题 2025-04-27 14:45:57 +08:00
Jianwu Huang
1291910961 Merge pull request #46 from JefferyHcool/dev
fix:修复文件缺失问题
2025-04-27 13:22:25 +08:00
思诺特
c1237996e7 fix:修复文件缺失问题 2025-04-27 13:21:37 +08:00
Jianwu Huang
8ff074c0d9 Merge pull request #45 from JefferyHcool/dev
fix:修复文件缺失问题
2025-04-27 13:08:09 +08:00
思诺特
f43c423a65 fix:修复文件缺失问题 2025-04-27 13:06:59 +08:00
Jianwu Huang
af18ba0250 Update README.md 2025-04-27 09:26:55 +08:00
Jianwu Huang
6055118539 Merge pull request #44 from JefferyHcool/dev
refactor(backend): 重构后端配置并优化 GPT 模型及转录功能
2025-04-27 09:06:11 +08:00
思诺特
30da57ddab refactor(backend): 重构后端配置并优化 GPT 模型及转录功能
-移除 .env.example 中的后端专用 AI 配置
- 在 GPT 模型中添加新字段:style、extras 和 _format
- 修改转录器的 on_finish 方法调用
- 更新 GPT 提示模板,增加时间标记要求
2025-04-27 09:05:37 +08:00
Jianwu Huang
03300d86c3 Merge pull request #42 from JefferyHcool/dev
feat: 新增模型管理和供应商配置功能
2025-04-26 23:42:19 +08:00
思诺特
171dea5e0d feat: 新增模型管理和供应商配置功能
### v1.1.0
- #### Added
  - 新增 AI 笔记风格选择
  - 新增 AI 笔记返回格式选择
  - 添加 AI 自定义笔记备注 Prompt
  - 添加任务失败重试
  - 添加全局设置页,可在设置页进行模型设置

- #### Optimize
  - 优化前端样式,优化用户体验
  - 增加生成中间产物,可用于失败后加快生成速度
- #### Fix
  - 修复视频截图视频过早删除错误
2025-04-26 23:40:17 +08:00
思诺特
1323cfd1ec Merge remote-tracking branch 'origin/master' into dev 2025-04-26 11:24:56 +08:00
Jianwu Huang
d519f284e9 Merge pull request #40 from SurfRid3r/master
fix(transcriber): 优化获取转录器实例的返回值,确保返回正确的 fast-whisper 实例
2025-04-26 11:21:33 +08:00
思诺特
7cfade6f78 Merge remote-tracking branch 'origin/master' into dev
# Conflicts:
#	backend/app/transcriber/transcriber_provider.py
2025-04-26 11:19:34 +08:00
SurfRid3r
d26655a0a2 Merge pull request #3 from SurfRid3r/fix_transcriber_surfrid3r
fix(transcriber): 优化获取转录器实例的返回值,确保返回正确的 fast-whisper 实例
2025-04-26 00:28:44 +08:00
SurfRid3r
eff6e7fe75 fix(transcriber): 优化获取转录器实例的返回值,确保返回正确的 fast-whisper 实例 2025-04-26 00:10:04 +08:00
Jianwu Huang
30e6a6ddd7 Merge pull request #37 from JefferyHcool/hotfix/1.0.1
fix(transcriber):  修复转义器初始化异常的问题,fixes #36
2025-04-25 11:58:58 +08:00
思诺特
84da8ba7c2 fix(transcriber): 修复转义器初始化异常的问题,fixes #36 2025-04-25 11:57:53 +08:00
Jianwu Huang
4012a45c93 Update README.md 2025-04-25 08:57:00 +08:00
Jianwu Huang
76ce0f58ef Update README.md
更新微信群二维码
2025-04-24 09:18:25 +08:00
Jianwu Huang
82e69734ee Update README.md
更新微信群二维码
2025-04-24 09:14:10 +08:00
Jianwu Huang
b17a6f39a5 Merge pull request #29 from SurfRid3r/dev_surfrid3r
feat:将transcriber作为环境变量配置并增加了 MLX‑Whisper 转录器,提升 macOS 平台转录性能
2025-04-24 09:09:36 +08:00
Jefferyhcool
842ae97883 update:404.json 2025-04-22 17:36:52 +08:00
Jefferyhcool
bb974b0b89 :feat 新增模型配置页面和相关功能
- 新增模型配置页面组件和路由
- 实现模型配置表单和相关逻辑- 添加全局配置入口和功能- 优化首页布局和样式- 新增 404 页面组件
- 更新部分组件样式和结构
2025-04-22 17:01:02 +08:00
SurfRid3r
7344d93053 Merge pull request #2 from SurfRid3r/dev_surfrid3r_mlxwhisper
feat: 引入 MLX‑Whisper 转录器,提升 macOS 平台whisper转录性能
2025-04-20 23:03:50 +08:00
SurfRid3r
a567788448 feat(transcriber): 更新转录器支持和模型下载逻辑
- 修改 NoteGenerator 类以支持动态选择转录器类型。
- 更新 MLXWhisperTranscriber 类,添加模型下载逻辑,确保模型存在时自动下载。
- 在 transcriber_provider.py 中优化 MLX Whisper 的环境变量处理,确保在不可用时回退到 fast-whisper。
2025-04-20 22:50:07 +08:00
SurfRid3r
369de19572 feat(transcriber): 添加 MLX Whisper 转录器支持
- 新增 MLXWhisperTranscriber 类,支持在 Apple 平台上进行转录。
- 更新 transcriber_provider.py,动态导入 MLX Whisper 转录器并添加相应的环境变量检查。
- 修改 .env.example 文件,更新 TRANSCRIBER_TYPE 配置说明以包含 mlx-whisper 选项。
2025-04-20 00:37:48 +08:00
SurfRid3r
0b7f6ca4ee feat:将transcriber作为环境变量配置 2025-04-19 23:13:49 +08:00
Jianwu Huang
2aad103a77 Update README.md
修改下载地址到最新的release地址
2025-04-18 14:49:46 +08:00
Jianwu Huang
c21669d518 Update README.md 2025-04-18 10:44:30 +08:00
Jianwu Huang
40450199f2 Merge pull request #22 from JefferyHcool/hotfix/1.0.1
refactor: 更新环境变量配置示例
2025-04-18 09:58:15 +08:00
思诺特
d7e800697b refactor: 更新环境变量配置示例 2025-04-18 09:56:14 +08:00
Jianwu Huang
e845b3011b Merge pull request #21 from JefferyHcool/hotfix/1.0.1
fix(env): 修复其他提供商没有办法使用识别问题
2025-04-18 09:54:23 +08:00
思诺特
58f9a57886 fix(env): 修复其他提供商没有办法使用识别问题 2025-04-18 09:47:16 +08:00
210 changed files with 24211 additions and 1753 deletions

321
.dockerignore Normal file
View File

@@ -0,0 +1,321 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.DS_Store
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
BiliNote/pnpm-lock.yaml
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
.BiliNote-dev/*
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
.idea/
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
/backend/data/*
/backend/static/*
/backend/note_tasks.db
/backend/bin/
/backend/logs/
/backend/note_results
/backend/models
/backend/.idea/*
/backend/bili_note.db
/backend/uploads/*
/BiliNote_frontend/.idea/*

View File

@@ -1,30 +1,24 @@
# 通用端口配置
BACKEND_PORT=8001
BACKEND_PORT=8483 # 后端端口
FRONTEND_PORT=3015
BACKEND_HOST=0.0.0.0 # 默认为 0.0.0.0,表示监听所有 IP 地址 不建议动
# 前端访问后端用(生产环境建议写公网或宿主机 IP
VITE_API_BASE_URL=http://127.0.0.1:8001
VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8001/static/screenshots
APP_PORT= 3015 # docker 部署时用
# 前端访问后端用 (开发环境使用)
VITE_API_BASE_URL=http://127.0.0.1:8483
VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8483/static/screenshots
VITE_FRONTEND_PORT=3015
# 生产环境配置
ENV=production
STATIC=/static
OUT_DIR=./static/screenshots
NOTE_OUTPUT_DIR=note_results
IMAGE_BASE_URL=/static/screenshots
DATA_DIR=data
# AI 相关配置
OPENAI_API_KEY=
OPENAI_API_BASE_URL=
OPENAI_MODEL=
DEEP_SEEK_API_KEY=
DEEP_SEEK_API_BASE_URL=
DEEP_SEEK_MODEL=
QWEN_API_KEY=
QWEN_API_BASE_URL=
QWEN_MODEL=
# FFMPEG 配置
FFMPEG_BIN_PATH=
FFMPEG_BIN_PATH=
# transcriber 相关配置
TRANSCRIBER_TYPE=fast-whisper # fast-whisper/bcut/kuaishou/mlx-whisper(仅Apple平台)/groq
WHISPER_MODEL_SIZE=base
GROQ_TRANSCRIBER_MODEL=whisper-large-v3-turbo # groq提供的faster-whisper 默认为 whisper-large-v3-turbo

49
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,49 @@
---
name: Bug report
about: 上报一些bug
title: "[BUG]"
labels: bug
assignees: JefferyHcool
---
---
name: 🐛 Bug 反馈
about: 提交一个 Bug 报告,帮助我们改进
title: "[Bug] "
labels: bug
assignees: ''
---
**版本说明**
请说明的你的版本号
**部署方式**
使用的是什么方式部署代码环境部署docker部署桌面端在线预览
**描述问题**
清晰、简明地描述你遇到的问题是什么。
**复现步骤**
复现该问题的步骤:
1. 进入页面 '...'
2. 点击 '...'
3. 滚动到 '...'
4. 出现错误
**预期行为**
清晰、简明地描述你本来预期发生的行为。
**截图**
如果适用,请添加截图以帮助说明问题。
**桌面端(请补充以下信息)**
- 操作系统:例如 Windows / macOS / Ubuntu
- 浏览器:例如 Chrome、Safari
**其他补充信息**
请补充任何其他相关信息。

View File

@@ -0,0 +1,29 @@
---
name: 新增功能建议
about: 一些新的功能建议
title: "[FEATHURE]"
labels: enhancement
assignees: JefferyHcool
---
---
name: ✨ 功能请求
about: 提出一个新的功能建议
title: "[Feature] "
labels: enhancement
assignees: ''
---
**这个功能请求是否与某个问题相关?请描述**
清晰简要地描述问题是什么。例如:每次遇到 [...] 都让我感到很沮丧。
**描述你希望实现的解决方案**
清晰简要地描述你希望发生的事情。
**描述你考虑过的备选方案**
清晰简要地描述你考虑过的其他解决方案或功能。
**其他补充信息**
请在此添加关于功能请求的其他上下文或截图。

67
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
# .github/workflows/release.yml
name: Build Desktop App (Python Backend + Tauri Frontend)
on:
push:
tags:
- 'v*' # 发布 tag 时触发
workflow_dispatch:
jobs:
build:
strategy:
matrix:
platform: [macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
# 设置 Python 环境
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
# 安装 Python 依赖并执行你的 build.sh
- name: Install Python dependencies & Build backend
shell: bash
run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt
if [ "$RUNNER_OS" = "Windows" ]; then
backend\\build.bat
else
chmod +x backend/build.sh
./backend/build.sh
fi
# 设置 Node 环境 + 安装前端依赖
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Enable Corepack + Install pnpm
working-directory: BillNote_frontend
run: |
corepack enable
pnpm install
# 设置 Rust 环境
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
# 打包 Tauri 应用
- name: Build Tauri App
working-directory: BillNote_frontend
run: pnpm tauri build
# 可选:上传构建产物
- name: Upload Desktop Bundle
uses: actions/upload-artifact@v4
with:
name: app-${{ matrix.platform }}
path: BillNote_frontend/src-tauri/target/release/bundle/

10
.gitignore vendored
View File

@@ -190,7 +190,7 @@ cover/
# Translations
*.mo
*.pot
.idea/
# Django stuff:
*.log
local_settings.py
@@ -315,4 +315,10 @@ cython_debug/
/backend/logs/
/backend/note_results
/backend/models
/backend/.idea
/backend/.idea/*
/backend/bili_note.db
/backend/uploads/*
/backend/.idea/*
/backend/config/*
/BiliNote_frontend/.idea/*
/BiliNote_frontend/src-tauri/bin/

View File

@@ -1,3 +0,0 @@
# 前端专用
VITE_API_BASE_URL=http://127.0.0.1:8000
VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8000/static/screenshots

View File

@@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://127.0.0.1:8483/api
VITE_PLATFORM=tauri

View File

@@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
/pnpm-lock.yaml
/src-tauri/bin/

View File

@@ -0,0 +1,8 @@
dist
build
node_modules
*.svg
*.lock
*.png
public
coverage

View File

@@ -0,0 +1,11 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -1,5 +1,5 @@
# === 前端构建阶段 ===
FROM node:18-alpine AS build
FROM node:18-alpine AS builder
# 安装 pnpm
RUN npm install -g pnpm
@@ -13,20 +13,13 @@ COPY ./BillNote_frontend /app
# 安装依赖并构建
RUN pnpm install && pnpm run build
# === nginx 运行阶段 ===
FROM nginx:alpine
# --- 阶段2使用 nginx 作为静态服务器 ---
FROM nginx:1.25-alpine
# 删除默认配置(可选)
RUN rm -rf /etc/nginx/conf.d/default.conf
COPY ./BillNote_frontend/deploy/default.conf /etc/nginx/conf.d/default.conf
# 拷贝模板配置
COPY ./BillNote_frontend/deploy/default.conf.template /etc/nginx/templates/default.conf.template
# 拷贝构建产物
COPY --from=build /app/dist /usr/share/nginx/html
# 拷贝启动脚本
COPY ./BillNote_frontend/deploy/start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 80
# 使用启动脚本启动容器
CMD ["/start.sh"]
COPY --from=builder /app/dist /usr/share/nginx/html

View File

@@ -18,4 +18,4 @@
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
}

View File

@@ -0,0 +1,11 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
}

View File

@@ -19,10 +19,7 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
}
)

View File

@@ -11,40 +11,62 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@lobehub/icons": "^1.97.1",
"@lobehub/icons-static-svg": "^1.45.0",
"@lottiefiles/dotlottie-react": "^0.13.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.3",
"@tauri-apps/plugin-shell": "~2.2.2",
"@uiw/react-markdown-preview": "^5.1.3",
"antd": "^5.24.8",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",
"github-markdown-css": "^5.8.1",
"katex": "^0.16.21",
"katex": "^0.16.22",
"lottie-react": "^2.4.1",
"lucide-react": "^0.487.0",
"markdown-navbar": "^1.4.3",
"markmap-common": "^0.18.9",
"markmap-lib": "^0.18.11",
"markmap-toolbar": "^0.18.10",
"markmap-view": "^0.18.10",
"next-themes": "^0.4.6",
"pinyin-match": "^1.2.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.55.0",
"react-hot-toast": "^2.5.2",
"react-markdown": "^10.1.0",
"react-intersection-observer": "^9.16.0",
"react-markdown": "^8.0.7",
"react-medium-image-zoom": "^5.2.14",
"react-resizable-panels": "^2.1.8",
"react-router-dom": "^7.5.1",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "1.0.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "3.0.1",
"remark-math": "^5.1.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.1.0",
"tailwindcss": "^4.1.3",
"tw-animate-css": "^1.2.5",
"uuid": "^11.1.0",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4.1.3",
"@tauri-apps/cli": "^2.5.0",
"@types/node": "^22.14.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
@@ -54,6 +76,8 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

5027
BillNote_frontend/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.5.0", features = ["devtools"] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2"
[package.metadata.tauri.bundle.macOS]
frameworks = ["bin/BiliNoteBackend/_internal/"]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "BiliNoteBackend",
"sidecar": true
}
]
},
"shell:allow-open"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,285 @@
use tauri::{Manager, Emitter};
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use std::env;
use std::collections::HashMap;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
let exe_path = env::current_exe().expect("无法获取当前可执行文件路径");
let sidecar_dir = exe_path.parent().expect("无法获取可执行文件的父目录");
// 收集所有系统环境变量
let mut all_env_vars = HashMap::new();
for (key, value) in env::vars() {
all_env_vars.insert(key, value);
}
// 增强 PATH 环境变量,添加常见的二进制路径
let current_path = all_env_vars.get("PATH").cloned().unwrap_or_default();
let additional_paths = get_additional_binary_paths();
let enhanced_path = enhance_path_variable(&current_path, &additional_paths);
all_env_vars.insert("PATH".to_string(), enhanced_path);
// 打印一些关键环境变量用于调试
println!("Enhanced PATH: {}", all_env_vars.get("PATH").unwrap_or(&"Not found".to_string()));
println!("Total environment variables: {}", all_env_vars.len());
// 检查 ffmpeg 是否在 PATH 中可用
check_ffmpeg_availability();
// 启动 Python 后端侧车
let mut sidecar_command = app.shell().sidecar("BiliNoteBackend").unwrap();
// 设置所有环境变量到 sidecar
for (key, value) in &all_env_vars {
sidecar_command = sidecar_command.env(key, value);
}
let (mut rx, _child) = sidecar_command
.current_dir(sidecar_dir)
.spawn()
.expect("Failed to spawn sidecar");
// 获取主窗口句柄用于发送事件
let window = app.get_webview_window("main").unwrap();
tauri::async_runtime::spawn(async move {
// 读取诸如 stdout 之类的事件
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let output = String::from_utf8_lossy(&line);
println!("Backend stdout: {}", output);
// 发送到前端
window
.emit("backend-message", Some(format!("'{}'", output)))
.expect("failed to emit event");
}
CommandEvent::Stderr(line) => {
let error = String::from_utf8_lossy(&line);
eprintln!("Backend stderr: {}", error);
window
.emit("backend-error", Some(format!("'{}'", error)))
.expect("failed to emit event");
}
CommandEvent::Terminated(payload) => {
println!("Backend terminated with code: {:?}", payload.code);
window
.emit("backend-terminated", Some(payload.code))
.expect("failed to emit event");
break;
}
_ => {
println!("Backend event: {:?}", event);
}
}
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_system_env_vars,
find_executable_path,
run_command_with_env,
test_ffmpeg_access
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// 获取额外的二进制路径
fn get_additional_binary_paths() -> Vec<String> {
if cfg!(target_os = "windows") {
vec![
"C:\\ffmpeg\\bin".to_string(),
"C:\\Program Files\\ffmpeg\\bin".to_string(),
"C:\\Program Files (x86)\\ffmpeg\\bin".to_string(),
"C:\\tools\\ffmpeg\\bin".to_string(),
"C:\\ProgramData\\chocolatey\\bin".to_string(),
]
} else if cfg!(target_os = "macos") {
vec![
"/usr/local/bin".to_string(),
"/opt/homebrew/bin".to_string(),
"/usr/bin".to_string(),
"/bin".to_string(),
"/opt/local/bin".to_string(), // MacPorts
]
} else {
vec![
"/usr/local/bin".to_string(),
"/usr/bin".to_string(),
"/bin".to_string(),
"/snap/bin".to_string(),
"/opt/bin".to_string(),
"/usr/local/sbin".to_string(),
]
}
}
// 增强 PATH 环境变量
fn enhance_path_variable(current_path: &str, additional_paths: &[String]) -> String {
let path_separator = if cfg!(target_os = "windows") { ";" } else { ":" };
let mut paths: Vec<String> = additional_paths.to_vec();
// 添加当前 PATH
if !current_path.is_empty() {
paths.push(current_path.to_string());
}
paths.join(path_separator)
}
// 检查 ffmpeg 可用性
fn check_ffmpeg_availability() {
use std::process::Command;
match Command::new("ffmpeg").arg("-version").output() {
Ok(output) => {
if output.status.success() {
println!("✓ FFmpeg is available in PATH");
let version_info = String::from_utf8_lossy(&output.stdout);
let first_line = version_info.lines().next().unwrap_or("Unknown version");
println!("FFmpeg version: {}", first_line);
} else {
println!("✗ FFmpeg found but returned error");
}
}
Err(e) => {
println!("✗ FFmpeg not found in PATH: {}", e);
// 尝试在常见路径中查找
let common_paths = get_additional_binary_paths();
for path in common_paths {
let ffmpeg_path = if cfg!(target_os = "windows") {
format!("{}\\ffmpeg.exe", path)
} else {
format!("{}/ffmpeg", path)
};
if std::path::Path::new(&ffmpeg_path).exists() {
println!("✓ Found FFmpeg at: {}", ffmpeg_path);
return;
}
}
println!("✗ FFmpeg not found in common installation paths");
}
}
}
// Tauri 命令:获取系统环境变量
#[tauri::command]
fn get_system_env_vars() -> HashMap<String, String> {
env::vars().collect()
}
// Tauri 命令:查找可执行文件路径
#[tauri::command]
fn find_executable_path(executable_name: String) -> Option<String> {
use std::process::Command;
// 首先尝试直接执行
if Command::new(&executable_name).arg("--version").output().is_ok() {
return Some(executable_name);
}
// 使用 which/where 命令查找
let which_cmd = if cfg!(target_os = "windows") { "where" } else { "which" };
if let Ok(output) = Command::new(which_cmd).arg(&executable_name).output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(path);
}
}
}
// 在常见路径中搜索
let common_paths = get_additional_binary_paths();
for base_path in common_paths {
let executable_path = if cfg!(target_os = "windows") {
format!("{}\\{}.exe", base_path, executable_name)
} else {
format!("{}/{}", base_path, executable_name)
};
if std::path::Path::new(&executable_path).exists() {
return Some(executable_path);
}
}
None
}
// Tauri 命令:使用完整环境变量运行命令
#[tauri::command]
async fn run_command_with_env(
program: String,
args: Vec<String>
) -> Result<String, String> {
use std::process::Command;
let mut cmd = Command::new(&program);
cmd.args(&args);
// 设置所有环境变量
for (key, value) in env::vars() {
cmd.env(key, value);
}
// 增强 PATH
let current_path = env::var("PATH").unwrap_or_default();
let additional_paths = get_additional_binary_paths();
let enhanced_path = enhance_path_variable(&current_path, &additional_paths);
cmd.env("PATH", enhanced_path);
match cmd.output() {
Ok(output) => {
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
Err(e) => Err(format!("Failed to execute {}: {}", program, e))
}
}
// Tauri 命令:测试 ffmpeg 访问
#[tauri::command]
async fn test_ffmpeg_access() -> Result<String, String> {
run_command_with_env("ffmpeg".to_string(), vec!["-version".to_string()]).await
}
// 可选:添加一个函数来动态更新 sidecar 的环境变量
#[tauri::command]
async fn update_sidecar_environment(
app_handle: tauri::AppHandle,
additional_env_vars: HashMap<String, String>
) -> Result<(), String> {
// 这个函数可以用来在运行时更新环境变量
// 注意:这需要重启 sidecar 才能生效
for (key, value) in additional_env_vars {
env::set_var(key, value);
}
Ok(())
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

View File

@@ -0,0 +1,46 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "BiliNote",
"version": "1.8.1",
"identifier": "com.jefferyhuang.bilinote",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3015",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build --mode tauri "
},
"app": {
"windows": [
{
"title": "BiliNote",
"width": 1600,
"height": 1000,
"resizable": true,
"fullscreen": false,
"devtools": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"externalBin": [
"bin/BiliNoteBackend/BiliNoteBackend"
],
"resources": {
"bin/BiliNoteBackend/_internal":"_internal"
},
"macOS":{
"files": {
"Frameworks": "bin/BiliNoteBackend/_internal"
}
},
"active": true,
"targets": "all",
"icon": [
"icons/icon.ico",
"icons/icon.png"
]
}
}

View File

@@ -1,16 +1,70 @@
import './App.css'
import {HomePage} from "./pages/Home.tsx";
import {useTaskPolling} from "@/hooks/useTaskPolling.ts";
import { HomePage } from './pages/HomePage/Home.tsx'
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
import SettingPage from './pages/SettingPage/index.tsx'
import { BrowserRouter, HashRouter, Navigate, Routes } from 'react-router-dom'
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'
import Prompt from '@/pages/SettingPage/Prompt.tsx'
import AboutPage from '@/pages/SettingPage/about.tsx'
import Downloader from '@/pages/SettingPage/Downloader.tsx'
import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx'
import { useEffect } from 'react'
import { systemCheck } from '@/services/system.ts'
import { useCheckBackend } from '@/hooks/useCheckBackend.ts'
import BackendInitDialog from '@/components/BackendInitDialog'
function App() {
useTaskPolling(3000) // 每 3 秒轮询一次
useTaskPolling(3000) // 每 3 秒轮询一次
const { loading, initialized } = useCheckBackend()
// 在后端初始化完成后执行系统检查
useEffect(() => {
if (initialized) {
systemCheck()
}
}, [initialized])
// 如果后端还未初始化,显示初始化对话框
if (!initialized) {
return (
<>
<HomePage></HomePage>
</>
<>
<BackendInitDialog open={loading} />
</>
)
}
// 后端已初始化,渲染主应用
return (
<>
<HashRouter>
<Routes>
<Route path="/" element={<Index />}>
<Route index element={<HomePage />} />
<Route path="settings" element={<SettingPage />}>
<Route index element={<Navigate to="model" replace />} />
<Route path="model" element={<Model />}>
<Route path="new" element={<ProviderForm isCreate />} />
<Route path=":id" element={<ProviderForm />} />
</Route>
<Route path="download" element={<Downloader />}>
<Route path=":id" element={<DownloaderForm />} />
</Route>
<Route path="about" element={<AboutPage />}></Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</HashRouter>
</>
)
}
export default App
export default App

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -0,0 +1,12 @@
<svg width="415" height="412" viewBox="0 0 415 412" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 28C0 12.536 12.536 0 28 0H387C402.464 0 415 12.536 415 28V384C415 399.464 402.464 412 387 412H28C12.536 412 0 399.464 0 384V28Z" fill="#3C77FB"/>
<rect x="60" y="64" width="296" height="283" rx="37" fill="white"/>
<path d="M268.422 175.657C276.308 180.298 276.308 191.702 268.422 196.343L186.335 244.641C178.336 249.348 168.25 243.58 168.25 234.298V137.702C168.25 128.42 178.336 122.652 186.335 127.359L268.422 175.657Z" fill="#3C77FB"/>
<path d="M17 282C17 270.954 25.9543 262 37 262H83C94.0457 262 103 270.954 103 282V282C103 293.046 94.0457 302 83 302H37C25.9543 302 17 293.046 17 282V282Z" fill="#3C77FB"/>
<path d="M38 281.5C38 274.044 44.0442 268 51.5 268H82.5C89.9558 268 96 274.044 96 281.5V281.5C96 288.956 89.9558 295 82.5 295H51.5C44.0442 295 38 288.956 38 281.5V281.5Z" fill="white"/>
<path d="M17 206C17 194.954 25.9543 186 37 186H83C94.0457 186 103 194.954 103 206V206C103 217.046 94.0457 226 83 226H37C25.9543 226 17 217.046 17 206V206Z" fill="#3C77FB"/>
<path d="M38 205.5C38 198.044 44.0442 192 51.5 192H82.5C89.9558 192 96 198.044 96 205.5V205.5C96 212.956 89.9558 219 82.5 219H51.5C44.0442 219 38 212.956 38 205.5V205.5Z" fill="white"/>
<path d="M17 130C17 118.954 25.9543 110 37 110H83C94.0457 110 103 118.954 103 130V130C103 141.046 94.0457 150 83 150H37C25.9543 150 17 141.046 17 130V130Z" fill="#3C77FB"/>
<path d="M38 129.5C38 122.044 44.0442 116 51.5 116H82.5C89.9558 116 96 122.044 96 129.5V129.5C96 136.956 89.9558 143 82.5 143H51.5C44.0442 143 38 136.956 38 129.5V129.5Z" fill="white"/>
<path d="M145 290C145 285.582 148.582 282 153 282H284C288.418 282 292 285.582 292 290V299C292 303.418 288.418 307 284 307H153C148.582 307 145 303.418 145 299V290Z" fill="#3C77FB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Loader2 } from 'lucide-react'
interface Props {
open: boolean
}
function BackendInitDialog({ open }: Props) {
return (
<Dialog open={open}>
<DialogContent className="text-center">
<DialogHeader>
<DialogTitle className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin w-5 h-5" />
</DialogTitle>
</DialogHeader>
<p className="text-muted-foreground mt-2">,</p>
</DialogContent>
</Dialog>
)
}
export default BackendInitDialog

View File

@@ -0,0 +1,95 @@
// 下载器 Cookie 设置表单(最简化版)
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { getDownloaderCookie, updateDownloaderCookie } from '@/services/downloader' // 你自定义的请求
import { useParams } from 'react-router-dom'
import { videoPlatforms } from '@/constant/note.ts'
const CookieSchema = z.object({
cookie: z.string().min(10, '请填写有效 Cookie'),
})
const DownloaderForm = () => {
const form = useForm({
resolver: zodResolver(CookieSchema),
defaultValues: { cookie: '' },
})
const { id } = useParams()
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadCookie = async () => {
setLoading(true) // 🔁 切换平台时显示 loading
try {
const res = await getDownloaderCookie(id)
const cookie = res?.cookie || ''
form.reset({ cookie }) // ✅ 正确重置表单值
} catch (e) {
toast.error('加载 Cookie 失败: ' + e)
form.reset({ cookie: '' }) // ❗失败时也要清空旧值
} finally {
setLoading(false)
}
}
if (id) loadCookie()
}, [id]) // 🔁 每当 id 变化时触发
const onSubmit = async values => {
try {
await updateDownloaderCookie({
platform: id,
cookie: String(values.cookie),
})
toast.success('保存成功')
} catch (e) {
toast.error('保存失败')
}
}
if (loading) return <div className="p-4">...</div>
return (
<div className="max-w-xl p-4">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
<div className="text-lg font-bold">
{videoPlatforms.find(item => item.value === id)?.label} Cookie
</div>
<FormField
control={form.control}
name="cookie"
render={({ field }) => (
<FormItem className="flex flex-col gap-2">
<FormLabel>Cookie</FormLabel>
<FormControl>
<Input {...field} placeholder="输入 Cookie" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit"></Button>
</form>
</Form>
</div>
)
}
export default DownloaderForm

View File

@@ -0,0 +1,34 @@
import ProviderCard from '@/components/Form/DownloaderForm/providerCard.tsx'
import { Button } from '@/components/ui/button.tsx'
import { useProviderStore } from '@/store/providerStore'
import { useNavigate } from 'react-router-dom'
import { DouyinLogo, KuaishouLogo } from '@/components/Icons/platform.tsx'
import { videoPlatforms } from '@/constant/note.ts'
const Provider = () => {
const navigate = useNavigate()
const handleClick = () => {
navigate(`/settings/model/new`)
}
return (
<div className="flex flex-col gap-2">
<div className="text-sm font-light"></div>
<div>
{videoPlatforms &&
videoPlatforms.map((provider, index) => {
if (provider.value !== 'local')
return (
<ProviderCard
key={index}
providerName={provider.label}
Icon={provider?.logo}
id={provider.value}
/>
)
})}
</div>
</div>
)
}
export default Provider

View File

@@ -0,0 +1,6 @@
.card {
transition: all 0.2s ease-in-out;
}
.card:hover {
background-color: #f7f7f7;
}

View File

@@ -0,0 +1,42 @@
import { Switch } from '@/components/ui/switch.tsx'
import { FC } from 'react'
import styles from './index.module.css'
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: any
}
const ProviderCard: FC<IProviderCardProps> = ({ providerName, Icon, id }: IProviderCardProps) => {
const navigate = useNavigate()
const updateProvider = useProviderStore(state => state.updateProvider)
const handleClick = () => {
navigate(`/settings/download/${id}`)
}
const rawId = useParams()
console.log('rawId', rawId)
// @ts-ignore
const { id: currentId } = useParams()
const isActive = currentId === id
return (
<div
onClick={() => {
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' : '')
}
>
<div className="flex items-center gap-2 text-lg">
<div className="flex h-6 w-6 items-center">{<Icon></Icon>}</div>
<div className="font-semibold">{providerName}</div>
</div>
</div>
)
}
export default ProviderCard

View File

@@ -0,0 +1,340 @@
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
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, deleteModelById } 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'
import { Tags } from 'lucide-react'
import { Tag } from 'antd'
import { useModelStore } from '@/store/modelStore'
// ✅ 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 ProviderFormValues = z.infer<typeof ProviderSchema>
// ✅ Model表单schema
const ModelSchema = z.object({
modelName: z.string().min(1, '请选择或填写模型名称'),
})
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 }) => {
let { 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 loadModelsById= useModelStore(state => state.loadModelsById)
const [modelOptions, setModelOptions] = useState<IModel[]>([]) // ⚡新增,保存模型列表
const [models, setModels]= useState([])
const [modelLoading, setModelLoading] = useState(false)
const randomColor = ()=>{
return '#' + Math.floor(Math.random() * 16777215).toString(16)
}
const [search, setSearch] = useState('')
const providerForm = useForm<ProviderFormValues>({
resolver: zodResolver(ProviderSchema),
defaultValues: {
name: '',
apiKey: '',
baseUrl: '',
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(() => {
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)
}
const models = await loadModelsById(id!)
if(models){
console.log('🔧 模型列表:', models)
setModels(models)
}
setLoading(false)
}
load()
}, [id])
const handelDelete=async (modelId)=>{
if (!window.confirm('确定要删除这个模型吗?')) return
try {
const res = await deleteModelById(modelId)
console.log('🔧 删除结果:', res)
toast.success('删除成功')
} catch (e) {
toast.error('删除异常')
}
}
// 测试连通性
const handleTest = async () => {
const values = providerForm.getValues()
if (!values.apiKey || !values.baseUrl) {
toast.error('请填写 API Key 和 Base URL')
return
}
try {
if (!id){
toast.error('请先保存供应商信息')
return
}
setTesting(true)
await testConnection({
id
})
toast.success('测试连通性成功 🎉')
} catch (error) {
toast.error(`连接失败: ${data.data.msg || '未知错误'}`)
// toast.error('测试连通性异常')
} finally {
setTesting(false)
}
}
// 加载模型列表
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
}
}
// 保存Provider信息
const onProviderSubmit = async (values: ProviderFormValues) => {
if (isEditMode) {
await updateProvider({ ...values, id: id! })
toast.success('更新供应商成功')
} else {
id = await addNewProvider({ ...values })
toast.success('新增供应商成功')
}
// 刷新页面
}
// 保存Model信息
const onModelSubmit = async (values: ModelFormValues) => {
toast.success(`保存模型: ${values.modelName}`)
await loadModelsById(id!)
}
if (loading) return <div className="p-4">...</div>
return (
<div className="flex flex-col gap-8 p-4">
{/* Provider信息表单 */}
<Form {...providerForm}>
<form
onSubmit={providerForm.handleSubmit(onProviderSubmit)}
className="flex max-w-xl flex-col gap-4"
>
<div className="text-lg font-bold">
{isEditMode ? '编辑模型供应商' : '新增模型供应商'}
</div>
{!isBuiltIn && (
<div className="text-sm text-red-500 italic">
OpenAI SDK
</div>
)}
<FormField
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>
)}
/>
<FormField
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>
)}
/>
<FormField
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={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={!providerForm.formState.isDirty}>
{isEditMode ? '保存修改' : '保存创建'}
</Button>
</div>
</form>
</Form>
{/* 模型信息表单 */}
<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 className="flex flex-col gap-2">
<span className="font-bold"></span>
<div className={'flex flex-wrap gap-2 rounded p-2.5'}>
{
models && models.map(model => {
return (
<>
<Tag onClose={()=>{
handelDelete(model.id)
}} key={model.id} closable color={'blue'}>
{model.model_name}
</Tag></>
)
})
}
</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, index) => (
<SelectItem key={`${model.id}-${index}`} value={model.id}>
{model.id}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleSubmit} disabled={submitting || !selectedModel}>
{submitting ? '保存中...' : '保存模型'}
</Button>
</div>
)
}

View File

@@ -0,0 +1,44 @@
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 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'}
onClick={() => {
handleClick()
}}
className="w-full"
>
</Button>
</div>
<div className="text-sm font-light"></div>
<div>
{providers &&
providers.map((provider, index) => {
return (
<ProviderCard
key={index}
providerName={provider.name}
Icon={provider.logo}
id={provider.id}
enable={provider.enabled}
/>
)
})}
</div>
</div>
)
}
export default Provider

View File

@@ -0,0 +1,6 @@
.card {
transition: all 0.2s ease-in-out;
}
.card:hover {
background-color: #f7f7f7;
}

View File

@@ -0,0 +1,66 @@
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/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,
enable,
}: IProviderCardProps) => {
const navigate = useNavigate()
const updateProvider = useProviderStore(state => state.updateProvider)
const handleClick = () => {
navigate(`/settings/model/${id}`)
}
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 isActive = currentId === id
return (
<div
onClick={() => {
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' : '')
}
>
<div className="flex items-center text-lg">
<div className="flex h-9 w-9 items-center">
<AILogo name={Icon} />
</div>
<div className="font-semibold">{providerName}</div>
</div>
<div>
<Switch
onClick={e => {
e.preventDefault()
handleEnable()
}}
checked={enable == 1}
/>
</div>
</div>
)
}
export default ProviderCard

View File

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

View File

@@ -0,0 +1,24 @@
import * as Icons from '@lobehub/icons';
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 }}>🚫</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,168 @@
export const KuaishouLogo = () => {
return (
<svg
t="1746695310517"
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1680"
width="200"
height="200"
>
<path
d="M299.27936 624.43008v87.48544c0 14.64832 10.70592 21.24288 23.78752 14.65856l83.49696-42.01984v-32.76288L323.072 609.7664c-13.08672-6.58432-23.79264 0.01536-23.79264 14.66368zM654.42304 436.03456c36.72064 0 66.59584-29.87008 66.59584-66.59072s-29.8752-66.59584-66.59584-66.59584c-36.71552 0-66.5856 29.8752-66.5856 66.59584s29.87008 66.59072 66.5856 66.59072zM443.56096 435.65056c47.73376 0 86.56384-38.8352 86.56384-86.56896s-38.83008-86.56896-86.56384-86.56896-86.56896 38.8352-86.56896 86.56896 38.8352 86.56896 86.56896 86.56896z"
fill="#FF4A08"
p-id="1681"
></path>
<path
d="M849.92 51.2H174.08c-67.8656 0-122.88 55.0144-122.88 122.88v675.84c0 67.8656 55.0144 122.88 122.88 122.88h675.84c67.8656 0 122.88-55.0144 122.88-122.88V174.08c0-67.8656-55.0144-122.88-122.88-122.88zM443.56096 204.8c54.05184 0 101.22752 29.89056 125.93664 73.99936 22.24128-20.85376 52.11136-33.664 84.93056-33.664 68.54656 0 124.30848 55.76704 124.30848 124.30848s-55.76704 124.30336-124.30848 124.30336c-41.40544 0-78.12608-20.37248-100.73088-51.60448-26.48576 31.29856-66.01728 51.22048-110.13632 51.22048-79.55968 0-144.2816-64.72704-144.2816-144.2816S364.00128 204.8 443.56096 204.8z m336.65536 505.63584c0 59.97568-48.78848 108.76416-108.76416 108.76416H515.328c-47.05792 0-87.22432-30.04416-102.34368-71.96672l-87.81824 42.40384c-9.43616 4.5568-18.97984 6.8608-28.37504 6.8608h-0.00512c-30.70976 0-53.00224-24.3712-53.00224-57.9328v-140.5696c0-33.57696 22.29248-57.94304 53.00736-57.94304 9.3952 0 18.93888 2.30912 28.36992 6.86592l87.59808 42.29632c14.93504-42.26048 55.26528-72.63232 102.56896-72.63232h156.11904c59.97568 0 108.76416 48.7936 108.76416 108.76928v85.08416z"
fill="#FF4A08"
p-id="1682"
></path>
<path
d="M671.45216 574.28992H515.328c-28.14976 0-51.05664 22.90688-51.05664 51.05664v85.08928c0 28.14976 22.90688 51.05664 51.05664 51.05664h156.11904c28.14976 0 51.05664-22.90688 51.05664-51.05664v-85.08928c0-28.14976-22.90176-51.05664-51.05152-51.05664z"
fill="#FF4A08"
p-id="1683"
></path>
</svg>
)
}
export const DouyinLogo = () => {
return (
<svg
t="1746695428425"
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2731"
width="200"
height="200"
>
<path
d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z"
fill="#111111"
p-id="2732"
></path>
<path
d="M204.27776 670.59712a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V134.35904h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z"
fill="#FF4040"
p-id="2733"
></path>
<path
d="M164.92544 631.23456a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V94.99648h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z"
fill="#00F5FF"
p-id="2734"
></path>
<path
d="M410.91072 427.58144c-158.8224 20.15232-284.44672 222.72-154.112 405.00224 120.40192 98.47808 373.68832 41.20576 380.70272-171.85792l-0.17408-324.1472a280.7296 280.7296 0 0 0 142.88896 38.62528V261.2224a144.98816 144.98816 0 0 1-72.8064-54.82496 135.23968 135.23968 0 0 1-54.70208-72.45824h-123.66848l-0.08192 561.41824c-0.11264 78.46912-130.9696 106.41408-164.18816 30.2592-83.18976-39.77216-64.37888-190.9248 46.31552-192.57344z"
fill="#FFFFFF"
p-id="2735"
></path>
</svg>
)
}
export const BiliBiliLogo = () => {
return (
<svg
t="1746696526393"
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="3757"
width="200"
height="200"
>
<path
d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z"
fill="#EC5D85"
p-id="3758"
></path>
<path
d="M512 241.96096h52.224l65.06496-96.31744c49.63328-50.31936 89.64096 0.43008 63.85664 45.71136l-34.31424 51.5072c257.64864 5.02784 257.64864 43.008 257.64864 325.03808 0 325.94944 0 336.46592-404.48 336.46592S107.52 893.8496 107.52 567.90016c0-277.69856 0-318.80192 253.14304-324.95616l-39.43424-58.368c-31.26272-54.90688 37.33504-90.40896 64.68608-42.37312l60.416 99.80928c18.18624-0.0512 41.18528-0.0512 65.66912-0.0512z"
fill="#EF85A7"
p-id="3759"
></path>
<path
d="M512 338.5856c332.8 0 332.8 0 332.8 240.64s0 248.39168-332.8 248.39168-332.8-7.75168-332.8-248.39168 0-240.64 332.8-240.64z"
fill="#EC5D85"
p-id="3760"
></path>
<path
d="M281.6 558.08a30.72 30.72 0 0 1-27.47392-16.97792 30.72 30.72 0 0 1 13.73184-41.216l122.88-61.44a30.72 30.72 0 0 1 41.216 13.74208 30.72 30.72 0 0 1-13.74208 41.216l-122.88 61.44a30.59712 30.59712 0 0 1-13.73184 3.23584zM752.64 558.08a30.60736 30.60736 0 0 1-12.8512-2.83648l-133.12-61.44a30.72 30.72 0 0 1-15.04256-40.7552 30.72 30.72 0 0 1 40.76544-15.02208l133.12 61.44A30.72 30.72 0 0 1 752.64 558.08zM454.656 666.88a15.36 15.36 0 0 1-12.288-6.1952 15.36 15.36 0 0 1 3.072-21.49376l68.5056-50.91328 50.35008 52.62336a15.36 15.36 0 0 1-22.20032 21.23776l-31.5904-33.024-46.71488 34.72384a15.28832 15.28832 0 0 1-9.13408 3.04128z"
fill="#EF85A7"
p-id="3761"
></path>
<path
d="M65.536 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM362.25024 383.03744l34.816 303.17568h34.64192L405.23776 381.1328zM309.52448 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM446.86336 542.98624h45.80352V705.3312h-33.87392zM296.6016 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM326.99392 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM470.62016 459.88864h19.456v62.27968h-19.456zM440.23808 459.88864h22.20032v62.27968h-16.62976z"
fill="#FFFFFF"
p-id="3762"
></path>
<path
d="M243.56864 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z"
fill="#EB5480"
p-id="3763"
></path>
<path
d="M513.29024 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM810.00448 383.03744l34.816 303.17568h34.64192L852.992 381.1328zM757.27872 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM894.6176 542.98624h45.80352V705.3312H906.5472zM744.35584 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM774.74816 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM918.3744 459.88864h19.456v62.27968h-19.456zM887.99232 459.88864h22.20032v62.27968h-16.62976z"
fill="#FFFFFF"
p-id="3764"
></path>
<path
d="M691.32288 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z"
fill="#EB5480"
p-id="3765"
></path>
</svg>
)
}
export const YoutubeLogo = () => {
return (
<svg
t="1746696577253"
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4785"
width="200"
height="200"
>
<path
d="M426.666667 682.666667V384l256 149.845333L426.666667 682.666667z m587.093333-355.541334s-10.026667-71.04-40.704-102.357333c-38.954667-41.088-82.602667-41.258667-102.613333-43.648C727.168 170.666667 512.213333 170.666667 512.213333 170.666667h-0.426666s-214.954667 0-358.229334 10.453333c-20.053333 2.389333-63.658667 2.56-102.656 43.648-30.677333 31.317333-40.661333 102.4-40.661333 102.4S0 410.538667 0 493.952v78.293333c0 83.456 10.24 166.912 10.24 166.912s9.984 71.04 40.661333 102.357334c38.997333 41.088 90.154667 39.765333 112.938667 44.074666C245.76 893.568 512 896 512 896s215.168-0.341333 358.442667-10.752c20.053333-2.432 63.658667-2.602667 102.613333-43.690667 30.72-31.317333 40.704-102.4 40.704-102.4s10.24-83.413333 10.24-166.869333v-78.250667c0-83.456-10.24-166.912-10.24-166.912z"
fill="#FF0000"
p-id="4786"
></path>
</svg>
)
}
export const LocalLogo = () => {
return (
<svg
t="1746696617516"
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5795"
width="200"
height="200"
>
<path
d="M948.736 144.384H461.568l-56.576-83.456c-6.144-7.168-15.36-10.752-24.576-9.728H79.872c-17.152-0.512-34.048 5.632-46.592 17.408-12.544 11.776-19.968 28.16-20.48 45.312v222.464c0-18.944 7.424-37.12 20.992-50.432 13.312-13.312 31.488-20.992 50.432-20.992h855.808c18.944 0 37.12 7.424 50.432 20.992 13.312 13.312 20.992 31.488 20.992 50.432V213.248c1.28-36.096-26.624-66.816-62.72-68.864z m0 0"
fill="#FFD569"
p-id="5796"
></path>
<path
d="M939.776 265.216H84.224C44.8 265.216 12.8 297.216 12.8 336.64v570.368c0 18.944 7.424 37.12 20.992 50.432 13.312 13.312 31.488 20.992 50.432 20.992h855.808c18.944 0 37.12-7.424 50.432-20.992 13.312-13.312 20.992-31.488 20.992-50.432V336.64c0-18.944-7.424-37.12-20.992-50.432-13.568-13.312-31.744-20.992-50.688-20.992z m-213.76 467.968c0.256 6.4-3.328 12.288-9.216 14.848-1.792 0.256-3.84 0.256-5.632 0-4.096 0-7.936-1.792-10.752-4.864l-54.784-59.136v77.056c0.256 8.704-6.4 15.872-14.848 16.384h-317.44c-7.936-0.512-14.336-6.912-14.848-14.848V495.616c-0.256-8.704 6.4-15.872 14.848-16.384h317.44c8.704 0.512 15.616 7.68 15.36 16.384v76.544l54.784-57.344c3.84-4.864 10.496-6.144 16.128-3.584 5.632 2.816 9.472 8.704 9.216 14.848v207.104z m0 0"
fill="#FFC225"
p-id="5797"
></path>
</svg>
)
}

View File

@@ -0,0 +1,34 @@
// components/LazyImage.tsx
import { useInView } from 'react-intersection-observer'
import { FC, useState } from 'react'
import clsx from 'clsx'
interface LazyImageProps {
src: string
alt?: string
className?: string
placeholder?: string
}
const LazyImage: FC<LazyImageProps> = ({ src, alt, className, placeholder = '.src/assets/placeholder.png' }) => {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.1 })
const [loaded, setLoaded] = useState(false)
return (
<div ref={ref} className={clsx('overflow-hidden', className)}>
{inView ? (
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setLoaded(true)}
className={clsx('transition-opacity duration-300', loaded ? 'opacity-100' : 'opacity-0') + ' h-10 w-14 rounded-md object-cover'}
/>
) : (
<img src={placeholder} alt="loading" className="opacity-30" />
)}
</div>
)
}
export default LazyImage

View File

@@ -0,0 +1,13 @@
import { FC } from 'react'
import Lottie from 'lottie-react'
import Animation from '@/assets/Lottie/404.json'
const NotFound: FC = () => {
return (
<div className="flex items-center justify-center">
<Lottie animationData={Animation} loop autoplay />
</div>
)
}
export default NotFound

View File

@@ -3,16 +3,11 @@ import Lottie from 'lottie-react'
import loadingJson from '@/assets/Lottie/idle.json'
const Idle: FC = () => {
return (
<div className="flex justify-center items-center ">
<Lottie
animationData={loadingJson}
loop
autoplay
style={{ width: 350, height: 350 }}
/>
</div>
)
return (
<div className="flex items-center justify-center">
<Lottie animationData={loadingJson} loop autoplay style={{ width: 350, height: 350 }} />
</div>
)
}
export default Idle

View File

@@ -3,16 +3,11 @@ import Lottie from 'lottie-react'
import loadingJson from '@/assets/Lottie/loading.json'
const Loading: FC = () => {
return (
<div className="flex justify-center items-center ">
<Lottie
animationData={loadingJson}
loop
autoplay
style={{ width: 150, height: 150 }}
/>
</div>
)
return (
<div className="flex items-center justify-center">
<Lottie animationData={loadingJson} loop autoplay style={{ width: 150, height: 150 }} />
</div>
)
}
export default Loading

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

@@ -1,26 +1,24 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
@@ -30,17 +28,10 @@ function Badge({
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }

View File

@@ -1,36 +1,33 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
}
)
@@ -41,11 +38,11 @@ function Button({
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : 'button'
return (
<Comp

View File

@@ -1,13 +1,13 @@
import * as React from "react"
import * as React from 'react'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function Card({ className, ...props }: React.ComponentProps<"div">) {
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className
)}
{...props}
@@ -15,12 +15,12 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className
)}
{...props}
@@ -28,65 +28,48 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
className={cn('leading-none font-semibold', className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }

View File

@@ -1,18 +1,15 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from 'lucide-react'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}

View File

@@ -0,0 +1,141 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import {
Controller,
FormProvider,
@@ -9,10 +9,10 @@ import {
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
} from 'react-hook-form'
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
const Form = FormProvider
@@ -23,9 +23,7 @@ type FormFieldContextValue<
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -48,7 +46,7 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error('useFormField should be used within <FormField>')
}
const { id } = itemContext
@@ -67,35 +65,26 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
<div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
@@ -109,33 +98,29 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
const body = error ? String(error?.message ?? '') : props.children
if (!body) {
return null
@@ -145,7 +130,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
className={cn('text-destructive text-sm', className)}
{...props}
>
{body}

View File

@@ -1,16 +1,16 @@
import * as React from "react"
import * as React from 'react'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}

View File

@@ -1,19 +1,16 @@
"use client"
'use client'
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...props}

View File

@@ -0,0 +1,54 @@
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function ScrollArea({
className,
@@ -11,7 +11,7 @@ function ScrollArea({
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
className={cn('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
@@ -28,7 +28,7 @@ function ScrollArea({
function ScrollBar({
className,
orientation = "vertical",
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
@@ -36,11 +36,9 @@ function ScrollBar({
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
className
)}
{...props}

View File

@@ -1,34 +1,28 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: 'sm' | 'default'
}) {
return (
<SelectPrimitive.Trigger
@@ -51,7 +45,7 @@ function SelectTrigger({
function SelectContent({
className,
children,
position = "popper",
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
@@ -59,9 +53,9 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
@@ -70,9 +64,9 @@ function SelectContent({
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)}
>
{children}
@@ -83,14 +77,11 @@ function SelectContent({
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
)
@@ -127,7 +118,7 @@ function SelectSeparator({
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
)
@@ -140,10 +131,7 @@ function SelectScrollUpButton({
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon className="size-4" />
@@ -158,10 +146,7 @@ function SelectScrollDownButton({
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon className="size-4" />

View File

@@ -1,18 +1,18 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from 'next-themes'
import { Toaster as Sonner, ToasterProps } from 'sonner'
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = 'system' } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
theme={theme as ToasterProps['theme']}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
} as React.CSSProperties
}
{...props}

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

@@ -0,0 +1,64 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
function TooltipProvider({
delayDuration = 0,
@@ -16,9 +16,7 @@ function TooltipProvider({
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
@@ -26,9 +24,7 @@ function Tooltip({
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
@@ -44,7 +40,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className
)}
{...props}

View File

@@ -0,0 +1,35 @@
/* -------------------- 常量 -------------------- */
import {
BiliBiliLogo,
DouyinLogo,
KuaishouLogo,
LocalLogo,
YoutubeLogo,
} from '@/components/Icons/platform.tsx'
export const noteFormats = [
{ label: '目录', value: 'toc' },
{ label: '原片跳转', value: 'link' },
{ label: '原片截图', value: 'screenshot' },
{ label: 'AI总结', value: 'summary' },
] as const
export 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' },
] as const
export const videoPlatforms = [
{ label: '哔哩哔哩', value: 'bilibili', logo: BiliBiliLogo },
{ label: 'YouTube', value: 'youtube', logo: YoutubeLogo },
{ label: '抖音', value: 'douyin', logo: DouyinLogo },
{ label: '快手', value: 'kuaishou', logo: KuaishouLogo },
{ label: '本地视频', value: 'local', logo: LocalLogo },
] as const

View File

@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react'
import request from '@/utils/request'
const MAX_RETRIES = 3
const RETRY_INTERVAL = 10000 // 10秒
export const useCheckBackend = () => {
const [loading, setLoading] = useState(false)
const [initialized, setInitialized] = useState(false)
useEffect(() => {
let retries = 0
const check = async () => {
try {
await request.get('/sys_check')
setInitialized(true)
setLoading(false)
} catch {
if (retries === 0) {
// 第一次失败时开始显示加载状态
setLoading(true)
}
if (retries < MAX_RETRIES) {
retries++
setTimeout(check, RETRY_INTERVAL)
} else {
// 达到重试上限,继续轮询直到后端就绪
waitUntilBackendReady()
}
}
}
const waitUntilBackendReady = async () => {
while (true) {
try {
await request.get('/sys_health')
setInitialized(true)
setLoading(false)
break
} catch {
await new Promise(res => setTimeout(res, RETRY_INTERVAL))
}
}
}
check()
}, [])
return { loading, initialized }
}

View File

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

View File

@@ -1,7 +1,32 @@
@import "tailwindcss";
@import "tw-animate-css";
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
html, body, #root {
height: 100%;
overflow: hidden;
}
/* 修改滚动条轨道颜色 */
::-webkit-scrollbar {
width: 8px; /* 控制滚动条的宽度 */
}
/* 修改滚动条的轨道颜色 */
::-webkit-scrollbar-track {
background-color: #f1f1f1; /* 轨道的背景颜色 */
}
/* 修改滚动条的滑块颜色 */
::-webkit-scrollbar-thumb {
background-color: #888; /* 滑块的颜色 */
border-radius: 4px; /* 圆角 */
}
/* 当鼠标悬停时,修改滑块颜色 */
::-webkit-scrollbar-thumb:hover {
background-color: #555; /* 悬停时的颜色 */
}
:root {
--radius: 0.625rem;
@@ -11,7 +36,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #3C77FB;
--primary: #3c77fb;
--primary-light: #e0eeff;
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
@@ -21,7 +46,7 @@
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: #e6f7ff;
--border: var( --color-neutral-200);
--input: oklch(0.922 0 0);
--ring: #096dd9;
--chart-1: oklch(0.646 0.222 41.116);
@@ -46,8 +71,8 @@
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: #3C77FB;
--primary-light:#e0eeff;
--primary: #3c77fb;
--primary-light: #e0eeff;
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);

View File

@@ -12,7 +12,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #3C77FB;
--primary: #3c77fb;
--primary-light: #e0eeff;
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
@@ -47,8 +47,8 @@
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: #3C77FB;
--primary-light:#e0eeff;
--primary: #3c77fb;
--primary-light: #e0eeff;
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);

View File

@@ -1,43 +1,79 @@
import type { FC, ReactNode } from 'react'
import { Button } from "@/components/ui/button.tsx"
import React, { FC } from 'react'
import { SlidersHorizontal } from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx'
interface HomeLayoutProps {
form: ReactNode
preview: ReactNode
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable'
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
import logo from '@/assets/icon.svg'
interface IProps {
NoteForm: React.ReactNode
Preview: React.ReactNode
History: React.ReactNode
}
const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
const [, setShowSettings] = useState(false)
const HomeLayout: FC<HomeLayoutProps> = ({ form, preview }) => {
return (
<div className="min-h-screen flex flex-col bg-white">
<div className="flex flex-1">
{/* 左侧部分Header + 表单 */}
<aside className="w-[400px] bg-white border-r border-neutral-200 flex flex-col">
{/* Header */}
<header className="h-16 flex items-center px-6 gap-2">
<div className="w-10 h-10 rounded-2xl overflow-hidden flex justify-center items-center">
<img src="/icon.svg" alt="logo" className="w-full h-full object-contain" />
</div>
<div className="text-2xl font-bold text-gray-800">BiliNote</div>
</header>
return (
<div className="flex h-screen flex-col overflow-hidden">
<ResizablePanelGroup direction="horizontal" className="h-full w-full">
{/* 左边表单 */}
<ResizablePanel defaultSize={18} minSize={10} maxSize={35}>
<aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white">
<header className="flex h-16 items-center justify-between px-6">
<div className="flex items-center gap-2">
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-2xl">
<img src={logo} alt="logo" className="h-full w-full object-contain" />
</div>
<div className="text-2xl font-bold text-gray-800">BiliNote</div>
</div>
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger onClick={() => setShowSettings(true)}>
<Link to={'/settings'}>
<SlidersHorizontal className="text-muted-foreground hover:text-primary cursor-pointer" />
</Link>
</TooltipTrigger>
<TooltipContent>
<span></span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</header>
<ScrollArea className="flex-1 overflow-auto">
<div className=' p-4' >{NoteForm}</div>
</ScrollArea>
</aside>
</ResizablePanel>
{/* 表单内容 */}
<div className="flex-1 p-4 overflow-auto">
{form}
</div>
</aside>
<ResizableHandle />
{/* 右侧预览区域 */}
<main className="flex-1 h-screen p-6 bg-white overflow-hidden">
{preview}
</main>
</div>
{/* 中间历史 */}
<ResizablePanel defaultSize={16} minSize={10} maxSize={30}>
<aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white">
<ScrollArea className="flex-1 overflow-auto">
<div className="">{History}</div>
</ScrollArea>
</aside>
</ResizablePanel>
{/* 页脚 */}
{/*<footer className="h-12 bg-white shadow-inner flex items-center justify-center text-sm text-neutral-600">*/}
{/* © 2025 BiliNote. All rights reserved.*/}
{/*</footer>*/}
</div>
)
<ResizableHandle />
{/* 右边预览 */}
<ResizablePanel defaultSize={55} minSize={30}>
<main className="flex h-full flex-col overflow-hidden bg-white p-6">{Preview}</main>
</ResizablePanel>
</ResizablePanelGroup>
</div>
)
}
export default HomeLayout

View File

@@ -1,32 +1,32 @@
import type { ReactNode, FC } from "react"
import type { ReactNode, FC } from 'react'
// import "@/global.css"
import { Toaster } from 'react-hot-toast'
interface RootLayoutProps {
children: ReactNode
children: ReactNode
}
export const metadata = {
title: "BiliNote - 视频笔记生成器",
description: "通过视频链接结合大模型自动生成对应的笔记",
title: 'BiliNote - 视频笔记生成器',
description: '通过视频链接结合大模型自动生成对应的笔记',
}
const RootLayout: FC<RootLayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-neutral-100 text-neutral-900 font-sans">
<Toaster
position="top-center" // 顶部居中显示
toastOptions={{
style: {
borderRadius: '8px',
background: '#333',
color: '#fff',
},
}}
/>
{children}
</div>
)
return (
<div className="min-h-screen bg-neutral-100 font-sans text-neutral-900">
<Toaster
position="top-center" // 顶部居中显示
toastOptions={{
style: {
borderRadius: '8px',
background: '#333',
color: '#fff',
},
}}
/>
{children}
</div>
)
}
export default RootLayout

View File

@@ -0,0 +1,65 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx'
import { Link, Outlet } from 'react-router-dom'
import { SlidersHorizontal } from 'lucide-react'
import React from 'react'
import logo from '@/assets/icon.svg'
interface ISettingLayoutProps {
Menu: React.ReactNode
}
const SettingLayout = ({ Menu }: ISettingLayoutProps) => {
return (
<div
className="h-full w-full"
style={{
backgroundColor: 'var(--color-muted)',
}}
>
<div className="flex flex-1">
{/* 左侧部分Header + 表单 */}
<aside className="flex w-[300px] flex-col border-r border-neutral-200 bg-white">
{/* Header */}
<header className="flex h-16 items-center justify-between px-6">
<div className="flex items-center gap-2">
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-2xl">
<img src={logo} alt="logo" className="h-full w-full object-contain" />
</div>
<div className="text-2xl font-bold text-gray-800">BiliNote</div>
</div>
<div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Link to={'/'}>
<SlidersHorizontal className="text-muted-foreground hover:text-primary cursor-pointer" />
</Link>
</TooltipTrigger>
<TooltipContent>
<span></span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</header>
{/* 表单内容 */}
<div className="flex-1 overflow-auto p-4">
{/*<NoteForm />*/}
{Menu}
</div>
</aside>
{/* 右侧预览区域 */}
<main className="h-screen flex-1 overflow-hidden">
<Outlet />
</main>
</div>
</div>
)
}
export default SettingLayout

View File

@@ -0,0 +1,8 @@
import { loadCSS, loadJS } from 'markmap-common'
import { Transformer } from 'markmap-lib'
import * as markmap from 'markmap-view'
export const transformer = new Transformer()
const { scripts, styles } = transformer.getAssets()
loadCSS(styles)
loadJS(scripts, { getMarkmap: () => markmap })

View File

@@ -1,5 +1,5 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))

View File

@@ -2,12 +2,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import RootLayout from "./layouts/RootLayout.tsx";
import RootLayout from './layouts/RootLayout.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RootLayout>
<App />
</RootLayout>
</StrictMode>,
<StrictMode>
<RootLayout>
<App />
</RootLayout>
</StrictMode>
)

View File

@@ -1,42 +0,0 @@
import React,{FC,useEffect,useState} from "react";
import HomeLayout from "@/layouts/HomeLayout.tsx";
import NoteForm from '@/pages/components/NoteForm'
import MarkdownViewer from '@/pages/components/MarkdownViewer'
import NoteFormWrapper from "@/pages/components/NoteFormWrapper.tsx";
import {get_task_status} from "@/services/note.ts";
import {useTaskStore} from "@/store/taskStore";
type ViewStatus = 'idle' | 'loading' | 'success'
export const HomePage:FC =()=>{
const tasks = useTaskStore((state) => state.tasks)
const currentTaskId = useTaskStore((state) => state.currentTaskId)
const currentTask = tasks.find((t) => t.id === currentTaskId)
const [status, setStatus] = useState<ViewStatus>('idle')
const content = currentTask?.markdown || ''
useEffect(() => {
if (!currentTask) {
setStatus('idle')
} else if (currentTask.status === 'PENDING') {
setStatus('loading')
} else if (currentTask.status === 'SUCCESS') {
setStatus('success')
}
}, [currentTask])
// useEffect( () => {
// get_task_status('d4e87938-c066-48a0-bbd5-9bec40d53354').then(res=>{
// console.log('res1',res)
// setContent(res.data.result.markdown)
// })
// }, [tasks]);
return (
<HomeLayout
form={<NoteForm/>}
preview={<MarkdownViewer status={status} content={content} />}
/>
)
}

View File

@@ -0,0 +1,43 @@
import { FC, useEffect, useState } from 'react'
import HomeLayout from '@/layouts/HomeLayout.tsx'
import NoteForm from '@/pages/HomePage/components/NoteForm.tsx'
import MarkdownViewer from '@/pages/HomePage/components/MarkdownViewer.tsx'
import { useTaskStore } from '@/store/taskStore'
import History from '@/pages/HomePage/components/History.tsx'
type ViewStatus = 'idle' | 'loading' | 'success' | 'failed'
export const HomePage: FC = () => {
const tasks = useTaskStore(state => state.tasks)
const currentTaskId = useTaskStore(state => state.currentTaskId)
const currentTask = tasks.find(t => t.id === currentTaskId)
const [status, setStatus] = useState<ViewStatus>('idle')
const content = currentTask?.markdown || ''
useEffect(() => {
if (!currentTask) {
setStatus('idle')
} else if (currentTask.status === 'PENDING') {
setStatus('loading')
} else if (currentTask.status === 'SUCCESS') {
setStatus('success')
} else if (currentTask.status === 'FAILED') {
setStatus('failed')
}
}, [currentTask])
// useEffect( () => {
// get_task_status('d4e87938-c066-48a0-bbd5-9bec40d53354').then(res=>{
// console.log('res1',res)
// setContent(res.data.result.markdown)
// })
// }, [tasks]);
return (
<HomeLayout
NoteForm={<NoteForm />}
Preview={<MarkdownViewer status={status} />}
History={<History />}
/>
)
}

View File

@@ -0,0 +1,26 @@
import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx'
import { useTaskStore } from '@/store/taskStore'
import { Info, Clock, Loader2 } from 'lucide-react'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
const History = () => {
const currentTaskId = useTaskStore(state => state.currentTaskId)
const setCurrentTask = useTaskStore(state => state.setCurrentTask)
return (
<>
<div className={'flex h-full w-full flex-col gap-4 px-2.5 py-1.5'}>
{/*生成历史 */}
<div className="my-4 flex h-[40px] items-center gap-2">
<Clock className="h-4 w-4 text-neutral-500" />
<h2 className="text-base font-medium text-neutral-900"></h2>
</div>
<ScrollArea className="w-full sm:h-[480px] md:h-[720px] lg:h-[92%]">
{/*<div className="w-full flex-1 overflow-y-auto">*/}
<NoteHistory onSelect={setCurrentTask} selectedId={currentTaskId} />
{/*</div>*/}
</ScrollArea>
</div>
</>
)
}
export default History

View File

@@ -0,0 +1,189 @@
'use client'
import { useEffect, useState } from 'react'
import { Copy, Download, BrainCircuit } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
interface VersionNote {
ver_id: string
model_name?: string
style?: string
created_at?: string
}
interface NoteHeaderProps {
currentTask?: {
markdown: VersionNote[] | string
}
isMultiVersion: boolean
currentVerId: string
setCurrentVerId: (id: string) => void
modelName: string
style: string
noteStyles: { value: string; label: string }[]
onCopy: () => void
onDownload: () => void
createAt?: string | Date
setShowTranscribe: (show: boolean) => void
}
export function MarkdownHeader({
currentTask,
isMultiVersion,
currentVerId,
setCurrentVerId,
modelName,
style,
noteStyles,
onCopy,
onDownload,
createAt,
showTranscribe,
setShowTranscribe,
viewMode,
setViewMode,
}: NoteHeaderProps) {
const [copied, setCopied] = useState(false)
useEffect(() => {
let timer: NodeJS.Timeout
if (copied) {
timer = setTimeout(() => setCopied(false), 2000)
}
return () => clearTimeout(timer)
}, [copied])
const handleCopy = () => {
onCopy()
setCopied(true)
}
const styleName = noteStyles.find(v => v.value === style)?.label || style
const reversedMarkdown: VersionNote[] = Array.isArray(currentTask?.markdown)
? [...currentTask!.markdown].reverse()
: []
const formatDate = (date: string | Date | undefined) => {
if (!date) return ''
const d = typeof date === 'string' ? new Date(date) : date
if (isNaN(d.getTime())) return ''
return d
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
.replace(/\//g, '-')
}
return (
<div className="sticky top-0 z-10 flex flex-wrap items-center justify-between gap-3 border-b bg-white/95 px-4 py-2 backdrop-blur-sm">
{/* 左侧区域:版本 + 标签 + 创建时间 */}
<div className="flex flex-wrap items-center gap-3">
{isMultiVersion && (
<Select value={currentVerId} onValueChange={setCurrentVerId}>
<SelectTrigger className="h-8 w-[160px] text-sm">
<div className="flex items-center">
{(() => {
const idx = currentTask?.markdown.findIndex(v => v.ver_id === currentVerId)
return idx !== -1 ? `版本(${currentVerId.slice(-6)}` : ''
})()}
</div>
</SelectTrigger>
<SelectContent>
{(currentTask?.markdown || []).map((v, idx) => {
const shortId = v.ver_id.slice(-6)
return (
<SelectItem key={v.ver_id} value={v.ver_id}>
{`版本(${shortId}`}
</SelectItem>
)
})}
</SelectContent>
</Select>
)}
<Badge variant="secondary" className="bg-pink-100 text-pink-700 hover:bg-pink-200">
{modelName}
</Badge>
<Badge variant="secondary" className="bg-cyan-100 text-cyan-700 hover:bg-cyan-200">
{styleName}
</Badge>
{createAt && (
<div className="text-muted-foreground text-sm">: {formatDate(createAt)}</div>
)}
</div>
{/* 右侧操作按钮 */}
<div className="flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => {
setViewMode(viewMode == 'preview' ? 'map' : 'preview')
}}
variant="ghost"
size="sm"
className="h-8 px-2"
>
<BrainCircuit className="mr-1.5 h-4 w-4" />
<span className="text-sm">{viewMode == 'preview' ? '思维导图' : 'markdown'}</span>
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={handleCopy} variant="ghost" size="sm" className="h-8 px-2">
<Copy className="mr-1.5 h-4 w-4" />
<span className="text-sm">{copied ? '已复制' : '复制'}</span>
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onDownload} variant="ghost" size="sm" className="h-8 px-2">
<Download className="mr-1.5 h-4 w-4" />
<span className="text-sm"> Markdown</span>
</Button>
</TooltipTrigger>
<TooltipContent> Markdown </TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => {
setShowTranscribe(!showTranscribe)
}}
variant="ghost"
size="sm"
className="h-8 px-2"
>
{/*<Download className="mr-1.5 h-4 w-4" />*/}
<span className="text-sm"></span>
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
)
}

View File

@@ -0,0 +1,491 @@
import { useState, useEffect, useRef } from 'react'
import ReactMarkdown from 'react-markdown'
import { Button } from '@/components/ui/button.tsx'
import { Copy, Download, ArrowRight, Play, ExternalLink } from 'lucide-react'
import { toast } from 'react-hot-toast'
import Error from '@/components/Lottie/error.tsx'
import Loading from '@/components/Lottie/Loading.tsx'
import Idle from '@/components/Lottie/Idle.tsx'
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark as codeStyle } from 'react-syntax-highlighter/dist/esm/styles/prism'
import Zoom from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'
import gfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import 'katex/dist/katex.min.css'
import 'github-markdown-css/github-markdown-light.css'
import { FC } from 'react'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
import { useTaskStore } from '@/store/taskStore'
import { noteStyles } from '@/constant/note.ts'
import { MarkdownHeader } from '@/pages/HomePage/components/MarkdownHeader.tsx'
import TranscriptViewer from '@/pages/HomePage/components/transcriptViewer.tsx'
import MarkmapEditor from '@/pages/HomePage/components/MarkmapComponent.tsx'
interface VersionNote {
ver_id: string
content: string
style: string
model_name: string
created_at?: string
}
interface MarkdownViewerProps {
content: string | VersionNote[]
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> = ({ status }) => {
const [copied, setCopied] = useState(false)
const [currentVerId, setCurrentVerId] = useState<string>('')
const [selectedContent, setSelectedContent] = useState<string>('')
const [modelName, setModelName] = useState<string>('')
const [style, setStyle] = useState<string>('')
const [createTime, setCreateTime] = useState<string>('')
const baseURL = String(import.meta.env.VITE_API_BASE_URL).replace('/api','') || ''
const getCurrentTask = useTaskStore.getState().getCurrentTask
const currentTask = useTaskStore(state => state.getCurrentTask())
const taskStatus = currentTask?.status || 'PENDING'
const retryTask = useTaskStore.getState().retryTask
const isMultiVersion = Array.isArray(currentTask?.markdown)
const [showTranscribe, setShowTranscribe] = useState(false)
const [viewMode, setViewMode] = useState<'map' | 'preview'>('preview')
const svgRef = useRef<SVGSVGElement>(null)
// 多版本内容处理
useEffect(() => {
if (!currentTask) return
if (!isMultiVersion) {
setCurrentVerId('') // 清空旧版本 ID
setModelName(currentTask.formData.model_name)
setStyle(currentTask.formData.style)
setCreateTime(currentTask.createdAt)
setSelectedContent(currentTask?.markdown)
} else {
const latestVersion = [...currentTask.markdown].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)[0]
if (latestVersion) {
setCurrentVerId(latestVersion.ver_id)
}
}
}, [currentTask?.id, taskStatus])
useEffect(() => {
if (!currentTask || !isMultiVersion) return
const currentVer = currentTask.markdown.find(v => v.ver_id === currentVerId)
if (currentVer) {
setModelName(currentVer.model_name)
setStyle(currentVer.style)
setCreateTime(currentVer.created_at || '')
setSelectedContent(currentVer.content)
}
}, [currentVerId, currentTask?.id])
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(selectedContent)
setCopied(true)
toast.success('已复制到剪贴板')
setTimeout(() => setCopied(false), 2000)
} catch (e) {
toast.error('复制失败')
}
}
const alertButton = {
id: 'alert',
title: '测试警告',
content: '⚠️',
onClick: () => alert('你点击了自定义按钮!'),
}
const exportButton = {
id: 'export',
title: '导出思维导图',
content: '⤓',
onClick: () => {
const svgEl = svgRef.current
if (!svgEl) return
// 同上面的序列化逻辑
const serializer = new XMLSerializer()
const source = serializer.serializeToString(svgEl)
const blob = new Blob(['<?xml version="1.0" encoding="UTF-8"?>', source], {
type: 'image/svg+xml;charset=utf-8',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'mindmap.svg'
a.click()
URL.revokeObjectURL(url)
},
}
const handleDownload = () => {
const task = getCurrentTask()
const name = task?.audioMeta.title || 'note'
const blob = new Blob([selectedContent], { type: 'text/markdown;charset=utf-8' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `${name}.md`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
if (status === 'loading') {
return (
<div className="flex h-screen w-full flex-col items-center justify-center space-y-4 text-neutral-500">
<StepBar steps={steps} currentStep={taskStatus} />
<Loading className="h-5 w-5" />
<div className="text-center text-sm">
<p className="text-lg font-bold"></p>
<p className="mt-2 text-xs text-neutral-500"></p>
</div>
</div>
)
}
if (status === 'idle') {
return (
<div className="flex h-screen w-full flex-col items-center justify-center space-y-3 text-neutral-500">
<Idle />
<div className="text-center">
<p className="text-lg font-bold"></p>
<p className="mt-2 text-xs text-neutral-500">YouTube </p>
</div>
</div>
)
}
if (status === 'failed' && !isMultiVersion) {
return (
<div className="flex h-screen w-full flex-col items-center justify-center gap-4 space-y-3">
<Error />
<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 (
<div className="flex h-screen w-full flex-col overflow-hidden">
<MarkdownHeader
currentTask={currentTask}
isMultiVersion={isMultiVersion}
currentVerId={currentVerId}
setCurrentVerId={setCurrentVerId}
modelName={modelName}
style={style}
noteStyles={noteStyles}
onCopy={handleCopy}
onDownload={handleDownload}
createAt={createTime}
showTranscribe={showTranscribe}
setShowTranscribe={setShowTranscribe}
viewMode={viewMode}
setViewMode={setViewMode}
/>
{viewMode === 'map' ? (
<div className="flex w-full flex-1 overflow-hidden bg-white">
<div className={'w-full'}>
<MarkmapEditor
value={selectedContent}
onChange={() => {}}
height="100%" // 根据需求可以设定百分比或固定高度
/>
</div>
</div>
) : (
<div className="flex flex-1 overflow-hidden bg-white py-2">
{selectedContent && selectedContent !== 'loading' && selectedContent !== 'empty' ? (
<>
<ScrollArea className="w-full">
<div className={'markdown-body w-full px-2'}>
<ReactMarkdown
remarkPlugins={[gfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Headings with improved styling and anchor links
h1: ({ children, ...props }) => (
<h1
className="text-primary my-6 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl"
{...props}
>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2
className="text-primary mt-10 mb-4 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
{...props}
>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3
className="text-primary mt-8 mb-4 scroll-m-20 text-xl font-semibold tracking-tight"
{...props}
>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4
className="text-primary mt-6 mb-2 scroll-m-20 text-lg font-semibold tracking-tight"
{...props}
>
{children}
</h4>
),
// Paragraphs with better line height
p: ({ children, ...props }) => (
<p className="leading-7 [&:not(:first-child)]:mt-6" {...props}>
{children}
</p>
),
// Enhanced links with special handling for "原片" links
a: ({ href, children, ...props }) => {
const isOriginLink =
typeof children[0] === 'string' &&
(children[0] as string).startsWith('原片 @')
if (isOriginLink) {
const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/)
const timeText = timeMatch ? timeMatch[1] : '原片'
return (
<span className="origin-link my-2 inline-flex">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
{...props}
>
<Play className="h-3.5 w-3.5" />
<span>{timeText}</span>
</a>
</span>
)
}
// Default link styling with external indicator
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
{...props}
>
{children}
{href?.startsWith('http') && (
<ExternalLink className="ml-0.5 inline-block h-3 w-3" />
)}
</a>
)
},
// Enhanced image with zoom capability
img: ({ node, ...props }) =>{
let src = baseURL +props.src
props.src = src
return(
<div className="my-8 flex justify-center">
<Zoom>
<img
{...props}
className="max-w-full cursor-zoom-in rounded-lg object-cover shadow-md transition-all hover:shadow-lg"
style={{ maxHeight: '500px' }}
/>
</Zoom>
</div>
)},
// Better strong/bold text
strong: ({ children, ...props }) => (
<strong className="text-primary font-bold" {...props}>
{children}
</strong>
),
// Enhanced list items with support for "fake headings"
li: ({ children, ...props }) => {
const rawText = String(children)
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
if (isFakeHeading) {
return (
<div className="text-primary my-4 text-lg font-bold">{children}</div>
)
}
return (
<li className="my-1" {...props}>
{children}
</li>
)
},
// Enhanced unordered lists
ul: ({ children, ...props }) => (
<ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props}>
{children}
</ul>
),
// Enhanced ordered lists
ol: ({ children, ...props }) => (
<ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props}>
{children}
</ol>
),
// Enhanced blockquotes
blockquote: ({ children, ...props }) => (
<blockquote
className="border-primary/20 text-muted-foreground mt-6 border-l-4 pl-4 italic"
{...props}
>
{children}
</blockquote>
),
// Enhanced code blocks with syntax highlighting and copy button
code: ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '')
const codeContent = String(children).replace(/\n$/, '')
if (!inline && match) {
return (
<div className="group bg-muted relative my-6 overflow-hidden rounded-lg border shadow-sm">
<div className="bg-muted text-muted-foreground flex items-center justify-between px-4 py-1.5 text-sm font-medium">
<div>{match[1].toUpperCase()}</div>
<button
onClick={() => {
navigator.clipboard.writeText(codeContent)
toast.success('代码已复制')
}}
className="bg-background/80 hover:bg-background flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
>
<Copy className="h-3.5 w-3.5" />
</button>
</div>
<SyntaxHighlighter
style={codeStyle}
language={match[1]}
PreTag="div"
className="!bg-muted !m-0 !p-0"
customStyle={{
margin: 0,
padding: '1rem',
background: 'transparent',
fontSize: '0.9rem',
}}
{...props}
>
{codeContent}
</SyntaxHighlighter>
</div>
)
}
// Inline code styling
return (
<code
className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm"
{...props}
>
{children}
</code>
)
},
// Enhanced tables
table: ({ children, ...props }) => (
<div className="my-6 w-full overflow-y-auto">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
// Table headers
th: ({ children, ...props }) => (
<th
className="border-muted-foreground/20 border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</th>
),
// Table cells
td: ({ children, ...props }) => (
<td
className="border-muted-foreground/20 border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</td>
),
// Horizontal rule
hr: ({ ...props }) => (
<hr className="border-muted-foreground/20 my-8" {...props} />
),
}}
>
{selectedContent}
</ReactMarkdown>
</div>
</ScrollArea>
{showTranscribe && (
<div className={'ml-2 w-2/4'}>
<TranscriptViewer />
</div>
)}
</>
) : (
<div className="flex h-full w-full items-center justify-center">
<div className="w-[300px] flex-col justify-items-center">
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<ArrowRight className="text-primary h-8 w-8" />
</div>
<p className="mb-2 text-neutral-600">"生成笔记"</p>
<p className="text-xs text-neutral-500">YouTube等视频网站</p>
</div>
</div>
)}
</div>
)}
</div>
)
}
export default MarkdownViewer

View File

@@ -0,0 +1,118 @@
import React, { useEffect, useRef, useState } from 'react'
import { Markmap } from 'markmap-view'
import { transformer } from '@/lib/markmap.ts'
import { Toolbar, ToolbarButton } from 'markmap-toolbar'
import 'markmap-toolbar/dist/style.css'
export interface MarkmapEditorProps {
/** 要渲染的 Markdown 文本 */
value: string
/** 内容变化时的回调 */
onChange: (value: string) => void
/** Toolbar 上要展示的 item id 列表,默认使用 Toolbar.defaultItems */
toolbarItems?: string[]
/** 自定义按钮列表,会依次注册 */
customButtons?: ToolbarButton[]
/** 容器 SVG 的高度,默认为 600px */
height?: string
}
export default function MarkmapEditor({
value,
onChange,
toolbarItems,
customButtons = [],
height = '600px',
}: MarkmapEditorProps) {
const svgRef = useRef<SVGSVGElement>(null)
const mmRef = useRef<Markmap>()
const toolbarRef = useRef<HTMLDivElement>(null)
// 用于跟踪是否处于全屏状态
const [isFullscreen, setIsFullscreen] = useState(false)
// 监听全屏状态变化
useEffect(() => {
const handler = () => {
setIsFullscreen(!!document.fullscreenElement)
}
document.addEventListener('fullscreenchange', handler)
return () => {
document.removeEventListener('fullscreenchange', handler)
}
}, [])
// 进入全屏
const enterFullscreen = () => {
const el = svgRef.current?.parentElement
if (el && el.requestFullscreen) {
el.requestFullscreen()
}
}
// 退出全屏
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
// 初始化 Markmap 实例 + Toolbar
useEffect(() => {
if (!svgRef.current || mmRef.current) return
const mm = Markmap.create(svgRef.current)
mmRef.current = mm
if (toolbarRef.current) {
toolbarRef.current.innerHTML = ''
const toolbar = new Toolbar()
toolbar.attach(mm)
customButtons.forEach(btn => toolbar.register(btn))
toolbar.setItems(toolbarItems ?? Toolbar.defaultItems)
toolbarRef.current.appendChild(toolbar.render())
}
}, [customButtons, toolbarItems])
// 当 value 变化时,重新渲染数据
useEffect(() => {
const mm = mmRef.current
if (!mm) return
const { root } = transformer.transform(value)
mm.setData(root).then(() => mm.fit())
}, [value])
// 文本输入变化回调(如果你自行添加 textarea 编辑区)
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(e.target.value)
}
return (
<div className="relative flex h-full flex-col bg-white">
{/* 全屏/退出全屏 按钮 */}
<div className="absolute top-2 right-2 z-20 flex space-x-2">
{isFullscreen ? (
<button
onClick={exitFullscreen}
className="rounded p-1 hover:bg-gray-200"
title="退出全屏"
>
🗗
</button>
) : (
<button onClick={enterFullscreen} className="rounded p-1 hover:bg-gray-200" title="全屏">
🗖
</button>
)}
</div>
{/* 如果需要编辑区,就自己加一个 <textarea> 并把 handleChange 绑上 */}
{/* <textarea value={value} onChange={handleChange} className="mb-2 p-2 border rounded" /> */}
{/* 思维导图区 */}
<svg ref={svgRef} className="w-full flex-1" style={{ height, overflow: 'auto' }} />
{/* 如果你还想保留 markmap-toolbar */}
{/* <div ref={toolbarRef} className="absolute right-2 bottom-2 z-10" /> */}
</div>
)
}

View File

@@ -0,0 +1,559 @@
/* NoteForm.tsx ---------------------------------------------------- */
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form.tsx'
import { useEffect,useState } from 'react'
import { useForm, useWatch } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Info, Loader2, Plus } from 'lucide-react'
import { message, Alert } from 'antd'
import { generateNote } from '@/services/note.ts'
import { uploadFile } from '@/services/upload.ts'
import { useTaskStore } from '@/store/taskStore'
import { useModelStore } from '@/store/modelStore'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx'
import { Checkbox } from '@/components/ui/checkbox.tsx'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
import { Button } from '@/components/ui/button.tsx'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select.tsx'
import { Input } from '@/components/ui/input.tsx'
import { Textarea } from '@/components/ui/textarea.tsx'
import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts'
import { fetchModels } from '@/services/model.ts'
import { useNavigate } from 'react-router-dom'
/* -------------------- 校验 Schema -------------------- */
const formSchema = z
.object({
video_url: z.string(),
platform: z.string().nonempty('请选择平台'),
quality: z.enum(['fast', 'medium', 'slow']),
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(),
video_understanding: z.boolean().optional(),
video_interval: z.coerce.number().min(1).max(30).default(4).optional(),
grid_size: z
.tuple([z.coerce.number().min(1).max(10), z.coerce.number().min(1).max(10)])
.default([3, 3])
.optional(),
})
.superRefine(({ video_url, platform }, ctx) => {
if (platform === 'local' || platform === 'douyin') {
if (!video_url) {
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
}
} else {
try {
const url = new URL(video_url)
if (!['http:', 'https:'].includes(url.protocol)) throw new Error()
} catch {
ctx.addIssue({ code: 'custom', message: '请输入正确的视频链接', path: ['video_url'] })
}
}
})
type NoteFormValues = z.infer<typeof formSchema>
/* -------------------- 可复用子组件 -------------------- */
const SectionHeader = ({ title, tip }: { title: string; tip?: string }) => (
<div className="my-3 flex items-center justify-between">
<h2 className="block">{title}</h2>
{tip && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent className="text-xs">{tip}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)
const CheckboxGroup = ({
value = [],
onChange,
disabledMap,
}: {
value?: string[]
onChange: (v: string[]) => void
disabledMap: Record<string, boolean>
}) => (
<div className="flex flex-wrap space-x-1.5">
{noteFormats.map(({ label, value: v }) => (
<label key={v} className="flex items-center space-x-2">
<Checkbox
checked={value.includes(v)}
disabled={disabledMap[v]}
onCheckedChange={checked =>
onChange(checked ? [...value, v] : value.filter(x => x !== v))
}
/>
<span>{label}</span>
</label>
))}
</div>
)
/* -------------------- 主组件 -------------------- */
const NoteForm = () => {
const navigate = useNavigate();
const [isUploading, setIsUploading] = useState(false)
const [uploadSuccess, setUploadSuccess] = useState(false)
/* ---- 全局状态 ---- */
const { addPendingTask, currentTaskId, setCurrentTask, getCurrentTask, retryTask } =
useTaskStore()
const { loadEnabledModels, modelList, showFeatureHint, setShowFeatureHint } = useModelStore()
/* ---- 表单 ---- */
const form = useForm<NoteFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
platform: 'bilibili',
quality: 'medium',
model_name: modelList[0]?.model_name || '',
style: 'minimal',
video_interval: 4,
grid_size: [3, 3],
format: [],
},
})
const currentTask = getCurrentTask()
/* ---- 派生状态(只 watch 一次,提高性能) ---- */
const platform = useWatch({ control: form.control, name: 'platform' }) as string
const videoUnderstandingEnabled = useWatch({ control: form.control, name: 'video_understanding' })
const editing = currentTask && currentTask.id
const goModelAdd = () => {
navigate("/settings/model");
};
/* ---- 副作用 ---- */
useEffect(() => {
loadEnabledModels()
return
}, [])
useEffect(() => {
if (!currentTask) return
const { formData } = currentTask
console.log('currentTask.formData.platform:', formData.platform)
form.reset({
platform: formData.platform || 'bilibili',
video_url: formData.video_url || '',
model_name: formData.model_name || modelList[0]?.model_name || '',
style: formData.style || 'minimal',
quality: formData.quality || 'medium',
extras: formData.extras || '',
screenshot: formData.screenshot ?? false,
link: formData.link ?? false,
video_understanding: formData.video_understanding ?? false,
video_interval: formData.video_interval ?? 4,
grid_size: formData.grid_size ?? [3, 3],
format: formData.format ?? [],
})
}, [
// 当下面任意一个变了,就重新 reset
currentTaskId,
// modelList 用来兜底 model_name
modelList.length,
// 还要加上 formData 的各字段,或者直接 currentTask
currentTask?.formData,
])
/* ---- 帮助函数 ---- */
const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status)
const generating = isGenerating()
const handleFileUpload = async (file: File, cb: (url: string) => void) => {
const formData = new FormData()
formData.append('file', file)
setIsUploading(true)
setUploadSuccess(false)
try {
const data = await uploadFile(formData)
cb(data.url)
setUploadSuccess(true)
} catch (err) {
console.error('上传失败:', err)
message.error('上传失败,请重试')
} finally {
setIsUploading(false)
}
}
const onSubmit = async (values: NoteFormValues) => {
console.log('Not even go here')
const payload: NoteFormValues = {
...values,
provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id,
task_id: currentTaskId || '',
}
if (currentTaskId) {
retryTask(currentTaskId, payload)
return
}
message.success('已提交任务')
const data = await generateNote(payload)
addPendingTask(data.task_id, values.platform, payload)
}
const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
console.warn('表单校验失败:', errors)
message.error('请完善所有必填项后再提交')
}
const handleCreateNew = () => {
// 🔁 这里清空当前任务状态
// 比如调用 resetCurrentTask() 或者 navigate 到一个新页面
setCurrentTask(null)
}
const FormButton = () => {
const label = generating ? '正在生成…' : editing ? '重新生成' : '生成笔记'
return (
<div className="flex gap-2">
<Button
type="submit"
className={!editing ? 'w-full' : 'w-2/3' + ' bg-primary'}
disabled={generating}
>
{generating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{label}
</Button>
{editing && (
<Button type="button" variant="outline" className="w-1/3" onClick={handleCreateNew}>
<Plus className="mr-2 h-4 w-4" />
</Button>
)}
</div>
)
}
/* -------------------- 渲染 -------------------- */
return (
<div className="h-full w-full">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit, onInvalid)} className="space-y-4">
{/* 顶部按钮 */}
<FormButton></FormButton>
{/* 视频链接 & 平台 */}
<SectionHeader title="视频链接" tip="支持 B 站、YouTube 等平台" />
<div className="flex gap-2">
{/* 平台选择 */}
<FormField
control={form.control}
name="platform"
render={({ field }) => (
<FormItem>
<Select
disabled={!!editing}
value={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{videoPlatforms?.map(p => (
<SelectItem key={p.value} value={p.value}>
<div className="flex items-center justify-center gap-2">
<div className="h-4 w-4">{p.logo()}</div>
<span>{p.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* 链接输入 / 上传框 */}
<FormField
control={form.control}
name="video_url"
render={({ field }) => (
<FormItem className="flex-1">
{platform === 'local' ? (
<>
<Input disabled={!!editing} placeholder="请输入本地视频路径" {...field} />
</>
) : (
<Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} />
)}
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="video_url"
render={({ field }) => (
<FormItem className="flex-1">
{platform === 'local' && (
<>
<div
className="hover:border-primary mt-2 flex h-40 cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-gray-300 transition-colors"
onDragOver={e => {
e.preventDefault()
e.stopPropagation()
}}
onDrop={e => {
e.preventDefault()
const file = e.dataTransfer.files?.[0]
if (file) handleFileUpload(file, field.onChange)
}}
onClick={() => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'video/*'
input.onchange = e => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) handleFileUpload(file, field.onChange)
}
input.click()
}}
>
{isUploading ? (
<p className="text-center text-sm text-blue-500"></p>
) : uploadSuccess ? (
<p className="text-center text-sm text-green-500"></p>
) : (
<p className="text-center text-sm text-gray-500">
<br />
<span className="text-xs text-gray-400"></span>
</p>
)}
</div>
</>
)}
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-2">
{/* 模型选择 */}
{
modelList.length>0?( <FormField
className="w-full"
control={form.control}
name="model_name"
render={({ field }) => (
<FormItem>
<SectionHeader title="模型选择" tip="不同模型效果不同,建议自行测试" />
<Select
onOpenChange={()=>{
loadEnabledModels()
}}
value={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="w-full min-w-0 truncate">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{modelList.map(m => (
<SelectItem key={m.id} value={m.model_name}>
{m.model_name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>): (
<FormItem>
<SectionHeader title="模型选择" tip="不同模型效果不同,建议自行测试" />
<Button type={'button'} variant={
'outline'
} onClick={()=>{goModelAdd()}}></Button>
<FormMessage />
</FormItem>
)
}
{/* 笔记风格 */}
<FormField
className="w-full"
control={form.control}
name="style"
render={({ field }) => (
<FormItem>
<SectionHeader title="笔记风格" tip="选择生成笔记的呈现风格" />
<Select
value={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="w-full min-w-0 truncate">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{noteStyles.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* 视频理解 */}
<SectionHeader title="视频理解" tip="将视频截图发给多模态模型辅助分析" />
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="video_understanding"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel></FormLabel>
<Checkbox
checked={videoUnderstandingEnabled}
onCheckedChange={v => form.setValue('video_understanding', v)}
/>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
{/* 采样间隔 */}
<FormField
control={form.control}
name="video_interval"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Input disabled={!videoUnderstandingEnabled} type="number" {...field} />
<FormMessage />
</FormItem>
)}
/>
{/* 拼图大小 */}
<FormField
control={form.control}
name="grid_size"
render={({ field }) => (
<FormItem>
<FormLabel> × </FormLabel>
<div className="flex items-center space-x-2">
<Input
disabled={!videoUnderstandingEnabled}
type="number"
value={field.value?.[0] || 3}
onChange={e => field.onChange([+e.target.value, field.value?.[1] || 3])}
className="w-16"
/>
<span>x</span>
<Input
disabled={!videoUnderstandingEnabled}
type="number"
value={field.value?.[1] || 3}
onChange={e => field.onChange([field.value?.[0] || 3, +e.target.value])}
className="w-16"
/>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<Alert
closable
type="error"
message={
<div>
<strong></strong>
<p>使</p>
</div>
}
className="text-sm"
/>
</div>
{/* 笔记格式 */}
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<SectionHeader title="笔记格式" tip="选择要包含的笔记元素" />
<CheckboxGroup
value={field.value}
onChange={field.onChange}
disabledMap={{
link: platform === 'local',
screenshot: !videoUnderstandingEnabled,
}}
/>
<FormMessage />
</FormItem>
)}
/>
{/* 备注 */}
<FormField
control={form.control}
name="extras"
render={({ field }) => (
<FormItem>
<SectionHeader title="备注" tip="可在 Prompt 结尾附加自定义说明" />
<Textarea placeholder="笔记需要罗列出 xxx 关键点…" {...field} />
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
)
}
export default NoteForm

View File

@@ -0,0 +1,15 @@
import { useForm } from 'react-hook-form'
import { Form } from '@/components/ui/form.tsx'
import NoteForm from './NoteForm.tsx'
const NoteFormWrapper = () => {
const form = useForm()
return (
<Form {...form}>
<NoteForm />
</Form>
)
}
export default NoteFormWrapper

View File

@@ -0,0 +1,186 @@
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 } 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)
const baseURL = import.meta.env.VITE_API_BASE_URL || 'api/'
const [rawSearch, setRawSearch] = useState('')
const [search, setSearch] = useState('')
const fuse = new Fuse(tasks, {
keys: ['audioMeta.title'],
threshold: 0.4 // 匹配精度(越低越严格)
})
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
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
key={task.id}
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

View File

@@ -0,0 +1,53 @@
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>
{/* 连接线 */}
<div className={`h-1 w-full ${isActive ? 'bg-primary' : 'bg-gray-300'}`}></div>
</div>
)
})}
</div>
)
}
export default StepBar

View File

@@ -0,0 +1,117 @@
"use client"
import { useTaskStore } from "@/store/taskStore"
import { useEffect, useState, useRef } from "react"
import { Play } from "lucide-react"
import { cn } from "@/lib/utils"
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
interface Segment {
start: number
end: number
text: string
}
interface Task {
transcript?: {
segments?: Segment[]
}
}
const TranscriptViewer = () => {
const getCurrentTask = useTaskStore((state) => state.getCurrentTask)
const currentTaskId = useTaskStore((state) => state.currentTaskId)
const [task, setTask] = useState<Task | null>(null)
const [activeSegment, setActiveSegment] = useState<number | null>(null)
const segmentRefs = useRef<(HTMLDivElement | null)[]>([])
useEffect(() => {
setTask(getCurrentTask())
}, [currentTaskId, getCurrentTask])
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, "0")}`
}
const handleSegmentClick = (index: number) => {
setActiveSegment(index)
// Here you could add functionality to play the audio from this segment
}
const scrollToSegment = (index: number) => {
segmentRefs.current[index]?.scrollIntoView({
behavior: "smooth",
block: "center",
})
}
return (
<div className="transcript-viewer flex h-full w-full flex-col rounded-md border bg-white p-4 shadow-sm">
<h2 className="mb-4 text-lg font-medium"></h2>
{!task?.transcript?.segments?.length ? (
<div className="flex h-full items-center justify-center text-muted-foreground"></div>
) : (
<>
<div className="mb-3 grid grid-cols-[80px_1fr] gap-2 border-b pb-2 text-xs font-medium text-muted-foreground">
<div></div>
<div></div>
</div>
<ScrollArea className="w-full overflow-y-auto">
<div className="space-y-1">
{task.transcript.segments.map((segment, index) => (
<div
key={index}
ref={(el) => (segmentRefs.current[index] = el)}
className={cn(
"group grid grid-cols-[80px_1fr] gap-2 rounded-md p-2 transition-colors hover:bg-slate-50",
activeSegment === index && "bg-slate-100",
)}
onClick={() => handleSegmentClick(index)}
>
<div className="flex items-center gap-1 text-xs text-slate-500">
<button
className="invisible rounded-full p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-700 group-hover:visible"
onClick={(e) => {
e.stopPropagation()
// Add play functionality here
}}
>
{/*<Play className="h-3 w-3" />*/}
</button>
<span>{formatTime(segment.start)}</span>
</div>
<div className="text-sm leading-relaxed text-slate-700">
{segment.speaker && (
<span className="mr-2 rounded bg-slate-200 px-1.5 py-0.5 text-xs font-medium text-slate-700">
{segment.speaker}
</span>
)}
{segment.text}
</div>
</div>
))}
</div>
</ScrollArea>
</>
)}
{task?.transcript?.segments?.length > 0 && (
<div className="mt-4 flex justify-between border-t pt-3 text-xs text-slate-500">
<span> {task.transcript.segments.length} </span>
<span>: {formatTime(task.transcript.segments[task.transcript.segments.length - 1]?.end || 0)}</span>
</div>
)}
</div>
)
}
export default TranscriptViewer

View File

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

View File

@@ -0,0 +1,25 @@
// src/pages/NotFoundPage.tsx
import NotFound from '@/components/Lottie/404.tsx'
import { Button } from '@/components/ui/button.tsx'
import { useNavigate } from 'react-router-dom'
const NotFoundPage = () => {
const navigate = useNavigate()
return (
<div className="flex min-h-screen w-full flex-col items-center justify-center text-gray-500">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold"></h1>
<p className="mb-4 text-lg"></p>
<Button onClick={() => navigate('/')} className="hover:underline">
</Button>
</div>
<div>
<NotFound />
</div>
</div>
)
}
export default NotFoundPage

View File

@@ -0,0 +1,16 @@
import Provider from '@/components/Form/modelForm/Provider.tsx'
import { Outlet } from 'react-router-dom'
import Options from '@/components/Form/DownloaderForm/Options.tsx'
const Downloader = () => {
return (
<div className={'flex h-full bg-white'}>
<div className={'flex-1/5 border-r border-neutral-200 p-2'}>
<Options></Options>
</div>
<div className={'flex-4/5'}>
<Outlet />
</div>
</div>
)
}
export default Downloader

View File

@@ -0,0 +1,68 @@
import {
BotMessageSquare,
SquareChevronRight,
Captions,
HardDriveDownload,
Wrench,
Info,
} from 'lucide-react'
import MenuBar, { IMenuProps } from '@/pages/SettingPage/components/menuBar.tsx'
const Menu = () => {
const menuList: IMenuProps[] = [
{
id: 'model',
name: 'AI 模型设置',
icon: <BotMessageSquare />,
path: '/settings/model',
},
// TODO :下一版本升级优化
// {
// id: ' transcriber',
// name: '音频转译配置',
// icon: <Captions />,
// path: '/settings/transcriber',
// },
// //下载配置
{
id: 'download',
name: '下载配置',
icon: <HardDriveDownload />,
path: '/settings/download',
},
// //其他配置
// {
// id: 'prompt',
// name: '提示词设置',
// icon: <SquareChevronRight />,
// path: '/settings/prompt',
// },
{
id: 'about',
name: '关于',
icon: <Info />,
path: '/settings/about',
},
// {
// id: 'other',
// name: '其他配置',
// icon: <Wrench />,
// path: '/settings/other',
// },
]
return (
<div className="flex h-full flex-col">
<div className={'flex w-full flex-col gap-2'}>
<div className="text-2xl font-medium"></div>
<div className="text-sm font-light text-gray-800"></div>
</div>
<div className="mt-6 flex-1">
{menuList &&
menuList.map(item => {
return <MenuBar key={item.id} menuItem={item} />
})}
</div>
</div>
)
}
export default Menu

Some files were not shown because too many files have changed in this diff Show More