Release/v0.2.2 (#258)
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* Fix/interface home (#182)
* [fix]Fix the interface for statistics of recent activities and applications
* [changes]Modify the code based on the AI review
1.Use the boolean auxiliary methods provided by SQLAlchemy instead of using == True in the is_active filter.
2.The calculation of the "PROJECT_ROOT" has now been hardcoded with five levels of nested os.path.dirname calls.
* [fix]Fix the interface for statistics of recent activities and applications
* [changes]Modify the code based on the AI review
1.Use the boolean auxiliary methods provided by SQLAlchemy instead of using == True in the is_active filter.
2.The calculation of the "PROJECT_ROOT" has now been hardcoded with five levels of nested os.path.dirname calls.
* Fix/optimize inerface (#183)
* [changes]Optimize the time consumption of the "/end_users" interface
* [fix]Optimize the time consumption of the "/hot_memory_tags" interface
* [changes]Optimize the time consumption of the "/end_users" interface
* [fix]Optimize the time consumption of the "/hot_memory_tags" interface
* [changes]Improve the code based on AI review
* Fix/memory mcp2 1 (#184)
* 优化快速检索的回复内容
* 优化快速检索的回复内容
* Fix/memory mcp2 1 (#185)
* 优化快速检索的回复内容
* 优化快速检索的回复内容
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* Fix/memory mcp2 1 (#188)
* 优化快速检索的回复内容
* 优化快速检索的回复内容
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* LLM生存缺少config_id认证,修复BUG
* LLM生存缺少config_id认证,修复BUG
* LLM生存缺少config_id认证,修复BUG
* 解决冲突
* 解决冲突
* feat(home page): version description update
* Fix/memory mcp2 1 (#190)
* 优化快速检索的回复内容
* 优化快速检索的回复内容
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* 路径的BUG修复
* LLM生存缺少config_id认证,修复BUG
* LLM生存缺少config_id认证,修复BUG
* LLM生存缺少config_id认证,修复BUG
* 深度检索优化,搜索不到数据/提问的概念过于蘑菇,以引导的方式继续提问
* 深度检索优化,搜索不到数据/提问的概念过于蘑菇,以引导的方式继续提问
* 深度检索优化,搜索不到数据/提问的概念过于蘑菇,以引导的方式继续提问
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* feat(web): memory related interface parameter transfer adjustment
* 感知meta_data字段BUG修复
* Fix/memory bug fix (#171)
* feat(sandbox): add Python 3 code execution sandbox support
* feat(workflow): emit SSE events for node exception output
* perf(sandbox): optimize code encryption handling
* perf(workflow): update standard node output structure
* [add] migration script
* [modify] migration script
* feat(web): add workflow runtime info
* fix(web): handleSSE bugfix
* fix(sandbox): prevent imports from being blocked when network is disabled
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* Fix/memory bug fix (#199)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* user_id->显示为config_id_old传输
* feat(web): update read_all_config select valueKey
* user_id->显示为config_id_old传输
* feat(workflow): Add a new node for executing code
* fix(web): KnowledgeConfigModal bugfix
* fix(web): iteration's variable add parameter-extractor node
* fix(sandbox): treat non-zero exit codes as errors instead of relying only on stderr
* Fix/memory bug fix (#200)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* Refactor/benchmark test (#196)
* [changes]refactor locomo_test
* [fix]Fix the circular import of ModelParameters
* [changes]The benchmark test can run stably.
* [fix]Complete end-to-end LoCoMo repair
* [fix]Complete the end-to-end longmemeval and memsciqa fixes
* [changes]Complete the benchmark test description document to ensure that the configuration parameters take effect.
* [changes]refactor locomo_test
* [fix]Fix the circular import of ModelParameters
* [changes]The benchmark test can run stably.
* [fix]Complete end-to-end LoCoMo repair
* [fix]Complete the end-to-end longmemeval and memsciqa fixes
* [changes]Complete the benchmark test description document to ensure that the configuration parameters take effect.
* [changes]Benchmark test adaptation for end_user_id
* [changes]refactor locomo_test
* [fix]Fix the circular import of ModelParameters
* [changes]The benchmark test can run stably.
* [fix]Complete end-to-end LoCoMo repair
* [fix]Complete the end-to-end longmemeval and memsciqa fixes
* [changes]Complete the benchmark test description document to ensure that the configuration parameters take effect.
* [fix]Complete the end-to-end longmemeval and memsciqa fixes
* [changes]Complete the benchmark test description document to ensure that the configuration parameters take effect.
* [changes]Benchmark test adaptation for end_user_id
* [modify] migration script
* delete benchmark-test (#204)
* Refactor: Move evaluation folder to redbear-mem-benchmark submodule
* [changes]Restore .gitmodules
* feat(web): workflow add code node
* 检查需要更改的格式问题
* Fix/redbear benchmark (#205)
* Refactor: Move evaluation folder to redbear-mem-benchmark submodule
* [changes]Update submodule reference
* Refactor: Move evaluation folder to redbear-mem-benchmark submodule
* [changes]Update submodule reference
* Remove duplicate evaluation submodule, use redbear-mem-benchmark instead
* Fix/memory bug fix (#207)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* fix(web): remove URI decode and encode
* [add] plugin system and base sso module
* 修复宿主列表获取memory_config_idBUG
* Fix/memory bug fix (#209)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
* 修复宿主列表获取memory_config_idBUG
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* [modify] file local server url
* [add] migration script
* fix(workflow): fix activation and branch control issues in streaming output
* fix(workflow): fix function cache not taking effect and potential list index overflow
* style(workflow): enforce PEP8 style and remove redundant imports
* fix(workflow): fix streaming output error when variable is not a string
* [fix]remove aspose-slides
* perf(workflow): enhance streaming output node activation performance
* feat(workflow): store token usage in message table
* feat(web): add PageEmpty component
* feat(web): add PageTabs component
* perf(workflow): make memory configuration backward compatible
* feat(web): update model management
* config_id做映射
* config_id做映射
* Fix/memory bug fix (#211)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
* 修复宿主列表获取memory_config_idBUG
* config_id做映射
* config_id做映射
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* feat(web): getModelListUrl add is_active param
* config_id做映射+1
* config_id做映射+1
* config_id做映射+1
* feat(web): remove file url replace
* Fix/memory bug fix (#212)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
* 修复宿主列表获取memory_config_idBUG
* config_id做映射
* config_id做映射
* config_id做映射+1
* config_id做映射+1
* config_id做映射+1
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics
* feat(web): model logo update
* 应用层memory_content->memory_config
* fix(web): correct spelling
* 应用层memory_content->memory_config
* 应用层memory_content->memory_config
* Fix/memory bug fix (#215)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
* 修复宿主列表获取memory_config_idBUG
* config_id做映射
* config_id做映射
* config_id做映射+1
* config_id做映射+1
* config_id做映射+1
* 应用层memory_content->memory_config
* 应用层memory_content->memory_config
* 应用层memory_content->memory_config
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics
* fix(web): model loading update
* 统一字段为config_id_old
* 统一字段为config_id_old
* feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics
* 统一字段为config_id_old
* 统一字段为config_id_old
* memory_content暂时不修改
* memory_content暂时不修改
* Fix/memory bug fix (#217)
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 图谱数据量限制数量去掉
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 用户详情优化
* 读取的接口,去掉全局锁
* 输出数组
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化1.0(优化隐私输出、时间检索)
* 反思优化测试接口
* 反思优化测试接口
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 读取接口内层嵌套BUG修复
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段
* 把group_id替换end_user_id
* 把group_id替换end_user_id_
* 把group_id替换end_user_id_
* config_config替换成memory_config
* config_config替换成memory_config
* [fix]Fix the memory interface to use end_user_id.
* config_config替换成memory_config
* config_config替换成memory_config
* config_config替换成memory_config
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID
* config_id字段改成UUID,与develop校对恢复
* 检查项目,修复group_id的遗留问题
* 检查项目,修复group_id的遗留问题
* 解决冲突
* 解决冲突
* end_user_id清理干净
* end_user_id清理干净
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 修复遗留合并BUG
* 感知meta_data字段BUG修复
* user_id->现实为config_id_old
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* user_id->显示为config_id_old传输
* 检查需要更改的格式问题
* 修复宿主列表获取memory_config_idBUG
* config_id做映射
* config_id做映射
* config_id做映射+1
* config_id做映射+1
* config_id做映射+1
* 应用层memory_content->memory_config
* 应用层memory_content->memory_config
* 应用层memory_content->memory_config
* 统一字段为config_id_old
* 统一字段为config_id_old
* 统一字段为config_id_old
* 统一字段为config_id_old
* memory_content暂时不修改
* memory_content暂时不修改
---------
Co-authored-by: lanceyq <1982376970@qq.com>
* feat(web): add app statistics
* fix(workflow): fix streaming output issues with multi-output End nodes
End nodes with multiple output segments could cause cursor errors or leave some
segments inactive, resulting in incorrect final outputs.
Unified _emit_active_chunks and _update_scope_activate to ensure all segments
are activated in order and streamed correctly.
* feat(web): add apps statistics api
* fix(web): agent's knowledge_bases bugfix
* Revert "feat(web): update read_all_config select valueKey"
This reverts commit 46f0f3cee9.
* [add] migrations script
* perf(workflow): make memory write node backward-compatible and defer config validation
* 旧数据兼容
* 旧数据兼容
* 旧数据兼容
* 旧数据兼容
* fix(web): model bugfix
* fix(web): model bugfix
* 提交遗漏 (#228)
* [fix] chat api for workflow
* [fix] web search set for v1 api
* fix(web): model bugfix
* fix(web): model list remove is_active
* fix(model): bug fix
* [add]migration script
* [fix] api
* [fix] api
* fix(web): model bugfix
* fix(model): the model type does not allow modification, delete tts and speech2text type
* fix(model): bug fix
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* Add/develop memory (#239)
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* feat(web): model ui update
* feat(web): model ui update
* Add/develop memory (#243)
* 遗漏的历史映射
* 遗漏的历史映射
* fix(model): bug fix
* feat(web): model ui update
* Add/develop memory (#247)
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* [modify] migration script
* [add] migration script
* fix(web): change form message
* fix(web): the memoryContent field is compatible with numbers and strings
* feat(web): code node hidden
* fix(model):
1. create a basic model to check if the name and provider are duplicated.
2. The result shows error models because the provider created API Keys for all matching models.
---------
Co-authored-by: lixinyue <2569494688@qq.com>
Co-authored-by: lanceyq <1982376970@qq.com>
Co-authored-by: yujiangping <yujiangping@taofen8.com>
Co-authored-by: 乐力齐 <162269739+lanceyq@users.noreply.github.com>
Co-authored-by: lixinyue11 <94037597+lixinyue11@users.noreply.github.com>
Co-authored-by: yingzhao <zhaoyingyz@126.com>
Co-authored-by: Timebomb2018 <18868801967@163.com>
Co-authored-by: Mark <zhuwenhui5566@163.com>
Co-authored-by: zhaoying <yzhao96@best-inc.com>
Co-authored-by: Eternity <1533512157@qq.com>
Co-authored-by: lixiangcheng1 <lixiangcheng1@wanda.cn>
@@ -108,4 +108,8 @@ export const getShareToken = (share_token: string, user_id: string) => {
|
||||
// 复制应用
|
||||
export const copyApplication = (app_id: string, new_name: string) => {
|
||||
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
|
||||
}
|
||||
}
|
||||
// 数据统计
|
||||
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
||||
return request.get(`/apps/${app_id}/statistics`, data)
|
||||
}
|
||||
|
||||
25
web/src/api/fileStorage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { request, API_PREFIX } from '@/utils/request'
|
||||
|
||||
// Upload file,file storage has expiration period
|
||||
export const fileUploadUrl = `${API_PREFIX}/storage/files`
|
||||
export const fileUpload = (formData?: unknown) => {
|
||||
return request.uploadFile('/storage/files', formData)
|
||||
}
|
||||
|
||||
// Get file access URL (no token required)
|
||||
export const getFileUrl = (file_id: string) => `/storage/files/${file_id}/url`
|
||||
export const getFileLink = (fileId: string, data: { permanent?: boolean } = { permanent: true }) => {
|
||||
return request.get(getFileUrl(fileId), data)
|
||||
}
|
||||
|
||||
// Get file internally
|
||||
export const getInternalFileUrl = (file_id: string) => `/storage/files/${file_id}`
|
||||
export const getInternalFile = (fileId: string) => {
|
||||
return request.get(getInternalFileUrl(fileId))
|
||||
}
|
||||
|
||||
// Delete file
|
||||
export const deleteFileUrl = (file_id: string) => `/storage/files/${file_id}`
|
||||
export const deleteFile = (fileId: string) => {
|
||||
return request.delete(deleteFileUrl(fileId))
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export const getModelTypeList = async () => {
|
||||
};
|
||||
// 获取模型列表
|
||||
export const getModelList = async (pageInfo: PageRequest) => {
|
||||
const response = await request.get(`${apiPrefix}/models`, pageInfo);
|
||||
const response = await request.get(`${apiPrefix}/models`, { ...pageInfo, is_active: true });
|
||||
return response as any;
|
||||
};
|
||||
//获取模型提供者
|
||||
|
||||
@@ -116,20 +116,20 @@ export const getRagContent = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
|
||||
}
|
||||
// Emotion distribution analysis
|
||||
export const getWordCloud = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 })
|
||||
export const getWordCloud = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/wordcloud`, { end_user_id, limit: 20 })
|
||||
}
|
||||
// High-frequency emotion keywords
|
||||
export const getEmotionTags = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 })
|
||||
export const getEmotionTags = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/tags`, { end_user_id, limit: 20 })
|
||||
}
|
||||
// Emotion health index
|
||||
export const getEmotionHealth = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 })
|
||||
export const getEmotionHealth = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/health`, { end_user_id })
|
||||
}
|
||||
// Personalized suggestions
|
||||
export const getEmotionSuggestions = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 })
|
||||
export const getEmotionSuggestions = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/suggestions`, { end_user_id })
|
||||
}
|
||||
export const generateSuggestions = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/generate_suggestions`, { end_user_id })
|
||||
@@ -138,8 +138,8 @@ export const analyticsRefresh = (end_user_id: string) => {
|
||||
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
|
||||
}
|
||||
// Forgetting stats
|
||||
export const getForgetStats = (group_id: string) => {
|
||||
return request.get(`/memory/forget-memory/stats`, { group_id })
|
||||
export const getForgetStats = (end_user_id: string) => {
|
||||
return request.get(`/memory/forget-memory/stats`, { end_user_id })
|
||||
}
|
||||
// Implicit Memory - Preferences
|
||||
export const getImplicitPreferences = (end_user_id: string) => {
|
||||
@@ -165,20 +165,20 @@ export const getShortTerm = (end_user_id: string) => {
|
||||
return request.get(`/memory/short/short_term`, { end_user_id })
|
||||
}
|
||||
// Perceptual Memory - Visual memory
|
||||
export const getPerceptualLastVisual = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_visual`)
|
||||
export const getPerceptualLastVisual = (end_user_id: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user_id}/last_visual`)
|
||||
}
|
||||
// Perceptual Memory - Audio memory
|
||||
export const getPerceptualLastListen = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_listen`)
|
||||
export const getPerceptualLastListen = (end_user_id: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user_id}/last_listen`)
|
||||
}
|
||||
// Perceptual Memory - Text memory
|
||||
export const getPerceptualLastText = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_text`)
|
||||
export const getPerceptualLastText = (end_user_id: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user_id}/last_text`)
|
||||
}
|
||||
// Perceptual Memory - Perceptual memory timeline
|
||||
export const getPerceptualTimeline = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/timeline`)
|
||||
export const getPerceptualTimeline = (end_user_id: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user_id}/timeline`)
|
||||
}
|
||||
// Episodic Memory - Overview
|
||||
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
|
||||
@@ -201,14 +201,14 @@ export const getExplicitMemory = (end_user_id: string) => {
|
||||
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
|
||||
return request.post(`/memory/explicit-memory/details`, data)
|
||||
}
|
||||
export const getConversations = (end_user: string) => {
|
||||
return request.get(`/memory/work/${end_user}/conversations`)
|
||||
export const getConversations = (end_user_id: string) => {
|
||||
return request.get(`/memory/work/${end_user_id}/conversations`)
|
||||
}
|
||||
export const getConversationMessages = (end_user: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user}/messages`, { conversation_id })
|
||||
export const getConversationMessages = (end_user_id: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user_id}/messages`, { conversation_id })
|
||||
}
|
||||
export const getConversationDetail = (end_user: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user}/detail`, { conversation_id })
|
||||
export const getConversationDetail = (end_user_id: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user_id}/detail`, { conversation_id })
|
||||
}
|
||||
export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => {
|
||||
return request.post(`/memory/forget-memory/trigger`, data)
|
||||
|
||||
@@ -1,23 +1,68 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ModelFormData } from '@/views/ModelManagement/types'
|
||||
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'
|
||||
|
||||
// 模型列表
|
||||
// Model list
|
||||
export const getModelListUrl = '/models'
|
||||
export const getModelList = (data: { type: string; pagesize: number; page: number; }) => {
|
||||
export const getModelList = (data: Query) => {
|
||||
return request.get(getModelListUrl, data)
|
||||
}
|
||||
// 创建模型
|
||||
export const addModel = (data: ModelFormData) => {
|
||||
return request.post('/models', data)
|
||||
}
|
||||
// 更新模型
|
||||
export const updateModel = (apiKeyId: string, data: ModelFormData) => {
|
||||
return request.put(`/models/apikeys/${apiKeyId}`, data)
|
||||
}
|
||||
// 模型类型列表
|
||||
// Model type list
|
||||
export const modelTypeUrl = '/models/type'
|
||||
// 模型供应商列表
|
||||
// Model provider list
|
||||
export const modelProviderUrl = '/models/provider'
|
||||
export const getModelProviderList = () => {
|
||||
return request.get(modelProviderUrl)
|
||||
}
|
||||
// New model list
|
||||
export const getModelNewListUrl = '/models/new'
|
||||
export const getModelNewList = (data: Query) => {
|
||||
return request.get(getModelNewListUrl, data)
|
||||
}
|
||||
// Get model information
|
||||
export const getModelInfo = (model_id: string) => {
|
||||
return request.get(`/models/${model_id}`)
|
||||
}
|
||||
// Create composite model
|
||||
export const addCompositeModel = (data: CompositeModelForm) => {
|
||||
return request.post('/models/composite', data)
|
||||
}
|
||||
// Update composite model
|
||||
export const updateCompositeModel = (model_id: string, data: CompositeModelForm) => {
|
||||
return request.put(`/models/composite/${model_id}`, data)
|
||||
}
|
||||
// Delete composite model
|
||||
export const deleteCompositeModel = (model_id: string) => {
|
||||
return request.delete(`/models/composite/${model_id}`)
|
||||
}
|
||||
// Create API keys for all matching models by provider
|
||||
export const updateProviderApiKeys = (data: KeyConfigModalForm) => {
|
||||
return request.post('/models/provider/apikeys', data)
|
||||
}
|
||||
// Create model API key
|
||||
export const addModelApiKey = (model_id: string, data: MultiKeyForm) => {
|
||||
return request.post(`/models/${model_id}/apikeys`, data)
|
||||
}
|
||||
// Delete model API key
|
||||
export const deleteModelApiKey = (api_key_id: string) => {
|
||||
return request.delete(`/models/apikeys/${api_key_id}`)
|
||||
}
|
||||
// Update model status
|
||||
export const updateModelStatus = (model_id: string, data: { is_active: boolean; }) => {
|
||||
return request.put(`/models/${model_id}`, data)
|
||||
}
|
||||
// Model plaza list
|
||||
export const getModelPlaza = (data: { search?: string; provider?: string; }) => {
|
||||
return request.get('/models/model_plaza', data)
|
||||
}
|
||||
// Add model to plaza
|
||||
export const addModelPlaza = (model_base_id: string) => {
|
||||
return request.post(`/models/model_plaza/${model_base_id}/add`)
|
||||
}
|
||||
// Create custom model
|
||||
export const addCustomModel = (data: CustomModelForm) => {
|
||||
return request.post('/models/model_plaza', data)
|
||||
}
|
||||
// Update custom model
|
||||
export const updateCustomModel = (model_base_id: string, data: CustomModelForm) => {
|
||||
return request.put(`/models/model_plaza/${model_base_id}`, data)
|
||||
}
|
||||
BIN
web/src/assets/images/empty/pageEmpty.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
15
web/src/assets/images/model/bedrock.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16762_59518)">
|
||||
<path d="M12.6667 0H3.33333C1.49238 0 0 1.49238 0 3.33333V12.6667C0 14.5076 1.49238 16 3.33333 16H12.6667C14.5076 16 16 14.5076 16 12.6667V3.33333C16 1.49238 14.5076 0 12.6667 0Z" fill="url(#paint0_linear_16762_59518)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 12.093L6.3825 12.6323L5.75184 12.2116L6.4385 11.9823L6.22784 11.3503L5.04917 11.743L4.6665 11.4883V9.66631C4.6665 9.54031 4.59517 9.42497 4.4825 9.3683L3.33317 8.79364V7.20564L4.33317 6.70564L5.33317 7.20564V8.33297C5.33317 8.45964 5.4045 8.57497 5.51717 8.63164L6.8505 9.29831L7.14917 8.70164L5.99984 8.12697V7.20564L7.14917 6.63164C7.26184 6.57497 7.33317 6.45964 7.33317 6.33297V5.33297H6.6665V6.12697L5.6665 6.62697L4.6665 6.12697V4.51164L5.33317 4.06697V5.33297H5.99984V3.62297L6.3825 3.36764L7.99984 3.90697V12.093ZM11.6665 11.333C11.8498 11.333 11.9998 11.4823 11.9998 11.6663C11.9998 11.8503 11.8498 11.9996 11.6665 11.9996C11.4832 11.9996 11.3332 11.8503 11.3332 11.6663C11.3332 11.4823 11.4832 11.333 11.6665 11.333ZM10.9998 3.99964C11.1832 3.99964 11.3332 4.14897 11.3332 4.33297C11.3332 4.51697 11.1832 4.6663 10.9998 4.6663C10.8165 4.6663 10.6665 4.51697 10.6665 4.33297C10.6665 4.14897 10.8165 3.99964 10.9998 3.99964ZM12.3332 7.99964C12.5165 7.99964 12.6665 8.14897 12.6665 8.33297C12.6665 8.51697 12.5165 8.66631 12.3332 8.66631C12.1498 8.66631 11.9998 8.51697 11.9998 8.33297C11.9998 8.14897 12.1498 7.99964 12.3332 7.99964ZM11.3945 8.66631C11.5325 9.05364 11.8992 9.33297 12.3332 9.33297C12.8845 9.33297 13.3332 8.88497 13.3332 8.33297C13.3332 7.78164 12.8845 7.33297 12.3332 7.33297C11.8992 7.33297 11.5325 7.61297 11.3945 7.99964H8.6665V6.66631H10.9998C11.1838 6.66631 11.3332 6.51764 11.3332 6.33297V5.27164C11.7205 5.13364 11.9998 4.76697 11.9998 4.33297C11.9998 3.78164 11.5512 3.33297 10.9998 3.33297C10.4485 3.33297 9.99984 3.78164 9.99984 4.33297C9.99984 4.76697 10.2792 5.13364 10.6665 5.27164V5.99964H8.6665V3.6663C8.6665 3.52297 8.5745 3.39564 8.4385 3.3503L6.4385 2.68364C6.3405 2.65097 6.23384 2.66564 6.1485 2.7223L4.1485 4.05564C4.05584 4.11764 3.99984 4.22164 3.99984 4.33297V6.12697L2.8505 6.70164C2.73784 6.75831 2.6665 6.87364 2.6665 6.99964V8.99964C2.6665 9.12631 2.73784 9.24164 2.8505 9.29831L3.99984 9.87231V11.6663C3.99984 11.7776 4.05584 11.8823 4.1485 11.9436L6.1485 13.277C6.20384 13.3143 6.26784 13.333 6.33317 13.333C6.3685 13.333 6.40384 13.3276 6.4385 13.3156L8.4385 12.649C8.5745 12.6043 8.6665 12.477 8.6665 12.333V10.6663H10.1952L10.7638 11.2356L10.7725 11.227C10.7072 11.3603 10.6665 11.5083 10.6665 11.6663C10.6665 12.2176 11.1152 12.6663 11.6665 12.6663C12.2178 12.6663 12.6665 12.2176 12.6665 11.6663C12.6665 11.115 12.2178 10.6663 11.6665 10.6663C11.5078 10.6663 11.3598 10.707 11.2272 10.773L11.2358 10.7643L10.5692 10.0976C10.5065 10.035 10.4218 9.99964 10.3332 9.99964H8.6665V8.66631H11.3945Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_16762_59518" x1="0" y1="1600" x2="1600" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#055F4E"/>
|
||||
<stop offset="1" stop-color="#56C0A7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_16762_59518">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
BIN
web/src/assets/images/model/dashscope.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
web/src/assets/images/model/gpustack.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
15
web/src/assets/images/model/ollama.svg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
4
web/src/assets/images/model/openai.svg
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
24
web/src/assets/images/model/xinference.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Xorbits Square" clip-path="url(#clip0_9850_26870)">
|
||||
<path id="Vector" d="M8.00391 12.3124C8.69334 13.0754 9.47526 13.7494 10.3316 14.3188C11.0667 14.8105 11.8509 15.2245 12.6716 15.5541C14.1617 14.1465 15.3959 12.4907 16.3192 10.6606L21.7051 0L12.3133 7.38353C10.5832 8.74456 9.12178 10.416 8.00391 12.3124Z" fill="url(#paint0_linear_9850_26870)"/>
|
||||
<path id="Vector_2" d="M7.23504 18.9512C6.56092 18.5012 5.92386 18.0265 5.3221 17.5394L2.06445 24L7.91975 19.3959C7.69034 19.2494 7.46092 19.103 7.23504 18.9512Z" fill="url(#paint1_linear_9850_26870)"/>
|
||||
<path id="Vector_3" d="M19.3161 8.57474C21.0808 10.9147 21.5961 13.5159 20.3996 15.3053C18.6526 17.9189 13.9161 17.8183 9.82024 15.0812C5.72435 12.3441 3.82024 8.0065 5.56729 5.39297C6.76377 3.60356 9.36318 3.0865 12.2008 3.81886C7.29318 1.73474 2.62376 1.94121 0.813177 4.64474C-1.45976 8.04709 1.64435 14.1177 7.74494 18.1889C13.8455 22.26 20.6361 22.8124 22.9091 19.4118C24.7179 16.703 23.1173 12.3106 19.3161 8.57474Z" fill="url(#paint2_linear_9850_26870)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_9850_26870" x1="2.15214" y1="24.3018" x2="21.2921" y2="0.0988218" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_9850_26870" x1="2.06269" y1="24.2294" x2="21.2027" y2="0.028252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_9850_26870" x1="-0.613606" y1="3.843" x2="21.4449" y2="18.7258" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A0CF5"/>
|
||||
<stop offset="1" stop-color="#AB66F3"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_9850_26870">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -8,6 +8,7 @@ import { type FC, useRef, useEffect } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { ChatContentProps } from './types'
|
||||
import { Spin } from 'antd'
|
||||
|
||||
/**
|
||||
* 聊天内容显示组件
|
||||
@@ -21,7 +22,8 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
empty,
|
||||
labelPosition = 'bottom',
|
||||
labelFormat,
|
||||
errorDesc
|
||||
errorDesc,
|
||||
renderRuntime
|
||||
}) => {
|
||||
// 滚动容器引用,用于控制自动滚动到底部
|
||||
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
|
||||
@@ -45,8 +47,8 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
'rb:left-0 rb:text-left': item.role === 'assistant', // 助手消息左对齐
|
||||
})}>
|
||||
{/* 流式加载时且内容为空则不显示 */}
|
||||
{streamLoading && item.content === ''
|
||||
? null
|
||||
{streamLoading && item.content === '' && !renderRuntime
|
||||
? <Spin />
|
||||
: <>
|
||||
{/* 顶部标签(如时间戳、用户名等) */}
|
||||
{labelPosition === 'top' &&
|
||||
@@ -55,16 +57,17 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
</div>
|
||||
}
|
||||
{/* 消息气泡框 */}
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-[520px] rb:wrap-break-word', contentClassNames, {
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-130 rb:wrap-break-word', contentClassNames, {
|
||||
// 错误消息样式(内容为null且非助手消息)
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null,
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null && !renderRuntime,
|
||||
// 助手消息样式
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
|
||||
// 用户消息样式
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant' && (item.content || item.content === ''),
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant' && (item.content || item.content === '' || typeof renderRuntime === 'function'),
|
||||
})}>
|
||||
{item.subContent && renderRuntime && renderRuntime(item, index)}
|
||||
{/* 使用Markdown组件渲染消息内容 */}
|
||||
<Markdown content={item.content ?? errorDesc ?? ''} />
|
||||
<Markdown content={renderRuntime ? item.content ?? '' : item.content ?? errorDesc ?? ''} />
|
||||
</div>
|
||||
{/* 底部标签(如时间戳、用户名等) */}
|
||||
{labelPosition === 'bottom' &&
|
||||
|
||||
@@ -19,7 +19,9 @@ export interface ChatItem {
|
||||
/** 消息内容 */
|
||||
content?: string | null;
|
||||
/** 创建时间 */
|
||||
created_at?: number | string
|
||||
created_at?: number | string;
|
||||
status?: string;
|
||||
subContent?: Record<string, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,4 +83,5 @@ export interface ChatContentProps {
|
||||
/** 标签格式化函数 */
|
||||
labelFormat: (item: ChatItem) => any;
|
||||
errorDesc?: string;
|
||||
renderRuntime?: (item: ChatItem, index: number) => ReactNode;
|
||||
}
|
||||
16
web/src/components/Empty/PageEmpty.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
|
||||
import Empty from './index'
|
||||
const PageEmpty = ({ size = [240, 210] }: { size?: number | number[] }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Empty
|
||||
url={pageEmptyIcon}
|
||||
title={t('empty.pageEmpty')}
|
||||
subTitle={t('empty.pageEmptyDesc')}
|
||||
size={size}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PageEmpty;
|
||||
@@ -6,6 +6,9 @@ import CopyBtn from './CopyBtn';
|
||||
|
||||
type ICodeBlockProps = {
|
||||
value: string;
|
||||
needCopy?: boolean;
|
||||
size?: 'small' | 'default';
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
// enum languageType {
|
||||
@@ -16,6 +19,9 @@ type ICodeBlockProps = {
|
||||
|
||||
const CodeBlock: FC<ICodeBlockProps> = ({
|
||||
value,
|
||||
needCopy = true,
|
||||
size = 'default',
|
||||
showLineNumbers = false
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -23,24 +29,26 @@ const CodeBlock: FC<ICodeBlockProps> = ({
|
||||
<SyntaxHighlighter
|
||||
style={atelierHeathLight}
|
||||
customStyle={{
|
||||
padding: '16px 20px 16px 24px',
|
||||
padding: '8px 12px 8px 12px',
|
||||
backgroundColor: '#F0F3F8',
|
||||
borderRadius: 8,
|
||||
fontSize: size === 'small' ? 12 : 14,
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
language="json"
|
||||
showLineNumbers={false}
|
||||
showLineNumbers={showLineNumbers}
|
||||
PreTag="div"
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
<CopyBtn
|
||||
{needCopy && <CopyBtn
|
||||
value={value}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 20,
|
||||
}}
|
||||
/>
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
13
web/src/components/PageTabs/index.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.page-tabs:global(.ant-segmented) {
|
||||
background-color: rgba(91, 97, 103, 0.08);
|
||||
padding: 4px;
|
||||
}
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
|
||||
line-height: 24px;
|
||||
min-height: 24px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-selected) {
|
||||
box-shadow: 0px 2px 4px 0px rgba(33, 35, 50, 0.16);
|
||||
}
|
||||
18
web/src/components/PageTabs/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { type FC } from 'react';
|
||||
import { Segmented, type SegmentedProps } from 'antd';
|
||||
import styles from './index.module.css';
|
||||
|
||||
const PageTabs: FC<SegmentedProps> = ({
|
||||
value,
|
||||
options,
|
||||
onChange
|
||||
}) => {
|
||||
return <Segmented
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
className={styles.pageTabs}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default PageTabs;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Card } from 'antd';
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface RbCardProps {
|
||||
@@ -9,7 +9,7 @@ interface RbCardProps {
|
||||
extra?: ReactNode;
|
||||
children?: ReactNode;
|
||||
avatar?: ReactNode;
|
||||
avatarUrl?: string;
|
||||
avatarUrl?: string | null;
|
||||
bodyPadding?: string;
|
||||
bodyClassName?: string;
|
||||
headerType?: 'border' | 'borderless' | 'borderBL' | 'borderL';
|
||||
@@ -50,7 +50,7 @@ const RbCard: FC<RbCardProps> = ({
|
||||
<Card
|
||||
{...props}
|
||||
title={typeof title === 'function' ? title() : title ?
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:flex rb:items-center rb:gap-2">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
|
||||
: avatar ? avatar : null
|
||||
@@ -59,11 +59,11 @@ const RbCard: FC<RbCardProps> = ({
|
||||
clsx(
|
||||
{
|
||||
'rb:max-w-full': !avatarUrl && !avatar,
|
||||
'rb:max-w-[calc(100%-60px)]': avatarUrl || avatar,
|
||||
'rb:max-w-[calc(100%-80px)]': avatarUrl || avatar,
|
||||
}
|
||||
)
|
||||
}>
|
||||
<div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div>
|
||||
<Tooltip title={title}><div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div></Tooltip>
|
||||
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
|
||||
</div>
|
||||
</div> : null
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Upload, Modal, Image, App } from 'antd';
|
||||
import { Upload, Image, App } from 'antd';
|
||||
import type { GetProp, UploadFile, UploadProps } from 'antd';
|
||||
// import { UploadOutlined, } from '@ant-design/icons';
|
||||
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PlusIcon from '@/assets/images/plus.svg'
|
||||
import { cookieUtils } from '@/utils/request'
|
||||
import { fileUploadUrl } from '@/api/fileStorage'
|
||||
import styles from './index.module.less'
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
|
||||
interface UploadImagesProps extends Omit<UploadProps, 'onChange' | 'fileList'> {
|
||||
/** 上传接口地址 */
|
||||
action?: string;
|
||||
/** 是否支持多选 */
|
||||
multiple?: boolean;
|
||||
/** 已上传的文件列表 */
|
||||
fileList?: UploadFile[];
|
||||
fileList?: UploadFile[] | UploadFile;
|
||||
/** 文件列表变化回调 */
|
||||
onChange?: (fileList: UploadFile[]) => void;
|
||||
onChange?: (fileList?: UploadFile[] | UploadFile) => void;
|
||||
/** 禁用上传 */
|
||||
disabled?: boolean;
|
||||
/** 文件大小限制(MB) */
|
||||
@@ -28,6 +28,7 @@ interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
|
||||
isAutoUpload?: boolean;
|
||||
/** 最大上传文件数 */
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
}
|
||||
const ALL_FILE_TYPE: {
|
||||
[key: string]: string;
|
||||
@@ -59,7 +60,7 @@ const getBase64 = (file: FileType): Promise<string> => {
|
||||
* 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能
|
||||
*/
|
||||
const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
action = '/api/upload',
|
||||
action = fileUploadUrl,
|
||||
multiple = false,
|
||||
fileList: propFileList = [],
|
||||
onChange,
|
||||
@@ -68,27 +69,42 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
fileType = ['png', 'jpg', 'gif'],
|
||||
isAutoUpload = true,
|
||||
maxCount = 1,
|
||||
className = 'rb:size-24! rb:leading-1!',
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
|
||||
const { message, modal } = App.useApp()
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [accept, setAccept] = useState<string | undefined>();
|
||||
// const [loading, setLoading] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(propFileList) && typeof propFileList === 'object') {
|
||||
setFileList([propFileList]);
|
||||
}
|
||||
}, [propFileList])
|
||||
|
||||
const updateValue = (list: UploadFile[]) => {
|
||||
if (maxCount === 1) {
|
||||
onChange?.(list[0])
|
||||
} else {
|
||||
onChange?.(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件移除
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
confirm({
|
||||
title: '确定要删除此文件吗?',
|
||||
okText: '确定',
|
||||
modal.confirm({
|
||||
title: t('common.confirmRemoveFile'),
|
||||
okText: `${t('common.confirm')}`,
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
cancelText: `${t('common.cancel')}`,
|
||||
onOk: () => {
|
||||
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||
setFileList(newFileList);
|
||||
onChange?.(newFileList);
|
||||
updateValue(newFileList)
|
||||
},
|
||||
});
|
||||
return false; // 阻止默认删除行为,由confirm控制
|
||||
@@ -100,7 +116,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
if (fileSize && file.size) {
|
||||
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(`文件大小不能超过 ${fileSize}MB`);
|
||||
message.error(t('common.fileSizeTip', { size: fileSize }));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +124,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
if (accept && accept.length > 0 && file.type) {
|
||||
const isAccept = accept.includes(file.type);
|
||||
if (!isAccept) {
|
||||
message.error(`不支持的文件类型: ${file.type}`);
|
||||
message.error(`${t('common.fileAcceptTip')}${file.type}`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +135,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
}
|
||||
const newFileList = [...fileList, file];
|
||||
setFileList(newFileList);
|
||||
onChange?.(newFileList);
|
||||
updateValue(newFileList);
|
||||
return Upload.LIST_IGNORE; // 阻止自动上传
|
||||
}
|
||||
|
||||
@@ -129,17 +145,13 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
// 处理上传状态变化
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
|
||||
setFileList(newFileList);
|
||||
if (onChange) {
|
||||
onChange(newFileList);
|
||||
}
|
||||
updateValue(newFileList);
|
||||
};
|
||||
|
||||
// 清空已上传文件
|
||||
const clearFiles = () => {
|
||||
setFileList([]);
|
||||
if (onChange) {
|
||||
onChange([]);
|
||||
}
|
||||
updateValue([]);
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
@@ -167,7 +179,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
fileList,
|
||||
beforeUpload,
|
||||
headers: {
|
||||
authorization: cookieUtils.get('authToken') || '',
|
||||
authorization: `Bearer ${cookieUtils.get('authToken') }`,
|
||||
},
|
||||
onPreview: handlePreview,
|
||||
onRemove: handleRemove,
|
||||
@@ -180,6 +192,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
showRemoveIcon: true,
|
||||
showDownloadIcon: false,
|
||||
},
|
||||
className: `${styles.imageUpload} ${className}`,
|
||||
...props,
|
||||
};
|
||||
|
||||
@@ -193,16 +206,9 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
<>
|
||||
<Upload
|
||||
{...uploadProps}
|
||||
style={{
|
||||
width: '136px',
|
||||
height: '136px',
|
||||
}}
|
||||
>
|
||||
{fileList.length < maxCount && (
|
||||
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center">
|
||||
<img src={PlusIcon} className="rb:w-[32px] rb:h-[32px]" />
|
||||
<div className="rb:mt-[12px] rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">{t('common.clickUploadIcon')}</div>
|
||||
</div>
|
||||
<img src={PlusIcon} className="rb:size-7" />
|
||||
)}
|
||||
</Upload>
|
||||
{previewImage && (
|
||||
|
||||
7
web/src/components/Upload/index.module.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -419,6 +419,9 @@ export const en = {
|
||||
statusEnabled: 'Available',
|
||||
statusDisabled: 'Unavailable',
|
||||
remove: 'Remove',
|
||||
|
||||
fileSizeTip: 'File size cannot exceed {{size}}MB',
|
||||
fileAcceptTip: 'Unsupported file type:'
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -510,6 +513,64 @@ export const en = {
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
modelNew: {
|
||||
group: 'Model Group',
|
||||
list: 'Model List',
|
||||
square: 'Model Plaza',
|
||||
createGroupModel: 'Create Model Group',
|
||||
groupSearchPlaceholder: 'Search model groups',
|
||||
listSearchPlaceholder: 'Search available models',
|
||||
squareSearchPlaceholder: 'Search platform models',
|
||||
status: 'Model Status',
|
||||
created_at: 'Created At',
|
||||
configureBtn: 'Click to Configure',
|
||||
showModel: 'Show Model',
|
||||
keyConfig: 'Configure KEY',
|
||||
|
||||
modelConfiguration: 'Model Configuration',
|
||||
logo: 'Model LOGO',
|
||||
name: 'Model Name',
|
||||
type: 'Model Type',
|
||||
modelImplement: 'Model Implementation',
|
||||
addImplement: 'Add Implementation',
|
||||
noAuth: 'Unauthorized (Limited to 1 implementation)',
|
||||
implementConfig: 'Configure Model Implementation',
|
||||
provider: 'Model Provider',
|
||||
api_key_ids: 'Select Model',
|
||||
viewAll: 'More',
|
||||
modelCount: 'Total {{count}} models',
|
||||
modelList: 'Model List',
|
||||
added: ' Added',
|
||||
addSuccess: 'Added successfully',
|
||||
model_name: 'Model Name',
|
||||
tags: 'Tags',
|
||||
createCustomModel: 'Add Custom Model',
|
||||
edit: 'Edit',
|
||||
selectOneTip: 'Model API KEY not configured, please configure it in the model list first',
|
||||
load_balance_strategy: 'Concurrency Strategy',
|
||||
round_robin: 'Sequential Execution - Call each model in order',
|
||||
none: 'None',
|
||||
|
||||
api_key: 'API KEY',
|
||||
api_base: 'API Base URL',
|
||||
description: 'Description',
|
||||
add: 'Add',
|
||||
item: 'item',
|
||||
apiKeyNum: ' API Keys',
|
||||
official: 'Official',
|
||||
deprecated: 'Deprecated',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
knowledgeBase: {
|
||||
pleaseUploadFileFirst: 'Please upload file first',
|
||||
shareSuccess: 'Share successfully',
|
||||
@@ -866,7 +927,7 @@ export const en = {
|
||||
|
||||
minimumRetention: 'Minimum retention (λ_time)',
|
||||
minimumRetentionDesc: 'Controls the minimum retention threshold of memory retention',
|
||||
forgettingRate: 'Forgetting rate (λ_mem)',
|
||||
forgettingRate: 'Forgetting rate (λ_mem)',
|
||||
forgettingRateDesc: 'Control the speed of memory forgetting, the higher the value, the faster the forgetting',
|
||||
offset: 'Offset (offset)',
|
||||
offsetDesc: 'The offset of the minimum preservation degree',
|
||||
@@ -934,7 +995,7 @@ export const en = {
|
||||
number: 'Number',
|
||||
checkbox: 'Checkbox',
|
||||
apiVariable: 'API Variable',
|
||||
|
||||
|
||||
displayName: 'Display Name',
|
||||
maxLength: 'Max Length',
|
||||
required: 'Required',
|
||||
@@ -1175,6 +1236,12 @@ export const en = {
|
||||
priority: 'Structured Integration',
|
||||
addTool: 'Add Tool',
|
||||
tool: 'Tool',
|
||||
|
||||
statistics: 'Data Statistics',
|
||||
daily_conversations: 'Daily Conversations',
|
||||
daily_new_users: 'Daily New Users',
|
||||
daily_api_calls: 'Daily API Calls',
|
||||
daily_tokens: 'Token Consumption',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -1534,7 +1601,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
noPermissionDesc: ' Please contact the administrator to grant permission',
|
||||
tableEmpty: 'No data available.',
|
||||
loadingEmpty: 'The content is loading…',
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen',
|
||||
pageEmpty: 'Oops! No search results available at the moment',
|
||||
pageEmptyDesc: "Red Bear tilts its head and waits for you to change a new keyword, let's explore together.",
|
||||
},
|
||||
apiKey: {
|
||||
name: 'Project Name',
|
||||
@@ -1765,7 +1834,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
externalInteraction: 'External Interaction',
|
||||
"http-request": 'HTTP Request',
|
||||
tool: 'Tools',
|
||||
code_execution: 'Code Execution',
|
||||
code: 'Code Execution',
|
||||
"jinja-render": 'Template Rendering',
|
||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||
'memory-read': 'Memory Retrieval',
|
||||
@@ -1858,6 +1927,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
'object': 'Object',
|
||||
addParams: 'Add Extract Variable',
|
||||
promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
},
|
||||
@@ -1962,6 +2032,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
config_id: 'Memory Configuration',
|
||||
search_switch: 'Search Mode',
|
||||
},
|
||||
|
||||
'code': {
|
||||
input_variables: 'Input Variables',
|
||||
output_variables: 'Output Variables',
|
||||
refreshTip: '同步函数签名至代码',
|
||||
},
|
||||
name: 'Key',
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
@@ -1982,6 +2058,10 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
arrange: 'Arrange',
|
||||
redo: 'Redo',
|
||||
undo: 'Undo',
|
||||
|
||||
input: 'Input',
|
||||
output: 'Output',
|
||||
error: 'Error Message',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
|
||||
@@ -658,7 +658,13 @@ export const zh = {
|
||||
priority: '结构化整合',
|
||||
addTool: '添加工具',
|
||||
tool: '工具',
|
||||
variableConfig: '配置变量'
|
||||
variableConfig: '配置变量',
|
||||
|
||||
statistics: '数据统计',
|
||||
daily_conversations: '消息会话数',
|
||||
daily_new_users: '新增用户数',
|
||||
daily_api_calls: '调用次数',
|
||||
daily_tokens: 'Token消耗',
|
||||
},
|
||||
role: {
|
||||
roleManagement: '角色管理',
|
||||
@@ -967,6 +973,9 @@ export const zh = {
|
||||
statusEnabled: '可用',
|
||||
statusDisabled: '不可用',
|
||||
remove: '删除',
|
||||
|
||||
fileSizeTip: '文件大小不能超过 {{size}}MB',
|
||||
fileAcceptTip: '不支持的文件类型:'
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1076,6 +1085,64 @@ export const zh = {
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
modelNew: {
|
||||
group: '模型组合',
|
||||
list: '模型列表',
|
||||
square: '模型广场',
|
||||
createGroupModel: '创建模型组合',
|
||||
groupSearchPlaceholder: '搜索模型组合',
|
||||
listSearchPlaceholder: '搜索可用模型',
|
||||
squareSearchPlaceholder: '搜索平台模型',
|
||||
status: '模型状态',
|
||||
created_at: '创建时间',
|
||||
configureBtn: '点击配置',
|
||||
showModel: '显示模型',
|
||||
keyConfig: '配置 KEY',
|
||||
|
||||
modelConfiguration: '模型配置',
|
||||
logo: '模型LOGO',
|
||||
name: '模型名称',
|
||||
type: '模型类型',
|
||||
modelImplement: '模型实现',
|
||||
addImplement: '添加实现',
|
||||
noAuth: '未授权(限1个实现)',
|
||||
implementConfig: '配置模型实现',
|
||||
provider: '模型供应商',
|
||||
api_key_ids: '选择模型',
|
||||
viewAll: '更多',
|
||||
modelCount: '共 {{count}} 个模型',
|
||||
modelList: '模型列表',
|
||||
added: ' 已添加',
|
||||
addSuccess: '添加成功',
|
||||
model_name: '模型名称',
|
||||
tags: '标签',
|
||||
createCustomModel: '添加自定义模型',
|
||||
edit: '编辑',
|
||||
selectOneTip: '模型未配置API KEY,请先在模型列表配置',
|
||||
load_balance_strategy: '并发策略',
|
||||
round_robin: '顺序执行 - 按顺序依次调用每个模型',
|
||||
none: '无',
|
||||
|
||||
api_key: 'API KEY',
|
||||
api_base: 'API Base URL',
|
||||
description: '描述',
|
||||
add: '添加',
|
||||
item: '个',
|
||||
apiKeyNum: '个 API Key',
|
||||
official: '官方',
|
||||
deprecated: '已弃用',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
'Asia/Kolkata': '印度标准时间 (UTC+5:30)',
|
||||
@@ -1607,13 +1674,10 @@ export const zh = {
|
||||
noPermissionDesc: '请联系管理员授予权限',
|
||||
tableEmpty: '目前没有数据',
|
||||
loadingEmpty: '内容正在加载中…',
|
||||
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
|
||||
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上',
|
||||
pageEmpty: '哎呀!暂无搜索结果',
|
||||
pageEmptyDesc: '红熊歪着头等待您更换新的关键词,让我们一起探索吧。',
|
||||
},
|
||||
count: '计数: {{count}}',
|
||||
increment: '增加',
|
||||
decrement: '减少',
|
||||
reset: '重置',
|
||||
switchLanguage: '切换语言',
|
||||
|
||||
home: {
|
||||
title: '首页',
|
||||
@@ -1858,7 +1922,7 @@ export const zh = {
|
||||
externalInteraction: '外部交互',
|
||||
"http-request": 'HTTP请求',
|
||||
tool: '工具 (Tool)',
|
||||
code_execution: '代码执行',
|
||||
code: '代码执行',
|
||||
"jinja-render": '模板渲染',
|
||||
cognitiveUpgrading: '认知升级(创新)',
|
||||
'memory-read': '记忆提取',
|
||||
@@ -1952,6 +2016,7 @@ export const zh = {
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
'object': 'Object',
|
||||
addParams: '添加提取变量',
|
||||
promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
},
|
||||
@@ -2056,6 +2121,12 @@ export const zh = {
|
||||
config_id: '记忆配置',
|
||||
search_switch: '检索模式',
|
||||
},
|
||||
|
||||
'code': {
|
||||
input_variables: '输入变量',
|
||||
output_variables: '输出变量',
|
||||
refreshTip: '同步函数签名至代码',
|
||||
},
|
||||
name: '键',
|
||||
type: '类型',
|
||||
value: '值',
|
||||
@@ -2076,6 +2147,10 @@ export const zh = {
|
||||
arrange: '整理',
|
||||
redo: '重做',
|
||||
undo: '撤销',
|
||||
|
||||
input: '输入',
|
||||
output: '输出',
|
||||
error: '错误信息',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
|
||||
@@ -22,7 +22,7 @@ export const lightTheme: ThemeConfig = {
|
||||
// colorBgContainer: '#FBFDFF',
|
||||
colorError: '#FF5D34',
|
||||
sizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
@@ -105,6 +105,9 @@ export const lightTheme: ThemeConfig = {
|
||||
},
|
||||
Select: {
|
||||
lineHeightSM: 26
|
||||
},
|
||||
Upload: {
|
||||
pictureCardSize: 96,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -23,9 +23,10 @@ interface data {
|
||||
}
|
||||
|
||||
|
||||
export const API_PREFIX = '/api'
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: '/api', // 与vite.config.ts中的代理配置对应
|
||||
baseURL: API_PREFIX, // 与vite.config.ts中的代理配置对应
|
||||
// timeout: 10000, // 请求超时时间
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
@@ -126,7 +127,7 @@ service.interceptors.response.use(
|
||||
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
||||
// 处理网络错误、超时等
|
||||
let msg = error.response?.data?.error || error.response?.error;
|
||||
const status = error?.response ? error.response.status : error;
|
||||
|
||||
@@ -123,6 +123,20 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
let response = await makeSSERequest(url, data, token || '', config);
|
||||
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
case 502:
|
||||
const errorData = await response.json();
|
||||
errorData.error || i18n.t('common.serviceUpgrading');
|
||||
message.warning(errorData.error || i18n.t('common.serviceUpgrading'));
|
||||
break
|
||||
case 400:
|
||||
const error = await response.json();
|
||||
message.warning(error.error);
|
||||
throw error || 'Bad Request';
|
||||
case 504:
|
||||
const errorJson = await response.json();
|
||||
message.warning(errorJson.error || i18n.t('common.serverError'));
|
||||
break
|
||||
case 401:
|
||||
if (url?.includes('/public')) {
|
||||
return message.warning(i18n.t('common.publicApiCannotRefreshToken'));
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
} from './types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import { getModelList } from '@/api/models';
|
||||
import { saveAgentConfig } from '@/api/application'
|
||||
import Knowledge from './components/Knowledge/Knowledge'
|
||||
@@ -96,8 +96,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<Config | null>(null);
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<Model | null>(null)
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<ModelListItem | null>(null)
|
||||
const [chatList, setChatList] = useState<ChatData[]>([])
|
||||
const values = Form.useWatch<Config>([], form)
|
||||
const [isSave, setIsSave] = useState(false)
|
||||
@@ -126,12 +126,16 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
getApplicationConfig(id as string).then(res => {
|
||||
const response = res as Config
|
||||
let allTools = Array.isArray(response.tools) ? response.tools : []
|
||||
const memoryContent = response.memory?.memory_content
|
||||
const parsedMemoryContent = memoryContent === null || memoryContent === ''
|
||||
? undefined
|
||||
: !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
|
||||
form.setFieldsValue({
|
||||
...response,
|
||||
tools: allTools,
|
||||
memory: {
|
||||
...response.memory,
|
||||
memory_content: response.memory?.memory_content ? Number(response.memory?.memory_content) : undefined
|
||||
memory_content: parsedMemoryContent
|
||||
}
|
||||
})
|
||||
setData({
|
||||
@@ -212,7 +216,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
...data.knowledge_retrieval,
|
||||
...knowledgeRest,
|
||||
knowledge_bases: knowledge_bases.map(item => ({
|
||||
kb_id: item.id,
|
||||
kb_id: item.kb_id || item.id,
|
||||
...(item.config || {})
|
||||
}))
|
||||
} as KnowledgeConfig : null,
|
||||
@@ -237,9 +241,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
})
|
||||
}
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -249,7 +253,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
useEffect(() => {
|
||||
if (values?.default_model_config_id && modelList.length > 0) {
|
||||
const filterValue = modelList.find(item => item.id === values.default_model_config_id)
|
||||
setDefaultModel(filterValue as Model | null)
|
||||
setDefaultModel(filterValue as ModelListItem | null)
|
||||
setChatList([{
|
||||
label: filterValue?.name || '',
|
||||
model_config_id: filterValue?.id || '',
|
||||
|
||||
@@ -225,7 +225,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
<Form.Item name="default_model_config_id" noStyle>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
86
web/src/views/ApplicationConfig/Statistics.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { Row, Col, Flex, DatePicker } from 'antd';
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import { getAppStatistics } from '@/api/application';
|
||||
import LineCard from './components/LineCard'
|
||||
import type { StatisticsData, StatisticsItem } from './types'
|
||||
|
||||
const TotalObj: Record<string, keyof StatisticsData> = {
|
||||
daily_conversations: 'total_conversations',
|
||||
daily_new_users: 'total_new_users',
|
||||
daily_api_calls: 'total_api_calls',
|
||||
daily_tokens: 'total_tokens',
|
||||
}
|
||||
const Statistics: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const [data, setData] = useState<StatisticsData>({
|
||||
daily_conversations: [],
|
||||
total_conversations: 0,
|
||||
daily_new_users: [],
|
||||
total_new_users: 0,
|
||||
daily_api_calls: [],
|
||||
total_api_calls: 0,
|
||||
daily_tokens: [],
|
||||
total_tokens: 0
|
||||
})
|
||||
const [query, setQuery] = useState({
|
||||
start_date: dayjs().subtract(6, 'd'),
|
||||
end_date: dayjs().subtract(0, 'd'),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [application, query])
|
||||
const getData = () => {
|
||||
if (!application?.id) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
start_date: query.start_date.startOf('d').valueOf(),
|
||||
end_date: query.end_date.endOf('d').valueOf(),
|
||||
}
|
||||
|
||||
getAppStatistics(application.id, params)
|
||||
.then(res => {
|
||||
setData(res as StatisticsData)
|
||||
})
|
||||
}
|
||||
const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => {
|
||||
if (!date || !date[0] || !date[1]) return
|
||||
setQuery({
|
||||
start_date: date[0],
|
||||
end_date: date[1],
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Flex justify="end">
|
||||
<RangePicker defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
|
||||
</Flex>
|
||||
</Col>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
if (key.includes('total')) {
|
||||
return null
|
||||
}
|
||||
const totalKey = TotalObj[key];
|
||||
return (
|
||||
<Col span={12} key={key}>
|
||||
<LineCard
|
||||
type={key}
|
||||
total={totalKey ? (data[totalKey] as number) : 0}
|
||||
chartData={value as StatisticsItem[]}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Statistics;
|
||||
@@ -181,7 +181,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -17,7 +17,7 @@ import CopyModal from './CopyModal'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const tabKeys = ['arrangement', 'api', 'release']
|
||||
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
|
||||
const menuIcons: Record<string, string> = {
|
||||
edit: editIcon,
|
||||
copy: copyIcon,
|
||||
|
||||
@@ -66,7 +66,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
useEffect(() => {
|
||||
if (values?.retrieve_type) {
|
||||
const fieldsToReset = Object.keys(values).filter(key =>
|
||||
key !== 'kb_id' && key !== 'retrieve_type'
|
||||
key !== 'kb_id' && key !== 'retrieve_type' && key !== 'top_k'
|
||||
) as (keyof KnowledgeConfigForm)[];
|
||||
form.resetFields(fieldsToReset);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
127
web/src/views/ApplicationConfig/components/LineCard.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { type FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts';
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import Card from './Card'
|
||||
import type { StatisticsItem } from '../types'
|
||||
|
||||
interface LineCardProps {
|
||||
chartData: StatisticsItem[];
|
||||
type: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const SeriesConfig = {
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
showSymbol: true,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'top'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
}
|
||||
|
||||
const ColorObj: Record<string, string> = {
|
||||
daily_conversations: '#FFB048',
|
||||
daily_new_users: '#4DA8FF',
|
||||
daily_api_calls: '#155EEF',
|
||||
daily_tokens: '#AD88FF'
|
||||
}
|
||||
|
||||
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [chartData])
|
||||
|
||||
const getSeries = () => {
|
||||
return [{
|
||||
...SeriesConfig,
|
||||
name: t(`application.${type}`),
|
||||
data: chartData.map(vo => vo.count),
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: ColorObj[type] },
|
||||
{ offset: 1, color: '#FFFFFF' }
|
||||
])
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<div>{t(`application.${type}`)} <span className="rb:text-[#155EEF] rb:font-medium rb:text-[18px]">{total}</span></div>}
|
||||
>
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: [ColorObj[type]],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
grid: {
|
||||
top: 10,
|
||||
left: 15,
|
||||
right: 40,
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
align: 'right',
|
||||
lineHeight: 17,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineCard
|
||||
@@ -9,6 +9,7 @@ import ReleasePage from './ReleasePage'
|
||||
import Cluster from './Cluster'
|
||||
import { getApplication } from '@/api/application'
|
||||
import Workflow from '@/views/Workflow';
|
||||
import Statistics from './Statistics'
|
||||
|
||||
const ApplicationConfig: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
@@ -68,6 +69,7 @@ const ApplicationConfig: React.FC = () => {
|
||||
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
|
||||
{activeTab === 'api' && <Api application={application} />}
|
||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||
{activeTab === 'statistics' && <Statistics application={application} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,4 +150,19 @@ export interface AiPromptForm {
|
||||
}
|
||||
export interface ChatVariableConfigModalRef {
|
||||
handleOpen: (values: Variable[]) => void;
|
||||
}
|
||||
|
||||
export interface StatisticsItem {
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
export interface StatisticsData {
|
||||
daily_conversations: StatisticsItem[];
|
||||
daily_new_users: StatisticsItem[];
|
||||
daily_api_calls: StatisticsItem[];
|
||||
daily_tokens: StatisticsItem[];
|
||||
total_conversations: number;
|
||||
total_new_users: number;
|
||||
total_api_calls: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const configList = [
|
||||
key: 'emotion_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
},
|
||||
{
|
||||
key: 'emotion_min_intensity',
|
||||
|
||||
@@ -39,7 +39,7 @@ const MemberManagement: React.FC = () => {
|
||||
onOk: () => {
|
||||
deleteMember(member.id)
|
||||
.then(() => {
|
||||
message.success(t('member.deleteSuccess'));
|
||||
message.success(t('common.deleteSuccess'));
|
||||
refreshTable();
|
||||
})
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const MemberManagement: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-end rb:mb-[12px]">
|
||||
<div className="rb:flex rb:justify-end rb:mb-3">
|
||||
<Button type="primary" onClick={() => handleEdit()}>
|
||||
{t('member.createMember')}
|
||||
</Button>
|
||||
|
||||
@@ -45,7 +45,7 @@ const searchSwitchList = [
|
||||
]
|
||||
|
||||
export interface TestParams {
|
||||
group_id: string;
|
||||
end_user_id: string;
|
||||
message: string;
|
||||
search_switch: string;
|
||||
history: { role: string; content: string }[];
|
||||
@@ -107,7 +107,7 @@ const MemoryConversation: FC = () => {
|
||||
setLoading(true)
|
||||
readService({
|
||||
message: msg,
|
||||
group_id: userId,
|
||||
end_user_id: userId,
|
||||
search_switch: search_switch,
|
||||
history: [],
|
||||
})
|
||||
@@ -204,7 +204,7 @@ const MemoryConversation: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-6">{log.title}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-6">{log.title}</div>
|
||||
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0
|
||||
? <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{log.data.map(vo => (
|
||||
|
||||
@@ -1093,606 +1093,4 @@ export const groupDataByType = (data: any[], groupKey: string) => {
|
||||
})
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
export const mockTestResult = {
|
||||
"generated_at": "2025-12-12T09:48:43.389893",
|
||||
"entities": {
|
||||
"extracted_count": 148
|
||||
},
|
||||
"dedup": {
|
||||
"total_merged_count": 39,
|
||||
"breakdown": {
|
||||
"exact": 30,
|
||||
"fuzzy": 0,
|
||||
"llm": 9
|
||||
},
|
||||
"impact": [
|
||||
{
|
||||
"name": "记忆熊",
|
||||
"type": "Person",
|
||||
"appear_count": 9,
|
||||
"merge_count": 8
|
||||
},
|
||||
{
|
||||
"name": "宋朝",
|
||||
"type": "Organization",
|
||||
"appear_count": 5,
|
||||
"merge_count": 2
|
||||
},
|
||||
{
|
||||
"name": "军费",
|
||||
"type": "EconomicMetric",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "学生",
|
||||
"type": "Person",
|
||||
"appear_count": 6,
|
||||
"merge_count": 5
|
||||
},
|
||||
{
|
||||
"name": "废除丞相制度",
|
||||
"type": "Event",
|
||||
"appear_count": 6,
|
||||
"merge_count": 3
|
||||
},
|
||||
{
|
||||
"name": "六部",
|
||||
"type": "Organization",
|
||||
"appear_count": 4,
|
||||
"merge_count": 3
|
||||
},
|
||||
{
|
||||
"name": "六部缺乏协调机制",
|
||||
"type": "Concept",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "丞相",
|
||||
"type": "Position",
|
||||
"appear_count": 4,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "总理",
|
||||
"type": "Position",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "各部委",
|
||||
"type": "Organization",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "六部直接对皇帝负责",
|
||||
"type": "AdministrativeStructure",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "秦国",
|
||||
"type": "Organization",
|
||||
"appear_count": 5,
|
||||
"merge_count": 2
|
||||
},
|
||||
{
|
||||
"name": "文官集团",
|
||||
"type": "Organization",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"disambiguation": {
|
||||
"block_count": 1,
|
||||
"effects": [
|
||||
{
|
||||
"left": {
|
||||
"name": "节度使",
|
||||
"type": "Role"
|
||||
},
|
||||
"right": {
|
||||
"name": "节度使",
|
||||
"type": "Person"
|
||||
},
|
||||
"result": "成功区分"
|
||||
}
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"chunks": 2
|
||||
},
|
||||
"triplets": {
|
||||
"count": 88
|
||||
},
|
||||
"core_entities": [
|
||||
{
|
||||
"type": "Organization",
|
||||
"type_cn": "组织",
|
||||
"count": 16,
|
||||
"entities": [
|
||||
"厂卫机构",
|
||||
"西厂",
|
||||
"东厂",
|
||||
"工部",
|
||||
"地方军阀"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Event",
|
||||
"type_cn": "事件",
|
||||
"count": 12,
|
||||
"entities": [
|
||||
"均田制瓦解",
|
||||
"无法批阅完所有政务",
|
||||
"废除丞相制度",
|
||||
"持续战争",
|
||||
"政令执行困难"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Condition",
|
||||
"type_cn": "Condition",
|
||||
"count": 9,
|
||||
"entities": [
|
||||
"缺乏协作机制",
|
||||
"作战效率低下",
|
||||
"厢军装备不足",
|
||||
"军权分散",
|
||||
"军事专业化难以提升"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Person",
|
||||
"type_cn": "人物",
|
||||
"count": 8,
|
||||
"entities": [
|
||||
"官员",
|
||||
"宦官",
|
||||
"节度使",
|
||||
"皇帝",
|
||||
"文士"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Concept",
|
||||
"type_cn": "Concept",
|
||||
"count": 8,
|
||||
"entities": [
|
||||
"行政紧张",
|
||||
"军力不足",
|
||||
"秦国统一六国的原因",
|
||||
"六部缺乏协调机制",
|
||||
"专业分工"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Action",
|
||||
"type_cn": "Action",
|
||||
"count": 6,
|
||||
"entities": [
|
||||
"再花钱募兵",
|
||||
"建立军功爵制度",
|
||||
"裁撤兵员",
|
||||
"削减装备",
|
||||
"建立法律制度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Outcome",
|
||||
"type_cn": "Outcome",
|
||||
"count": 5,
|
||||
"entities": [
|
||||
"打仗更吃亏",
|
||||
"提升国家组织能力",
|
||||
"降低行政效率",
|
||||
"士兵效忠个人而非国家",
|
||||
"政令推行困难"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicMetric",
|
||||
"type_cn": "EconomicMetric",
|
||||
"count": 4,
|
||||
"entities": [
|
||||
"财政",
|
||||
"财政支出",
|
||||
"支出",
|
||||
"军费"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Statement",
|
||||
"type_cn": "Statement",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"没有银子",
|
||||
"禁军由文官控制导致作战效率低下",
|
||||
"武器没材料"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "State",
|
||||
"type_cn": "State",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"军队更弱",
|
||||
"理解不足",
|
||||
"不足"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalPeriod",
|
||||
"type_cn": "HistoricalPeriod",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"春秋战国史",
|
||||
"唐朝史",
|
||||
"宋朝"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Attribute",
|
||||
"type_cn": "Attribute",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"资源丰富",
|
||||
"易守难攻",
|
||||
"政策连续性强"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Right",
|
||||
"type_cn": "Right",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"军事指挥权",
|
||||
"财政调度权",
|
||||
"募兵权"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Policy",
|
||||
"type_cn": "Policy",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"商鞅变法",
|
||||
"禁军由文官控制"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryCondition",
|
||||
"type_cn": "MilitaryCondition",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"军力不足",
|
||||
"缺乏战略纵深"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Role",
|
||||
"type_cn": "Role",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"节度使",
|
||||
"协调中枢"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Position",
|
||||
"type_cn": "Position",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"总理",
|
||||
"丞相"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalCharacteristic",
|
||||
"type_cn": "PoliticalCharacteristic",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"旧贵族势力弱",
|
||||
"中央集权程度高"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Phenomenon",
|
||||
"type_cn": "Phenomenon",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"宋朝军事弱势"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Factor",
|
||||
"type_cn": "Factor",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"制度性因素"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicFactor",
|
||||
"type_cn": "EconomicFactor",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政压力"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicIndicator",
|
||||
"type_cn": "EconomicIndicator",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政支出"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryStrategy",
|
||||
"type_cn": "MilitaryStrategy",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"对外战略被动"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryCapability",
|
||||
"type_cn": "MilitaryCapability",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"机动能力弱"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PersonGroup",
|
||||
"type_cn": "PersonGroup",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"武将"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicCondition",
|
||||
"type_cn": "EconomicCondition",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政压力"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "InstitutionalPolicy",
|
||||
"type_cn": "InstitutionalPolicy",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"废除丞相制度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "StateOfAffairs",
|
||||
"type_cn": "StateOfAffairs",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央决策高度集中于皇帝"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Institution",
|
||||
"type_cn": "Institution",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"科举"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Function",
|
||||
"type_cn": "Function",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"统筹大事小情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "AdministrativeStructure",
|
||||
"type_cn": "AdministrativeStructure",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"六部直接对皇帝负责"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "AdministrativeProblem",
|
||||
"type_cn": "AdministrativeProblem",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"皇帝一人批不完政务"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Behavior",
|
||||
"type_cn": "Behavior",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"互相推诿责任"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Resource",
|
||||
"type_cn": "Resource",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"银子"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Situation",
|
||||
"type_cn": "Situation",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"没人拍板"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalState",
|
||||
"type_cn": "HistoricalState",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"秦国"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Location",
|
||||
"type_cn": "地点",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"关中"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalEvent",
|
||||
"type_cn": "HistoricalEvent",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"安史之乱"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalAction",
|
||||
"type_cn": "PoliticalAction",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央整顿"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalPhenomenon",
|
||||
"type_cn": "PoliticalPhenomenon",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"藩镇割据加剧"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicEntity",
|
||||
"type_cn": "EconomicEntity",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央财政"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "System",
|
||||
"type_cn": "System",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"募兵制"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "WorkRole",
|
||||
"type_cn": "WorkRole",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"掌控禁军"
|
||||
]
|
||||
}
|
||||
],
|
||||
"triplet_samples": [
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "宋朝军事弱势"
|
||||
},
|
||||
{
|
||||
"subject": "宋朝军事弱势",
|
||||
"predicate": "RESULTED_IN",
|
||||
"predicate_cn": "resulted in",
|
||||
"object": "制度性因素"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "禁军由文官控制导致作战效率低下"
|
||||
},
|
||||
{
|
||||
"subject": "禁军由文官控制",
|
||||
"predicate": "RESULTED_IN",
|
||||
"predicate_cn": "resulted in",
|
||||
"object": "作战效率低下"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "厢军装备不足"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "宋朝"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "军费"
|
||||
}
|
||||
],
|
||||
"self_reflexion": [
|
||||
{
|
||||
"conflict": {
|
||||
"data": [
|
||||
{
|
||||
"id": "76be6d82d8804beda6baa3d3447d6cbc",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "4a0804127d35456f86d4f06e1fa458f7",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": null,
|
||||
"entity_ids": []
|
||||
}
|
||||
],
|
||||
"conflict": true,
|
||||
"conflict_memory": {
|
||||
"id": "e268a6fff35543fab471986c188e023e",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": null,
|
||||
"entity_ids": []
|
||||
}
|
||||
},
|
||||
"reflexion": {
|
||||
"reason": "同一学生在不同时间点重复提出对'六部缺乏协调机制'具体影响的理解困难,表明原有解释未能有效解决其认知障碍,存在记忆冗余与教学反馈失效的冲突。",
|
||||
"solution": "保留后出现的记忆记录(chunk_id为4a0804127d35456f86d4f06e1fa458f7)作为最新学习状态,将其设为有效;将前次相同内容的记忆(id为e268a6fff35543fab471986c188e023e)标记为失效,避免重复干预,并基于后续完整解释优化知识呈现逻辑。"
|
||||
},
|
||||
"resolved": {
|
||||
"original_memory_id": "e268a6fff35543fab471986c188e023e",
|
||||
"resolved_memory": {
|
||||
"id": "e268a6fff35543fab471986c188e023e",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": "2025-12-12 09:48:00.166068",
|
||||
"entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Card from './components/Card'
|
||||
import type { ConfigForm, Variable } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
@@ -43,7 +43,7 @@ const MemoryExtractionEngine: FC = () => {
|
||||
const values = Form.useWatch<ConfigForm>([], form)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (values?.reflexion_range === 'database') {
|
||||
@@ -55,9 +55,9 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}, [values])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface Memory {
|
||||
include_dialogue_context: boolean;
|
||||
max_context: string;
|
||||
lambda_mem: string;
|
||||
lambda_mem: string;
|
||||
offset: string;
|
||||
state: boolean;
|
||||
created_at: string;
|
||||
|
||||
92
web/src/views/ModelManagement/Group.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: true,
|
||||
is_active: true,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setList(response[0]?.models || [])
|
||||
})
|
||||
}
|
||||
const formatData = (data: ModelListItem) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`modelNew.type`),
|
||||
children: data.type ? t(`modelNew.${data.type}`) : '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`modelNew.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: t(`modelNew.created_at`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
avatarUrl={item.logo}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default Group
|
||||
86
web/src/views/ModelManagement/List.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useRef, useState, useEffect, type FC } from 'react';
|
||||
import { Button, Flex, Row, Col } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import KeyConfigModal from './components/KeyConfigModal'
|
||||
import ModelListDetail from './components/ModelListDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelList: FC<{ query: any }> = ({ query }) => {
|
||||
const { t } = useTranslation();
|
||||
const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
|
||||
const modelListDetailRef = useRef<ModelListDetailRef>(null)
|
||||
const [list, setList] = useState<ProviderModelItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: false,
|
||||
})
|
||||
.then(res => {
|
||||
setList((res || []) as ProviderModelItem[])
|
||||
})
|
||||
}
|
||||
|
||||
const handleShowModel = (vo: ProviderModelItem) => {
|
||||
modelListDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleKeyConfig = (vo: ProviderModelItem) => {
|
||||
keyConfigModalRef.current?.handleOpen(vo)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.provider}
|
||||
title={t(`modelNew.${item.provider}`)}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.provider[0].toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<KeyConfigModal
|
||||
ref={keyConfigModalRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
<ModelListDetail
|
||||
ref={modelListDetailRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
104
web/src/views/ModelManagement/Square.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import ModelSquareDetail from './components/ModelSquareDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
|
||||
const [list, setList] = useState<ModelPlaza[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelPlaza(query)
|
||||
.then(res => {
|
||||
setList((res as ModelPlaza[]) || [])
|
||||
})
|
||||
}
|
||||
|
||||
const handleMore = (vo: ModelPlaza) => {
|
||||
modelSquareDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList()
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: list.map(vo => (
|
||||
<div key={vo.provider}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">
|
||||
<div className="rb:font-medium">{t(`modelNew.${vo.provider}`)}</div>
|
||||
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})></Button>
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{vo.models.slice(0, 6).map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<ModelSquareDetail
|
||||
ref={modelSquareDetailRef}
|
||||
refresh={getList}
|
||||
handleEdit={handleEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default ModelSquare
|
||||
@@ -1,171 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelFormData, Model, ConfigModalRef, ConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateModel, addModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
|
||||
const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<Model>({} as Model);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<ModelFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch<ModelFormData>([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setModel({} as Model);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: Model) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
// 设置表单值
|
||||
const apiKeyInfo = model.api_keys[0]
|
||||
form.setFieldsValue({
|
||||
provider: apiKeyInfo.provider,
|
||||
model_name: apiKeyInfo.model_name,
|
||||
api_key: apiKeyInfo.api_key,
|
||||
api_base: apiKeyInfo.api_base
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
const data = {
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
api_keys: {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
},
|
||||
}
|
||||
setLoading(true)
|
||||
const res = isEdit
|
||||
? updateModel(model.api_keys[0].id, {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
} as ModelFormData)
|
||||
: addModel(data as ModelFormData)
|
||||
|
||||
res.then(() => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('model.modelConfiguration')}` : t('model.createModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{}}
|
||||
>
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('model.displayName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.displayName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('model.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('model.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="model_name"
|
||||
label={t('model.modelName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.modelName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('model.apiKey')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.apiKey') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('model.apiEndpoint')}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ConfigModal;
|
||||
168
web/src/views/ModelManagement/components/CustomModelModal.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomModelForm, ModelPlazaItem, CustomModelModalRef, CustomModelModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelPlazaItem>({} as ModelPlazaItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CustomModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const formValues = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelPlazaItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelPlazaItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleUpdate = (data: CustomModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, provider, ...rest} = data
|
||||
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh && refresh()
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
const { logo, ...rest } = values;
|
||||
let formData: CustomModelForm = {
|
||||
...rest
|
||||
}
|
||||
formData.is_official = false;
|
||||
|
||||
if (typeof logo === 'object' && logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id)
|
||||
.then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
})
|
||||
.catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
console.log('formValues', formValues)
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t('modelNew.tags')}
|
||||
>
|
||||
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CustomModelModal;
|
||||
173
web/src/views/ModelManagement/components/GroupModelModal.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/models'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import ModelImplement from './ModelImplement'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CompositeModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const type = Form.useWatch(['type'], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelListItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
api_key_ids: model.api_keys,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
})
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { api_key_ids = [], logo, ...rest } = values
|
||||
|
||||
const formData: CompositeModelForm = {
|
||||
...rest,
|
||||
api_key_ids: api_key_ids.map(vo => (vo as ModelApiKey).id)
|
||||
}
|
||||
|
||||
if (logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id).then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
}).catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
const handleUpdate = (data: CompositeModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, ...rest } = data
|
||||
const res = isEdit
|
||||
? updateCompositeModel(model.id, { ...rest })
|
||||
: addCompositeModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh?.();
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createGroupModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ balance_strategy: 'none' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="load_balance_strategy"
|
||||
label={t('modelNew.load_balance_strategy')}
|
||||
>
|
||||
<Select
|
||||
options={['round_robin', 'none'].map(key => ({
|
||||
label: t(`modelNew.${key}`),
|
||||
value: key
|
||||
}))}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_key_ids">
|
||||
<ModelImplement type={type} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default GroupModelModal;
|
||||
92
web/src/views/ModelManagement/components/KeyConfigModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updateProviderApiKeys } from '@/api/models'
|
||||
|
||||
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ProviderModelItem>({} as ProviderModelItem);
|
||||
const [form] = Form.useForm<KeyConfigModalForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ProviderModelItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setVisible(true);
|
||||
setModel(vo);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
|
||||
updateProviderApiKeys({
|
||||
...values,
|
||||
provider: model.provider
|
||||
}).then((res) => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(res as string)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.provider} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.save`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default KeyConfigModal;
|
||||
@@ -0,0 +1,181 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Cascader, App, type CascaderProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelProviderUrl, getModelNewList } from '@/api/models'
|
||||
import type { ProviderModelItem } from '../../types'
|
||||
|
||||
const { SHOW_CHILD } = Cascader;
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
[key: string]: any;
|
||||
}
|
||||
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
|
||||
refresh,
|
||||
type,
|
||||
groupedByProvider
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<SubModelModalForm>();
|
||||
const [selecteds, setSelecteds] = useState<any[]>([])
|
||||
const [modelList, setModelList] = useState<Option[]>([])
|
||||
const provider = Form.useWatch(['provider'], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && groupedByProvider) {
|
||||
const lastModels = groupedByProvider[provider] || []
|
||||
const list = lastModels.map(vo => [{ name: vo.model_name, id: vo.model_config_ids[0], value: vo.model_config_ids[0], provider }, { value: vo.id }])
|
||||
setSelecteds(list)
|
||||
form.setFieldValue('api_key_ids', lastModels.map(vo => [vo.model_config_ids[0], vo.id]))
|
||||
}
|
||||
}, [groupedByProvider, provider])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
setSelecteds([])
|
||||
setModelList([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields()
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh?.(selecteds.map(vo => ({
|
||||
...vo[0],
|
||||
model_name: vo[0].name,
|
||||
model_config_ids: [vo[0].id],
|
||||
id: vo[1].value,
|
||||
api_key: vo[1].label
|
||||
})))
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
|
||||
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
|
||||
const lastFilterLit = value.filter(vo => vo.length !== 1)
|
||||
if (filterList.length) {
|
||||
message.warning(`【${filterList.map(vo => vo.label)}】${t('modelNew.selectOneTip')}`)
|
||||
form.setFieldValue('api_key_ids', lastFilterLit)
|
||||
}
|
||||
setSelecteds(selectedOptions)
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
|
||||
form.setFieldValue('api_key_ids', undefined)
|
||||
if (provider) {
|
||||
getModelNewList({
|
||||
provider: provider,
|
||||
is_composite: false,
|
||||
is_active: true,
|
||||
type
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
const list = response[0]?.models || []
|
||||
setModelList(list.map(vo => {
|
||||
const children = vo.api_keys.map(item => ({
|
||||
label: item.api_key,
|
||||
value: item.id,
|
||||
}))
|
||||
return {
|
||||
...vo,
|
||||
label: vo.name,
|
||||
value: vo.id,
|
||||
children: children
|
||||
}
|
||||
}))
|
||||
|
||||
if (api_key_ids?.length) {
|
||||
form.setFieldsValue({
|
||||
api_key_ids: api_key_ids
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setModelList([])
|
||||
}
|
||||
}
|
||||
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
|
||||
labels.map((label, i) => {
|
||||
const option = selectedOptions[i];
|
||||
if (i === labels.length - 1) {
|
||||
return (
|
||||
<span key={option?.value || i}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span key={option?.value || i}>{label} / </span>;
|
||||
});
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('modelNew.implementConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
onChange={(value) => handleChangeProvider(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="api_key_ids"
|
||||
label={t('modelNew.api_key_ids')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.api_key_ids') }) }]}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={modelList}
|
||||
onChange={handleChange}
|
||||
multiple
|
||||
autoClearSearchValue
|
||||
className="rb:w-full!"
|
||||
showCheckedStrategy={SHOW_CHILD}
|
||||
changeOnSelect
|
||||
displayRender={displayRender}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubModelModal;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { type FC, useRef } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flex, Button, Space, App } from 'antd'
|
||||
|
||||
import type { SubModelModalRef, ModelList } from './types'
|
||||
import SubModelModal from './SubModelModal'
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
interface ModelImplementProps {
|
||||
type?: string;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const { modal, message } = App.useApp();
|
||||
const subModelModalRef = useRef<SubModelModalRef>(null)
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!type || type.trim() === '') {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
|
||||
return
|
||||
}
|
||||
subModelModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleDelete = (vo: any) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
onChange?.(value?.filter((item: any) => item.id !== vo.id))
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleRefresh = (list: ModelList[]) => {
|
||||
const existingModels = value || [];
|
||||
let updatedModels = [...existingModels];
|
||||
|
||||
const provider = list[0].provider
|
||||
|
||||
updatedModels = updatedModels.filter(item => item.provider !== provider)
|
||||
updatedModels = [...updatedModels, ...list]
|
||||
|
||||
onChange?.([...updatedModels]);
|
||||
}
|
||||
|
||||
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
|
||||
const provider = item.provider || 'unknown';
|
||||
if (!acc[provider]) acc[provider] = [];
|
||||
acc[provider].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, ModelList[]>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center">
|
||||
{t('modelNew.modelImplement')}
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleAdd} className="rb:px-2! rb:h-6!">+ {t('modelNew.addImplement')}</Button>
|
||||
<Button size="small" className="rb:px-2! rb:h-6!">{t('modelNew.noAuth')}</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
|
||||
<div className="rb:bg-[#F5F6F7] rb:rounded-lg rb:p-3 rb:mt-2">
|
||||
{!value || value.length === 0
|
||||
? <Empty size={88} />
|
||||
: value.map((item: any) => {
|
||||
return (
|
||||
<div key={item.id} className="rb:mb-4 rb:last:rb:mb-0 rb:bg-[#FBFDFF] rb:rounded-lg rb:p-3">
|
||||
<Flex gap={8} justify="space-between" align="center" className="rb:mb-2 rb:last:rb:mb-0">
|
||||
<div className="rb:font-medium">{item.model_name}</div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
</Flex>
|
||||
<div className="rb:text-[#5B6167] rb:my-2">{item.api_key}</div>
|
||||
<Tag className="rb:mb-2">{t(`modelNew.${item.provider}`)}</Tag>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<SubModelModal
|
||||
ref={subModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
type={type}
|
||||
groupedByProvider={groupedByProvider}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelImplement
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ModelListItem } from '../../types'
|
||||
|
||||
export interface ModelList extends ModelListItem {
|
||||
api_key_id: string;
|
||||
}
|
||||
export interface SubModelModalForm {
|
||||
provider: string;
|
||||
api_key_ids: string[][];
|
||||
}
|
||||
export interface SubModelModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface SubModelModalProps {
|
||||
type?: string;
|
||||
refresh?: (vo: ModelList[]) => void;
|
||||
groupedByProvider?: Record<string, ModelList[]>
|
||||
}
|
||||
142
web/src/views/ModelManagement/components/ModelListDetail.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, ModelListDetailRef, MultiKeyConfigModalRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import MultiKeyConfigModal from './MultiKeyConfigModal'
|
||||
import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
|
||||
import { getLogoUrl } from '../utils'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
|
||||
interface ModelListDetailProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [data, setData] = useState<ProviderModelItem>({} as ProviderModelItem)
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [type, setType] = useState<string | undefined | null>(null)
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setType(null)
|
||||
setOpen(true)
|
||||
getData(vo)
|
||||
}
|
||||
|
||||
const getData = (vo: ProviderModelItem) => {
|
||||
if (!vo.provider) return
|
||||
|
||||
getModelNewList({
|
||||
provider: vo.provider
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setData(response[0])
|
||||
setList(response[0].models)
|
||||
})
|
||||
}
|
||||
const handleKeyConfig = (vo: ModelListItem) => {
|
||||
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
|
||||
}
|
||||
const handleChange = (vo: ModelListItem) => {
|
||||
setLoading(true)
|
||||
updateModelStatus(vo.id, { is_active: !vo.is_active })
|
||||
.finally(() => {
|
||||
getData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setType(null)
|
||||
setOpen(false)
|
||||
refresh?.()
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
getData(data)
|
||||
}
|
||||
const handleTypeChange = (value: string) => {
|
||||
setType(value)
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const filterList = useMemo(() => {
|
||||
if (!type) return list
|
||||
return list.filter(vo => vo.type === type)
|
||||
}, [type, list])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<CustomSelect
|
||||
value={type}
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
onChange={handleTypeChange}
|
||||
className="rb:w-full"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{filterList.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
|
||||
{filterList.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space className="rb:mt-1!">
|
||||
<Tag>{t(`modelNew.${item.type}`)}</Tag>
|
||||
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
extra={<Switch defaultChecked={item.is_active} disabled={loading} onChange={() => handleChange(item)} />}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
|
||||
</Tooltip>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
<MultiKeyConfigModal
|
||||
ref={multiKeyConfigModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelListDetail;
|
||||
106
web/src/views/ModelManagement/components/ModelSquareDetail.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useImperativeHandle, forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { getLogoUrl } from '../utils'
|
||||
|
||||
interface ModelSquareDetailProps {
|
||||
refresh: () => void;
|
||||
handleEdit: (vo: ModelPlazaItem) => void;
|
||||
}
|
||||
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [list, setList] = useState<ModelPlazaItem[]>([])
|
||||
|
||||
const handleOpen = (vo: ModelPlaza) => {
|
||||
setModel(vo)
|
||||
setOpen(true)
|
||||
getList(vo)
|
||||
}
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
refresh()
|
||||
}
|
||||
const getList = (vo: ModelPlaza) => {
|
||||
getModelPlaza({ provider: vo.provider })
|
||||
.then(res => {
|
||||
const response = res as ModelPlaza[]
|
||||
setList(response.length > 0 ? response[0].models : [])
|
||||
})
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${model.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="rb:h-full rb:overflow-y-auto">
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelSquareDetail;
|
||||
122
web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models'
|
||||
|
||||
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [form] = Form.useForm<MultiKeyForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
refresh?.()
|
||||
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ModelListItem) => {
|
||||
setVisible(true);
|
||||
getData(vo)
|
||||
};
|
||||
|
||||
const getData = (vo: ModelListItem) => {
|
||||
if (!vo.id) return
|
||||
|
||||
getModelInfo(vo?.id)
|
||||
.then(res => {
|
||||
setModel(res as ModelListItem)
|
||||
})
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
addModelApiKey(model.id, {
|
||||
...values,
|
||||
model_config_id: model.id,
|
||||
model_name: model.name,
|
||||
provider: model.provider,
|
||||
}).then(() => {
|
||||
message.success(t('common.saveSuccess'))
|
||||
form.resetFields();
|
||||
getData(model)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleDelete = (api_key_id: string) => {
|
||||
deleteModelApiKey(api_key_id)
|
||||
.then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
getData(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.name} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
{model.api_keys && model.api_keys.length > 0 && (
|
||||
<div className="rb:mb-4">
|
||||
{model.api_keys.map((key) => (
|
||||
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
|
||||
<div>
|
||||
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
|
||||
</div>
|
||||
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" block onClick={handleSave} loading={loading}>+ {t('modelNew.add')}</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default MultiKeyConfigModal;
|
||||
@@ -1,99 +1,124 @@
|
||||
import { useState, useRef, type FC } from 'react';
|
||||
import { Row, Col, Button } from 'antd'
|
||||
import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import ConfigModal from './components/ConfigModal'
|
||||
import type { Model, DescriptionItem, ConfigModalRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import GroupModelModal from './components/GroupModelModal'
|
||||
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef, Query } from './types'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import PageTabs from '@/components/PageTabs'
|
||||
import GroupModel from './Group'
|
||||
import ModelList from './List'
|
||||
import ModelSquare from './Square'
|
||||
import CustomModelModal from './components/CustomModelModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
|
||||
const tabKeys = ['group', 'list', 'square']
|
||||
const ModelManagement: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState({})
|
||||
const configModalRef = useRef<ConfigModalRef>(null)
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const [activeTab, setActiveTab] = useState('group');
|
||||
const configModalRef = useRef<GroupModelModalRef>(null)
|
||||
const customModelModalRef = useRef<CustomModelModalRef>(null)
|
||||
const groupRef = useRef<BaseRef>(null)
|
||||
const squareRef = useRef<BaseRef>(null)
|
||||
const [form] = Form.useForm<Query>()
|
||||
const query = Form.useWatch([], form)
|
||||
|
||||
const formatData = (data: Model) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`model.type`),
|
||||
children: data.type || '-',
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
label: t(`model.provider`),
|
||||
children: data.api_keys[0].provider || '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`model.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: t(`model.created`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(value => ({
|
||||
value,
|
||||
label: t(`modelNew.${value}`),
|
||||
}))
|
||||
}
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value as string);
|
||||
form.resetFields()
|
||||
}
|
||||
|
||||
const handleEdit = (model?: Model) => {
|
||||
configModalRef?.current?.handleOpen(model)
|
||||
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
|
||||
switch(activeTab) {
|
||||
case 'group':
|
||||
configModalRef?.current?.handleOpen(vo as ModelListItem)
|
||||
break
|
||||
case 'square':
|
||||
customModelModalRef?.current?.handleOpen(vo as ModelPlazaItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({ search: value })
|
||||
const handleRefresh = () => {
|
||||
switch (activeTab) {
|
||||
case 'group':
|
||||
groupRef.current?.getList()
|
||||
break
|
||||
case 'square':
|
||||
squareRef.current?.getList()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:w-full">
|
||||
<Row className='rb:mb-[16px] rb:w-full'>
|
||||
<Col span={6}>
|
||||
<SearchInput
|
||||
placeholder={t('model.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18} className="rb:text-right">
|
||||
<Button type="primary" onClick={() => handleEdit()}>{t('model.createModel')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<>
|
||||
<Flex justify="space-between" align="center">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getModelListUrl}
|
||||
query={query}
|
||||
renderItem={(item: Model) => (
|
||||
<RbCard
|
||||
title={item.name}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-[8px]" type="primary" ghost block onClick={() => handleEdit(item)}>{t('model.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
)}
|
||||
/>
|
||||
<Form form={form}>
|
||||
<Space size={12}>
|
||||
{activeTab === 'list' &&
|
||||
<Form.Item name="type" noStyle>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{(activeTab === 'list' || activeTab === 'square') &&
|
||||
<Form.Item name="provider" noStyle>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.provider')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab !== 'list' &&
|
||||
<Form.Item name="search" noStyle>
|
||||
<SearchInput
|
||||
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
|
||||
className="rb:w-70!"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
|
||||
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
|
||||
</Space>
|
||||
</Form>
|
||||
</Flex>
|
||||
|
||||
<ConfigModal
|
||||
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">
|
||||
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
|
||||
{activeTab === 'list' && <ModelList query={query} />}
|
||||
{activeTab === 'square' && <ModelSquare ref={squareRef} query={query} handleEdit={handleEdit} />}
|
||||
</div>
|
||||
<GroupModelModal
|
||||
ref={configModalRef}
|
||||
refresh={() => scrollListRef?.current?.refresh()}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
<CustomModelModal
|
||||
ref={customModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,139 @@
|
||||
// 模型表单数据类型
|
||||
export interface ModelFormData extends ApiKey {
|
||||
name: string;
|
||||
type: string;
|
||||
api_keys: ApiKey;
|
||||
}
|
||||
export interface Query {
|
||||
type?: string;
|
||||
provider?: string;
|
||||
is_active?: boolean;
|
||||
is_public?: boolean;
|
||||
is_composite?: boolean;
|
||||
search?: string;
|
||||
|
||||
pagesize?: number;
|
||||
page?: number;
|
||||
}
|
||||
export interface DescriptionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
children: string;
|
||||
}
|
||||
export interface CompositeModelForm {
|
||||
logo?: any;
|
||||
name: string;
|
||||
type?: string;
|
||||
description: string;
|
||||
api_key_ids: ModelApiKey[] | string[];
|
||||
}
|
||||
export interface GroupModelModalRef {
|
||||
handleOpen: (model?: ModelListItem) => void;
|
||||
}
|
||||
export interface GroupModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface ModelListDetailRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
|
||||
// 模型类型定义
|
||||
export interface Model {
|
||||
|
||||
export interface ModelApiKey {
|
||||
model_name: string;
|
||||
description: string | null;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
config: any;
|
||||
is_active: boolean;
|
||||
priority: string;
|
||||
id: string;
|
||||
usage_count: string;
|
||||
last_used_at: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
model_config_ids: string[];
|
||||
}
|
||||
export interface ModelListItem {
|
||||
model_name?: string;
|
||||
model_config_ids: string[];
|
||||
name: string;
|
||||
type: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
config: any;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
id: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
api_keys: ModelApiKey[]
|
||||
}
|
||||
export interface ProviderModelItem {
|
||||
provider: string;
|
||||
logo?: string;
|
||||
tags: string[];
|
||||
models: ModelListItem[];
|
||||
}
|
||||
export interface KeyConfigModalForm {
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
}
|
||||
export interface KeyConfigModalRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
export interface KeyConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface MultiKeyForm {
|
||||
model_config_id?: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
}
|
||||
|
||||
export interface MultiKeyConfigModalRef {
|
||||
handleOpen: (vo: ModelListItem, provider?: string) => void;
|
||||
}
|
||||
export interface MultiKeyConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelPlaza {
|
||||
provider: string;
|
||||
models: ModelPlazaItem[];
|
||||
}
|
||||
export interface ModelPlazaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
config: Record<string, unknown>;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
created_at: string | number;
|
||||
updated_at: string | number;
|
||||
api_keys: ApiKey[];
|
||||
|
||||
// provider: string;
|
||||
// temperature: number,
|
||||
// topP: number,
|
||||
// status: string;
|
||||
// vectorDimension: number;
|
||||
// batchSize: number;
|
||||
// truncateStrategy: string;
|
||||
// created: string;
|
||||
// updatedAt: string;
|
||||
// descriptionItems?: Record<string, unknown>[];
|
||||
// basicParameters?: string;
|
||||
// normalization?: string;
|
||||
// maxInputLength?: number;
|
||||
// encodingFormat?: string;
|
||||
// enablePooling?: boolean;
|
||||
// poolingStrategy?: string;
|
||||
// apiKey?: string;
|
||||
// apiEndpoint?: string;
|
||||
// timeout?: number;
|
||||
// autoRetry?: boolean;
|
||||
// retryCount?: number;
|
||||
}
|
||||
interface ApiKey {
|
||||
model_name?: string;
|
||||
provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
config?: Record<string, unknown>;
|
||||
is_active?: boolean;
|
||||
priority?: string;
|
||||
id: string;
|
||||
model_config_id?: string;
|
||||
usage_count?: string;
|
||||
last_used_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
is_deprecated: boolean;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
add_count: number;
|
||||
is_added: boolean;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
export interface ConfigModalRef {
|
||||
handleOpen: (model?: Model) => void;
|
||||
export interface ModelSquareDetailRef {
|
||||
handleOpen: (vo: ModelPlaza) => void;
|
||||
}
|
||||
export interface ConfigModalProps {
|
||||
export interface CustomModelForm {
|
||||
name: string;
|
||||
type?: string;
|
||||
provider?: string;
|
||||
logo?: any;
|
||||
description: string;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
export interface CustomModelModalRef {
|
||||
handleOpen: (vo?: ModelPlazaItem) => void;
|
||||
}
|
||||
export interface CustomModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface BaseRef {
|
||||
getList: () => void;
|
||||
}
|
||||
26
web/src/views/ModelManagement/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import bedrockIcon from '@/assets/images/model/bedrock.svg'
|
||||
import dashscopeIcon from '@/assets/images/model/dashscope.png'
|
||||
import gpustackIcon from '@/assets/images/model/gpustack.png'
|
||||
import ollamaIcon from '@/assets/images/model/ollama.svg'
|
||||
import openaiIcon from '@/assets/images/model/openai.svg'
|
||||
import xinferenceIcon from '@/assets/images/model/xinference.svg'
|
||||
|
||||
export const ICONS = {
|
||||
bedrock: bedrockIcon,
|
||||
dashscope: dashscopeIcon,
|
||||
gpustack: gpustackIcon,
|
||||
ollama: ollamaIcon,
|
||||
openai: openaiIcon,
|
||||
xinference: xinferenceIcon
|
||||
}
|
||||
|
||||
export const getLogoUrl = (logo?: string) => {
|
||||
if (!logo) {
|
||||
return undefined
|
||||
}
|
||||
if (logo.startsWith('http')) {
|
||||
return logo
|
||||
}
|
||||
|
||||
return ICONS[logo as keyof typeof ICONS] || undefined
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const configList = [
|
||||
key: 'reflection_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
},
|
||||
// 迭代周期
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm', pagesize: 100 }}
|
||||
params={{ type: 'llm', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -80,7 +80,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -94,7 +94,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createWorkspace } from '@/api/workspaces'
|
||||
import RadioGroupCard from '@/components/RadioGroupCard'
|
||||
import { getModelListUrl, getModelList } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -29,7 +29,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
const [form] = Form.useForm<SpaceModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<Space | null>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -80,9 +80,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
}, [])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -134,7 +134,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -148,7 +148,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -59,6 +59,11 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
})
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!data.file_path) return
|
||||
window.open(data.file_path, '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t(`perceptualDetail.${type}`)}
|
||||
@@ -78,17 +83,17 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
<Image src={data.file_path} alt={data.file_name} />
|
||||
// <img src={data.file_path} alt={data.file_name} className="rb:max-w-full rb:max-h-full rb:object-contain" />
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
<div className="rb:text-[#5B6167]">{data.file_name}</div>
|
||||
)
|
||||
) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? (
|
||||
<audio controls className="rb:w-full">
|
||||
<source src={data.file_path} />
|
||||
</audio>
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
<div className="rb:text-[#5B6167] rb:cursor-pointer" onClick={handleDownload}>{data.file_name}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rb:text-gray-400">No file</div>
|
||||
<div className="rb:text-[#5B6167]">{t('empty.tableEmpty')}</div>
|
||||
)}
|
||||
</div>
|
||||
<Space size={4} direction="vertical" className="rb:w-full rb:mt-3">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form, App } from 'antd'
|
||||
import { Space, Button } from 'antd'
|
||||
import { Input, Form, App, Space, Button, Collapse } from 'antd'
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
|
||||
import CodeBlock from '@/components/Markdown/CodeBlock'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
@@ -13,8 +14,11 @@ import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import dayjs from 'dayjs'
|
||||
import type { ChatRef, VariableConfigModalRef, StartVariableItem, GraphRef } from '../../types'
|
||||
import type { ChatRef, VariableConfigModalRef, GraphRef } from '../../types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import type { Variable } from '../Properties/VariableList/types'
|
||||
import styles from './chat.module.css'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId, graphRef }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -24,7 +28,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<StartVariableItem[]>([])
|
||||
const [variables, setVariables] = useState<Variable[]>([])
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
|
||||
@@ -39,7 +43,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
if (startNodes.length) {
|
||||
const curVariables = startNodes[0].config.variables?.defaultValue
|
||||
|
||||
curVariables.forEach((vo: StartVariableItem) => {
|
||||
curVariables.forEach((vo: Variable) => {
|
||||
if (typeof vo.default !== 'undefined') {
|
||||
vo.value = vo.default
|
||||
}
|
||||
@@ -60,7 +64,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
const handleSave = (values: StartVariableItem[]) => {
|
||||
const handleSave = (values: Variable[]) => {
|
||||
setVariables([...values])
|
||||
}
|
||||
const handleSend = () => {
|
||||
@@ -97,13 +101,28 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
subContent: [],
|
||||
}])
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setStreamLoading(false)
|
||||
|
||||
data.forEach(item => {
|
||||
const { chunk, conversation_id } = item.data as { chunk: string; conversation_id: string | null; };
|
||||
const { chunk, conversation_id, node_id, input, output, error, elapsed_time, status } = item.data as {
|
||||
chunk: string;
|
||||
conversation_id: string | null;
|
||||
node_id: string;
|
||||
node_name?: string;
|
||||
input?: any;
|
||||
output?: any;
|
||||
elapsed_time?: string;
|
||||
error?: any;
|
||||
state: Record<string, any>;
|
||||
status?: 'completed' | 'failed'
|
||||
};
|
||||
|
||||
const node = graphRef.current?.getNodes().find(n => n.id === node_id);
|
||||
const { name, icon } = node?.getData() || {}
|
||||
|
||||
console.log('node', node?.getData())
|
||||
|
||||
switch(item.event) {
|
||||
case 'message':
|
||||
@@ -119,6 +138,66 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'node_start':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
const newSubContent = newList[lastIndex].subContent || []
|
||||
const filterIndex = newSubContent.findIndex(vo => vo.id === node_id)
|
||||
if (filterIndex > -1) {
|
||||
newSubContent[filterIndex] = {
|
||||
...newSubContent[filterIndex],
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
icon,
|
||||
content: {},
|
||||
}
|
||||
} else {
|
||||
newSubContent.push({
|
||||
id: node_id,
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
icon,
|
||||
content: {},
|
||||
})
|
||||
}
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
subContent: newSubContent
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'node_end':
|
||||
case 'node_error':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
const newSubContent = newList[lastIndex].subContent || []
|
||||
const filterIndex = newSubContent.findIndex(vo => vo.node_id === node_id)
|
||||
if (filterIndex > -1 && newSubContent[filterIndex].content) {
|
||||
newSubContent[filterIndex] = {
|
||||
...newSubContent[filterIndex],
|
||||
content: {
|
||||
input,
|
||||
output,
|
||||
error,
|
||||
},
|
||||
status: status || 'completed',
|
||||
elapsed_time
|
||||
}
|
||||
}
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
subContent: newSubContent
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'workflow_end':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
@@ -126,6 +205,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status,
|
||||
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content
|
||||
}
|
||||
}
|
||||
@@ -142,14 +222,31 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
}
|
||||
|
||||
form.setFieldValue('message', undefined)
|
||||
setStreamLoading(true)
|
||||
draftRun(appId, {
|
||||
message: message,
|
||||
variables: params,
|
||||
stream: true,
|
||||
conversation_id: conversationId
|
||||
}, handleStreamMessage)
|
||||
.catch((error) => {
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status: 'failed',
|
||||
content: null,
|
||||
subContent: error.error
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
@@ -158,6 +255,11 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
handleClose
|
||||
}));
|
||||
|
||||
const getStatus = (status?: string) => {
|
||||
return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]'
|
||||
}
|
||||
|
||||
console.log('chatList', chatList)
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<div className="rb:flex rb:items-center rb:gap-2.5">
|
||||
@@ -173,10 +275,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
onClose={handleClose}
|
||||
>
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:pt-[24px] rb:h-[calc(100%-76px)]': true,
|
||||
|
||||
}}
|
||||
classNames="rb:mx-[16px] rb:pt-[24px] rb:h-[calc(100%-76px)]"
|
||||
contentClassNames="rb:max-w-[400px]!'"
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chatList}
|
||||
@@ -184,6 +283,87 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
labelPosition="bottom"
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
renderRuntime={(item, index) => {
|
||||
return (
|
||||
<div key={index} className="rb:w-100 rb:mb-2">
|
||||
<Collapse
|
||||
className={styles[item.status || 'default']}
|
||||
items={[{
|
||||
key: 0,
|
||||
label: <div className={getStatus(item.status)}>
|
||||
{item.status === 'completed' ? <CheckCircleFilled className="rb:mr-1" /> : item.status === 'failed' ? <CloseCircleFilled className="rb:mr-1" /> : <LoadingOutlined className="rb:mr-1" />}
|
||||
{t('application.workflow')}
|
||||
</div>,
|
||||
className: styles.collapseItem,
|
||||
children: (
|
||||
Array.isArray(item.subContent)
|
||||
? <Space size={8} direction="vertical" className="rb:w-full!">
|
||||
{item.subContent?.map(vo => (
|
||||
<Collapse
|
||||
key={vo.node_id}
|
||||
items={[{
|
||||
key: vo.node_id,
|
||||
label: <div className={clsx("rb:flex rb:justify-between rb:items-center", getStatus(vo.status))}>
|
||||
<div className="rb:flex rb:items-center rb:gap-1 rb:flex-1">
|
||||
{vo.icon && <img src={vo.icon} className="rb:size-4" />}
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{vo.node_name || vo.node_id}</div>
|
||||
</div>
|
||||
<span>
|
||||
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
||||
{vo.status === 'completed' ? <CheckCircleFilled className="rb:ml-1" /> : vo.status === 'failed' ? <CloseCircleFilled className="rb:ml-1" /> : <LoadingOutlined className="rb:ml-1" />}
|
||||
</span>
|
||||
</div>,
|
||||
className: styles.collapseItem,
|
||||
children: (
|
||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
||||
{vo.status === 'failed' &&
|
||||
<div className={clsx("rb:bg-[#F0F3F8] rb:rounded-md", getStatus(vo.status))}>
|
||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||
{t(`workflow.error`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>{t('common.copy')}</Button>
|
||||
</div>
|
||||
<div className="rb:pb-2 rb:px-3 rb:max-h-40 rb:overflow-auto">
|
||||
<Markdown content={vo.content?.error || ''} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{['input', 'output'].map(key => (
|
||||
<div key={key} className="rb:bg-[#F0F3F8] rb:rounded-md">
|
||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||
{t(`workflow.${key}`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>{t('common.copy')}</Button>
|
||||
</div>
|
||||
<div className="rb:max-h-40 rb:overflow-auto">
|
||||
<CodeBlock
|
||||
size="small"
|
||||
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
||||
needCopy={false}
|
||||
showLineNumbers={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
: <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
||||
<Markdown content={item.subContent || ''} />
|
||||
</div>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
|
||||
45
web/src/views/Workflow/components/Chat/chat.module.css
Normal file
@@ -0,0 +1,45 @@
|
||||
.completed {
|
||||
background-color: rgba(54, 159, 33, 0.06);
|
||||
border-color: rgba(54, 159, 33, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.failed {
|
||||
background-color: rgba(255, 138, 76, 0.08);
|
||||
border-color: rgba(255, 138, 76, 0.20);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.default {
|
||||
background-color: rgba(91, 97, 103, 0.08);
|
||||
border-color: rgba(91, 97, 103, 0.30);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.collapse-item {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
||||
height: 16px;
|
||||
}
|
||||
.completed:global(.ant-collapse .ant-collapse-content),
|
||||
.failed:global(.ant-collapse .ant-collapse-content) {
|
||||
background-color: transparent;
|
||||
border-top: none;
|
||||
}
|
||||
:global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse) {
|
||||
/* background-color: #F0F3F8; */
|
||||
background-color: #FBFDFF;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse>.ant-collapse-item:last-child),
|
||||
.collapse-item :global(.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header) {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding: 0 4px 4px 4px;
|
||||
}
|
||||
@@ -15,22 +15,24 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||
import CommandPlugin from './plugin/CommandPlugin';
|
||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||
import Python3HighlightPlugin from './plugin/Python3HighlightPlugin';
|
||||
import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin';
|
||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||
import BlurPlugin from './plugin/BlurPlugin';
|
||||
import { VariableNode } from './nodes/VariableNode'
|
||||
|
||||
interface LexicalEditorProps {
|
||||
export interface LexicalEditorProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
options: Suggestion[];
|
||||
options?: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
enableJinja2?: boolean;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea'
|
||||
type?: 'input' | 'textarea',
|
||||
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -54,20 +56,25 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
onChange,
|
||||
options,
|
||||
options = [],
|
||||
variant = 'borderless',
|
||||
enableJinja2 = false,
|
||||
size = 'default',
|
||||
type = 'textarea'
|
||||
type = 'textarea',
|
||||
language = 'string'
|
||||
}) => {
|
||||
|
||||
const [_count, setCount] = useState(0);
|
||||
const [enableJinja2, setEnableJinja2] = useState(false)
|
||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (enableJinja2) {
|
||||
const styleId = 'jinja2-styles';
|
||||
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
|
||||
setEnableJinja2(language === 'jinja2');
|
||||
setEnableLineNumbers(needsLineNumbers);
|
||||
|
||||
if (needsLineNumbers) {
|
||||
const styleId = 'code-editor-styles';
|
||||
let existingStyle = document.getElementById(styleId);
|
||||
|
||||
|
||||
if (!existingStyle) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
@@ -119,6 +126,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
}
|
||||
.editor-content-with-numbers {
|
||||
white-space: pre-wrap;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
.editor-content-with-numbers p {
|
||||
margin: 0;
|
||||
@@ -128,7 +136,8 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}, [enableJinja2]);
|
||||
}, [language])
|
||||
|
||||
const initialConfig = {
|
||||
namespace: 'AutocompleteEditor',
|
||||
theme: enableJinja2 ? jinja2Theme : theme,
|
||||
@@ -168,7 +177,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<div style={{ position: 'relative' }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
enableJinja2 ? (
|
||||
enableLineNumbers ? (
|
||||
<div className="editor-with-line-numbers" style={{
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
@@ -212,8 +221,8 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
style={{
|
||||
minHeight: placeHolderMinheight,
|
||||
position: 'absolute',
|
||||
top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||
left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
top: enableLineNumbers ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||
left: enableLineNumbers ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
color: '#A8A9AA',
|
||||
fontSize: fontSize,
|
||||
lineHeight: placeHolderMinheight,
|
||||
@@ -227,12 +236,14 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
{enableJinja2 && <Jinja2HighlightPlugin />}
|
||||
{enableJinja2 && <LineNumberPlugin />}
|
||||
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
||||
{language === 'python3' && <Python3HighlightPlugin />}
|
||||
{language === 'javascript' && <JavaScriptHighlightPlugin />}
|
||||
{enableLineNumbers && <LineNumberPlugin />}
|
||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||
{enableJinja2 && <BlurPlugin />}
|
||||
{enableLineNumbers && <BlurPlugin />}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
||||
|
||||
const JS_KEYWORDS = new Set([
|
||||
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
||||
'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import',
|
||||
'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
|
||||
'typeof', 'var', 'void', 'while', 'with', 'yield', 'true', 'false', 'null', 'undefined'
|
||||
]);
|
||||
|
||||
const JavaScriptHighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (textNode.hasFormat('code')) return;
|
||||
if (!needsHighlight(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const selection = $getSelection();
|
||||
let selectionOffset = null;
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchor = selection.anchor;
|
||||
if (anchor.getNode() === textNode) {
|
||||
selectionOffset = anchor.offset;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = tokenizeJavaScript(text);
|
||||
if (tokens.length <= 1) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
newNode.toggleFormat('code');
|
||||
|
||||
switch (token.type) {
|
||||
case 'keyword':
|
||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
||||
break;
|
||||
case 'string':
|
||||
newNode.setStyle('color: #032f62;');
|
||||
break;
|
||||
case 'comment':
|
||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||
break;
|
||||
case 'function':
|
||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
|
||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
||||
let currentOffset = 0;
|
||||
for (const node of newNodes) {
|
||||
const nodeLength = node.getTextContent().length;
|
||||
if (currentOffset + nodeLength >= selectionOffset) {
|
||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
||||
break;
|
||||
}
|
||||
currentOffset += nodeLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function needsHighlight(text: string): boolean {
|
||||
return /[a-zA-Z0-9_/"'`]/.test(text);
|
||||
}
|
||||
|
||||
function tokenizeJavaScript(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Single-line comments
|
||||
if (text.slice(i, i + 2) === '//') {
|
||||
let start = i;
|
||||
while (i < text.length && text[i] !== '\n') i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multi-line comments
|
||||
if (text.slice(i, i + 2) === '/*') {
|
||||
let start = i;
|
||||
i += 2;
|
||||
while (i < text.length && text.slice(i, i + 2) !== '*/') i++;
|
||||
if (i < text.length) i += 2;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
|
||||
const quote = text[i];
|
||||
let start = i++;
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === quote && text[i - 1] !== '\\') {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (/\d/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (/[a-zA-Z_$]/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++;
|
||||
const word = text.slice(start, i);
|
||||
|
||||
if (JS_KEYWORDS.has(word)) {
|
||||
tokens.push({ text: word, type: 'keyword' });
|
||||
} else if (i < text.length && text[i] === '(') {
|
||||
tokens.push({ text: word, type: 'function' });
|
||||
} else {
|
||||
tokens.push({ text: word, type: 'text' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other characters
|
||||
let start = i;
|
||||
while (i < text.length && !/[a-zA-Z0-9_$/"'`]/.test(text[i])) i++;
|
||||
if (start < i) {
|
||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default JavaScriptHighlightPlugin;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
||||
|
||||
const PYTHON_KEYWORDS = new Set([
|
||||
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
|
||||
'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
|
||||
'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while',
|
||||
'with', 'yield'
|
||||
]);
|
||||
|
||||
const Python3HighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (textNode.hasFormat('code')) return;
|
||||
if (!needsHighlight(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const selection = $getSelection();
|
||||
let selectionOffset = null;
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchor = selection.anchor;
|
||||
if (anchor.getNode() === textNode) {
|
||||
selectionOffset = anchor.offset;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = tokenizePython(text);
|
||||
if (tokens.length <= 1) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
newNode.toggleFormat('code');
|
||||
|
||||
switch (token.type) {
|
||||
case 'keyword':
|
||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
||||
break;
|
||||
case 'string':
|
||||
newNode.setStyle('color: #032f62;');
|
||||
break;
|
||||
case 'comment':
|
||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||
break;
|
||||
case 'function':
|
||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
|
||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
||||
let currentOffset = 0;
|
||||
for (const node of newNodes) {
|
||||
const nodeLength = node.getTextContent().length;
|
||||
if (currentOffset + nodeLength >= selectionOffset) {
|
||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
||||
break;
|
||||
}
|
||||
currentOffset += nodeLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function needsHighlight(text: string): boolean {
|
||||
return /[a-zA-Z0-9_#"']/.test(text);
|
||||
}
|
||||
|
||||
function tokenizePython(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Comments
|
||||
if (text[i] === '#') {
|
||||
let start = i;
|
||||
while (i < text.length && text[i] !== '\n') i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (text[i] === '"' || text[i] === "'") {
|
||||
const quote = text[i];
|
||||
let start = i++;
|
||||
const isTriple = text.slice(start, start + 3) === quote.repeat(3);
|
||||
if (isTriple) i += 2;
|
||||
|
||||
while (i < text.length) {
|
||||
if (isTriple && text.slice(i, i + 3) === quote.repeat(3)) {
|
||||
i += 3;
|
||||
break;
|
||||
} else if (!isTriple && text[i] === quote && text[i - 1] !== '\\') {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (/\d/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (/[a-zA-Z_]/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++;
|
||||
const word = text.slice(start, i);
|
||||
|
||||
if (PYTHON_KEYWORDS.has(word)) {
|
||||
tokens.push({ text: word, type: 'keyword' });
|
||||
} else if (i < text.length && text[i] === '(') {
|
||||
tokens.push({ text: word, type: 'function' });
|
||||
} else {
|
||||
tokens.push({ text: word, type: 'text' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other characters
|
||||
let start = i;
|
||||
while (i < text.length && !/[a-zA-Z0-9_#"']/.test(text[i])) i++;
|
||||
if (start < i) {
|
||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default Python3HighlightPlugin;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider, Space, Select } from 'antd';
|
||||
|
||||
interface OutputListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]',
|
||||
'object'
|
||||
]
|
||||
const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add({ type: 'string' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-45!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'type']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-22!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default OutputList;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd'
|
||||
import { Node } from '@antv/x6'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MappingList from '../MappingList'
|
||||
import Editor from '../../Editor'
|
||||
import OutputList from './OutputList'
|
||||
|
||||
interface MappingItem {
|
||||
name?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface CodeExecutionProps {
|
||||
options: Suggestion[]
|
||||
selectedNode: Node
|
||||
}
|
||||
|
||||
const codeTemplate = {
|
||||
python3: `def main(arg1: str, arg2: str):
|
||||
return {
|
||||
"result": arg1 + arg2,
|
||||
}`,
|
||||
javascript: `function main({arg1, arg2}) {
|
||||
return {
|
||||
result: arg1 + arg2
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const values = Form.useWatch([], form) || {}
|
||||
|
||||
const handleRefresh = () => {
|
||||
const code = form.getFieldValue('code') || ''
|
||||
const language = form.getFieldValue('language') || 'javascript'
|
||||
const currentInput = form.getFieldValue('input_variables') || []
|
||||
|
||||
// Get input_variables names to replace in code
|
||||
const inputNames = currentInput.map((item: MappingItem) => item.name).filter(Boolean).join(', ')
|
||||
|
||||
let newTemplate = code
|
||||
|
||||
if (language === 'javascript') {
|
||||
// Replace function parameters: function name({arg1, arg2}) or function name(arg1, arg2)
|
||||
newTemplate = code.replace(
|
||||
/function(\s+\w+\s*\(\s*)(\{?)([^})]*)\}?(\s*\))/,
|
||||
(_match: string, prefix: string, brace: string, _params: string, suffix: string) => {
|
||||
return `function${prefix}${brace}${inputNames}${brace ? '}' : ''}${suffix}`
|
||||
}
|
||||
)
|
||||
} else if (language === 'python3') {
|
||||
// Replace Python function parameters: def name(arg1, arg2):
|
||||
newTemplate = code.replace(
|
||||
/def(\s+\w+\s*\()([^)]*)(\))/,
|
||||
(_match: string, prefix: string, _params: string, suffix: string) => {
|
||||
return `def${prefix}${inputNames}${suffix}`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
form.setFieldValue('code', newTemplate)
|
||||
}
|
||||
const handleChangeLanguage = (value: string) => {
|
||||
form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate])
|
||||
form.setFieldsValue({
|
||||
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
||||
code: codeTemplate[value as keyof typeof codeTemplate]
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="input_variables" noStyle>
|
||||
<MappingList
|
||||
label={t('workflow.config.code.input_variables')}
|
||||
name="input_variables"
|
||||
options={options}
|
||||
valueKey="variable"
|
||||
extra={<Tooltip title={t('workflow.config.code.refreshTip')}>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
className="rb:py-0! rb:px-1.5! rb:text-[12px]! rb:group"
|
||||
size="small"
|
||||
>
|
||||
<div onClick={handleRefresh} className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"></div>
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="language" noStyle>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'PYTHON3', value: 'python3' },
|
||||
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:font-medium!"
|
||||
onChange={handleChangeLanguage}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="code" noStyle>
|
||||
<Editor size="small" language={values.language} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider />
|
||||
<Form.Item name="output_variables" noStyle>
|
||||
<OutputList
|
||||
label={t('workflow.config.code.output_variables')}
|
||||
name="output_variables"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeExecution
|
||||
@@ -144,6 +144,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
@@ -155,7 +156,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={true} />
|
||||
<AddButton block={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
filterBooleanType={true}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="mapping" noStyle>
|
||||
<MappingList name="mapping" options={options} />
|
||||
<MappingList label={t('workflow.config.jinja-render.mapping')} name="mapping" options={options} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="template">
|
||||
@@ -184,7 +184,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
title={t('workflow.config.jinja-render.template')}
|
||||
isArray={false}
|
||||
parentName="template"
|
||||
enableJinja2={true}
|
||||
language="jinja2"
|
||||
options={templateOptions}
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
|
||||
@@ -66,7 +66,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
useEffect(() => {
|
||||
if (values?.retrieve_type) {
|
||||
const fieldsToReset = Object.keys(values).filter(key =>
|
||||
key !== 'kb_id' && key !== 'retrieve_type'
|
||||
key !== 'kb_id' && key !== 'retrieve_type' && key !== 'top_k'
|
||||
) as (keyof KnowledgeConfigForm)[];
|
||||
form.resetFields(fieldsToReset);
|
||||
}
|
||||
@@ -108,6 +108,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
label: t(`application.${key}`),
|
||||
value: key,
|
||||
}))}
|
||||
// onChange={handleChange}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* Top K */}
|
||||
@@ -116,13 +117,12 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
label={t('application.top_k')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
extra={t('application.top_k_desc')}
|
||||
initialValue={5}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={20}
|
||||
onChange={(value) => form.setFieldValue('top_k', value)}
|
||||
// onChange={(value) => form.setFieldValue('top_k', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 语义相似度阈值 similarity_threshold */}
|
||||
|
||||
@@ -98,7 +98,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider } from 'antd';
|
||||
import { Button, Form, Input, Divider, Space } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface MappingListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
extra?: ReactNode;
|
||||
valueKey?: string;
|
||||
}
|
||||
const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueKey = 'value' }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
@@ -17,16 +20,19 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.jinja-render.mapping')}
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
@@ -43,7 +49,7 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
name={[name, valueKey]}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { type FC, type ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import Editor from '../Editor'
|
||||
import Editor, { type LexicalEditorProps } from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string;
|
||||
options?: Suggestion[];
|
||||
title?: string | ReactNode;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
isArray?: boolean;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
language?: LexicalEditorProps['language'];
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
}
|
||||
@@ -29,8 +29,8 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
options = [],
|
||||
language,
|
||||
size = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -81,13 +81,15 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
{typeof title === 'string'
|
||||
? <div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
: title}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -132,7 +134,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ const processNodeVariables = (
|
||||
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case 'var-aggregator':
|
||||
if (config.group.defaultValue) {
|
||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
||||
@@ -106,6 +106,11 @@ const processNodeVariables = (
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
case 'code':
|
||||
(config.output_variables.defaultValue || []).forEach((cv: any) => {
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ import MemoryConfig from './MemoryConfig'
|
||||
import VariableList from './VariableList'
|
||||
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
||||
import styles from './properties.module.css'
|
||||
import Editor from "../Editor";
|
||||
import Editor, { type LexicalEditorProps } from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
import JinjaRender from './JinjaRender'
|
||||
import CodeExecution from './CodeExecution'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -364,6 +365,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'code'
|
||||
? <CodeExecution
|
||||
selectedNode={selectedNode}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
/>
|
||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||
const config = configs[key] || {}
|
||||
|
||||
@@ -438,7 +444,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
language={config.language as LexicalEditorProps['language']}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
titleVariant={config.titleVariant}
|
||||
size="small"
|
||||
|
||||
@@ -87,4 +87,7 @@
|
||||
.properties :global(.ant-select .ant-select-arrow) {
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
.properties :global(.ant-input-sm) {
|
||||
padding: 3.6px 7px;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -284,7 +284,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
input: {
|
||||
type: 'variableList',
|
||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop'],
|
||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code'],
|
||||
filterVariableNames: ['message']
|
||||
},
|
||||
parallel: {
|
||||
@@ -431,7 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "code_execution", icon: codeExecutionIcon },
|
||||
// { type: "code", icon: codeExecutionIcon,
|
||||
// config: {
|
||||
// input_variables: {
|
||||
// type: 'inputList',
|
||||
// defaultValue: [{ name: 'arg1' }, { name: 'arg2' }]
|
||||
// },
|
||||
// language: {
|
||||
// type: 'select',
|
||||
// defaultValue: 'python3'
|
||||
// },
|
||||
// code: {
|
||||
// type: 'messageEditor',
|
||||
// isArray: false,
|
||||
// language: ['python3', 'javascript'],
|
||||
// titleVariant: 'borderless',
|
||||
// defaultValue: `def main(arg1: str, arg2: str):
|
||||
// return {
|
||||
// "result": arg1 + arg2,
|
||||
// }`
|
||||
// },
|
||||
// output_variables: {
|
||||
// type: 'outputList',
|
||||
// defaultValue: [{name: 'result', type: 'string'}]
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||
config: {
|
||||
mapping: {
|
||||
@@ -441,12 +466,12 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
template: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
language: 'jinja2',
|
||||
titleVariant: 'borderless',
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -109,6 +109,12 @@ export const useWorkflowGraph = ({
|
||||
: group_variables
|
||||
} else if (type === 'http-request' && (key === 'headers' || key === 'params') && config[key] && typeof config[key] === 'object' && !Array.isArray(config[key]) && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
||||
} else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
try {
|
||||
nodeLibraryConfig.config[key].defaultValue = atob(config[key] as string)
|
||||
} catch {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
} else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
@@ -588,77 +594,6 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
const nodeChangePosition = ({ node, options }: { node: Node; options: { skipParentHandler?: boolean } }) => {
|
||||
const embedPadding = 50; // Define the embed padding constant
|
||||
if (options.skipParentHandler) {
|
||||
return
|
||||
}
|
||||
|
||||
const children = node.getChildren()
|
||||
if (children && children.length) {
|
||||
node.prop('originPosition', node.getPosition())
|
||||
}
|
||||
|
||||
const parent = node.getParent()
|
||||
if (parent && parent.isNode()) {
|
||||
let originSize = parent.prop('originSize')
|
||||
if (originSize == null) {
|
||||
originSize = parent.getSize()
|
||||
parent.prop('originSize', originSize)
|
||||
}
|
||||
|
||||
let originPosition = parent.prop('originPosition')
|
||||
if (originPosition == null) {
|
||||
originPosition = parent.getPosition()
|
||||
parent.prop('originPosition', originPosition)
|
||||
}
|
||||
|
||||
let x = originPosition.x
|
||||
let y = originPosition.y
|
||||
let cornerX = originPosition.x + originSize.width
|
||||
let cornerY = originPosition.y + originSize.height
|
||||
let hasChange = false
|
||||
|
||||
const children = parent.getChildren()
|
||||
if (children) {
|
||||
children.forEach((child) => {
|
||||
const bbox = child.getBBox().inflate(embedPadding)
|
||||
const corner = bbox.getCorner()
|
||||
|
||||
if (bbox.x < x) {
|
||||
x = bbox.x
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (bbox.y < y) {
|
||||
y = bbox.y
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (corner.x > cornerX) {
|
||||
cornerX = corner.x
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (corner.y > cornerY) {
|
||||
cornerY = corner.y
|
||||
hasChange = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasChange) {
|
||||
parent.prop(
|
||||
{
|
||||
position: { x, y },
|
||||
size: { width: cornerX - x, height: cornerY - y },
|
||||
},
|
||||
{ skipParentHandler: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
@@ -912,7 +847,13 @@ export const useWorkflowGraph = ({
|
||||
|
||||
if (data.config) {
|
||||
Object.keys(data.config).forEach(key => {
|
||||
if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
if (data.type === 'code' && key === 'code' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const code = data.config[key].defaultValue || ''
|
||||
itemConfig = {
|
||||
...itemConfig,
|
||||
code: btoa(code || '')
|
||||
}
|
||||
} else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const { messages, ...rest } = data.config[key].defaultValue
|
||||
let memoryMessage = { role: 'USER', content: data.config[key].defaultValue.messages }
|
||||
itemConfig = {
|
||||
|
||||