Release/v0.2.3 (#355)
* feat(web): add PageEmpty component
* feat(web): add PageTabs component
* feat(web): add PageEmpty component
* feat(web): add PageTabs component
* feat(prompt): add history tracking for prompt releases
* feat(web): add prompt menu
* refactor: The PageScrollList component supports two generic parameters
* feat(web): BodyWrapper compoent update PageLoading
* feat(web): add Ontology menu
* feat(web): memory management add scene
* feat(tasks): add celery task configuration for periodic jobs
- Add ignore_result=True to prevent storing results for periodic tasks
- Set max_retries=0 to skip failed periodic tasks without retry attempts
- Configure acks_late=False for immediate acknowledgment in beat tasks
- Add time_limit and soft_time_limit to regenerate_memory_cache task (3600s/3300s)
- Add time_limit and soft_time_limit to workspace_reflection_task (300s/240s)
- Add time_limit and soft_time_limit to run_forgetting_cycle_task (7200s/7000s)
- Improve task reliability and resource management for scheduled jobs
* feat(sandbox): add Node.js code execution support to sandbox
* Release/v0.2.2 (#260)
* [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: Mark <zhuwenhui5566@163.com>
Co-authored-by: zhaoying <yzhao96@best-inc.com>
Co-authored-by: yingzhao <zhaoyingyz@126.com>
Co-authored-by: Timebomb2018 <18868801967@163.com>
* Feature/ontology class clean (#249)
* [add] Complete ontology engineering feature implementation
* [add] Add ontology feature integration and validation utilities
* [add] Add OWL validator and validation utilities
* [fix] Add missing render_ontology_extraction_prompt function
* [fix]Add dependencies, fix functionality
* [add] migration script
* feat(celery): add dedicated periodic tasks worker and queue (#261)
* fix(web): conflict resolve
* Fix/v022 bug (#263)
* [fix]Fix the issue of inconsistent language in explicit and episodic memory.
* [fix]Fix the issue of inconsistent language in explicit and episodic memory.
* [add]Add scene_id
* [fix]Based on the AI review to fix the code
* Fix/develop memory reflex (#265)
* 遗漏的历史映射
* 遗漏的历史映射
* 反思后台报错处理
* [add] migration script
* fix: chat conversation_id add node_start
* feat(web): show code node
* fix(web): Restructure the CustomSelect component, repair the interface that is called multiple times when the form is updated
* feat(web): RadioGroupCard support block mode
* feat(web): create space add icon
* feat(app and model): token consumption statistics
* Add/develop memory (#264)
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 遗漏的历史映射
* 新增长期记忆功能
* 新增长期记忆功能
* 新增长期记忆功能
* 知识库检索多余字段
* 长期
* feat(app and model): token consumption statistics of the cluster
* memory_BUG_fix
* fix(web): prompt history remove pageLoading
* fix(prompt): remove hard-coded import of prompt file paths (#279)
* Fix/develop memory bug (#274)
* 遗漏的历史映射
* 遗漏的历史映射
* fix_timeline_memories
* fix(web): update retrieve_type key
* Fix/develop memory bug (#276)
* 遗漏的历史映射
* 遗漏的历史映射
* fix_timeline_memories
* fix_timeline_memories
* write_gragp/bug_fix
* write_gragp/bug_fix
* write_gragp/bug_fix
* chore(celery): disable periodic task scheduling
* fix(prompt): remove hard-coded import of prompt file paths
---------
Co-authored-by: lixinyue11 <94037597+lixinyue11@users.noreply.github.com>
Co-authored-by: zhaoying <yzhao96@best-inc.com>
Co-authored-by: yingzhao <zhaoyingyz@126.com>
Co-authored-by: Ke Sun <kesun5@illinois.edu>
* fix(web): remove delete confirm content
* refactor(workflow): relocate template directory into workflow
* feat(memory): add long-term storage task routing and batching
* fix(web): PageScrollList loading update
* fix(web): PageScrollList loading update
* Ontology v1 bug (#291)
* [changes]Add 'id' as the secondary sorting key, and 'scene_id' now returns a UUID object
* [fix]Fix the "end_user" return to be sorted by update time.
* [fix]Set the default values of the memory configuration model based on the spatial model.
* [fix]Remove the entity extraction check combination model, read the configuration list, and add the return of scene_id
* [fix]Fix the "end_user" return to be sorted by update time.
* [fix]
* fix(memory): add Redis session validation
- Add macOS fork() safety configuration in celery_app.py to prevent initialization issues
- Add null/False checks for Redis session queries in term_memory_save to handle missing sessions gracefully
- Add null/False checks in memory_long_term_storage to prevent processing empty Redis results
- Add null/False checks in aggregate_judgment before format_parsing to avoid errors on missing data
- Initialize redis_messages variable in window_dialogue for consistency
- Add debug logging when no existing session found in Redis for better troubleshooting
- Add TODO comments for magic numbers (scope=6, time=5) to be extracted as constants
- Improve error handling when Redis returns False or empty results instead of crashing
* fix(web): PageScrollList style update
* fix(workflow): fix argument passing in code execution nodes
* fix(web): prompt add disabled
* fix(web): space icon required
* feat(app): modify the key of the token
* fix(fix the key of the app's token):
* fix(workflow): switch code input encoding to base64+URL encoding
* [add]The main project adds multi-API Key load balancing.
* [changes]Attribute security access, secure numerical conversion, unified use of local variables
* fix(web): save add session update
* fix(web): language editor support paste
* [changes]Active status filtering logic, API Key selection strategy
* memory_BUG
* memory_BUG_long_term
* [changes]
* memory_BUG_long_term
* memory_BUG_long_term
* Fix/release memory bug (#306)
* memory_BUG_fix
* memory_BUG
* memory_BUG_long_term
* memory_BUG_long_term
* memory_BUG_long_term
* knowledge_retrieval/bug/fix
* knowledge_retrieval/bug/fix
* knowledge_retrieval/bug/fix
* [fix]1.The "read_all_config" interface returns "scene_name";2.Memory configuration for lightweight query ontology scenarios
* fix(web): replace code editor
* [changes]Modify the description of the time for the recent event
* [changes]Modify the code based on the AI review
* feat(web): update memory config ontology api
* fix(web): ui update
* knowledge_retrieval/bug/fix
* knowledge_retrieval/bug/fix
* knowledge_retrieval/bug/fix
* feat(workflow): add token usage statistics for question classifier and parameter extraction
* feat(web): move prompt menu
* Multiple independent transactions - single transaction
* Multiple independent transactions - single transaction
* Multiple independent transactions - single transaction
* Multiple independent transactions - single transaction
* Write Missing None (#321)
* Write Missing None
* Write Missing None
* Write Missing None
* Apply suggestion from @sourcery-ai[bot]
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Write Missing None
---------
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Fix/release memory bug (#324)
* Write Missing None
* Write Missing None
* Write Missing None
* Apply suggestion from @sourcery-ai[bot]
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Write Missing None
* redis update
* redis update
* redis update
* redis update
---------
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Fix/writer memory bug (#326)
* [fix]Fix the bug
* [fix]Fix the bug
* [fix]Correct the direction indication.
* fix(web): markdown table ui update
* Fix/release memory bug (#332)
* Write Missing None
* Write Missing None
* Write Missing None
* Apply suggestion from @sourcery-ai[bot]
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Write Missing None
* redis update
* redis update
* redis update
* redis update
* writer_dup_bug/fix
---------
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Fix/fact summary (#333)
* [fix]Disable the contents related to fact_summary
* [fix]Disable the contents related to fact_summary
* [fix]Modify the code based on the AI review
* Fix/release memory bug (#335)
* Write Missing None
* Write Missing None
* Write Missing None
* Apply suggestion from @sourcery-ai[bot]
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Write Missing None
* redis update
* redis update
* redis update
* redis update
* writer_dup_bug/fix
* writer_graph_bug/fix
* writer_graph_bug/fix
---------
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
* Revert "feat(web): move prompt menu"
This reverts commit 9e6e8f50f8.
* fix(web): ui update
* fix(web): update text
* fix(web): ui update
* fix(model): change the "vl" model type of dashscope to "chat"
* fix(model): change the "vl" model type of dashscope to "chat"
---------
Co-authored-by: zhaoying <yzhao96@best-inc.com>
Co-authored-by: Eternity <1533512157@qq.com>
Co-authored-by: Mark <zhuwenhui5566@163.com>
Co-authored-by: yingzhao <zhaoyingyz@126.com>
Co-authored-by: Timebomb2018 <18868801967@163.com>
Co-authored-by: 乐力齐 <162269739+lanceyq@users.noreply.github.com>
Co-authored-by: lixinyue11 <94037597+lixinyue11@users.noreply.github.com>
Co-authored-by: lixinyue <2569494688@qq.com>
Co-authored-by: Eternity <61316157+myhMARS@users.noreply.github.com>
Co-authored-by: lanceyq <1982376970@qq.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,14 @@
|
||||
"@antv/layout": "^1.2.14-beta.8",
|
||||
"@antv/x6": "^3.0.1",
|
||||
"@antv/x6-react-shape": "^3.0.1",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.12",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -25,6 +33,7 @@
|
||||
"antd": "^5.27.4",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.18",
|
||||
@@ -55,6 +64,7 @@
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.6.0",
|
||||
|
||||
40
web/src/api/ontology.ts
Normal file
40
web/src/api/ontology.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData } from '@/views/Ontology/types'
|
||||
|
||||
// Scene list
|
||||
export const getOntologyScenesSimpleUrl = '/memory/ontology/scenes/simple'
|
||||
export const getOntologyScenesUrl = '/memory/ontology/scenes'
|
||||
export const getOntologyScenesList = (data: Query) => {
|
||||
return request.get(getOntologyScenesUrl, data)
|
||||
}
|
||||
|
||||
// Create scene
|
||||
export const createOntologyScene = (data: OntologyModalData) => {
|
||||
return request.post('/memory/ontology/scene', data)
|
||||
}
|
||||
// Update scene
|
||||
export const updateOntologyScene = (scene_id: string, data: OntologyModalData) => {
|
||||
return request.put(`/memory/ontology/scene/${scene_id}`, data)
|
||||
}
|
||||
// Delete scene
|
||||
export const deleteOntologyScene = (scene_id: string) => {
|
||||
return request.delete(`/memory/ontology/scene/${scene_id}`)
|
||||
}
|
||||
|
||||
// Get class list
|
||||
export const getOntologyclassesUrl = '/memory/ontology/classes'
|
||||
export const getOntologyClassList = (data: { scene_id: string; class_name?: string; }) => {
|
||||
return request.get(getOntologyclassesUrl, data)
|
||||
}
|
||||
// Extract ontology types
|
||||
export const extractOntologyTypes = (data: OntologyClassExtractModalData) => {
|
||||
return request.post('/memory/ontology/extract', data)
|
||||
}
|
||||
// Create ontology class
|
||||
export const createOntologyClass = (data: OntologyClassModalData) => {
|
||||
return request.post('/memory/ontology/class', data)
|
||||
}
|
||||
// Delete ontology class
|
||||
export const deleteOntologyClass = (class_id: string) => {
|
||||
return request.delete(`/memory/ontology/class/${class_id}`)
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { AiPromptForm } from '@/views/ApplicationConfig/types'
|
||||
import type { PromptReleaseData } from '@/views/Prompt/types'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
|
||||
// Create session
|
||||
export const createPromptSessions = () => {
|
||||
return request.post(`/prompt/sessions`)
|
||||
}
|
||||
export const getPrompt = (session_id: string) => {
|
||||
return request.get(`/prompt/sessions/${session_id}`)
|
||||
}
|
||||
// Get prompt optimization
|
||||
export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage)
|
||||
}
|
||||
// Prompt release list
|
||||
export const getPromptReleaseListUrl = '/prompt/releases/list'
|
||||
export const getPromptReleaseList = () => {
|
||||
return request.get(getPromptReleaseListUrl)
|
||||
}
|
||||
// Save prompt
|
||||
export const savePrompt = (data: PromptReleaseData) => {
|
||||
return request.post('/prompt/releases', data)
|
||||
}
|
||||
// Delete prompt
|
||||
export const deletePrompt = (prompt_id: string) => {
|
||||
return request.delete(`/prompt/releases/${prompt_id}`)
|
||||
}
|
||||
11
web/src/assets/images/menu/ontology.svg
Normal file
11
web/src/assets/images/menu/ontology.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>本体管理备份</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-54, -600)" fill="#5B6167" fill-rule="nonzero">
|
||||
<g id="本体管理备份" transform="translate(54, 600)">
|
||||
<path d="M12.9051096,10.4106225 C12.3694196,11.4980012 11.4899287,12.3774986 10.4105534,12.9131925 C10.2426506,14.0965163 9.22723825,15 8.00394627,15 C6.77265892,15 5.75724661,14.0885208 5.59733917,12.905197 C4.50996851,12.3695032 3.63047754,11.4900057 3.09478759,10.4106225 C1.91147246,10.2427185 1,9.22729869 1,8.00399772 C1,6.77270132 1.91147246,5.76527699 3.09478759,5.59737293 C3.63047754,4.50999429 4.50996851,3.63049686 5.59733917,3.09480297 C5.76524199,1.91147915 6.77265892,1 8.00394627,1 C9.22723826,1 10.2426506,1.90348372 10.4105534,3.08680754 C11.4899287,3.62250143 12.3694196,4.50199886 12.9051096,5.5893775 C14.0884247,5.75728156 14.9999489,6.76470589 14.9999489,7.99600228 C15.0078925,9.23529411 14.0884247,10.2507139 12.9051096,10.4106225 Z M8.00394627,13.7846945 C8.67555756,13.7846945 9.21924289,13.2410052 9.21924289,12.5693889 C9.21924289,11.8977727 8.67555756,11.3540834 8.00394627,11.3540834 C7.33233498,11.3540834 6.78864966,11.8977727 6.78864966,12.5693889 C6.78864966,13.2410052 7.33233498,13.7846945 8.00394627,13.7846945 Z M3.43858861,6.78069676 C2.76697732,6.78069676 2.22329199,7.32438608 2.22329199,7.99600228 C2.22329199,8.66761849 2.76697732,9.21130783 3.43858861,9.21130783 C4.11019989,9.21130783 4.65388521,8.67561394 4.65388521,8.00399772 C4.65388521,7.3323815 4.11019988,6.78069676 3.43858861,6.78069676 Z M8.00394627,2.21530554 C7.33233498,2.21530554 6.78864966,2.75899486 6.78864966,3.43860652 C6.78864966,4.11821817 7.33233498,4.65391206 8.00394627,4.65391206 C8.67555756,4.65391206 9.21924289,4.11022274 9.21924289,3.43860652 C9.21924289,2.7669903 8.67555756,2.21530554 8.00394627,2.21530554 L8.00394627,2.21530554 Z M10.2506459,4.38206739 C9.8828588,5.25356939 9.0113632,5.86921759 8.00394627,5.86921759 C6.99652934,5.86921759 6.12503374,5.25356939 5.75724661,4.38206739 C5.19757054,4.72587094 4.72584357,5.19760137 4.38204255,5.75728156 C5.25353815,6.12507139 5.86918182,6.98857795 5.86918182,8.00399772 C5.86918182,9.01142205 5.25353815,9.87492861 4.38204255,10.2507139 C4.72584357,10.8103941 5.19757054,11.2821245 5.75724661,11.625928 C6.12503374,10.754426 6.98853397,10.1387778 8.00394627,10.1387778 C9.0113632,10.1387778 9.87486343,10.754426 10.2506459,11.625928 C10.8023266,11.2821245 11.2740536,10.8103941 11.62585,10.2507139 C10.7543544,9.88292405 10.1387107,9.01941748 10.1387107,8.01199315 C10.1387107,7.00456882 10.7543544,6.14106226 11.62585,5.76527699 C11.2740536,5.19760138 10.8023266,4.73386637 10.2506459,4.38206739 Z M12.569304,6.78069676 C11.8976927,6.78069676 11.3540073,7.32438608 11.3540073,7.99600228 C11.3540073,8.66761849 11.8976927,9.21130783 12.569304,9.21130783 C13.2409152,9.21130783 13.7846006,8.6676185 13.7846006,7.99600228 C13.7846006,7.32438606 13.2409152,6.78069676 12.569304,6.78069676 L12.569304,6.78069676 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
11
web/src/assets/images/menu/ontology_active.svg
Normal file
11
web/src/assets/images/menu/ontology_active.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>本体管理</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-28, -600)" fill="#212332" fill-rule="nonzero">
|
||||
<g id="本体管理" transform="translate(28, 600)">
|
||||
<path d="M12.9051096,10.4106225 C12.3694196,11.4980012 11.4899287,12.3774986 10.4105534,12.9131925 C10.2426506,14.0965163 9.22723825,15 8.00394627,15 C6.77265892,15 5.75724661,14.0885208 5.59733917,12.905197 C4.50996851,12.3695032 3.63047754,11.4900057 3.09478759,10.4106225 C1.91147246,10.2427185 1,9.22729869 1,8.00399772 C1,6.77270132 1.91147246,5.76527699 3.09478759,5.59737293 C3.63047754,4.50999429 4.50996851,3.63049686 5.59733917,3.09480297 C5.76524199,1.91147915 6.77265892,1 8.00394627,1 C9.22723826,1 10.2426506,1.90348372 10.4105534,3.08680754 C11.4899287,3.62250143 12.3694196,4.50199886 12.9051096,5.5893775 C14.0884247,5.75728156 14.9999489,6.76470589 14.9999489,7.99600228 C15.0078925,9.23529411 14.0884247,10.2507139 12.9051096,10.4106225 Z M8.00394627,13.7846945 C8.67555756,13.7846945 9.21924289,13.2410052 9.21924289,12.5693889 C9.21924289,11.8977727 8.67555756,11.3540834 8.00394627,11.3540834 C7.33233498,11.3540834 6.78864966,11.8977727 6.78864966,12.5693889 C6.78864966,13.2410052 7.33233498,13.7846945 8.00394627,13.7846945 Z M3.43858861,6.78069676 C2.76697732,6.78069676 2.22329199,7.32438608 2.22329199,7.99600228 C2.22329199,8.66761849 2.76697732,9.21130783 3.43858861,9.21130783 C4.11019989,9.21130783 4.65388521,8.67561394 4.65388521,8.00399772 C4.65388521,7.3323815 4.11019988,6.78069676 3.43858861,6.78069676 Z M8.00394627,2.21530554 C7.33233498,2.21530554 6.78864966,2.75899486 6.78864966,3.43860652 C6.78864966,4.11821817 7.33233498,4.65391206 8.00394627,4.65391206 C8.67555756,4.65391206 9.21924289,4.11022274 9.21924289,3.43860652 C9.21924289,2.7669903 8.67555756,2.21530554 8.00394627,2.21530554 L8.00394627,2.21530554 Z M10.2506459,4.38206739 C9.8828588,5.25356939 9.0113632,5.86921759 8.00394627,5.86921759 C6.99652934,5.86921759 6.12503374,5.25356939 5.75724661,4.38206739 C5.19757054,4.72587094 4.72584357,5.19760137 4.38204255,5.75728156 C5.25353815,6.12507139 5.86918182,6.98857795 5.86918182,8.00399772 C5.86918182,9.01142205 5.25353815,9.87492861 4.38204255,10.2507139 C4.72584357,10.8103941 5.19757054,11.2821245 5.75724661,11.625928 C6.12503374,10.754426 6.98853397,10.1387778 8.00394627,10.1387778 C9.0113632,10.1387778 9.87486343,10.754426 10.2506459,11.625928 C10.8023266,11.2821245 11.2740536,10.8103941 11.62585,10.2507139 C10.7543544,9.88292405 10.1387107,9.01941748 10.1387107,8.01199315 C10.1387107,7.00456882 10.7543544,6.14106226 11.62585,5.76527699 C11.2740536,5.19760138 10.8023266,4.73386637 10.2506459,4.38206739 Z M12.569304,6.78069676 C11.8976927,6.78069676 11.3540073,7.32438608 11.3540073,7.99600228 C11.3540073,8.66761849 11.8976927,9.21130783 12.569304,9.21130783 C13.2409152,9.21130783 13.7846006,8.6676185 13.7846006,7.99600228 C13.7846006,7.32438606 13.2409152,6.78069676 12.569304,6.78069676 L12.569304,6.78069676 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
15
web/src/assets/images/menu/prompt.svg
Normal file
15
web/src/assets/images/menu/prompt.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>提示词备份</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-54, -575)" stroke="#5B6167" stroke-width="1.1">
|
||||
<g id="提示词备份" transform="translate(54, 575)">
|
||||
<g id="编组-34" transform="translate(2.5, 2)">
|
||||
<path d="M3.96581416,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 L9.39685919,0 C10.2252863,-1.55431223e-15 10.8968592,0.671572875 10.8968592,1.5 L10.8968592,2.99293149 L10.8968592,2.99293149" id="路径"></path>
|
||||
<path d="M3.26905776,3.27272727 L7.62780143,3.27272727 M5.4484296,3.27272727 L5.4484296,7.63636364" id="形状结合"></path>
|
||||
<polygon id="路径-11" points="9.22121994 6.54545455 6.91984008 10.2384806 7.8543485 12 9.77860327 12 12 8.17112299"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
15
web/src/assets/images/menu/prompt_active.svg
Normal file
15
web/src/assets/images/menu/prompt_active.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>提示词</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-28, -575)" stroke="#212332" stroke-width="1.1">
|
||||
<g id="提示词" transform="translate(28, 575)">
|
||||
<g id="编组-34" transform="translate(2.5, 2)">
|
||||
<path d="M3.96581416,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 L9.39685919,0 C10.2252863,-1.55431223e-15 10.8968592,0.671572875 10.8968592,1.5 L10.8968592,2.99293149 L10.8968592,2.99293149" id="路径"></path>
|
||||
<path d="M3.26905776,3.27272727 L7.62780143,3.27272727 M5.4484296,3.27272727 L5.4484296,7.63636364" id="形状结合"></path>
|
||||
<polygon id="路径-11" points="9.22121994 6.54545455 6.91984008 10.2384806 7.8543485 12 9.77860327 12 12 8.17112299"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web/src/assets/images/space/neo4j.png
Normal file
BIN
web/src/assets/images/space/neo4j.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web/src/assets/images/space/rag.png
Normal file
BIN
web/src/assets/images/space/rag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
150
web/src/components/CodeMirrorEditor/index.tsx
Normal file
150
web/src/components/CodeMirrorEditor/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-04 17:20:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 17:20:52
|
||||
*/
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { cpp } from '@codemirror/lang-cpp';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
|
||||
/**
|
||||
* Props for the CodeMirrorEditor component
|
||||
* @property {string} value - The initial code content to display in the editor
|
||||
* @property {string} language - Programming language for syntax highlighting (python, python3, javascript, typescript, java, cpp, c, rust)
|
||||
* @property {function} onChange - Callback function triggered when editor content changes, receives the new code value
|
||||
* @property {string} theme - Editor theme, either 'light' or 'dark'
|
||||
* @property {boolean} readOnly - Whether the editor is read-only
|
||||
* @property {string} height - Custom height for the editor
|
||||
* @property {string} size - Predefined size preset: 'default' (120px min-height, 14px font) or 'small' (60px min-height, 12px font)
|
||||
*/
|
||||
interface CodeMirrorEditorProps {
|
||||
value?: string;
|
||||
language?: 'python' | 'python3' | 'javascript' | 'typescript' | 'java' | 'cpp' | 'c' | 'rust';
|
||||
onChange?: (value: string) => void;
|
||||
theme?: 'light' | 'dark';
|
||||
readOnly?: boolean;
|
||||
height?: string;
|
||||
size?: 'default' | 'small';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of language identifiers to their corresponding CodeMirror language extensions
|
||||
* Supports multiple programming languages with syntax highlighting
|
||||
*/
|
||||
const languageExtensions: Record<string, any> = {
|
||||
python: python(),
|
||||
python3: python(),
|
||||
javascript: javascript(),
|
||||
typescript: javascript({ typescript: true }),
|
||||
java: java(),
|
||||
cpp: cpp(),
|
||||
c: cpp(),
|
||||
rust: rust(),
|
||||
};
|
||||
|
||||
/**
|
||||
* CodeMirrorEditor - A React wrapper component for CodeMirror 6 editor
|
||||
* Provides a code editor with syntax highlighting, theme support, and customizable sizing
|
||||
* Used in workflow code execution nodes for editing Python and JavaScript code
|
||||
*/
|
||||
const CodeMirrorEditor = ({
|
||||
value = '',
|
||||
language = 'javascript',
|
||||
onChange,
|
||||
theme = 'light',
|
||||
readOnly = false,
|
||||
size,
|
||||
}: CodeMirrorEditorProps) => {
|
||||
// Reference to the DOM element that will contain the editor
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
// Reference to the CodeMirror EditorView instance
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
|
||||
/**
|
||||
* Initialize CodeMirror editor when component mounts or when language/theme/readOnly changes
|
||||
* Sets up extensions for syntax highlighting, change listeners, and theme
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
// Get the appropriate language extension, fallback to JavaScript if not found
|
||||
const langExtension = languageExtensions[language] || languageExtensions.javascript;
|
||||
|
||||
// Configure editor extensions
|
||||
const extensions = [
|
||||
basicSetup, // Basic editor features (line numbers, bracket matching, etc.)
|
||||
langExtension, // Language-specific syntax highlighting
|
||||
// Listen for document changes and trigger onChange callback
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChange) {
|
||||
onChange(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorState.readOnly.of(readOnly), // Set read-only mode
|
||||
];
|
||||
|
||||
// Apply dark theme if specified
|
||||
if (theme === 'dark') {
|
||||
extensions.push(oneDark);
|
||||
}
|
||||
|
||||
// Create editor state with initial value and extensions
|
||||
const state = EditorState.create({
|
||||
doc: value,
|
||||
extensions,
|
||||
});
|
||||
|
||||
// Create and mount the editor view
|
||||
viewRef.current = new EditorView({
|
||||
state,
|
||||
parent: editorRef.current,
|
||||
});
|
||||
|
||||
// Cleanup: destroy editor instance when component unmounts or dependencies change
|
||||
return () => {
|
||||
viewRef.current?.destroy();
|
||||
};
|
||||
}, [language, theme, readOnly]);
|
||||
|
||||
/**
|
||||
* Update editor content when the value prop changes externally
|
||||
* Only updates if the new value differs from current editor content
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||
viewRef.current.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// Calculate minimum height based on size prop: small (60px) or default (120px)
|
||||
const minHeight = useMemo(() => {
|
||||
return `${size === 'small' ? 60 : 120}px`
|
||||
}, [size])
|
||||
|
||||
// Calculate font size based on size prop: small (12px) or default (14px)
|
||||
const fontSize = useMemo(() => {
|
||||
return `${size === 'small' ? 12 : 14}px`
|
||||
}, [size])
|
||||
|
||||
// Calculate line height based on size prop: small (16px) or default (20px)
|
||||
const lineHeight = useMemo(() => {
|
||||
return `${size === 'small' ? 16 : 20}px`
|
||||
}, [size])
|
||||
|
||||
return <div ref={editorRef} style={{ minHeight, fontSize, lineHeight }} />;
|
||||
};
|
||||
|
||||
export default CodeMirrorEditor;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, type FC, type Key } from 'react';
|
||||
import { useEffect, useState, useMemo, type FC, type Key } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import type { SelectProps, DefaultOptionType } from 'antd/es/select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -47,13 +47,14 @@ const CustomSelect: FC<CustomSelectProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<OptionType[]>([]);
|
||||
const memoizedParams = useMemo(() => params, [JSON.stringify(params)]);
|
||||
|
||||
useEffect(() => {
|
||||
request.get<ApiResponse<OptionType>>(url, params).then((res) => {
|
||||
request.get<ApiResponse<OptionType>>(url, memoizedParams).then((res) => {
|
||||
const data = Array.isArray(res) ? res : res?.items || [];
|
||||
setOptions(data);
|
||||
});
|
||||
}, [url, params]);
|
||||
}, [url, memoizedParams]);
|
||||
|
||||
const displayOptions = format ? format(options) : options;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { Skeleton } from 'antd'
|
||||
import Empty from './index'
|
||||
import PageEmpty from './PageEmpty'
|
||||
import PageLoading from './PageLoading'
|
||||
|
||||
interface BodyWrapperProps {
|
||||
children: ReactNode
|
||||
@@ -9,10 +9,10 @@ interface BodyWrapperProps {
|
||||
}
|
||||
const BodyWrapper: FC<BodyWrapperProps> = ({ children, loading = false, empty }) => {
|
||||
if (loading) {
|
||||
return <Skeleton active />
|
||||
return <PageLoading />
|
||||
}
|
||||
if (!loading && empty) {
|
||||
return <Empty />
|
||||
return <PageEmpty />
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ interface RbMarkdownProps {
|
||||
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏)
|
||||
editable?: boolean; // 是否可编辑,默认为 false
|
||||
onContentChange?: (content: string) => void; // 内容变化回调
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const components = {
|
||||
@@ -50,7 +51,7 @@ const components = {
|
||||
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
|
||||
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
|
||||
button: ({ children }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
|
||||
table: ({ children, ...props }: any) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table>,
|
||||
table: ({ children, ...props }: any) => <div className="rb:overflow-x-auto rb:max-w-full"><table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table></div>,
|
||||
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
|
||||
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
|
||||
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
|
||||
@@ -98,6 +99,7 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
showHtmlComments = false,
|
||||
editable = false,
|
||||
onContentChange,
|
||||
className
|
||||
}) => {
|
||||
const [editContent, setEditContent] = useState(content)
|
||||
const textareaRef = useRef<any>(null)
|
||||
@@ -162,7 +164,7 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
|
||||
// 预览模式
|
||||
return (
|
||||
<div className="rb:relative" onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<div className={`rb:relative ${className || ''}`} onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<style>{`
|
||||
.html-comment {
|
||||
color: #999;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { List, Skeleton} from 'antd';
|
||||
import { List } from 'antd';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { request } from '@/utils/request';
|
||||
import Empty from '@/components/Empty';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty'
|
||||
import PageLoading from '@/components/Empty/PageLoading'
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
interface ApiResponse {
|
||||
items?: Record<string, unknown>[];
|
||||
interface ApiResponse<T> {
|
||||
items?: T[];
|
||||
page: {
|
||||
page: number;
|
||||
pagesize: number;
|
||||
@@ -19,26 +20,27 @@ export interface PageScrollListRef {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
interface PageScrollListProps {
|
||||
interface PageScrollListProps<T, Q = Record<string, unknown>> {
|
||||
url: string;
|
||||
renderItem: (item: Record<string, unknown>) => React.ReactNode;
|
||||
query?: Record<string, unknown>;
|
||||
renderItem: (item: T) => React.ReactNode;
|
||||
query?: Q;
|
||||
column?: number;
|
||||
className?: string;
|
||||
needLoading?: boolean;
|
||||
}
|
||||
|
||||
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
||||
renderItem,
|
||||
query,
|
||||
url,
|
||||
column = 4,
|
||||
className = '',
|
||||
}, ref) => {
|
||||
needLoading = true,
|
||||
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh,
|
||||
}));
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -54,8 +56,8 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
...(query||{}),
|
||||
})
|
||||
.then((res) => {
|
||||
const response = res as ApiResponse;
|
||||
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : [];
|
||||
const response = res as ApiResponse<T>;
|
||||
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : [];
|
||||
if (flag) {
|
||||
setData(results);
|
||||
} else {
|
||||
@@ -104,9 +106,10 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
dataLength={data.length}
|
||||
next={loadMoreData}
|
||||
hasMore={hasMore}
|
||||
loader={<Skeleton active />}
|
||||
loader={loading && needLoading ? <PageLoading /> : false}
|
||||
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
|
||||
scrollableTarget="scrollableDiv"
|
||||
className='rb:h-full!'
|
||||
>
|
||||
{data.length > 0 ? (
|
||||
<List
|
||||
@@ -118,11 +121,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : !loading ? <Empty /> : null}
|
||||
) : !loading ? <PageEmpty /> : null}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}) as <T = Record<string, unknown>, Q = Record<string, unknown>>(props: PageScrollListProps<T, Q> & { ref?: React.Ref<PageScrollListRef> }) => React.ReactElement;
|
||||
|
||||
export default PageScrollList;
|
||||
@@ -16,6 +16,7 @@ interface RadioCardProps extends Omit<RadioGroupProps, 'onChange'> {
|
||||
onChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
|
||||
itemRender?: (option: RadioCardOption) => ReactNode;
|
||||
allowClear?: boolean;
|
||||
block?: boolean;
|
||||
}
|
||||
|
||||
const RadioGroupCard: FC<RadioCardProps> = ({
|
||||
@@ -24,7 +25,8 @@ const RadioGroupCard: FC<RadioCardProps> = ({
|
||||
onValueChange,
|
||||
onChange,
|
||||
itemRender,
|
||||
allowClear = true
|
||||
allowClear = true,
|
||||
block = false,
|
||||
}) => {
|
||||
// 监听value变化
|
||||
useEffect(() => {
|
||||
@@ -45,23 +47,30 @@ const RadioGroupCard: FC<RadioCardProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rb:grid rb:grid-cols-${options.length} rb:gap-3`}>
|
||||
<div className={clsx(`rb:grid rb:grid-cols-${block ? 1 : options.length}`, {
|
||||
'rb:gap-3': !block,
|
||||
'rb:gap-4': block,
|
||||
})}>
|
||||
{options.map(option => (
|
||||
<div key={String(option.value)} className={clsx("rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
|
||||
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF]': option.value === value,
|
||||
'rb:border-[#EBEBEB] rb:bg-[#ffffff]': option.value !== value,
|
||||
'rb:opacity-[0.75]': option.disabled
|
||||
})} onClick={() => handleChange(option)}>
|
||||
{itemRender ? itemRender(option) : (
|
||||
<>
|
||||
{option.icon && <img src={option.icon} className="rb:w-10 rb:h-10 rb:mb-3 rb:m-[0_auto]" />}
|
||||
<div key={String(option.value)} className={clsx("rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
|
||||
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF]': option.value === value,
|
||||
'rb:border-[#EBEBEB] rb:bg-[#ffffff]': option.value !== value,
|
||||
'rb:opacity-[0.75]': option.disabled,
|
||||
'rb:flex rb:items-center rb:text-left rb:gap-4': block,
|
||||
})} onClick={() => handleChange(option)}>
|
||||
{itemRender ? itemRender(option) : (
|
||||
<>
|
||||
{option.icon && <img src={option.icon} className={clsx("rb:w-10 rb:h-10", {
|
||||
'rb:m-[0_auto] rb:mb-3': !block,
|
||||
})} />}
|
||||
<div>
|
||||
<div className="rb:text-[14px] rb:font-medium">{option.label}</div>
|
||||
<div className="rb:mt-1.5 rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{option.labelDesc}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,6 +42,10 @@ import pricingIcon from '@/assets/images/menu/pricing.svg'
|
||||
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
|
||||
import spaceConfigIcon from '@/assets/images/menu/spaceConfig.svg'
|
||||
import spaceConfigActiveIcon from '@/assets/images/menu/spaceConfig_active.svg'
|
||||
import ontologyIcon from '@/assets/images/menu/ontology.svg'
|
||||
import ontologyActiveIcon from '@/assets/images/menu/ontology_active.svg'
|
||||
import promptIcon from '@/assets/images/menu/prompt.svg'
|
||||
import promptActiveIcon from '@/assets/images/menu/prompt_active.svg'
|
||||
|
||||
// 图标路径映射表
|
||||
const iconPathMap: Record<string, string> = {
|
||||
@@ -73,6 +77,10 @@ const iconPathMap: Record<string, string> = {
|
||||
'pricingActive': pricingActiveIcon,
|
||||
'spaceConfig': spaceConfigIcon,
|
||||
'spaceConfigActive': spaceConfigActiveIcon,
|
||||
'ontology': ontologyIcon,
|
||||
'ontologyActive': ontologyActiveIcon,
|
||||
'prompt': promptIcon,
|
||||
'promptActive': promptActiveIcon,
|
||||
};
|
||||
|
||||
const { Sider } = Layout;
|
||||
@@ -115,7 +123,7 @@ const Menu: FC<{
|
||||
// 叶子节点
|
||||
if (!subs || subs.length === 0) {
|
||||
if (!menu.path) return null;
|
||||
|
||||
|
||||
return {
|
||||
key: menu.path,
|
||||
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
|
||||
@@ -124,13 +132,13 @@ const Menu: FC<{
|
||||
{menu.i18nKey ? t(menu.i18nKey) : menu.label}
|
||||
</span>
|
||||
),
|
||||
icon: iconSrc ? <img
|
||||
src={iconSrc}
|
||||
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
|
||||
icon: iconSrc ? <img
|
||||
src={iconSrc}
|
||||
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
|
||||
/> : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 有子菜单的节点
|
||||
|
||||
const menuLabel = menu.i18nKey ? t(menu.i18nKey) : menu.label;
|
||||
@@ -138,15 +146,15 @@ const Menu: FC<{
|
||||
key: `submenu-${menu.id}`,
|
||||
title: menuLabel,
|
||||
label: menuLabel,
|
||||
icon: iconSrc ? <img
|
||||
src={iconSrc}
|
||||
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
|
||||
icon: iconSrc ? <img
|
||||
src={iconSrc}
|
||||
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
|
||||
/> : <UserOutlined/>,
|
||||
children: generateMenuItems(subs),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
|
||||
// 生成菜单项
|
||||
const menuItems = generateMenuItems(menus);
|
||||
// 初始加载菜单
|
||||
@@ -164,17 +172,17 @@ const Menu: FC<{
|
||||
for (const menu of menuList) {
|
||||
if (menu.path) {
|
||||
const menuPath = menu.path[0] !== '/' ? '/' + menu.path : menu.path;
|
||||
|
||||
|
||||
// 精确匹配或路径前缀匹配(确保是完整路径段匹配)
|
||||
const isExactMatch = menuPath === currentPath;
|
||||
const isPrefixMatch = currentPath.startsWith(menuPath + '/') ||
|
||||
currentPath === menuPath;
|
||||
|
||||
const isPrefixMatch = currentPath.startsWith(menuPath + '/') ||
|
||||
currentPath === menuPath;
|
||||
|
||||
if (isExactMatch || isPrefixMatch) {
|
||||
return { key: menu.path };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 递归检查子菜单
|
||||
if (menu.subs && menu.subs.length > 0) {
|
||||
const newParentPaths = [...parentPaths, `submenu-${menu.id}`];
|
||||
@@ -201,7 +209,7 @@ const Menu: FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider
|
||||
<Sider
|
||||
width={240}
|
||||
collapsedWidth={64}
|
||||
collapsed={collapsed}
|
||||
@@ -218,12 +226,12 @@ const Menu: FC<{
|
||||
{t(`space.${storageType}`)}
|
||||
</span>
|
||||
</div>
|
||||
: !collapsed
|
||||
? <div className="rb:flex">
|
||||
: !collapsed
|
||||
? <div className="rb:flex">
|
||||
<img src={logo} className={styles.logo} />
|
||||
{t('title')}
|
||||
</div>
|
||||
: null
|
||||
: null
|
||||
}
|
||||
<img src={collapsed ? menuUnfold : menuFold} className={styles.menuIcon} onClick={toggleSider} />
|
||||
</div>
|
||||
|
||||
@@ -112,7 +112,9 @@ export const en = {
|
||||
pricing: 'Pricing Management',
|
||||
orderPayment: 'Order Payment',
|
||||
orderHistory: 'Order History',
|
||||
spaceConfig: 'Space Configuration'
|
||||
spaceConfig: 'Space Configuration',
|
||||
ontology: 'Ontology Engineering',
|
||||
prompt: 'Prompt Engineering',
|
||||
},
|
||||
dashboard: {
|
||||
total_models: 'Available Models',
|
||||
@@ -421,7 +423,9 @@ export const en = {
|
||||
remove: 'Remove',
|
||||
|
||||
fileSizeTip: 'File size cannot exceed {{size}}MB',
|
||||
fileAcceptTip: 'Unsupported file type:'
|
||||
fileAcceptTip: 'Unsupported file type:',
|
||||
nextStep: 'Next Step',
|
||||
prevStep: 'Previous Step',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -868,7 +872,8 @@ export const en = {
|
||||
inactive: 'Inactive',
|
||||
configurationName: 'Configuration Name',
|
||||
emotionEngine: 'Emotion Engine',
|
||||
reflectionEngine: 'Self-Reflection Engine'
|
||||
reflectionEngine: 'Self-Reflection Engine',
|
||||
scene_id: 'Ontology Scenario',
|
||||
},
|
||||
member: {
|
||||
username: 'Username',
|
||||
@@ -1370,6 +1375,9 @@ export const en = {
|
||||
embeddingModel: 'Embedding Model',
|
||||
rerankModel: 'Rerank Model',
|
||||
configAlert: 'Space model configuration ensures that the space can correctly call the corresponding models to process business data during runtime.',
|
||||
|
||||
basic: 'Basic Config',
|
||||
models: 'Model Selection',
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: 'Memory Engine Module Configuration Center',
|
||||
@@ -2036,7 +2044,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'code': {
|
||||
input_variables: 'Input Variables',
|
||||
output_variables: 'Output Variables',
|
||||
refreshTip: '同步函数签名至代码',
|
||||
refreshTip: 'Sync function signature to code',
|
||||
},
|
||||
name: 'Key',
|
||||
type: 'Type',
|
||||
@@ -2435,6 +2443,49 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
question: 'Lessons Learned',
|
||||
summary: 'Core Insights',
|
||||
none: 'None'
|
||||
}
|
||||
},
|
||||
ontology: {
|
||||
searchPlaceholder: 'Search scenarios',
|
||||
create: 'Create Project',
|
||||
edit: 'Edit Project',
|
||||
scene_name: 'Scenario Name',
|
||||
scene_description: 'Scenario Description',
|
||||
descriptionPlaceholder: 'Describe the purpose of this scenario and the entity types to extract',
|
||||
typeCount: 'types',
|
||||
created_at: 'Created At',
|
||||
updated_at: 'Updated At',
|
||||
entityTypes: 'Entity Types',
|
||||
|
||||
addClass: 'Add Type',
|
||||
class_name: 'Type Name',
|
||||
class_description: 'Type Definition',
|
||||
classDescriptionPlaceholder: 'Describe the meaning and purpose of this type',
|
||||
|
||||
llm_id: 'Select Model',
|
||||
scenario: 'Scenario Description',
|
||||
scenarioPlaceholder: 'Please describe your business requirements',
|
||||
run: 'Inference',
|
||||
loadingConfirm: 'Inferring',
|
||||
extractConfirm: 'Add Selected Types',
|
||||
classType: 'Project Type',
|
||||
extract: 'Project Inference',
|
||||
source: 'Not Added',
|
||||
target: 'Added',
|
||||
},
|
||||
prompt: {
|
||||
editor: 'Prompt Generator',
|
||||
history: 'My History',
|
||||
historySearchPlaceholder: 'Search by name',
|
||||
model: 'Model',
|
||||
you: 'You',
|
||||
ai: 'AI Assistant',
|
||||
promptPlaceholder: 'Conversation optimization prompt will be displayed here',
|
||||
promptChatEmpty: 'No conversation content available',
|
||||
promptChatPlaceholder: 'Describe the prompt you need, e.g.: I need a customer service assistant',
|
||||
conversationOptimizationPrompt: 'Conversation Optimization Prompt',
|
||||
addVariable: 'Insert Variable',
|
||||
initialInput: 'Original Input',
|
||||
saveTitle: 'Title',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -111,7 +111,9 @@ export const zh = {
|
||||
pricing: '收费管理',
|
||||
orderPayment: '订单支付',
|
||||
orderHistory: '订单记录',
|
||||
spaceConfig: '空间配置'
|
||||
spaceConfig: '空间配置',
|
||||
ontology: '本体工程',
|
||||
prompt: '提示词工程',
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: '首页',
|
||||
@@ -975,7 +977,9 @@ export const zh = {
|
||||
remove: '删除',
|
||||
|
||||
fileSizeTip: '文件大小不能超过 {{size}}MB',
|
||||
fileAcceptTip: '不支持的文件类型:'
|
||||
fileAcceptTip: '不支持的文件类型:',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1239,7 +1243,8 @@ export const zh = {
|
||||
inactive: '不活跃',
|
||||
configurationName: '配置名称',
|
||||
emotionEngine: '情感引擎',
|
||||
reflectionEngine: '反思引擎'
|
||||
reflectionEngine: '反思引擎',
|
||||
scene_id: '本体场景',
|
||||
},
|
||||
member: {
|
||||
username: '用户名',
|
||||
@@ -1446,6 +1451,9 @@ export const zh = {
|
||||
embeddingModel: 'Embedding 模型',
|
||||
rerankModel: 'Rerank 模型',
|
||||
configAlert: '空间模型配置为空间的模型模型,保障空间运行时能正确的调用到相应的模型来处理业务数据。',
|
||||
|
||||
basic: '基础配置',
|
||||
models: '模型选择',
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: '记忆引擎模块配置中心',
|
||||
@@ -2524,6 +2532,49 @@ export const zh = {
|
||||
question: '踩过的坑',
|
||||
summary: '核心洞察',
|
||||
none: '无'
|
||||
}
|
||||
},
|
||||
ontology: {
|
||||
searchPlaceholder: '搜索场景',
|
||||
create: '新增工程',
|
||||
edit: '编辑工程',
|
||||
scene_name: '场景名称',
|
||||
scene_description: '场景描述',
|
||||
descriptionPlaceholder: '描述该场景的用途和提取的实体类型',
|
||||
typeCount: '个类型',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
entityTypes: '实体类型',
|
||||
|
||||
addClass: '添加类型',
|
||||
class_name: '类型名称',
|
||||
class_description: '类型定义',
|
||||
classDescriptionPlaceholder: '描述该类型的含义和用途',
|
||||
|
||||
llm_id: '选择模型',
|
||||
scenario: '场景描述',
|
||||
scenarioPlaceholder: '请描述您的业务需求',
|
||||
run: '推理',
|
||||
loadingConfirm: '推断中',
|
||||
extractConfirm: '添加选中类型',
|
||||
classType: '工程类型',
|
||||
extract: '工程推理',
|
||||
source: '未添加项',
|
||||
target: '已添加项',
|
||||
},
|
||||
prompt: {
|
||||
editor: '提示词生成器',
|
||||
history: '我的历史',
|
||||
historySearchPlaceholder: '按名称搜索',
|
||||
model: '模型',
|
||||
you: '你',
|
||||
ai: 'AI 助手',
|
||||
promptPlaceholder: '对话优化提示词将显示在这里',
|
||||
promptChatEmpty: '目前没有对话内容',
|
||||
promptChatPlaceholder: '描述你需要的提示词,例如:我需要一个客服助手',
|
||||
conversationOptimizationPrompt: '对话优化提示词',
|
||||
addVariable: '插入变量',
|
||||
initialInput: '原始输入',
|
||||
saveTitle: '标题',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createHashRouter, createRoutesFromElements, Route } from 'react-router-
|
||||
|
||||
// 导入路由配置JSON
|
||||
import routesConfig from './routes.json';
|
||||
import Ontology from '@/views/Ontology';
|
||||
|
||||
|
||||
// 递归函数,用于生成路由元素
|
||||
@@ -68,6 +69,9 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
Pricing: lazy(() => import('@/views/Pricing')),
|
||||
ToolManagement: lazy(() => import('@/views/ToolManagement')),
|
||||
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
|
||||
Ontology: lazy(() => import('@/views/Ontology')),
|
||||
OntologyDetail: lazy(() => import('@/views/Ontology/pages/Detail')),
|
||||
Prompt: lazy(() => import('@/views/Prompt')),
|
||||
Login: lazy(() => import('@/views/Login')),
|
||||
InviteRegister: lazy(() => import('@/views/InviteRegister')),
|
||||
NoPermission: lazy(() => import('@/views/NoPermission')),
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
|
||||
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
|
||||
{ "path": "/space-config", "element": "SpaceConfig" },
|
||||
{ "path": "/ontology", "element": "Ontology" },
|
||||
{ "path": "/prompt", "element": "Prompt" },
|
||||
{ "path": "/no-permission", "element": "NoPermission" },
|
||||
{ "path": "/*", "element": "NotFound" }
|
||||
]
|
||||
@@ -44,7 +46,8 @@
|
||||
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
|
||||
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
|
||||
{ "path": "/statement/:id", "element": "StatementDetail" },
|
||||
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }
|
||||
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" },
|
||||
{ "path": "/ontology/:id", "element": "OntologyDetail" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -332,6 +332,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"parent": 0,
|
||||
"code": "ontology",
|
||||
"label": "本体工程",
|
||||
"i18nKey": "menu.ontology",
|
||||
"path": "/ontology",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"parent": 0,
|
||||
@@ -362,6 +377,21 @@
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"parent": 0,
|
||||
"code": "prompt",
|
||||
"label": "提示词",
|
||||
"i18nKey": "menu.prompt",
|
||||
"path": "/prompt",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"parent": 0,
|
||||
|
||||
@@ -180,4 +180,9 @@ body {
|
||||
.x6-node foreignObject > body {
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.ͼ2 .cm-gutters {
|
||||
background-color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
@@ -58,13 +58,12 @@ const ApiKeyManagement: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PageScrollList
|
||||
<PageScrollList<ApiKey, { is_active: boolean; type: string }>
|
||||
ref={scrollListRef}
|
||||
url={getApiKeyListUrl}
|
||||
query={{ is_active: true, type: 'service' }}
|
||||
column={2}
|
||||
renderItem={(item: Record<string, unknown>) => {
|
||||
let apiKeyItem = item as unknown as ApiKey
|
||||
renderItem={(apiKeyItem) => {
|
||||
return (
|
||||
<RbCard
|
||||
title={apiKeyItem.name}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
@@ -21,7 +21,7 @@ import Editor from './Editor'
|
||||
|
||||
interface AiPromptModalProps {
|
||||
refresh: (value: string) => void;
|
||||
defaultModel: Model | null;
|
||||
defaultModel: ModelListItem | null;
|
||||
}
|
||||
|
||||
const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
||||
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
||||
import InsertTextPlugin from './plugin/InsertTextPlugin';
|
||||
import EditablePlugin from './plugin/EditablePlugin';
|
||||
|
||||
export interface EditorRef {
|
||||
insertText: (text: string) => void;
|
||||
@@ -23,6 +24,7 @@ interface LexicalEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
height?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -38,6 +40,7 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
||||
value,
|
||||
placeholder = "请输入内容...",
|
||||
onChange,
|
||||
disabled
|
||||
}, ref) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
@@ -92,7 +95,11 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
className={clsx("rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto", className)}
|
||||
className={clsx(
|
||||
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto",
|
||||
disabled && "rb:cursor-not-allowed rb:bg-[#F6F8FC] rb:text-[#5B6167]",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
@@ -105,6 +112,7 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
||||
<LineBreakPlugin onChange={onChange} />
|
||||
<InitialValuePlugin value={value} />
|
||||
<InsertTextPlugin />
|
||||
<EditablePlugin disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -114,6 +122,7 @@ const Editor = forwardRef<EditorRef, LexicalEditorProps>((props, ref) => {
|
||||
namespace: 'Editor',
|
||||
theme,
|
||||
nodes: [],
|
||||
editable: !props.disabled,
|
||||
onError: (error: Error) => {
|
||||
console.error(error);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-04 11:20:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 11:20:49
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
/**
|
||||
* Props for the EditablePlugin component
|
||||
*/
|
||||
interface EditablePluginProps {
|
||||
/** Whether the editor should be disabled (read-only mode) */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EditablePlugin - A Lexical editor plugin that controls the editable state of the editor
|
||||
*
|
||||
* This plugin allows you to dynamically toggle between editable and read-only modes.
|
||||
* When disabled is true, the editor becomes read-only and users cannot modify content.
|
||||
* When disabled is false or undefined, the editor is fully editable.
|
||||
*
|
||||
* @param {EditablePluginProps} props - Component props
|
||||
* @param {boolean} [props.disabled] - Controls whether the editor is in read-only mode
|
||||
* @returns {null} This plugin doesn't render any UI elements
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <LexicalComposer>
|
||||
* <EditablePlugin disabled={isReadOnly} />
|
||||
* </LexicalComposer>
|
||||
* ```
|
||||
*/
|
||||
export default function EditablePlugin({ disabled }: EditablePluginProps) {
|
||||
// Get the editor instance from Lexical composer context
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
// Update editor's editable state whenever the disabled prop changes
|
||||
useEffect(() => {
|
||||
// Set editor to editable when disabled is false, read-only when disabled is true
|
||||
editor.setEditable(!disabled);
|
||||
}, [editor, disabled]);
|
||||
|
||||
// This plugin doesn't render any UI, it only manages editor state
|
||||
return null;
|
||||
}
|
||||
@@ -117,7 +117,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
title={t('application.knowledgeBaseAssociation')}
|
||||
extra={
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('application.globalConfig')}</Button>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
...item,
|
||||
config: {
|
||||
similarity_threshold: 0.7,
|
||||
strategy: "hybrid",
|
||||
retrieve_type: "hybrid",
|
||||
top_k: 3,
|
||||
weight: 1,
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { Form, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelConfig, ModelConfigModalRef, Config, Source } from '../types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ModelConfigModalProps {
|
||||
modelList?: Model[];
|
||||
modelList?: ModelListItem[];
|
||||
refresh: (values: ModelConfig, type: Source) => void;
|
||||
data: Config;
|
||||
}
|
||||
@@ -76,9 +76,9 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleChange = (_value: string, option: Model | Model[] | undefined) => {
|
||||
const handleChange = (_value: string, option: ModelListItem | ModelListItem[] | undefined) => {
|
||||
if (source === 'chat') {
|
||||
form.setFieldValue('label', (option as Model).name)
|
||||
form.setFieldValue('label', (option as ModelListItem).name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button, Row, Col, App } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { Application, ApplicationModalRef } from './types';
|
||||
import type { Application, ApplicationModalRef, Query } from './types';
|
||||
import ApplicationModal from './components/ApplicationModal';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -14,7 +14,7 @@ import { formatDateTime } from '@/utils/format';
|
||||
const ApplicationManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { modal } = App.useApp();
|
||||
const [query, setQuery] = useState({});
|
||||
const [query, setQuery] = useState<Query>({} as Query);
|
||||
const applicationModalRef = useRef<ApplicationModalRef>(null);
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
|
||||
@@ -47,7 +47,7 @@ const ApplicationManagement: React.FC = () => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16} className="rb:mb-[16px]">
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
<Col span={12}>
|
||||
<SearchInput
|
||||
placeholder={t('application.searchPlaceholder')}
|
||||
@@ -62,22 +62,22 @@ const ApplicationManagement: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<PageScrollList
|
||||
<PageScrollList<Application, Query>
|
||||
ref={scrollListRef}
|
||||
url={getApplicationListUrl}
|
||||
query={query}
|
||||
renderItem={(item: Application) => (
|
||||
renderItem={(item) => (
|
||||
<RbCard
|
||||
title={item.name}
|
||||
avatar={
|
||||
<div className="rb:w-[48px] rb:h-[48px] rb:rounded-[8px] rb:mr-[13px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
<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>
|
||||
}
|
||||
>
|
||||
{['type', 'source', 'created_at'].map((key, index) => (
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-[20px] rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-[12px]': index !== 0
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
|
||||
<span className={clsx({
|
||||
@@ -89,14 +89,14 @@ const ApplicationManagement: React.FC = () => {
|
||||
: key === 'source' && !item.is_shared
|
||||
? t('application.configuration')
|
||||
: key === 'created_at'
|
||||
? formatDateTime(item[key as keyof Application], 'YYYY-MM-DD HH:mm:ss')
|
||||
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
|
||||
: t(`application.${item[key as keyof Application]}`)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:mt-[20px] rb:flex rb:justify-between rb:gap-[10px]">
|
||||
<div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
|
||||
<Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button>
|
||||
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// 应用数据类型
|
||||
export interface Query {
|
||||
search: string;
|
||||
}
|
||||
export interface Application {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
|
||||
@@ -178,6 +178,7 @@ const Conversation: FC = () => {
|
||||
data.forEach((item) => {
|
||||
switch(item.event) {
|
||||
case 'start':
|
||||
case 'node_start':
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
currentConversationId = newId
|
||||
break
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { MemoryFormData, Memory, MemoryFormRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createMemoryConfig, updateMemoryConfig } from '@/api/memory'
|
||||
import { getOntologyScenesSimpleUrl } from '@/api/ontology'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -38,6 +41,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
|
||||
form.setFieldsValue({
|
||||
config_name: memory.config_name,
|
||||
config_desc: memory.config_desc,
|
||||
scene_id: memory.scene_id
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
@@ -102,6 +106,20 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
|
||||
<Form.Item
|
||||
name="scene_id"
|
||||
label={t('memory.scene_id')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={getOntologyScenesSimpleUrl}
|
||||
hasAll={false}
|
||||
valueKey='scene_id'
|
||||
labelKey="scene_name"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import clsx from 'clsx'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
|
||||
const MemoryManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -96,12 +97,20 @@ const MemoryManagement: React.FC = () => {
|
||||
title={item.config_name}
|
||||
>
|
||||
<Tooltip title={item.config_desc}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.config_desc}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1 rb:h-[17px]">{item.config_desc}</div>
|
||||
</Tooltip>
|
||||
<RbAlert className="rb:mt-3 ">
|
||||
<div className={clsx("rb:flex rb:gap-5 rb:font-regular rb:text-[14px]")}>
|
||||
<span className="rb:text-[#5B6167]">{t('memory.scene_id')}: </span>
|
||||
<span className="rb:font-medium">
|
||||
{item.scene_name || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</RbAlert>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-x-4 rb:gap-y-3 rb:mt-3">
|
||||
{['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => (
|
||||
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:mt-3 rb:text-[#5B6167] rb:font-medium"
|
||||
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:text-[#5B6167] rb:font-medium"
|
||||
onClick={() => handleClick(item.config_id, key)}
|
||||
>
|
||||
{t(`memory.${key}`)}
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface MemoryFormData {
|
||||
config_id?: number;
|
||||
config_name: string;
|
||||
config_desc?: string;
|
||||
scene_id?: string;
|
||||
}
|
||||
|
||||
// 内存数据类型
|
||||
@@ -29,6 +30,8 @@ export interface Memory {
|
||||
updated_at: string;
|
||||
config_desc: string;
|
||||
workspace_id: string;
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
|
||||
@@ -27,7 +27,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
||||
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',
|
||||
|
||||
@@ -81,9 +81,9 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
|
||||
{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 key={key.id} className="rb:flex rb:gap-3 rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium rb:break-all">{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>
|
||||
|
||||
173
web/src/views/Ontology/components/OntologyClassExtractModal.tsx
Normal file
173
web/src/views/Ontology/components/OntologyClassExtractModal.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Transfer, type TransferProps, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { OntologyClassData, ExtractData, OntologyClassExtractModalData, OntologyClassExtractModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { extractOntologyTypes, createOntologyClass } from '@/api/ontology'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import Tag from '@/components/Tag';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface OntologyClassExtractModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, OntologyClassExtractModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<OntologyClassExtractModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<OntologyClassData | null>(null)
|
||||
const [extractData, setExtractData] = useState<ExtractData | null>(null)
|
||||
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<TransferProps['selectedKeys']>([]);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setData(null)
|
||||
setExtractData(null)
|
||||
};
|
||||
|
||||
const handleOpen = (vo: OntologyClassData) => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
setData(vo)
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!data?.scene_id) return;
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
extractOntologyTypes({
|
||||
...values,
|
||||
scene_id: data.scene_id,
|
||||
domain: data.scene_name,
|
||||
}).then((res) => {
|
||||
const response = res as ExtractData
|
||||
setExtractData(response)
|
||||
setSelectedKeys([])
|
||||
setTargetKeys(response.classes.map(vo => vo.id))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!extractData) {
|
||||
handleSave()
|
||||
} else {
|
||||
if (!data?.scene_id) return;
|
||||
if (!targetKeys || targetKeys.length === 0) {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('ontology.classType') }))
|
||||
return
|
||||
}
|
||||
console.log('targetKeys', targetKeys)
|
||||
createOntologyClass({
|
||||
scene_id: data?.scene_id,
|
||||
classes: extractData.classes.filter(vo => targetKeys?.includes(vo.id)).map(vo => ({ class_name: vo.name, class_description: vo.description }))
|
||||
}).then(() => {
|
||||
message.success(t('common.createSuccess'))
|
||||
refresh()
|
||||
handleClose()
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onChange: TransferProps['onChange'] = (nextTargetKeys) => {
|
||||
setTargetKeys(nextTargetKeys.filter(Boolean));
|
||||
};
|
||||
|
||||
const onSelectChange: TransferProps['onSelectChange'] = (
|
||||
sourceSelectedKeys,
|
||||
targetSelectedKeys,
|
||||
) => {
|
||||
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys].filter(Boolean));
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('ontology.extract')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={extractData ? `${t('ontology.extractConfirm')}(${targetKeys?.length})` : loading ? t('ontology.loadingConfirm') : t('ontology.run')}
|
||||
onOk={handleConfirm}
|
||||
confirmLoading={loading}
|
||||
okButtonProps={{ disabled: extractData !== null && targetKeys?.length === 0 }}
|
||||
width={1000}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="llm_id"
|
||||
label={t('ontology.llm_id')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="scenario"
|
||||
label={t('ontology.scenario')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.scenarioPlaceholder')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
{extractData && <RbCard
|
||||
title={t('ontology.classType')}
|
||||
bodyClassName='rb:flex rb:justify-center rb:h-[450px]!'
|
||||
>
|
||||
<Transfer
|
||||
titles={[t('ontology.source'), t('ontology.target')]}
|
||||
dataSource={extractData?.classes?.map(vo => ({ ...vo, key: vo.id }))}
|
||||
targetKeys={targetKeys}
|
||||
selectedKeys={selectedKeys}
|
||||
onChange={onChange}
|
||||
onSelectChange={onSelectChange}
|
||||
render={(item) => (<div>
|
||||
{item.name}
|
||||
<Flex wrap gap={8}>{item.examples.map((vo, index) => <Tag color="default" key={index}>{vo}</Tag>)}</Flex>
|
||||
</div>)}
|
||||
listStyle={{ width: '400px', height: '100%' }}
|
||||
/>
|
||||
</RbCard>}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default OntologyClassExtractModal;
|
||||
96
web/src/views/Ontology/components/OntologyClassModal.tsx
Normal file
96
web/src/views/Ontology/components/OntologyClassModal.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { AddClassItem, OntologyClassModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createOntologyClass } from '@/api/ontology'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface OntologyClassModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<AddClassItem>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [scene_id, setSceneId] = useState<string | null>(null)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (scene_id: string) => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
setSceneId(scene_id)
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!scene_id) return;
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
createOntologyClass({
|
||||
scene_id: scene_id,
|
||||
classes: [{ ...values }]
|
||||
}).then(() => {
|
||||
message.success(t('common.saveSuccess'));
|
||||
handleClose();
|
||||
refresh();
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('ontology.addClass')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="class_name"
|
||||
label={t('ontology.class_name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="class_description"
|
||||
label={t('ontology.class_description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.classDescriptionPlaceholder')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default OntologyClassModal;
|
||||
99
web/src/views/Ontology/components/OntologyModal.tsx
Normal file
99
web/src/views/Ontology/components/OntologyModal.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { OntologyItem, OntologyModalData, OntologyModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface OntologyModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [editVo, setEditVo] = useState<OntologyItem | null>(null)
|
||||
const [form] = Form.useForm<OntologyModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
const handleOpen = (vo?: OntologyItem) => {
|
||||
if (vo) {
|
||||
setEditVo(vo);
|
||||
form.setFieldsValue(vo);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
const request = editVo?.scene_id ? updateOntologyScene(editVo.scene_id, values) : createOntologyScene(values)
|
||||
request
|
||||
.then(() => {
|
||||
message.success(t('common.saveSuccess'));
|
||||
handleClose();
|
||||
refresh();
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo?.scene_id ? t('ontology.edit') : t('ontology.create')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={editVo?.scene_id ? t('common.save') : t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="scene_name"
|
||||
label={t('ontology.scene_name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="scene_description"
|
||||
label={t('ontology.scene_description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.descriptionPlaceholder')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default OntologyModal;
|
||||
45
web/src/views/Ontology/components/PageHeader.tsx
Normal file
45
web/src/views/Ontology/components/PageHeader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Layout, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logoutIcon from '@/assets/images/logout_hover.svg'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
interface ConfigHeaderProps {
|
||||
name?: string;
|
||||
subTitle?: ReactNode | string;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
subTitle,
|
||||
extra
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goBack = () => {
|
||||
navigate(-1)
|
||||
}
|
||||
return (
|
||||
<Header className="rb:w-full rb:h-16 rb:flex rb:justify-between rb:p-[0_16px_0_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||
<div className="rb:flex rb:flex-col rb:justify-center rb:gap-1 rb:mr-4">
|
||||
<div className="rb:text-[16px] rb:leading-6 rb:font-medium">
|
||||
{name}
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4">{subTitle}</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:gap-3">
|
||||
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={goBack}>
|
||||
<img src={logoutIcon} className="rb:w-4 rb:h-4" />
|
||||
{t('common.return')}
|
||||
</Button>
|
||||
{extra}
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
133
web/src/views/Ontology/index.tsx
Normal file
133
web/src/views/Ontology/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { type FC, useState, useRef, type MouseEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Row, Col, Button, Flex, Divider, Space, App, Tooltip } from 'antd'
|
||||
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import OntologyModal from './components/OntologyModal'
|
||||
import type { OntologyModalRef, OntologyItem, Query } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const Ontology: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { modal, message } = App.useApp();
|
||||
const [query, setQuery] = useState<Query>({});
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const entityModalRef = useRef<OntologyModalRef>(null)
|
||||
|
||||
const handleCreate = () => {
|
||||
entityModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEdit = (record: OntologyItem, e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
entityModalRef.current?.handleOpen(record)
|
||||
}
|
||||
const handleDelete = (item: OntologyItem, e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.scene_name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deleteOntologyScene(item.scene_id)
|
||||
.then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
scrollListRef.current?.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleJump = (record: OntologyItem) => {
|
||||
navigate(`/ontology/${record.scene_id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
<Col span={8}>
|
||||
<SearchInput
|
||||
placeholder={t('ontology.searchPlaceholder')}
|
||||
onSearch={(value) => setQuery({ scene_name: value })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16} className="rb:text-right">
|
||||
<Button type="primary" onClick={handleCreate}>
|
||||
+ {t('ontology.create')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<PageScrollList<OntologyItem, Query>
|
||||
ref={scrollListRef}
|
||||
url={getOntologyScenesUrl}
|
||||
query={query}
|
||||
column={3}
|
||||
renderItem={(item) =>(
|
||||
<RbCard
|
||||
title={item.scene_name}
|
||||
extra={<Tag>{item.type_num} {t('ontology.typeCount')}</Tag>}
|
||||
onClick={() => handleJump(item)}
|
||||
className="rb:cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{t(`ontology.scene_description`)}</span>
|
||||
<Tooltip title={item.scene_description} placement="topRight">
|
||||
<span className="rb:font-medium rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{item.scene_description}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{(['created_at', 'updated_at'] as const).map(key => (
|
||||
<div
|
||||
key={key}
|
||||
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{t(`ontology.${key}`)}</span>
|
||||
<span className="rb:font-medium">{formatDateTime(item[key])}</span>
|
||||
</div>
|
||||
))}
|
||||
<Divider size="middle" />
|
||||
<Flex gap={8} wrap>
|
||||
<div className="rb:text-[#5B6167] rb:leading-4.5">{t('ontology.entityTypes')}: </div>
|
||||
{item.entity_type?.map((type, i) => (
|
||||
<Tag key={i} color={i % 2 ? 'processing' : 'success'}>{type}</Tag>
|
||||
))}
|
||||
{item.type_num > 3 && (
|
||||
<Tag color="default">+{item.type_num - 3}</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={(e) => handleEdit(item, e)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={(e) => handleDelete(item, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</RbCard>
|
||||
)}
|
||||
/>
|
||||
|
||||
<OntologyModal
|
||||
ref={entityModalRef}
|
||||
refresh={() => scrollListRef.current?.refresh()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Ontology
|
||||
122
web/src/views/Ontology/pages/Detail.tsx
Normal file
122
web/src/views/Ontology/pages/Detail.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App, Row, Col, Tooltip, Space, Button } from 'antd'
|
||||
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import { getOntologyClassList, deleteOntologyClass } from '@/api/ontology'
|
||||
import type { OntologyClassData, OntologyClassModalRef, OntologyClassExtractModalRef, OntologyClassItem } from '@/views/Ontology/types'
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import OntologyClassModal from '../components/OntologyClassModal'
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
|
||||
const Detail: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const { modal, message } = App.useApp()
|
||||
const ontologyClassModalRef = useRef<OntologyClassModalRef>(null)
|
||||
const ontologyClassExtractModalRef = useRef<OntologyClassExtractModalRef>(null)
|
||||
const [query, setQuery] = useState<{
|
||||
class_name?: string;
|
||||
}>({});
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<OntologyClassData>({} as OntologyClassData)
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [id, query])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return;
|
||||
setLoading(true)
|
||||
getOntologyClassList({
|
||||
...query,
|
||||
scene_id: id
|
||||
})
|
||||
.then(res => {
|
||||
setData(res as OntologyClassData)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleDelete = (item: OntologyClassItem) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.class_name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deleteOntologyClass(item.class_id)
|
||||
.then(() => {
|
||||
getData();
|
||||
message.success(t('common.deleteSuccess'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleAdd = () => {
|
||||
ontologyClassModalRef.current?.handleOpen(data.scene_id)
|
||||
}
|
||||
const handleExtract = () => {
|
||||
ontologyClassExtractModalRef.current?.handleOpen(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
name={data.scene_name}
|
||||
subTitle={<div>{data.scene_description}</div>}
|
||||
extra={<Space>
|
||||
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
|
||||
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
|
||||
</Space>}
|
||||
/>
|
||||
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
<Col span={6} offset={18}>
|
||||
<SearchInput
|
||||
placeholder={t('ontology.searchPlaceholder')}
|
||||
onSearch={(value) => setQuery({ class_name: value })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<BodyWrapper loading={loading} empty={!data.items?.length}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{data.items?.map(item => (
|
||||
<Col key={item.class_id} span={6}>
|
||||
<RbCard
|
||||
title={item.class_name}
|
||||
extra={<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>}
|
||||
className="rb:bg-transparent!"
|
||||
>
|
||||
<Tooltip title={item.class_description}>
|
||||
<div className="rb:h-8.5 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-2">{item.class_description}</div>
|
||||
</Tooltip>
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</BodyWrapper>
|
||||
</div>
|
||||
|
||||
<OntologyClassModal
|
||||
ref={ontologyClassModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
<OntologyClassExtractModal
|
||||
ref={ontologyClassExtractModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
79
web/src/views/Ontology/types.ts
Normal file
79
web/src/views/Ontology/types.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export interface Query {
|
||||
pagesize?: number;
|
||||
page?: number;
|
||||
scene_name?: string;
|
||||
}
|
||||
|
||||
export interface OntologyItem {
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
scene_description: string;
|
||||
type_num: number;
|
||||
entity_type: string[];
|
||||
workspace_id: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
classes_count: number;
|
||||
}
|
||||
|
||||
export interface OntologyModalData {
|
||||
scene_name: string;
|
||||
scene_description: string;
|
||||
}
|
||||
|
||||
export interface OntologyModalRef {
|
||||
handleOpen: (data?: OntologyItem) => void;
|
||||
}
|
||||
|
||||
export interface OntologyClassItem {
|
||||
class_id: string;
|
||||
class_name: string;
|
||||
class_description: string;
|
||||
scene_id: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
export interface OntologyClassData {
|
||||
total: number;
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
scene_description: string;
|
||||
items: OntologyClassItem[];
|
||||
}
|
||||
|
||||
export interface AddClassItem {
|
||||
class_name: string;
|
||||
class_description: string;
|
||||
}
|
||||
export interface OntologyClassModalData {
|
||||
scene_id: string;
|
||||
classes: AddClassItem[]
|
||||
}
|
||||
export interface OntologyClassModalRef {
|
||||
handleOpen: (scene_id: string) => void;
|
||||
}
|
||||
export interface OntologyClassExtractModalData {
|
||||
llm_id: string;
|
||||
scene_id: string;
|
||||
scenario: string;
|
||||
domain: string; // scene_name
|
||||
}
|
||||
export interface OntologyClassExtractModalRef {
|
||||
handleOpen: (vo: OntologyClassData) => void;
|
||||
}
|
||||
|
||||
export interface ExtractClassItem {
|
||||
id: string;
|
||||
name: string;
|
||||
name_chinese: string;
|
||||
description: string;
|
||||
examples: string[];
|
||||
parent_class: string | null;
|
||||
entity_type: string;
|
||||
domain: string;
|
||||
}
|
||||
export interface ExtractData {
|
||||
domain: string;
|
||||
extracted_count: number;
|
||||
classes: ExtractClassItem[]
|
||||
}
|
||||
95
web/src/views/Prompt/History.tsx
Normal file
95
web/src/views/Prompt/History.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useRef, type MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, Space, App } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { HistoryQuery, HistoryItem, PromptDetailRef } from './types';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getPromptReleaseListUrl, deletePrompt } from '@/api/prompt'
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import PromptDetail from './components/PromptDetail'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
|
||||
const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void; }> = ({ query, edit }) => {
|
||||
const { t } = useTranslation();
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const detailRef = useRef<PromptDetailRef>(null)
|
||||
const { message, modal } = App.useApp()
|
||||
|
||||
const handleView = (item: HistoryItem) => {
|
||||
detailRef.current?.handleOpen(item)
|
||||
}
|
||||
const handleDelete = (item: HistoryItem, e?: MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.title }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deletePrompt(item.id).then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
scrollListRef.current?.refresh()
|
||||
detailRef.current?.handleClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
const handleEdit = (item: HistoryItem) => {
|
||||
edit(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getPromptReleaseListUrl}
|
||||
query={query}
|
||||
column={3}
|
||||
needLoading={false}
|
||||
renderItem={(item) => {
|
||||
const historyItem = item as unknown as HistoryItem;
|
||||
return (
|
||||
<RbCard
|
||||
className="rb:cursor-pointer"
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:p-4!"
|
||||
title={<Tooltip title={historyItem.title}>{historyItem.title}</Tooltip>}
|
||||
extra={<div className="rb:text-[12px] rb:text-[#5B6167]">{formatDateTime(historyItem.created_at, 'YYYY/MM/DD HH:mm')}</div>}
|
||||
onClick={() => handleView(historyItem)}
|
||||
>
|
||||
<div className="rb:text-[12px] rb:h-30 rb:overflow-hidden rb:px-3 rb:py-2.5 rb:bg-[#F6F8FC] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:shadow-[0px_4px_8px_0px_rgba(33,35,50,0.12)]">
|
||||
<Markdown content={historyItem.prompt} className="rb:h-full! rb:overflow-y-auto" />
|
||||
</div>
|
||||
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<EyeOutlined className="rb:text-[16px]" onClick={() => handleView(historyItem)} />
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(historyItem)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={(e) => handleDelete(historyItem, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</RbCard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<PromptDetail
|
||||
ref={detailRef}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
228
web/src/views/Prompt/Prompt.tsx
Normal file
228
web/src/views/Prompt/Prompt.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
import { Button, Form, Input, App, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import type { PromptVariableModalRef, AiPromptForm, HistoryItem, PromptSaveModalRef } from './types'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import PromptVariableModal from './components/PromptVariableModal'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import Editor from '@/views/ApplicationConfig/components/Editor'
|
||||
import PromptSaveModal from './components/PromptSaveModal'
|
||||
|
||||
const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ editVo, refresh }) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [form] = Form.useForm<AiPromptForm>()
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<string[]>([])
|
||||
const [promptSession, setPromptSession] = useState<string | null>(null)
|
||||
const aiPromptVariableModalRef = useRef<PromptVariableModalRef>(null)
|
||||
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
const currentPromptValueRef = useRef<string>(undefined)
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (editVo?.id) {
|
||||
form.setFieldValue('current_prompt', editVo.prompt)
|
||||
setChatList([])
|
||||
}
|
||||
updateSession()
|
||||
}, [editVo])
|
||||
|
||||
const updateSession = () => {
|
||||
console.log('updateSession')
|
||||
createPromptSessions().then(res => {
|
||||
const response = res as { id: string }
|
||||
setPromptSession(response.id)
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!promptSession) return
|
||||
if (!values.model_id) {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('prompt.model') }))
|
||||
return
|
||||
}
|
||||
if (!values.message) {
|
||||
message.warning(t('prompt.promptChatPlaceholder'))
|
||||
return
|
||||
}
|
||||
const messageContent = values.message
|
||||
setLoading(true)
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'user', content: messageContent}]
|
||||
})
|
||||
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.map(item => {
|
||||
const { content, desc, variables } = item.data as { content: string; desc: string; variables: string[] };
|
||||
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
currentPromptValueRef.current = ''
|
||||
if (editorRef.current?.clear) {
|
||||
editorRef.current.clear();
|
||||
}
|
||||
break;
|
||||
case 'message':
|
||||
if (typeof content === 'string') {
|
||||
currentPromptValueRef.current += content;
|
||||
if (editorRef.current?.appendText) {
|
||||
editorRef.current.appendText(content);
|
||||
editorRef.current.scrollToBottom();
|
||||
} else {
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
}
|
||||
}
|
||||
if (desc) {
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'assistant', content: desc }]
|
||||
})
|
||||
}
|
||||
if (variables) {
|
||||
setVariables(variables)
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
// 流结束时同步表单值
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
break
|
||||
}
|
||||
})
|
||||
};
|
||||
updatePromptMessages((promptSession) as string, values, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleCopy = () => {
|
||||
if (!values.current_prompt || values?.current_prompt?.trim() === '') return
|
||||
copy(values.current_prompt)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
const handleAdd = () => {
|
||||
aiPromptVariableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleVariableApply = (value: string) => {
|
||||
if (editorRef.current?.insertText) {
|
||||
editorRef.current.insertText(value)
|
||||
} else {
|
||||
form.setFieldValue('current_prompt', (values.current_prompt || '') + value)
|
||||
}
|
||||
}
|
||||
const handleSave = () => {
|
||||
if (!values.current_prompt || !promptSession) {
|
||||
return
|
||||
}
|
||||
promptSaveModalRef.current?.handleOpen({
|
||||
session_id: promptSession,
|
||||
prompt: values.current_prompt
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
form.setFieldValue('current_prompt', undefined)
|
||||
currentPromptValueRef.current = undefined;
|
||||
setChatList([])
|
||||
refresh()
|
||||
updateSession()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:-my-4">
|
||||
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-6 rb:pt-3">
|
||||
<Form.Item
|
||||
label={t('prompt.model')}
|
||||
name="model_id"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<ChatContent
|
||||
classNames="rb:h-[calc(100vh-260px)] rb:px-[16px] rb:py-[20px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]"
|
||||
contentClassNames="rb:max-w-[260px]!"
|
||||
empty={<Empty url={ConversationEmptyIcon} title={t('prompt.promptChatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chatList || []}
|
||||
streamLoading={false}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('prompt.you') : t('prompt.ai')}
|
||||
/>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:py-4">
|
||||
<Form.Item name="message" className="rb:mb-0!" style={{ width: 'calc(100% - 54px)' }}>
|
||||
<Input
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('prompt.promptChatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:pl-6 rb:pt-3">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t('prompt.conversationOptimizationPrompt')}></Form.Item>
|
||||
</Col>
|
||||
<Col span={12} className="rb:text-right">
|
||||
<Button onClick={handleAdd}>+ {t('prompt.addVariable')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="current_prompt">
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
placeholder={t('prompt.promptPlaceholder')}
|
||||
className="rb:h-[calc(100vh-260px)]"
|
||||
disabled={loading}
|
||||
// onChange={(value) => form.setFieldValue('current_prompt', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
||||
<Button type="primary" block disabled={!values?.current_prompt || loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block disabled={!values?.current_prompt || loading} onClick={handleCopy}>{t('common.copy')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<PromptVariableModal
|
||||
ref={aiPromptVariableModalRef}
|
||||
variables={variables}
|
||||
refresh={handleVariableApply}
|
||||
/>
|
||||
|
||||
<PromptSaveModal
|
||||
ref={promptSaveModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prompt;
|
||||
82
web/src/views/Prompt/components/PromptDetail.tsx
Normal file
82
web/src/views/Prompt/components/PromptDetail.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Flex, Button, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import type { HistoryItem, PromptDetailRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const PromptDetail = forwardRef<PromptDetailRef, { handleEdit: (item: HistoryItem) => void; handleDelete: (item: HistoryItem) => void; }>(({ handleEdit, handleDelete }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState<HistoryItem>({} as HistoryItem)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: HistoryItem) => {
|
||||
setVisible(true);
|
||||
setData(vo)
|
||||
};
|
||||
const handleCopy = (text = '') => {
|
||||
copy(text)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
return (
|
||||
<RbModal
|
||||
title={<div>
|
||||
{data.title}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-normal rb:mt-1!">{formatDateTime(data.created_at)}</div>
|
||||
</div>}
|
||||
open={visible}
|
||||
footer={
|
||||
<Flex justify="end" gap={8}>
|
||||
<Button danger onClick={() => handleDelete(data)}>{t('common.delete')}</Button>
|
||||
<Button type="primary" onClick={() => {
|
||||
handleClose()
|
||||
handleEdit(data)
|
||||
}}>{t('common.edit')}</Button>
|
||||
</Flex>
|
||||
}
|
||||
onCancel={handleClose}
|
||||
width={1000}
|
||||
>
|
||||
<Flex justify="space-between">
|
||||
{t('prompt.initialInput')}
|
||||
<Button className="rb:group" size="small" disabled={!data.first_message || data.first_message.trim() === ''} onClick={() => handleCopy(data.first_message)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<div className="rb:my-3 rb:bg-[#F6F8FC] rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<Markdown content={data.first_message} className="rb:min-h-5 rb:max-h-50 rb:overflow-y-auto" />
|
||||
</div>
|
||||
|
||||
<Flex justify="space-between">
|
||||
{t('prompt.conversationOptimizationPrompt')}
|
||||
<Button className="rb:group" size="small" onClick={() => handleCopy(data.prompt)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
</Button>
|
||||
</Flex>
|
||||
<div className="rb:relative rb:my-3 rb:overflow-hidden rb:bg-[#F6F8FC] rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<Markdown content={data.prompt} className="rb:min-h-5 rb:max-h-70 rb:overflow-y-auto" />
|
||||
</div>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptDetail;
|
||||
90
web/src/views/Prompt/components/PromptSaveModal.tsx
Normal file
90
web/src/views/Prompt/components/PromptSaveModal.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PromptSaveModalRef, PromptReleaseData } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { savePrompt } from '@/api/prompt'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface PromptSaveModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PromptSaveModal = forwardRef<PromptSaveModalRef, PromptSaveModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ title?: string; }>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<PromptReleaseData | null>(null)
|
||||
const title = Form.useWatch(['title'], form)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setData(null)
|
||||
};
|
||||
|
||||
const handleOpen = (vo: PromptReleaseData) => {
|
||||
setData(vo)
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!title || title.trim() === '') {
|
||||
message.warning(t('common.inputPlaceholder', { title: t('prompt.saveTitle') }))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
savePrompt({
|
||||
...data,
|
||||
title
|
||||
} as PromptReleaseData)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('prompt.saveTitle')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="title"
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptSaveModal;
|
||||
104
web/src/views/Prompt/components/PromptVariableModal.tsx
Normal file
104
web/src/views/Prompt/components/PromptVariableModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, AutoComplete, type AutoCompleteProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PromptVariableModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface PromptVariableModalProps {
|
||||
refresh: (value: string) => void;
|
||||
variables: string[];
|
||||
}
|
||||
|
||||
const PromptVariableModal = forwardRef<PromptVariableModalRef, PromptVariableModalProps>(({
|
||||
refresh,
|
||||
variables
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [options, setOptions] = useState<AutoCompleteProps['options']>([])
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(variables.map(key => ({
|
||||
value: key,
|
||||
label: `{{${key}}}`
|
||||
})))
|
||||
}, [variables])
|
||||
const handleSearch = (value: string) => {
|
||||
const filterKeys = variables?.filter(key => key.includes(value))
|
||||
|
||||
if (filterKeys.length) {
|
||||
setOptions(filterKeys.map(key => ({
|
||||
value: key,
|
||||
label: `{{${key}}}`
|
||||
})))
|
||||
} else {
|
||||
setOptions([{
|
||||
value: value,
|
||||
label: `{{${value}}}`
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
const variableName = form.getFieldValue('variableName')
|
||||
|
||||
if (!variableName) return
|
||||
|
||||
refresh(`{{${variableName}}}`)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.addVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
confirmLoading={loading}
|
||||
onOk={handleSave}
|
||||
okText={t('application.apply')}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
name="variableName"
|
||||
label={t('application.defineVariableName')}
|
||||
extra={t('application.defineVariableNameExtra')}
|
||||
>
|
||||
<AutoComplete
|
||||
placeholder={t('application.defineVariableNamePlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
options={options}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptVariableModal;
|
||||
59
web/src/views/Prompt/index.tsx
Normal file
59
web/src/views/Prompt/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { type SegmentedProps, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PageTabs from '@/components/PageTabs';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import PromptEditor from './Prompt';
|
||||
import History from './History'
|
||||
import type { HistoryQuery, HistoryItem } from './types';
|
||||
|
||||
const tabs = ['editor', 'history']
|
||||
const Prompt: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<SegmentedProps['value']>(tabs[0])
|
||||
const [query, setQuery] = useState<HistoryQuery>({});
|
||||
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
||||
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value)
|
||||
setEditVo(null)
|
||||
setQuery({})
|
||||
}
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery(prev => ({ ...prev, keyword: value }))
|
||||
}
|
||||
const handleEdit = (item: HistoryItem) => {
|
||||
console.log('edit', item)
|
||||
setEditVo(item)
|
||||
setActiveTab('editor')
|
||||
}
|
||||
const refresh = () => {
|
||||
setEditVo(null)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" className="rb:mb-4">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={tabs.map(key => ({ label: t(`prompt.${key}`), value: key }))}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
{activeTab === 'history' &&
|
||||
<SearchInput
|
||||
placeholder={t('prompt.historySearchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
className="rb:w-70"
|
||||
/>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<div className="rb:mt-4 rb:h-[calc(100vh-128px)]">
|
||||
{activeTab === 'editor' && <PromptEditor editVo={editVo} refresh={refresh} />}
|
||||
{activeTab === 'history' && <History query={query} edit={handleEdit} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prompt;
|
||||
35
web/src/views/Prompt/types.ts
Normal file
35
web/src/views/Prompt/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export interface PromptVariableModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
export interface AiPromptForm {
|
||||
model_id?: string;
|
||||
message?: string;
|
||||
current_prompt?: string;
|
||||
}
|
||||
|
||||
export interface PromptReleaseData {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
prompt: string;
|
||||
}
|
||||
export interface HistoryQuery extends Record<string, unknown> {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
id: string;
|
||||
title: string;
|
||||
prompt: string;
|
||||
created_at: number;
|
||||
first_message: string;
|
||||
}
|
||||
|
||||
export interface PromptDetailRef {
|
||||
handleOpen: (vo: HistoryItem) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export interface PromptSaveModalRef {
|
||||
handleOpen: (vo: PromptReleaseData) => void;
|
||||
}
|
||||
@@ -1,24 +1,31 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Steps, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SpaceModalData, SpaceModalRef, Space } from '../types'
|
||||
import type { SpaceModalData, SpaceModalRef, Space, StorageType } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createWorkspace } from '@/api/workspaces'
|
||||
import RadioGroupCard from '@/components/RadioGroupCard'
|
||||
import { getModelListUrl, getModelList } from '@/api/models'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
import ragIcon from '@/assets/images/space/rag.png'
|
||||
import neo4jIcon from '@/assets/images/space/neo4j.png'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface SpaceModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
const types = [
|
||||
const types: StorageType[] = [
|
||||
'rag',
|
||||
'neo4j',
|
||||
]
|
||||
const typeIcons: Record<StorageType, string> = {
|
||||
rag: ragIcon,
|
||||
neo4j: neo4jIcon
|
||||
}
|
||||
|
||||
const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
refresh
|
||||
@@ -29,7 +36,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<ModelListItem[]>([])
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -39,7 +46,11 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditVo(null)
|
||||
setCurrentStep(0)
|
||||
};
|
||||
const handlePrevStep = () => {
|
||||
setCurrentStep(prev => prev - 1)
|
||||
}
|
||||
|
||||
const handleOpen = (space?: Space) => {
|
||||
if (space) {
|
||||
@@ -58,33 +69,43 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
createWorkspace(values as SpaceModalData)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
if (currentStep === 0) {
|
||||
setCurrentStep(1)
|
||||
} else {
|
||||
const { icon, ...rest } = values
|
||||
let formData: SpaceModalData = {
|
||||
...rest
|
||||
}
|
||||
if (icon?.response?.data.file_id) {
|
||||
getFileLink(icon?.response?.data.file_id).then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.icon = logoRes.url
|
||||
formData.iconType = 'remote'
|
||||
handleUpdate(formData)
|
||||
}).catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
handleUpdate(formData)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getModels()
|
||||
}, [])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
const handleUpdate = (formData: SpaceModalData) => {
|
||||
setLoading(true)
|
||||
createWorkspace(formData)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
@@ -98,78 +119,105 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
title={t(`space.${editVo?.id ? 'editSpace' : 'createSpace'}`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
footer={[
|
||||
<Button key="close" onClick={currentStep === 0 ? handleClose : handlePrevStep}>{t(currentStep === 0 ? 'common.cancel' : 'common.prevStep')}</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSave}>{t(currentStep === 0 ? 'common.nextStep' : 'common.save')}</Button>,
|
||||
]}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep}
|
||||
items={['basic', 'models'].map(key => ({ title: t(`space.${key}`) } ))}
|
||||
className="rb:mb-6!"
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="icon"
|
||||
label={t('space.spaceIcon')}
|
||||
valuePropName="fileList"
|
||||
hidden={currentStep === 1}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.spaceIcon') }) }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('space.spaceName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
hidden={currentStep === 1}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('space.spaceName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('space.spaceName') })} />
|
||||
</FormItem>
|
||||
<Form.Item
|
||||
label={t('space.llmModel')}
|
||||
name="llm"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
fieldNames={{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
}}
|
||||
options={modelList}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.embeddingModel')}
|
||||
name="embedding"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.rerankModel')}
|
||||
name="rerank"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
name="storage_type"
|
||||
label={t('space.storageType')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
hidden={currentStep === 1}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.storageType') }) }]}
|
||||
>
|
||||
<RadioGroupCard
|
||||
options={types.map((type) => ({
|
||||
value: type,
|
||||
label: t(`space.${type}`),
|
||||
labelDesc: t(`space.${type}Desc`),
|
||||
// icon: typeIcons[type]
|
||||
icon: typeIcons[type]
|
||||
}))}
|
||||
block={true}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
|
||||
{currentStep === 1 && <>
|
||||
<Form.Item
|
||||
label={t('space.llmModel')}
|
||||
name="llm"
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.llmModel') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
placeholder={t('common.selectPlaceholder', { title: t('space.llmModel') })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.embeddingModel')}
|
||||
name="embedding"
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.embeddingModel') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
placeholder={t('common.selectPlaceholder', { title: t('space.embeddingModel') })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.rerankModel')}
|
||||
name="rerank"
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.rerankModel') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
placeholder={t('common.selectPlaceholder', { title: t('space.rerankModel') })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ const SpaceManagement: React.FC = () => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="rb:mb-[16px]" onClick={handleCreate}>
|
||||
<Button type="primary" className="rb:mb-4" onClick={handleCreate}>
|
||||
{t('space.createSpace')}
|
||||
</Button>
|
||||
<BodyWrapper loading={loading} empty={data.length === 0}>
|
||||
@@ -60,18 +60,19 @@ const SpaceManagement: React.FC = () => {
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.id}>
|
||||
<RbCard
|
||||
avatar={<div className="rb:w-[48px] rb:h-[48px] rb:rounded-[8px] rb:mr-[12px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
avatarUrl={item.icon}
|
||||
avatar={<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>}
|
||||
title={item.name}
|
||||
subTitle={<Tag className="rb:mt-[4px] rb:font-regular!" color={item.storage_type === 'rag' ? 'processing' : 'warning'}>{t(`space.${item.storage_type || 'neo4j'}`)}</Tag>}
|
||||
subTitle={<Tag className="rb:mt-1 rb:font-regular!" color={item.storage_type === 'rag' ? 'processing' : 'warning'}>{t(`space.${item.storage_type || 'neo4j'}`)}</Tag>}
|
||||
>
|
||||
<div className={clsx("rb:absolute rb:top-[-1px] rb:right-[-1px] rb:p-[2px_9px] rb:text-[#FFFFFF] rb:leading-[16px] rb:text-[12px] rb:font-regular rb:rounded-[0px_12px_0px_12px]", {
|
||||
<div className={clsx("rb:absolute rb:-top-px rb:-right-px rb:p-[2px_9px] rb:text-[#FFFFFF] rb:leading-4 rb:text-[12px] rb:font-regular rb:rounded-[0px_12px_0px_12px]", {
|
||||
'rb:bg-[#369F21]': item.is_active,
|
||||
'rb:bg-[#A8A9AA]': !item.is_active,
|
||||
})}>{item.is_active ? t('space.associated') : t('space.notAssociated')}</div>
|
||||
|
||||
<Button type="primary" ghost block className="rb:mt-[40px]" onClick={() => handleJump(item.id)}>
|
||||
<Button type="primary" ghost block className="rb:mt-10" onClick={() => handleJump(item.id)}>
|
||||
{t('space.enterSpace')}
|
||||
</Button>
|
||||
</RbCard>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// 应用数据类型
|
||||
|
||||
export type StorageType = 'rag' | 'neo4j';
|
||||
export interface Space {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -7,18 +9,19 @@ export interface Space {
|
||||
created_at: string | number;
|
||||
is_active: boolean;
|
||||
icon: string;
|
||||
storage_type: 'rag' | 'neo4j' | null;
|
||||
storage_type: StorageType | null;
|
||||
}
|
||||
|
||||
// 创建表单数据类型
|
||||
export interface SpaceModalData {
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
icon?: any;
|
||||
iconType?: 'remote';
|
||||
llm: string;
|
||||
embedding: string;
|
||||
rerank: string;
|
||||
storage_type: string;
|
||||
storage_type: StorageType;
|
||||
}
|
||||
|
||||
// 定义组件暴露的方法接口
|
||||
|
||||
@@ -15,8 +15,6 @@ 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'
|
||||
@@ -32,7 +30,7 @@ export interface LexicalEditorProps {
|
||||
lineHeight?: number;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea',
|
||||
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
|
||||
language?: 'string' | 'jinja2'
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -67,7 +65,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
|
||||
const needsLineNumbers = language === 'jinja2';
|
||||
setEnableJinja2(language === 'jinja2');
|
||||
setEnableLineNumbers(needsLineNumbers);
|
||||
|
||||
@@ -237,13 +235,11 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
{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} />
|
||||
{enableLineNumbers && <BlurPlugin />}
|
||||
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
||||
{enableJinja2 && <BlurPlugin />}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,12 @@ export default function BlurPlugin() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是粘贴操作导致的焦点变化
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!relatedTarget || relatedTarget === document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
$setSelection(null);
|
||||
});
|
||||
|
||||
@@ -8,12 +8,13 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
||||
interface InitialValuePluginProps {
|
||||
value: string;
|
||||
options?: Suggestion[];
|
||||
enableJinja2?: boolean;
|
||||
enableLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableLineNumbers = false }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const prevValueRef = useRef<string>('');
|
||||
const prevEnableLineNumbersRef = useRef<boolean>(enableLineNumbers);
|
||||
const isUserInputRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +33,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== prevValueRef.current && !isUserInputRef.current) {
|
||||
if ((value !== prevValueRef.current || enableLineNumbers !== prevEnableLineNumbersRef.current) && !isUserInputRef.current) {
|
||||
queueMicrotask(() => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@@ -40,7 +41,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
|
||||
const parts = value.split(/(\{\{[^}]+\}\})/);
|
||||
|
||||
if (enableJinja2) {
|
||||
if (enableLineNumbers) {
|
||||
// Handle newlines properly in Jinja2 mode
|
||||
const lines = value.split('\n');
|
||||
lines.forEach((line) => {
|
||||
@@ -104,8 +105,9 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
}
|
||||
|
||||
prevValueRef.current = value;
|
||||
prevEnableLineNumbersRef.current = enableLineNumbers;
|
||||
isUserInputRef.current = false;
|
||||
}, [value, options, editor, enableJinja2]);
|
||||
}, [value, options, editor, enableLineNumbers]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
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;
|
||||
@@ -1,159 +0,0 @@
|
||||
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;
|
||||
@@ -5,8 +5,8 @@ import { Node } from '@antv/x6'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MappingList from '../MappingList'
|
||||
import Editor from '../../Editor'
|
||||
import OutputList from './OutputList'
|
||||
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
|
||||
|
||||
interface MappingItem {
|
||||
name?: string
|
||||
@@ -33,7 +33,6 @@ const codeTemplate = {
|
||||
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const values = Form.useWatch([], form) || {}
|
||||
|
||||
const handleRefresh = () => {
|
||||
const code = form.getFieldValue('code') || ''
|
||||
@@ -66,7 +65,6 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
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]
|
||||
@@ -109,8 +107,15 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="code" noStyle>
|
||||
<Editor size="small" language={values.language} />
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
|
||||
{() => (
|
||||
<Form.Item name="code" noStyle>
|
||||
<CodeMirrorEditor
|
||||
language={form.getFieldValue('language')}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
<div
|
||||
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
|
||||
></div>
|
||||
{t('workflow.config.knowledge-retrieval.recallConfig')}
|
||||
{t('application.globalConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
...item,
|
||||
config: {
|
||||
similarity_threshold: 0.7,
|
||||
strategy: "hybrid",
|
||||
retrieve_type: "hybrid",
|
||||
top_k: 3,
|
||||
weight: 1,
|
||||
}
|
||||
|
||||
@@ -431,32 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { 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: "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: {
|
||||
|
||||
@@ -111,7 +111,7 @@ export const useWorkflowGraph = ({
|
||||
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)
|
||||
nodeLibraryConfig.config[key].defaultValue = decodeURIComponent(atob(config[key] as string))
|
||||
} catch {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
@@ -851,7 +851,7 @@ export const useWorkflowGraph = ({
|
||||
const code = data.config[key].defaultValue || ''
|
||||
itemConfig = {
|
||||
...itemConfig,
|
||||
code: btoa(code || '')
|
||||
code: btoa(encodeURIComponent(code || ''))
|
||||
}
|
||||
} else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const { messages, ...rest } = data.config[key].defaultValue
|
||||
@@ -885,7 +885,7 @@ export const useWorkflowGraph = ({
|
||||
...itemConfig,
|
||||
...(data.config[key].defaultValue || {}),
|
||||
knowledge_bases: knowledge_bases?.map((vo: any) => {
|
||||
const kb_config = vo.config || { similarity_threshold: vo.similarity_threshold, strategy: vo.strategy, top_k: vo.top_k, weight: vo.weight }
|
||||
const kb_config = vo.config || { similarity_threshold: vo.similarity_threshold, retrieve_type: vo.retrieve_type, top_k: vo.top_k, weight: vo.weight }
|
||||
return { kb_id: vo.kb_id || vo.id, ...kb_config, }
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user