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:
Ke Sun
2026-02-06 19:01:57 +08:00
committed by GitHub
parent eab7225d83
commit 79ab929fb0
187 changed files with 12252 additions and 1656 deletions

40
web/src/api/ontology.ts Normal file
View 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}`)
}

View File

@@ -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}`)
}

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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;

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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',
},
},
};

View File

@@ -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: '标题',
},
},
}

View File

@@ -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')),

View File

@@ -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" }
]
},
{

View File

@@ -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,

View File

@@ -180,4 +180,9 @@ body {
.x6-node foreignObject > body {
min-height: 100%;
max-height: 100%;
}
.ͼ2 .cm-gutters {
background-color: #FFFFFF;
border: none;
}

View File

@@ -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}

View File

@@ -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>(({

View File

@@ -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);
},

View File

@@ -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;
}

View File

@@ -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>
}

View File

@@ -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,
}

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -1,4 +1,7 @@
// 应用数据类型
export interface Query {
search: string;
}
export interface Application {
id: string;
workspace_id: string;

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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}`)}

View File

@@ -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;
}
// 定义组件暴露的方法接口

View File

@@ -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',

View File

@@ -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>

View 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;

View 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;

View 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;

View 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;

View 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

View 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

View 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[]
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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;
}
// 定义组件暴露的方法接口

View File

@@ -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>
);

View File

@@ -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);
});

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
}

View File

@@ -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: {

View File

@@ -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, }
})
}