feat(web): app share

This commit is contained in:
zhaoying
2026-03-13 17:27:52 +08:00
parent f0c3d5f308
commit 90c8ff35d1
41 changed files with 2044 additions and 163 deletions

View File

@@ -2,11 +2,11 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 12:08:42
* @Last Modified time: 2026-03-13 17:07:54
*/
import { request } from '@/utils/request'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
import type { Config } from '@/views/ApplicationConfig/types'
import type { Config, AppSharingForm } from '@/views/ApplicationConfig/types'
import { handleSSE, type SSEMessage } from '@/utils/stream'
import type { QueryParams } from '@/views/Conversation/types'
import type { WorkflowConfig } from '@/views/Workflow/types'
@@ -113,8 +113,8 @@ export const getShareToken = (share_token: string, user_id: string) => {
return request.post(`/public/share/${share_token}/token`, { user_id })
}
// Copy application
export const copyApplication = (app_id: string, new_name: string) => {
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
export const copyApplication = (app_id: string, new_name?: string) => {
return request.post(`/apps/${app_id}/copy`, { new_name })
}
// Data statistics
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
@@ -143,4 +143,26 @@ export const appExport = (app_id: string, appName: string, data?: { release_vers
// Import application
export const appImport = (formData: FormData) => {
return request.uploadFile(`/apps/import`, formData)
}
}
// Share application
export const appSharing = (app_id: string, data: AppSharingForm) => {
return request.post(`/apps/${app_id}/share`, data)
}
// Get my shared application records
export const mySharedOutList = () => {
return request.get(`/apps/my-shared-out`)
}
// Get sharing records for a specific application
export const getAppShares = (app_id: string) => {
return request.get(`/apps/${app_id}/shares`)
}
// Cancel a single share (source side operation)
export const cancelShare = (app_id: string, target_workspace_id?: string) => {
return request.delete(`/apps/${app_id}/share/${target_workspace_id}`)
}
// Cancel all shares under a workspace (source side operation)
export const cancelSpaceShare = (target_workspace_id?: string) => {
return request.delete(`/apps/share/${target_workspace_id}`)
}

View File

@@ -1,16 +1,16 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:26
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:26
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 15:29:03
*/
import { request } from '@/utils/request'
import type { SpaceModalData } from '@/views/SpaceManagement/types'
import type { SpaceConfigData } from '@/views/SpaceConfig/types'
// Workspace list
export const getWorkspaces = () => {
return request.get('/workspaces')
export const getWorkspaces = (data?: { include_current?: boolean }) => {
return request.get('/workspaces', data)
}
// Create workspace
export const createWorkspace = (values: SpaceModalData) => {

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>音乐</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.99">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-949, -851)" fill="#369F21" fill-rule="nonzero">
<g id="音乐" transform="translate(949, 851)">
<path d="M20,16.455738 C19.9998747,17.6766961 19.0342058,18.9480203 17.5433445,19.5353482 C15.7283829,20.2503561 13.8223534,19.6808938 13.2861085,18.2634188 C12.9392237,17.34645 13.2450501,16.2524025 14.0883859,15.3933709 C14.9317216,14.5343394 16.184444,14.0408311 17.3746646,14.0987459 C17.7820128,14.118567 18.1608598,14.201947 18.4969068,14.3398172 L18.4963476,7.04615138 C18.4963476,6.79692321 18.2897002,6.63396892 18.0924319,6.63396892 C18.0548497,6.63396892 18.0172674,6.64353993 17.9797072,6.65313341 L10.1453372,9.20299846 C9.96684901,9.25094338 9.85412426,9.41389767 9.85412426,9.59603891 L9.85412426,18.6260387 C9.84879171,19.3368367 9.52006796,20.0801486 8.91077204,20.700786 C8.06744003,21.5598158 6.81472186,22.0533241 5.62450435,21.995411 C4.43428684,21.9374978 3.48739235,21.3369617 3.14050687,20.4200187 C2.79362136,19.5030757 3.09944501,18.4090313 3.94277702,17.5500015 C4.78610903,16.6909716 6.0388272,16.1974633 7.22904471,16.2553765 C7.63671921,16.2752129 8.0158469,16.3587103 8.35209443,16.496778 L8.35111987,7.48711428 C8.35056786,6.63056391 8.90023435,5.87488803 9.70381726,5.62744028 L17.5288082,3.08714624 C18.7593794,2.70369927 19.9993516,3.6335475 19.9993516,4.93724922 Z" id="形状结合"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 57</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-443, -851)">
<g id="编组-57" transform="translate(443, 851)">
<path d="M6,1 L15.1715729,1 C15.7020059,1 16.2107137,1.21071368 16.5857864,1.58578644 L20.4142136,5.41421356 C20.7892863,5.78928632 21,6.29799415 21,6.82842712 L21,20 C21,21.6568542 19.6568542,23 18,23 L6,23 C4.34314575,23 3,21.6568542 3,20 L3,4 C3,2.34314575 4.34314575,1 6,1 Z" id="矩形" fill="#155EEF"></path>
<g id="编组-11" transform="translate(4.5, 10)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M3.74998106,1.31509018 C3.74735762,1.4334406 3.69649081,1.54592661 3.60857717,1.62778835 C3.52066353,1.70965009 3.40290911,1.75417757 3.28123343,1.75156962 L2.8124858,1.75156962 C2.30551317,1.74138936 1.88592101,2.13264132 1.87499054,2.62574433 L1.87499054,4.37409375 C1.88592099,4.86719677 2.30551316,5.25844875 2.8124858,5.24826849 L3.28123343,5.24826849 C3.40290911,5.24566054 3.52066353,5.29018802 3.60857717,5.37204975 C3.69649081,5.45391149 3.74735762,5.56639751 3.74998106,5.68474793 L3.74998106,6.55892264 C3.74735763,6.67727307 3.69649082,6.78975909 3.60857718,6.87162083 C3.52066354,6.95348258 3.40290911,6.99801006 3.28123343,6.99540211 L2.8124858,6.99540211 C2.08181824,7.01169692 1.37453656,6.74441594 0.846835274,6.25258491 C0.319133983,5.76075387 0.0144251536,5.08483394 0,4.37409375 L0,2.62574433 C0.0144251609,1.91500414 0.319133993,1.23908422 0.846835283,0.747253188 C1.37453657,0.255422158 2.08181824,-0.0118588152 2.8124858,0.00443599745 L3.28123343,0.00443599745 C3.40290911,0.00182804652 3.52066353,0.0463555261 3.60857717,0.128217263 C3.69649081,0.210078999 3.74735762,0.322565017 3.74998106,0.440915438 L3.74998106,1.31509018 Z" id="路径"></path>
<path d="M6.34371799,6.99540211 L5.6249716,6.99540211 C5.50329592,6.99801006 5.38554149,6.95348258 5.29762785,6.87162083 C5.20971421,6.78975909 5.1588474,6.67727307 5.15622397,6.55892264 L5.15622397,5.68474793 C5.15884741,5.56639751 5.20971422,5.45391149 5.29762786,5.37204975 C5.3855415,5.29018802 5.50329592,5.24566054 5.6249716,5.24826849 L6.34371799,5.24826849 C6.69246623,5.24826849 6.95371491,5.05738471 6.95371488,4.88352243 C6.94629613,4.7988134 6.90047953,4.72171762 6.82871553,4.67318553 L5.547472,3.65189656 C5.0625209,3.2801153 4.77555777,2.71562733 4.76622595,2.11509985 C4.85967193,0.868433381 5.96731214,-0.0728320292 7.24996338,0.00443599745 L7.96745977,0.00443599745 C8.08913545,0.00182804652 8.20688987,0.0463555261 8.29480351,0.128217263 C8.38271714,0.210078999 8.43358396,0.322565017 8.4362074,0.440915438 L8.4362074,1.31509018 C8.43358396,1.4334406 8.38271714,1.54592661 8.29480351,1.62778835 C8.20688987,1.70965009 8.08913545,1.75417757 7.96745977,1.75156962 L7.24996338,1.75156962 C6.90121515,1.75156962 6.63996646,1.94245339 6.63996649,2.11631568 C6.6477439,2.20058626 6.69352092,2.27717018 6.76496584,2.32543675 L8.04745938,3.35037315 C8.53241048,3.72215442 8.81937361,4.2866424 8.82870543,4.88716989 C8.73462185,6.13368729 7.62617195,7.0740823 6.34371799,6.99540211 L6.34371799,6.99540211 Z" id="路径"></path>
<path d="M11.2499432,0.440915438 L11.2499432,1.5764915 C11.2512026,2.67777506 11.5765122,3.75569874 12.1874385,4.68291209 C12.7984289,3.75572719 13.1237441,2.67778486 13.1249337,1.5764915 L13.1249337,0.440915438 C13.1275572,0.322565017 13.178424,0.210078999 13.2663376,0.128217263 C13.3542513,0.0463555261 13.4720057,0.00182804652 13.5936814,0.00443599745 L14.5311766,0.00443599745 C14.6528523,0.00182804652 14.7706067,0.0463555261 14.8585204,0.128217263 C14.946434,0.210078999 14.9973008,0.322565017 14.9999243,0.440915438 L14.9999243,1.5764915 C15.0087494,3.4937602 14.2460649,5.33830752 12.874935,6.71576346 C12.6935617,6.89486746 12.4459015,6.99571622 12.1874385,6.99571622 C11.9289755,6.99571622 11.6813152,6.89486746 11.4999419,6.71576346 C10.128812,5.33830752 9.36612749,3.4937602 9.37495266,1.5764915 L9.37495266,0.440915438 C9.37757609,0.322565012 9.42844291,0.21007899 9.51635656,0.128217253 C9.6042702,0.0463555155 9.72202463,0.00182803917 9.84370031,0.00443599745 L10.7811956,0.00443599745 C10.9028712,0.00182804652 11.0206257,0.0463555261 11.1085393,0.128217263 C11.1964529,0.210078999 11.2473198,0.322565017 11.2499432,0.440915438 L11.2499432,0.440915438 Z" id="路径"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Excel</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-296, -851)">
<g id="Excel" transform="translate(296, 851)">
<g id="编组-9" transform="translate(3, 1)">
<path d="M3,0 L12.5983161,0 L12.5983161,0 L18,5.5 L18,19 C18,20.6568542 16.6568542,22 15,22 L3,22 C1.34314575,22 -4.4408921e-16,20.6568542 -4.4408921e-16,19 L0,3 C0,1.34314575 1.34314575,0 3,0 Z" id="矩形" fill="#369F21"></path>
<path d="M4.47756884,8.31184687 L7.89908257,12.834 L7.89908257,12.834 L4.23221368,17.6923338 C4.16833301,17.776971 4.18515952,17.8973686 4.26979675,17.9612493 C4.30311465,17.9863962 4.34372024,18 4.38546294,18 L5.94228393,18 C6.09575976,18 6.23998192,17.9266088 6.33032081,17.8025374 L9,14.136 L9,14.136 L11.6696792,17.8025374 C11.7600181,17.9266088 11.9042402,18 12.0577161,18 L13.6127141,18 C13.7187527,18 13.8047141,17.9140387 13.8047141,17.808 C13.8047141,17.7660102 13.790949,17.7251781 13.7655273,17.6917583 L10.0703364,12.834 L10.0703364,12.834 L13.5200138,8.31246056 C13.5843332,8.22815626 13.5681322,8.10767294 13.4838279,8.04335355 C13.4503697,8.01782685 13.4094514,8.004 13.3673674,8.004 L11.8130677,8.004 C11.6595919,8.004 11.5153698,8.07739117 11.4250309,8.20146259 L9,11.532 L9,11.532 L6.57496912,8.20146259 C6.48463024,8.07739117 6.34040808,8.004 6.18693225,8.004 L4.63068156,8.004 C4.52464289,8.004 4.43868156,8.08996133 4.43868156,8.196 C4.43868156,8.2378202 4.45233583,8.27849685 4.47756884,8.31184687 Z" id="路径" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M12.6,0 L18,5.5 L14.328,5.5 C13.373652,5.5 12.6,4.72634805 12.6,3.772 L12.6,0 L12.6,0 Z" id="矩形" fill-opacity="0.5" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Word</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-555, -851)">
<g id="Word" transform="translate(555, 851)">
<g id="编组-9" transform="translate(3, 1)">
<path d="M3,0 L12.5983161,0 L12.5983161,0 L18,5.5 L18,19 C18,20.6568542 16.6568542,22 15,22 L3,22 C1.34314575,22 -4.4408921e-16,20.6568542 -4.4408921e-16,19 L0,3 C0,1.34314575 1.34314575,0 3,0 Z" id="矩形" fill="#155EEF"></path>
<path d="M12.6,0 L18,5.5 L14.76,5.5 C13.5670649,5.5 12.6,4.53293506 12.6,3.34 L12.6,0 L12.6,0 Z" id="矩形" fill-opacity="0.5" fill="#FFFFFF"></path>
<path d="M6.30572751,10.1239093 L4.42963674,12 L6.30572751,13.8760908 C6.61579533,14.1971281 6.61136076,14.7074326 6.29576054,15.0230328 C5.98016031,15.338633 5.4698558,15.3430676 5.14881851,15.0329998 L2.69427324,12.5784545 C2.37486988,12.2589547 2.37486988,11.7410454 2.69427324,11.4215455 L5.14881851,8.96700027 C5.4698558,8.65693244 5.98016031,8.661367 6.29576054,8.97696722 C6.61136076,9.29256744 6.61579533,9.80287195 6.30572751,10.1239093 L6.30572751,10.1239093 Z M11.6942725,13.8760908 L13.5703633,12 L11.6942725,10.1239093 C11.3842047,9.80287195 11.3886393,9.29256744 11.7042395,8.97696722 C12.0198397,8.661367 12.5301442,8.65693244 12.8511815,8.96700027 L15.3057268,11.4215455 C15.6251302,11.7410454 15.6251302,12.2589547 15.3057268,12.5784545 L12.8511815,15.0329998 C12.5301442,15.3430676 12.0198397,15.338633 11.7042395,15.0230328 C11.3886393,14.7074326 11.3842047,14.1971281 11.6942725,13.8760908 L11.6942725,13.8760908 Z M9.47536361,8.81400027 C9.65746679,8.40542152 10.1339436,8.21882948 10.5450755,8.39509277 C10.9562074,8.57135605 11.1495693,9.04512595 10.9791817,9.45872749 L8.52463643,15.1859998 C8.34253325,15.5945785 7.86605643,15.7811706 7.45492455,15.6049073 C7.04379267,15.428644 6.85043075,14.9548741 7.02081835,14.5412726 L9.47536361,8.81400027 Z" id="形状结合" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 58</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-823, -851)">
<g id="编组-58" transform="translate(823, 851)">
<g id="编组-14" transform="translate(2, 2)">
<rect id="矩形" fill="#9C6FFF" x="0" y="0" width="20" height="20" rx="4"></rect>
<circle id="椭圆形" fill="#FFFFFF" cx="6.7" cy="6.15" r="2.75"></circle>
<path d="M5.0161215,17.5 L12.9367104,17.5 C18.5091262,17.5 18.6712744,12.5070581 17.2413994,11.2815463 C16.8361052,10.9341783 16.4185445,10.5833222 15.9887171,10.2289779 L15.5545124,9.87379582 C14.8635955,9.31203668 13.8276109,9.39262388 13.2405761,10.0537924 C13.2084868,10.0899341 13.1781242,10.1274478 13.1495818,10.1662183 L11.2529579,12.7408093 C10.4310905,13.8557427 8.85224477,14.1797794 7.62487827,13.4854236 L6.18377361,12.6710243 C5.61174537,12.348034 4.87992638,12.4649012 4.44849452,12.9481383 L3.22751176,14.5162069 C2.11457779,15.8777759 2.497689,17.5 5.0161215,17.5 Z" id="路径-27" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>JSON</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-676, -851)" fill-rule="nonzero">
<g id="JSON" transform="translate(676, 851)">
<path d="M3,5.69333333 C3,4.05066667 3,3.22933333 3.30184615,2.6016 C3.56733763,2.04968249 3.990959,1.60095763 4.512,1.31973333 C5.10461538,1 5.88,1 7.43076923,1 L13.8595385,1 C14.7062308,1 15.1299231,1 15.528,1.1012 C15.8811528,1.19112104 16.2187447,1.33935687 16.5283846,1.54046667 C16.878,1.76706667 17.1770769,2.08386667 17.7759231,2.7182 L19.3779231,4.41513333 C19.9767692,5.04946667 20.2758462,5.36626667 20.4897692,5.7366 C20.6794615,6.0644 20.8193077,6.42226667 20.9044615,6.79626667 C21,7.21793333 21,7.66673333 21,8.5636 L21,18.3066667 C21,19.9493333 21,20.7706667 20.6981538,21.3984 C20.4326624,21.9503175 20.009041,22.3990424 19.488,22.6802667 C18.8953846,23 18.12,23 16.5692308,23 L7.43076923,23 C5.88,23 5.10461538,23 4.512,22.6802667 C3.990959,22.3990424 3.56733763,21.9503175 3.30184615,21.3984 C3,20.7706667 3,19.9493333 3,18.3066667 L3,5.69333333 Z" id="路径" fill="#369F21"></path>
<path d="M8.84341209,9 C6.94273128,9 6.56627954,9.55244338 6.56627954,10.5255406 L6.56627954,11.8664226 C6.56627954,12.3951132 6.46455747,12.6065895 6,12.6065895 L6,14.0179636 C6.46455747,14.0179636 6.56627954,14.2294398 6.56627954,14.7581304 L6.56627954,16.0990124 C6.56627954,17.0667461 6.94273128,17.6268517 8.84421306,17.6268517 L8.84421306,16.4553039 C8.34120945,16.4553039 8.2659191,16.3181509 8.2659191,15.9771837 L8.2659191,14.6286395 C8.2659191,13.7750724 7.83259912,13.433339 7.14457349,13.3199387 C7.8494193,13.1743572 8.25790949,12.8655713 8.25790949,11.9951473 L8.25790949,10.6466031 C8.25790949,10.3064022 8.33239888,10.1684829 8.84421306,10.1684829 L8.84421306,9 L8.84341209,9 Z M12.0552663,11.8426698 C11.5274329,11.8426698 11.1045254,12.2487655 11.1045254,12.7437426 C11.1102202,13.2425271 11.5338357,13.6443537 12.0552663,13.6455815 C12.5735717,13.6401939 12.9923657,13.2395657 12.9979976,12.7437426 C12.9979976,12.2495318 12.5718863,11.8419036 12.0552663,11.8419036 L12.0552663,11.8426698 Z M12.0552663,14.7504682 C11.5106127,14.7504682 11.1045254,15.1389409 11.1045254,15.6438788 C11.1045254,15.9610931 11.256708,16.2285033 11.5274329,16.392474 L11.1293552,18 L12.0632759,18 L12.6327593,16.7732845 C12.8786544,16.2369317 12.9979976,15.9610931 12.9979976,15.6523072 C12.9979976,15.1389409 12.5887064,14.7504682 12.0552663,14.7504682 Z M15.1565879,9 L15.1565879,10.1684829 C15.6643973,10.1684829 15.7420905,10.3064022 15.7420905,10.6473693 L15.7420905,11.9959135 C15.7420905,12.8655713 16.1481778,13.1743572 16.8530236,13.3199387 C16.164998,13.4341052 15.7340809,13.7750724 15.7340809,14.6286395 L15.7340809,15.9771837 C15.7340809,16.3181509 15.6563877,16.4553039 15.1565879,16.4553039 L15.1565879,17.6276179 C17.0572687,17.6276179 17.4313176,17.0667461 17.4313176,16.0990124 L17.4313176,14.7581304 C17.4313176,14.2294398 17.5330396,14.0179636 18,14.0179636 L18,12.6065895 C17.5330396,12.6065895 17.4313176,12.394347 17.4313176,11.8656564 L17.4313176,10.5255406 C17.4313176,9.55244338 17.0572687,9 15.1565879,9 L15.1565879,9 Z" id="形状" fill="#FFFFFF" opacity="0.98"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>PDF</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-554, -811)">
<g id="PDF" transform="translate(554, 811)">
<g id="编组-9" transform="translate(3, 1)">
<path d="M2.88,0 L12.5983161,0 L12.5983161,0 L18,5.5 L18,19.12 C18,20.7105801 16.7105801,22 15.12,22 L2.88,22 C1.28941992,22 1.33226763e-15,20.7105801 1.33226763e-15,19.12 L0,2.88 C0,1.28941992 1.28941992,-8.8817842e-16 2.88,-8.8817842e-16 Z" id="矩形" fill="#9C6FFF"></path>
<path d="M12.6,0 L18,5.5 L14.328,5.5 C13.373652,5.5 12.6,4.72634805 12.6,3.772 L12.6,0 L12.6,0 Z" id="矩形" fill-opacity="0.5" fill="#FFFFFF"></path>
</g>
<text id="MD" font-family="Rubik-Medium, Rubik" font-size="10" font-weight="400" fill="#FFFFFF">
<tspan x="4.5" y="17">MD</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>PDF</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-443, -811)">
<g id="PDF" transform="translate(443, 811)">
<g id="编组-9" transform="translate(3, 1)">
<path d="M2.88,0 L12.5983161,0 L12.5983161,0 L18,5.5 L18,19.12 C18,20.7105801 16.7105801,22 15.12,22 L2.88,22 C1.28941992,22 1.33226763e-15,20.7105801 1.33226763e-15,19.12 L0,2.88 C0,1.28941992 1.28941992,-8.8817842e-16 2.88,-8.8817842e-16 Z" id="矩形" fill="#FF5D34"></path>
<path d="M12.6,0 L18,5.5 L14.328,5.5 C13.373652,5.5 12.6,4.72634805 12.6,3.772 L12.6,0 L12.6,0 Z" id="矩形" fill-opacity="0.5" fill="#FFFFFF"></path>
<g id="pdf" transform="translate(2.7, 5.5)" fill="#FFFFFF" fill-rule="nonzero">
<rect id="矩形" opacity="0" x="0" y="0" width="12.5969524" height="12.8333333"></rect>
<path d="M10.8473784,9.58129971 C9.90260697,9.50981236 8.9928216,9.152451 8.25799118,8.50919049 C6.82335331,8.83082074 5.45867526,9.29540057 4.09400952,9.86719885 C3.00926922,11.8326989 1.99451336,12.8333333 1.11972637,12.8333333 C0.944771429,12.8333333 0.734818122,12.7976022 0.594861552,12.6903838 C0.209941005,12.5117031 0,12.1186106 0,11.725493 C0,11.4038628 0.0699844356,10.5104594 3.39417747,9.0452451 C4.16399396,7.61577452 4.75884321,6.15057284 5.24873426,4.61389636 C4.82882765,3.7562115 3.91904228,1.64773675 4.5488899,0.575652663 C4.75884321,0.182535062 5.17873752,-0.0318767295 5.6336425,0.00385438127 C5.98355238,0.00385438127 6.33346226,0.182547626 6.54341556,0.468434203 C6.99830824,1.11169471 6.96330988,2.46970306 6.36846063,4.47094679 C6.92832381,5.54304344 7.66314193,6.5079342 8.53792892,7.32988795 C9.27275935,7.18693838 10.0075775,7.07971992 10.7423956,7.07971992 C12.3870114,7.11546359 12.6319384,7.90167366 12.5969524,8.36624092 C12.5969524,9.58129971 11.4422276,9.58129971 10.8473784,9.58129971 L10.8473784,9.58129971 Z M1.04974193,11.7969552 L1.15472474,11.7612241 C1.64460348,11.5825309 2.02949943,11.2251695 2.30943717,10.7605897 C1.78457235,10.975014 1.36466574,11.3323754 1.04974193,11.7969678 L1.04974193,11.7969552 Z M5.70361464,1.0759636 L5.59864414,1.0759636 C5.56365807,1.0759636 5.49367363,1.0759636 5.45867526,1.11169471 C5.31871869,1.7192241 5.4236892,2.3624846 5.66862857,2.93427032 C5.87858188,2.32674093 5.87858188,1.68348042 5.70361464,1.0759636 Z M5.94856631,6.25777873 L5.91356795,6.32926608 L5.87858188,6.29352241 C5.56365807,7.11546359 5.21373589,7.93740477 4.82882765,8.72361485 L4.89882438,8.68787117 L4.89882438,8.75935852 C5.66862857,8.47345938 6.5084295,8.22330392 7.27823369,8.04461067 L7.24324762,8.00887956 L7.34821812,8.00887956 C6.82335331,7.47282495 6.33346226,6.86530812 5.94856631,6.25777873 Z M10.7074095,8.15182913 C10.3924857,8.15182913 10.112548,8.15182913 9.79762416,8.22330392 C10.1475463,8.4019846 10.4974685,8.47344682 10.8473784,8.50919049 C11.0923055,8.54493417 11.3372571,8.50919049 11.5472104,8.43771571 C11.5472104,8.33050981 11.4072416,8.15182913 10.7074095,8.15182913 L10.7074095,8.15182913 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>file-ppt-2-fill</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-676, -811)" fill-rule="nonzero">
<g id="file-ppt-2-fill" transform="translate(676, 811)">
<path d="M16.7502375,3.34859818 L20.5500475,3.34859818 C21.0746918,3.34859818 21.5,3.77897542 21.5,4.30987266 L21.5,19.6902643 C21.5,20.2211615 21.0746918,20.6515387 20.5500475,20.6515387 L16.7502375,20.6515387 L16.7502375,3.34859818 L16.7502375,3.34859818 Z M3.31695914,3.23036142 L15.2578621,1.50487373 C15.3940533,1.4851035 15.5320247,1.52606362 15.6360746,1.61715516 C15.7401245,1.7082467 15.8000282,1.84051837 15.800285,1.97974333 L15.800285,22.0203936 C15.7999894,22.1594252 15.7402137,22.2915157 15.6363863,22.382572 C15.5325589,22.4736282 15.3948533,22.514728 15.2588121,22.4952632 L3.31600918,20.7697755 C2.84783928,20.7023027 2.5,20.2966443 2.5,19.8181138 L2.5,4.18202315 C2.5,3.70349267 2.84783928,3.29783423 3.31600918,3.23036142 L3.31695914,3.23036142 Z" id="形状结合" fill="#FF5D34"></path>
<path d="M12.4504275,8.15497055 C12.7265699,8.15497055 12.9504275,8.37882818 12.9504275,8.65497055 L12.9504275,13.4226174 C12.9504275,13.6987598 12.7265699,13.9226174 12.4504275,13.9226174 L7.25071246,13.9226174 L7.25071246,15.5 C7.25071246,15.7761424 7.02685484,16 6.75071246,16 L5.85080744,16 C5.57466507,16 5.35080744,15.7761424 5.35080744,15.5 L5.35080744,8.65497055 C5.35080744,8.37882818 5.57466507,8.15497055 5.85080744,8.15497055 Z M11.0505225,10.0775195 L7.25071246,10.0775195 L7.25071246,12.0000685 L11.0505225,12.0000685 L11.0505225,10.0775195 Z" id="形状结合" fill="#FFFFFF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>txt</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-821, -811)">
<g id="txt" transform="translate(821, 811)">
<rect id="矩形" fill="#4DA8FF" x="2" y="2" width="20" height="20" rx="4"></rect>
<path d="M16,8 C16.5522847,8 17,8.44771525 17,9 C17,9.55228475 16.5522847,10 16,10 L13,10 L13,17 C13,17.5522847 12.5522847,18 12,18 C11.4477153,18 11,17.5522847 11,17 L11,10 L8,10 C7.44771525,10 7,9.55228475 7,9 C7,8.44771525 7.44771525,8 8,8 L16,8 Z" id="形状结合" fill="#FFFFFF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 59</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-949, -811)">
<g id="编组-59" transform="translate(949, 811)">
<g id="编组-15" transform="translate(1, 4.5)">
<path d="M3.6,0 L12.9,0 C14.8882251,-4.4408921e-16 16.5,1.6117749 16.5,3.6 L16.5,11.4 C16.5,13.3882251 14.8882251,15 12.9,15 L3.6,15 C1.6117749,15 -4.4408921e-16,13.3882251 -4.4408921e-16,11.4 L0,3.6 C-4.4408921e-16,1.6117749 1.6117749,0 3.6,-4.4408921e-16 Z M17.879231,4.05647049 L20.3282054,2.69447403 C20.8712,2.39248768 21.5561925,2.58786354 21.8581788,3.13085821 C21.9511875,3.29809499 22,3.48629259 22,3.67765286 L22,5.5744503 L22,5.5744503 L22,11.3223471 C22,11.9436675 21.4963203,12.4473471 20.875,12.4473471 C20.6836397,12.4473471 20.4954421,12.3985347 20.3282054,12.305526 L17.879231,10.9435295 C17.5937122,10.7847383 17.4166667,10.4836906 17.4166667,10.1569864 L17.4166667,4.84301355 C17.4166667,4.51630942 17.5937122,4.21526166 17.879231,4.05647049 Z" id="形状结合" fill="#4DA8FF"></path>
<path d="M7.56036738,10.1956474 L10.0614601,8.64798876 C10.6954768,8.25566369 10.8914066,7.42364931 10.4990815,6.78963262 C10.389064,6.61183886 10.2392539,6.46202877 10.0614601,6.35201124 L7.56036738,4.80435262 C6.92635069,4.41202755 6.09433631,4.60795731 5.70201124,5.241974 C5.56995501,5.45538338 5.5,5.7013784 5.5,5.95234138 L5.5,9.04765862 C5.5,9.79324304 6.10441559,10.3976586 6.85,10.3976586 C7.10096297,10.3976586 7.34695799,10.3277036 7.56036738,10.1956474 Z" id="路径-30" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Word</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-知识库-创建数据集-本地文件-2" transform="translate(-296, -811)">
<g id="Word" transform="translate(296, 811)">
<g id="编组-9" transform="translate(3, 1)">
<path d="M3,0 L12.5983161,0 L12.5983161,0 L18,5.5 L18,19 C18,20.6568542 16.6568542,22 15,22 L3,22 C1.34314575,22 -4.4408921e-16,20.6568542 -4.4408921e-16,19 L0,3 C0,1.34314575 1.34314575,0 3,0 Z" id="矩形" fill="#155EEF"></path>
<path d="M12.6,0 L18,5.5 L14.76,5.5 C13.5670649,5.5 12.6,4.53293506 12.6,3.34 L12.6,0 L12.6,0 Z" id="矩形" fill-opacity="0.5" fill="#FFFFFF"></path>
<path d="M9.00012556,10.5092 L10.4465805,15.7628533 C10.4850891,15.902729 10.6152746,16 10.7640042,16 L11.6343858,16 C11.7829678,16 11.9130713,15.9029416 11.9517273,15.7632533 L13.9886889,8.40325334 C13.996199,8.37610652 14,8.34811442 14,8.32 C14,8.14328 13.8528458,8 13.6713474,8 L12.6970019,8 C12.5438715,8 12.4110293,8.10296221 12.3767026,8.24826667 L11.1226189,13.5577333 L9.75980609,8.24250667 C9.72327861,8.10001319 9.59182177,8 9.44098566,8 L8.55932024,8 C8.40846371,8 8.27697746,8.0999939 8.24044504,8.24250667 L6.88037096,13.5472267 L5.6179067,8.24770667 C5.5833622,8.10267332 5.45065244,8 5.29774428,8 L4.32873942,8 C4.29993141,8 4.27124802,8.00368186 4.24342668,8.01096 C4.06814528,8.05682667 3.96423628,8.23237334 4.01134316,8.40304 L6.04277237,15.76304 C6.08134559,15.9028129 6.21146947,16 6.36011386,16 L7.23624697,16 C7.38497656,16 7.51516205,15.902729 7.55367062,15.7628533 L9.00012556,10.5092 Z" id="路径" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,13 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-06 21:11:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:11:14
*/
import { type FC, useRef, useState } from 'react'
import RecordRTC from 'recordrtc'
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
import { request } from '@/utils/request'
/** Props for the AudioRecorder component */
interface AudioRecorderProps {
/** Callback fired when recording is complete, receives uploaded file info and raw blob */
onRecordingComplete?: (file: { file_id: string; file_key: string; url: string; type?: string; }, blob?: Blob) => void
className?: string;
/** Upload endpoint URL, defaults to fileUploadUrlWithoutApiPrefix */
action?: string;
/** Additional config passed to the upload request */
requestConfig?: Record<string, any>;
}
@@ -17,9 +27,12 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
action = fileUploadUrlWithoutApiPrefix,
requestConfig = {}
}) => {
// Whether the recorder is currently capturing audio
const [isRecording, setIsRecording] = useState(false)
// Holds the RecordRTC instance across renders
const recorderRef = useRef<RecordRTC | null>(null)
/** Request microphone access and start recording */
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
@@ -34,6 +47,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
}
}
/** Stop recording, upload the audio blob, then invoke the completion callback */
const stopRecording = () => {
if (recorderRef.current) {
recorderRef.current.stopRecording(() => {
@@ -49,6 +63,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
type: blob.type,
url
}, blob)
// Release recorder resources after upload
recorderRef.current?.destroy()
recorderRef.current = null
})
@@ -57,12 +72,14 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
}
}
// Toggle between recording/idle states on click;
// swap background image to reflect current state
return (
<div
className={`rb:size-5.5 rb:cursor-pointer rb:bg-cover ${className} ${
isRecording
? `rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]`
: `rb:bg-[url('@/assets/images/conversation/audio.svg')] rb:hover:bg-[url('@/assets/images/conversation/audio_hover.svg')]`
: `rb:bg-[url('@/assets/images/conversation/audio.svg')]`
}`}
onClick={isRecording ? stopRecording : startRecording}
/>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 15:01:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:46:05
* @Last Modified time: 2026-03-12 14:59:38
*/
/**
@@ -15,7 +15,7 @@
*/
import { type FC, type ReactNode, useEffect } from 'react';
import { type RadioGroupProps } from 'antd';
import { type RadioGroupProps, Flex } from 'antd';
import clsx from 'clsx'
// Button checkbox component props
@@ -32,6 +32,7 @@ interface ButtonCheckboxProps extends Omit<RadioGroupProps, 'onChange'> {
checkedIcon?: string;
/** Button content */
children?: ReactNode
cicle?: boolean;
}
const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
@@ -41,6 +42,7 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
icon,
checkedIcon,
children,
cicle = false
}) => {
// Listen to value changes and trigger side effects via onValueChange callback
useEffect(() => {
@@ -57,21 +59,26 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
}
return (
<div
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
<Flex
align="center"
justify={cicle ? 'center' : 'start'}
gap={4}
className={clsx("rb:flex rb:items-center rb:cursor-pointer rb:border rb:hover:bg-[#F6F6F6]", {
'rb:size-7 rb:rounded-[14px] rb:border-[0.5px] rb:border-[#EBEBEB]': cicle,
'rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6': !cicle,
// Checked state: blue background and border
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)] rb:hover:bg-[rgba(21,94,239,0.06)] rb:text-[#155EEF]": checked,
// Unchecked state: gray border and dark text
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
})}
onClick={handleChange}
>
{/* Display unchecked icon when not checked */}
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{icon && !checked && <img src={icon} className="rb:size-4" />}
{/* Display checked icon when checked */}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{children}
</div>
</Flex>
);
};

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-06 21:05:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-12 13:57:49
*/
import { type FC } from 'react'
import ChatInput from './ChatInput'
@@ -25,7 +25,8 @@ const Chat: FC<ChatProps> = ({
labelFormat,
errorDesc,
fileList,
fileChange
fileChange,
renderRuntime
}) => {
return (
<div className="rb:h-full rb:relative rb:pt-2">
@@ -37,6 +38,7 @@ const Chat: FC<ChatProps> = ({
empty={empty}
labelFormat={labelFormat}
errorDesc={errorDesc}
renderRuntime={renderRuntime}
/>
{/* Chat input area */}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-10 16:45:54
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-06 21:05:09
* @Last Modified time: 2026-03-12 13:57:51
*/
import { type ReactNode } from 'react'
@@ -53,6 +53,7 @@ export interface ChatProps {
fileList?: any[];
/** Attachment update */
fileChange?: (fileList: any[]) => void;
renderRuntime?: (item: ChatItem, index: number) => ReactNode;
}
/**

View File

@@ -458,6 +458,7 @@ export const en = {
imageSquareRequired: 'Please upload a square image',
nameInvalid: 'Name cannot start or end with a space',
notAllSpaces: 'Cannot be all spaces',
view: 'View',
},
model: {
searchPlaceholder: 'search model…',
@@ -1370,7 +1371,58 @@ export const en = {
gotoList: 'Return to Application List',
gotoDetail: 'View Details',
dify: 'Dify',
pleaseUploadFile: 'Please upload workflow file',
setting: 'Settings',
funConfig: 'Features',
fileUpload: 'File Upload',
fileUploadDesc: 'The chat input box supports file uploads. Types include images, documents, and other types',
settings: 'File Upload Settings',
uploadType: 'Upload Type',
local: 'Local Upload',
both: 'Both',
maxSize: 'Maximum Upload',
maxSizeDesc: 'Documents < 200.00MB, Images < 10.00MB, Audio < 50.00MB, Video < 100.00MB',
supportedTypes: 'Supported File Types',
document: 'Document',
image: 'Image',
audio: 'Audio',
video: 'Video',
other: 'Other File Types',
otherFormats: 'Specify other file types',
maxCount: 'Max Files',
singleMaxSize: 'Max Size',
unix: 'items',
textTranfer: 'Text to Speech',
textTranferDesc: 'Text can be converted to speech',
apps: 'My Apps',
sharing: 'Sharing',
sharingApp: 'Shared Apps',
myShare: 'My Shares',
selectTargetSpace: 'Select Target Space',
alreadyShared: 'Already Shared',
permissionMode: 'Permission Mode',
readonlyMode: 'Use Shared',
readonlyModeDesc: 'Can test and run, cannot view internals',
editableMode: 'Copy Shared',
editableModeDesc: 'Copy full replica, free to edit',
confirmSharing: 'Confirm Sharing',
selectAtLeastOneSpace: 'Please select at least one target space',
test: 'Conversation Test',
log: 'Logs',
testChatEmpty: 'Send a message to test the shared app',
allCancel: 'Cancel All',
cancelShare: 'Cancel Sharing',
confirmAppCancelShareDesc: 'Are you sure to cancel sharing of 【{{app}}】 app with 【{{workspace}}】 space? The other party will no longer have access after cancellation.',
confirmWorkspaceCancelShareDesc: 'Are you sure to cancel all apps shared with 【{{workspace}}】? This action cannot be undone.',
sourceActive: 'Active',
sourceInactive: 'Inactive',
readonly: 'Use Only',
editable: 'Copyable',
version: 'Version',
permission: 'Permission',
souceStatus: 'Source App Status',
confirmCopyDesc: 'Are you sure to copy 【{{app}}】 app?',
noShareAuth: 'No permission to share apps',
},
userMemory: {
userMemory: 'User Memory',

View File

@@ -755,6 +755,58 @@ export const zh = {
gotoDetail: '查看详情',
dify: 'Dify',
pleaseUploadFile: '请上传工作流文件',
setting: '设置',
funConfig: '功能',
fileUpload: '文件上传',
fileUploadDesc: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型',
settings: '文件上传设置',
uploadType: '上传类型',
local: '本地上传',
both: '两者皆可',
maxSize: '最大上传大小',
maxSizeDesc: '文档 < 200.00MB,图片 < 10.00MB,音频 < 50.00MB,视频 < 100.00MB',
supportedTypes: '支持的文件类型',
document: '文档',
image: '图片',
audio: '音频',
video: '视频',
other: '其他文件类型',
otherFormats: '指定其他文件类型',
maxCount: '最大文件数',
singleMaxSize: '单文件最大大小',
unix: '个',
textTranfer: '文字转语音',
textTranferDesc: '文本可以转换成语言',
apps: '我的应用',
sharing: '共享',
sharingApp: '共享应用',
myShare: '我的共享',
selectTargetSpace: '选择目标空间',
alreadyShared: '已共享',
permissionMode: '权限模式',
readonlyMode: '使用共享',
readonlyModeDesc: '可测试运行,不可查看内部',
editableMode: '复制共享',
editableModeDesc: '复制完整副本,自由编辑',
confirmSharing: '确认共享',
selectAtLeastOneSpace: '请选择至少一个目标空间',
test: '对话测试',
log: '日志',
testChatEmpty: '发送消息测试共享应用效果',
allCancel: '全部取消',
cancelShare: '取消共享',
confirmAppCancelShareDesc: '确定取消该【{{app}}】应用对【{{workspace}}】空间的共享?取消后对方将无法访问。',
confirmWorkspaceCancelShareDesc: '确定取消所有共享给【{{workspace}}】的应用?此操作不可恢复。',
sourceActive: '生效中',
sourceInactive: '已失效',
readonly: '仅使用',
editable: '可复制',
version: '版本号',
permission: '权限',
souceStatus: '源应用状态',
confirmCopyDesc: '确定复制【{{app}}】应用?',
noShareAuth: '无共享应用的权限',
},
table: {
totalRecords: '共 {{total}} 条记录'
@@ -1038,6 +1090,7 @@ export const zh = {
imageSquareRequired: '请上传正方形比例图片',
nameInvalid: '不能是空格开头或结尾',
notAllSpaces: '不能是纯空格',
view: '查看',
},
model: {
searchPlaceholder: '搜索模型…',
@@ -2544,7 +2597,7 @@ export const zh = {
memoryHealthVisualization: '记忆健康可视化',
activationValueDistribution: '激活值分布',
forgettingTrend: '遗忘趋势近7天',
nodes_without_activation: '观察区',
low_activation_nodes: '遗忘区',
health_nodes: '健康区',

View File

@@ -45,6 +45,7 @@
"element": "BasicLayout",
"children": [
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
{ "path": "/application/config/:id/:source", "element": "ApplicationConfig" },
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
{ "path": "/statement/:id", "element": "StatementDetail" },
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" },

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 14:24:34
* @Last Modified time: 2026-03-13 16:58:15
*/
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
@@ -23,7 +23,8 @@ import type {
MemoryConfig,
AiPromptModalRef,
Source,
ChatVariableConfigModalRef
ChatVariableConfigModalRef,
FunConfigForm
} from './types'
import type { Variable } from './components/VariableList/types'
import type { KnowledgeConfig } from './components/Knowledge/types'
@@ -41,6 +42,7 @@ import ToolList from './components/ToolList/ToolList'
import SkillList from './components/Skill'
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
import type { Skill } from '@/views/Skills/types'
import FunConfig from './components/FunConfig'
/**
* Description wrapper component
@@ -99,7 +101,7 @@ const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[];
* @param name - Form field name
* @param url - API URL for options
*/
const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string }> = ({ title, desc, name, url }) => {
const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string; disabled?: boolean }> = ({ title, desc, name, url, disabled }) => {
const { t } = useTranslation();
return (
<>
@@ -115,6 +117,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
hasAll={false}
valueKey='config_id'
labelKey="config_name"
disabled={disabled}
/>
</Form.Item>
<DescWrapper desc={t(`application.${desc}`)} className="rb:mt-2" />
@@ -352,7 +355,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
}, [modelList, values?.default_model_config_id])
useImperativeHandle(ref, () => ({
handleSave
handleSave,
funConfig: values?.funConfig
}))
const aiPromptModalRef = useRef<AiPromptModalRef>(null)
@@ -406,7 +410,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
useEffect(() => {
setChatVariables(values?.variables || [])
}, [values?.variables])
console.log('values', values)
const handleSaveFunConfig = (value: FunConfigForm) => {
form.setFieldValue('funConfig', value)
}
console.log('agent', values)
return (
<>
{loading && <Spin fullscreen></Spin>}
@@ -418,6 +426,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
{defaultModel?.name ? <div className="rb:w-4 rb:h-4 rb:bg-[url('@/assets/images/application/model.svg')] rb:group-hover:bg-[url('@/assets/images/application/model_hover.svg')]"></div> : null}
{defaultModel?.name || t('application.chooseModel')}
</Button>
{/* <FunConfig value={values?.funConfig as FunConfigForm} refresh={handleSaveFunConfig} /> */}
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
@@ -426,6 +435,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<Form form={form}>
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
<Form.Item name="model_parameters" hidden noStyle></Form.Item>
<Form.Item name="funConfig" hidden noStyle></Form.Item>
<Space size={16} direction="vertical" style={{ width: '100%' }}>
<Card title={t('application.promptConfiguration')}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
@@ -464,11 +474,12 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<Card title={t('application.memoryConfiguration')}>
<Space size={24} direction='vertical' style={{ width: '100%' }}>
<SwitchWrapper title="dialogueHistoricalMemory" desc="dialogueHistoricalMemoryDesc" name={['memory', 'enabled']} />
<SelectWrapper
title="selectMemoryContent"
desc="selectMemoryContentDesc"
<SelectWrapper
title="selectMemoryContent"
desc="selectMemoryContentDesc"
name={['memory', 'memory_config_id']}
url={memoryConfigListUrl}
disabled={!values?.memory?.enabled}
/>
</Space>
</Card>
@@ -493,11 +504,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
{t('application.debuggingAndPreview')}
<Space size={10}>
{chatVariables.length > 0 &&
<Button type="primary" ghost onClick={handleOpenVariableConfig}>
{t('application.variableConfig')}
</Button>
}
<Button type="primary" ghost onClick={handleAddModel}>
+ {t('application.addModel')}
</Button>
@@ -511,6 +517,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
updateChatList={setChatList}
handleSave={handleSave}
chatVariables={chatVariables}
handleEditVariables={handleOpenVariableConfig}
/>
</RbCard>
</Col>

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 13:47:23
*/
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
@@ -165,7 +165,8 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id))
}
useImperativeHandle(ref, () => ({
handleSave
handleSave,
funConfig: data?.funConfig
}))
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-11 17:44:24
*/
import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,7 +14,8 @@ import RbCard from '@/components/RbCard/Card'
import { getReleaseList, rollbackRelease, appExport } from '@/api/application'
import ReleaseModal from './components/ReleaseModal'
import ReleaseShareModal from './components/ReleaseShareModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef } from './types'
import AppSharingModal from './components/AppSharingModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
import type { Application } from '@/views/ApplicationManagement/types'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
@@ -39,6 +40,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
const { message } = App.useApp()
const releaseModalRef = useRef<ReleaseModalRef>(null)
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
const appSharingModalRef = useRef<AppSharingModalRef>(null)
const [selectedVersion, setSelectedVersion] = useState<Release | null>(null);
const [releaseList, setReleaseList] = useState<Release[]>([])
@@ -129,6 +131,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
{data?.type !== 'multi_agent' && <Button onClick={handleExport}>{t('common.export')}</Button>}
{data.current_release_id !== selectedVersion.id && <Button onClick={handleRollback}>{t('application.willRollToThisVersion')}</Button>}
<Button type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</Button>
<Button type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</Button>
</>}
<Button type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</Button>
</Space>
@@ -178,6 +181,11 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
ref={releaseShareModalRef}
version={selectedVersion}
/>
<AppSharingModal
ref={appSharingModalRef}
appId={data.id}
version={selectedVersion}
/>
</div>
);
}

View File

@@ -0,0 +1,642 @@
import { type FC, useState, useRef, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { App, Flex, Dropdown, type MenuProps, Divider, Form, Space } from 'antd'
import { SettingOutlined } from '@ant-design/icons'
import clsx from 'clsx'
import dayjs from 'dayjs'
import ChatIcon from '@/assets/images/application/chat.png'
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal'
import { draftRun } from '@/api/application';
import Empty from '@/components/Empty'
import Chat from '@/components/Chat'
import AudioRecorder from '@/components/AudioRecorder'
import RbCard from '@/components/RbCard/Card'
import UploadFiles from '@/views/Conversation/components/FileUpload'
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
import Runtime from '@/views/Workflow/components/Chat/Runtime';
import { nodeLibrary } from '@/views/Workflow/constant'
// import ButtonCheckbox from '@/components/ButtonCheckbox';
// import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
// import OnlineIcon from '@/assets/images/conversation/online.svg'
// import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
// import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
import type { ChatItem } from '@/components/Chat/types'
import type { VariableConfigModalRef, WorkflowConfig } from '@/views/Workflow/types'
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
import type { TestChatProps } from './type';
import type { UploadFileListModalRef } from '@/views/Conversation/types'
import type { SSEMessage } from '@/utils/stream'
const formatParams = (message: string, conversation_id: string | null, files: any[] = [], variables: Record<string, any>) => {
return {
message,
conversation_id,
stream: true,
files: files.map(file => {
if (file.url) {
return file
} else {
return {
type: file.type,
transfer_method: 'local_file',
upload_file_id: file.response.data.file_id
}
}
}),
variables: Object.keys(variables).length > 0 ? variables : undefined
}
}
interface NodeData {
content: string;
conversation_id: string | null;
cycle_id: string;
cycle_idx: number;
node_id: string;
node_name?: string;
node_type?: string;
input?: any;
output?: any;
elapsed_time?: string;
error?: any;
state: Record<string, any>;
status?: 'completed' | 'failed'
}
interface FormData {
files: any[];
variables: Variable[]
}
const TestChat: FC<TestChatProps> = ({
application,
config
}) => {
const { t } = useTranslation()
const { message: messageApi } = App.useApp()
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
const [loading, setLoading] = useState(false) // Send button loading state
const [chatList, setChatList] = useState<ChatItem[]>([]) // Chat message history
const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
const [conversationId, setConversationId] = useState<string | null>(null) // Current conversation ID
const [message, setMessage] = useState<string | undefined>(undefined) // Current input message
const [form] = Form.useForm<FormData>()
const queryValues = Form.useWatch([], form)
useEffect(() => {
getVariables()
}, [application, config])
const getVariables = () => {
if (!application || !config) return
let initVariables: Variable[] = []
switch (application.type) {
case 'workflow':
const { nodes } = config as WorkflowConfig;
const startNodes = nodes.filter(vo => vo.type === 'start')
if (startNodes.length) {
const curVariables = startNodes[0].config.variables as Variable[]
curVariables.forEach((vo) => {
if (typeof vo.default !== 'undefined') {
vo.value = vo.default
}
const lastVo = curVariables.find(item => item.name === vo.name)
if (lastVo?.value) {
vo.value = lastVo.value
}
})
initVariables = curVariables
}
break
case 'agent':
initVariables = config.variables as Variable[]
break
}
form.setFieldValue('variables', [...initVariables])
}
/**
* Opens the variable configuration modal
*/
const handleEditVariables = () => {
variableConfigModalRef.current?.handleOpen(queryValues.variables)
}
/**
* Saves updated variable values from the modal
*/
const handleSave = (values: Variable[]) => {
form.setFieldValue('variables', [...values])
}
/**
* Handles file upload from local device
*/
const fileChange = (file?: any) => {
form.setFieldValue('files', [...(queryValues.files || []), file])
}
const handleRecordingComplete = async (file: any) => {
form.setFieldValue('files', [...(queryValues.files || []), file])
}
/**
* Handles dropdown menu actions for file upload
*/
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
switch(key) {
case 'define':
uploadFileListModalRef.current?.handleOpen()
break
}
}
/**
* Adds files from remote URL modal
*/
const addFileList = (list?: any[]) => {
if (!list || list.length <= 0) return
form.setFieldValue('files', [...(queryValues.files || []), ...(list || [])])
}
/**
* Updates the entire file list (used when removing files)
*/
const updateFileList = (list?: any[]) => {
form.setFieldValue('files', [...list || []])
}
const isNeedVariableConfig = useMemo(() => {
return queryValues?.variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
}, [queryValues?.variables])
const addUserMessage = (message: string, files: any[]) => {
const newUserMessage: ChatItem = {
role: 'user',
content: message,
created_at: Date.now(),
files
};
setChatList(prev => [...prev, newUserMessage])
}
const addAssistantMessage = () => {
const { type } = application || {}
setChatList(prev => [...prev, {
role: 'assistant',
content: '',
created_at: Date.now(),
subContent: type === 'workflow' ? [] : undefined,
}])
}
const updateAssistantMessage = (content: string) => {
setChatList(prev => {
let newList = [...prev]
const lastMsg = newList[newList.length - 1]
if (lastMsg.role === 'assistant') {
lastMsg.content += content
}
return newList
})
}
const updateErrorAssistantMessage = (message_length: number) => {
if (message_length > 0) return
setChatList(prev => {
let newList = [...prev]
const lastMsg = newList[newList.length - 1]
if (lastMsg.role === 'assistant') {
lastMsg.content = null
}
return newList
})
}
const handleSend = () => {
if (loading || !application || !message || !message?.trim()) return
// Validate required variables before sending
const { variables, files } = queryValues;
let isCanSend = true
const params: Record<string, any> = {}
if (variables && variables.length > 0) {
const needRequired: string[] = []
variables.forEach(vo => {
params[vo.name] = vo.value
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
isCanSend = false
needRequired.push(vo.name)
}
})
if (needRequired.length) {
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
}
}
if (!isCanSend) {
setLoading(false)
return
}
addUserMessage(message, files)
setMessage(undefined)
form.setFieldValue('files', [])
addAssistantMessage()
setStreamLoading(true)
setLoading(true)
draftRun(
application.id,
formatParams(message, conversationId, files, params),
handleStreamMessage
)
.catch(() => {
setLoading(false)
})
.finally(() => {
setLoading(false)
setStreamLoading(false)
})
}
const handleStreamMessage = (data: SSEMessage[]) => {
data.map(item => {
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
switch (item.event) {
case 'start':
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id);
}
break
case 'message':
updateAssistantMessage(content)
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id);
}
break;
case 'end':
updateErrorAssistantMessage(message_length)
setStreamLoading(false)
break;
}
})
};
const handleWorkflowSend = () => {
if (loading || !application || !message || !message?.trim()) return
// Validate required variables before sending
const { variables, files } = queryValues;
let isCanSend = true
const params: Record<string, any> = {}
if (variables.length > 0) {
const needRequired: string[] = []
variables.forEach(vo => {
params[vo.name] = vo.value ?? vo.defaultValue
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
isCanSend = false
needRequired.push(vo.name)
}
})
if (needRequired.length) {
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
}
}
if (!isCanSend) {
return
}
setLoading(true)
addUserMessage(message, files)
addAssistantMessage()
form.setFieldsValue({
files: [],
})
setMessage(undefined)
setStreamLoading(true)
draftRun(
application.id,
formatParams(message, conversationId, files, params),
handleWorkflowStreamMessage
)
.catch((error) => {
console.log('draftRun error', error)
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = {
...newList[lastIndex],
status: 'failed',
content: null,
subContent: error.error
}
}
return newList
})
}).finally(() => {
setLoading(false)
setStreamLoading(false)
})
}
const handleWorkflowStreamMessage = (data: SSEMessage[]) => {
data.forEach(item => {
const { content, conversation_id } = item.data as NodeData;
switch (item.event) {
// Append streaming text chunks to assistant message
case 'message':
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = {
...newList[lastIndex],
content: newList[lastIndex].content + content
}
}
return newList
})
break
// Track node execution start
case 'node_start':
addWorkflowNodeStartMessage(item.data as NodeData)
break
// Update node with execution results or errors
case 'node_end':
case 'node_error':
updateWorkflowNodeEndMessage(item.data as NodeData)
break
// Update node with subContent
case 'cycle_item':
updateWorkflowCycleMessage(item.data as NodeData)
break
// Mark workflow as complete
case 'workflow_end':
updateWorkflowEndMessage(item.data as NodeData)
setStreamLoading(false)
setLoading(false)
break
}
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id)
}
})
}
const addWorkflowNodeStartMessage = (data: NodeData) => {
const { node_id } = data;
const { nodes } = config as WorkflowConfig
const node = nodes.find(n => n.id === node_id);
const { name, type } = node || {}
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.id === node_id)
if (filterIndex > -1) {
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
node_id: node_id,
node_name: name,
node_type: type,
icon,
content: {},
}
} else {
newSubContent.push({
id: node_id,
node_id: node_id,
node_name: name,
node_type: type,
icon,
content: {},
})
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
return newList
})
}
const updateWorkflowNodeEndMessage = (data: NodeData) => {
const { node_id, input, output, error, elapsed_time, status } = data;
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.node_id === node_id)
if (filterIndex > -1 && newSubContent[filterIndex].content) {
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
content: {
input,
output,
error,
},
status: status || 'completed',
elapsed_time
}
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
return newList
})
}
const updateWorkflowCycleMessage = (data: NodeData) => {
const { node_id, cycle_id, cycle_idx, input, output, error, elapsed_time, status } = data;
const { nodes } = config as WorkflowConfig
const node = nodes.find(n => n.id === node_id);
const { name, type } = node || {}
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.id === cycle_id)
if (filterIndex > -1) {
const items = newSubContent[filterIndex].subContent || []
items.push({
cycle_id,
cycle_idx,
node_id,
node_name: name,
node_type: type,
icon,
content: {
cycle_idx,
input,
output,
error,
},
status: status || 'completed',
elapsed_time
})
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
subContent: [...items]
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
}
return newList
})
}
const updateWorkflowEndMessage = (data: NodeData) => {
const { error, status } = data as {
content: string;
conversation_id: string | null;
cycle_id: string;
cycle_idx: number;
node_id: string;
node_name?: string;
node_type?: string;
input?: any;
output?: any;
elapsed_time?: string;
error?: any;
state: Record<string, any>;
status?: 'completed' | 'failed'
};
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = {
...newList[lastIndex],
status,
error,
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content,
}
}
return newList
})
}
console.log('queryValues', queryValues)
return (
<div className="rb:w-250 rb:p-3 rb:mx-auto">
<RbCard
title={t('application.test')}
headerClassName="rb:min-h-[56px]!"
className="rb:h-[calc(100vh-88px)]!"
bodyClassName="rb:h-[calc(100%-56px)]! rb:overflow-y-auto rb:px-3! rb:py-0!"
>
<Chat
empty={<Empty url={ChatIcon} title={t('application.testChatEmpty')} isNeedSubTitle={false} size={[240, 200]} />}
contentClassName={clsx(`rb:mx-[16px] rb:pt-[24px]`, {
'rb:h-[calc(100%-140px)]': !queryValues?.files?.length,
'rb:h-[calc(100%-208px)]': !!queryValues?.files?.length,
})}
data={chatList}
streamLoading={streamLoading}
loading={loading}
onChange={setMessage}
onSend={application?.type === 'workflow' ? handleWorkflowSend : handleSend}
fileList={queryValues?.files || []}
fileChange={updateFileList}
labelFormat={(item) => item.role === 'user' ? t('application.you') : dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
errorDesc={t('application.ReplyException')}
renderRuntime={application?.type === 'workflow' ? (item, index) => {
return <Runtime item={item} index={index} />
} : undefined}
>
<Form form={form}>
<Flex justify="space-between" className="rb:flex-1">
<Space size={8} align="center">
<Form.Item name="files" noStyle>
<Dropdown
menu={{
items: [
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
{
key: 'upload', label: (
<UploadFiles
onChange={fileChange}
/>
)
},
],
onClick: handleShowUpload
}}
>
<Flex align="center" justify="center" className="rb:size-7 rb:cursor-pointer rb:rounded-[14px] rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6]">
<div
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')]"
></div>
</Flex>
</Dropdown>
</Form.Item>
{/* <Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
<ButtonCheckbox
icon={OnlineIcon}
checkedIcon={OnlineCheckedIcon}
>
{t(`memoryConversation.web_search`)}
</ButtonCheckbox>
</Form.Item>
<Tooltip title={t(`memoryConversation.memory`)}></Tooltip>
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
<ButtonCheckbox
icon={MemoryFunctionIcon}
checkedIcon={MemoryFunctionCheckedIcon}
cicle={true}
>
</ButtonCheckbox>
</Form.Item> */}
<Form.Item name="variables" className="rb:mb-0!" hidden={!queryValues?.variables?.length}>
<div
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
})}
onClick={handleEditVariables}
>
<SettingOutlined className="rb:mr-1" />
{t(`memoryConversation.variableConfig`)}
</div>
</Form.Item>
</Space>
<Space size={8} align="center">
<AudioRecorder
onRecordingComplete={handleRecordingComplete}
/>
<Divider type="vertical" className="rb:ml-0! rb:mr-2!" />
</Space>
</Flex>
</Form>
</Chat>
<VariableConfigModal
ref={variableConfigModalRef}
refresh={handleSave}
/>
<UploadFileListModal
ref={uploadFileListModalRef}
refresh={addFileList}
/>
</RbCard>
</div>
)
}
export default TestChat

View File

@@ -0,0 +1,8 @@
import type { Application } from '@/views/ApplicationManagement/types'
import type { Config } from '../types';
import type { WorkflowConfig } from '@/views/Workflow/types';
export interface TestChatProps {
application?: Application | null;
config: Config | WorkflowConfig | null
}

View File

@@ -0,0 +1,183 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:19:13
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:26:57
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Checkbox, App, Form } from 'antd';
import { useTranslation } from 'react-i18next';
import RbModal from '@/components/RbModal';
import { appSharing, getAppShares } from '@/api/application';
import { formatDateTime } from '@/utils/format';
import type { AppSharingModalRef, Release } from '../types';
import type { SpaceItem } from '@/views/KnowledgeBase/types';
import { getWorkspaces } from '@/api/workspaces';
import RadioGroupCard from '@/components/RadioGroupCard';
/** Props for the AppSharingModal component */
interface AppSharingModalProps {
/** ID of the application being shared */
appId: string;
/** The release version to share */
version: Release | null;
}
const AppSharingModal = forwardRef<AppSharingModalRef, AppSharingModalProps>(({ appId, version }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
// All workspaces available to share with (excluding the current one)
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
// IDs of workspaces that already have access to this app
const [sharedIds, setSharedIds] = useState<string[]>([]);
const [form] = Form.useForm<{ target_workspace_ids: string[]; permission: 'readonly' | 'editable' }>();
// Reactively track the currently selected workspace IDs in the form
const selectedIds: string[] = Form.useWatch('target_workspace_ids', form) ?? [];
/**
* Fetch workspaces and existing share records in parallel,
* sort already-shared spaces to the top, then open the modal.
* Shows a warning if the user has no shareable workspaces.
*/
const handleOpen = () => {
Promise.all([getWorkspaces({ include_current: false }), getAppShares(appId)]).then(([spaces, shared]) => {
// Normalise the shared workspace ID field across different API response shapes
const ids = ((shared as any[]) || []).map((s: any) => s.workspace_id || s.target_workspace_id || s.id);
// Sort: already-shared workspaces appear first
const sorted = (spaces as SpaceItem[]).sort((a, b) =>
ids.includes(b.id) ? 1 : ids.includes(a.id) ? -1 : 0
);
setSpaceList(sorted);
setSharedIds(ids);
if (sorted.length > 0) {
setVisible(true);
} else {
message.warning(t('application.noShareAuth'));
}
});
};
/** Close the modal and reset form fields */
const handleClose = () => {
setVisible(false);
form.resetFields();
};
// Expose open/close handlers to the parent via ref
useImperativeHandle(ref, () => ({ handleOpen, handleClose }));
/**
* Toggle a workspace in the selected list.
* Already-shared workspaces are read-only and cannot be toggled.
*/
const handleToggle = (id: string, isShared: boolean) => {
if (isShared) return;
const prev = form.getFieldValue('target_workspace_ids') as string[] ?? [];
form.setFieldValue(
'target_workspace_ids',
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
/** Validate the form then submit the sharing request */
const handleConfirm = () => {
form.validateFields().then(values => {
setLoading(true);
appSharing(appId, values)
.then(() => {
message.success(t('common.operateSuccess'));
handleClose();
})
.finally(() => setLoading(false));
});
};
// Normalise the version label to always start with "v"
const versionLabel = version?.version_name
? (version.version_name[0].toLowerCase() === 'v' ? version.version_name : `v${version.version_name}`)
: `v${version?.version}`;
return (
<RbModal
title={t('application.sharingApp')}
open={visible}
onCancel={handleClose}
okText={<>{t('application.confirmSharing')}({selectedIds.length})</>}
onOk={handleConfirm}
confirmLoading={loading}
width={600}
>
<Form form={form} layout="vertical" initialValues={{ target_workspace_ids: [], permission: 'readonly' }}>
{/* Version info: displays version number, release time and publisher */}
<div className="rb:rounded-lg rb:border rb:border-[#EBEBEB] rb:bg-[#FBFDFF] rb:p-4 rb:mb-4">
<div className="rb:text-sm rb:font-medium rb:mb-3">{t('application.VersionInformation')}</div>
<div className="rb:grid rb:grid-cols-3 rb:gap-4 rb:text-sm">
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.versionList').replace('列表', '号')}</div>
<div className="rb:font-medium">{versionLabel}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.releaseTime')}</div>
<div className="rb:font-medium">{formatDateTime(version?.published_at || 0, 'YYYY-MM-DD HH:mm:ss')}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.publisher')}</div>
<div className="rb:font-medium">{version?.publisher_name}</div>
</div>
</div>
</div>
{/* Target space: scrollable list of workspaces with checkbox selection */}
<Form.Item
name="target_workspace_ids"
label={t('application.selectTargetSpace')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<div className="rb:rounded-lg rb:border rb:border-[#EBEBEB] rb:divide-y rb:divide-[#EBEBEB] rb:max-h-50 rb:overflow-y-auto">
{spaceList.map(space => {
const isShared = sharedIds.includes(space.id);
return (
<div key={space.id} className="rb:flex rb:items-center rb:gap-2 rb:px-4 rb:py-3 rb:cursor-pointer" onClick={() => handleToggle(space.id, isShared)}>
<Checkbox
checked={isShared || selectedIds.includes(space.id)}
disabled={isShared} // already-shared workspaces cannot be unselected
onChange={() => handleToggle(space.id, isShared)}
/>
<span className="rb:flex-1 rb:text-sm">{space.name}</span>
{/* Badge shown when the app is already shared with this workspace */}
{isShared && (
<span className="rb:text-xs rb:text-[#5B6167]">{t('application.alreadyShared')}</span>
)}
</div>
);
})}
</div>
</Form.Item>
{/* Permission mode: readonly (use only) or editable (full copy) */}
<Form.Item
name="permission"
label={t('application.permissionMode')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
className="rb:mb-0!"
>
<RadioGroupCard
options={['readonly', 'editable'].map((type) => ({
value: type,
label: t(`application.${type}Mode`),
labelDesc: t(`application.${type}ModeDesc`),
}))}
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default AppSharingModal;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 17:03:46
* @Last Modified time: 2026-03-13 15:20:32
*/
/**
* Chat debugging component for application testing
@@ -13,7 +13,8 @@
import { type FC, useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd';
import { SettingOutlined } from '@ant-design/icons'
import ChatIcon from '@/assets/images/application/chat.png'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
@@ -45,13 +46,17 @@ interface ChatProps {
/** Source type: multi-agent cluster or single agent */
source?: 'multi_agent' | 'agent';
chatVariables?: Variable[]; // Add chatVariables prop
handleEditVariables?: () => void;
}
/**
* Chat debugging component
* Allows testing application with different model configurations side-by-side
*/
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent', chatVariables }) => {
const Chat: FC<ChatProps> = ({
chatList, data, updateChatList, handleSave, source = 'agent', chatVariables,
handleEditVariables
}) => {
const { t } = useTranslation();
const { message: messageApi } = App.useApp()
const [loading, setLoading] = useState(false)
@@ -434,6 +439,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
const updateFileList = (list?: any[]) => {
setFileList([...list || []])
}
const isNeedVariableConfig = chatVariables?.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
return (
<div className="rb:relative rb:h-full rb:flex rb:flex-col">
@@ -521,6 +527,18 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]"
></div>
</Dropdown>
{chatVariables && chatVariables.length > 0 && (
<div
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
})}
onClick={handleEditVariables}
>
<SettingOutlined className="rb:mr-1" />
{t(`memoryConversation.variableConfig`)}
</div>
)}
</Flex>
<Flex align="center">
<AudioRecorder onRecordingComplete={handleRecordingComplete} />

View File

@@ -2,9 +2,9 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:48:52
* @Last Modified time: 2026-03-13 17:12:59
*/
import { type FC, useRef, useMemo } from 'react';
import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
import type { MenuProps } from 'antd';
@@ -18,9 +18,10 @@ import exportIcon from '@/assets/images/export_hover.svg'
import deleteIcon from '@/assets/images/delete_hover.svg'
import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types';
import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal'
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types'
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FunConfigForm } from '../types'
import { deleteApplication, appExport } from '@/api/application'
import CopyModal from './CopyModal'
import FunConfig from './FunConfig'
const { Header } = Layout;
@@ -28,6 +29,11 @@ const { Header } = Layout;
* Tab keys for application configuration
*/
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
const sharingTabKeys = [
'test',
// 'log',
'api'
]
/**
* Menu icon mapping
@@ -64,22 +70,23 @@ interface ConfigHeaderProps {
const ConfigHeader: FC<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh,
workflowRef,
appRef,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams();
const { id, source } = useParams();
const applicationModalRef = useRef<ApplicationModalRef>(null);
const copyModalRef = useRef<CopyModalRef>(null);
/**
* Format tab items for display
*/
const formatTabItems = () => {
return tabKeys.map(key => ({
const formatTabItems = useMemo(() => {
return (source === 'sharing' ? sharingTabKeys : tabKeys).map(key => ({
key,
label: t(`application.${key}`),
}))
}
}, [source, sharingTabKeys, tabKeys])
/**
* Handle menu item click
*/
@@ -160,6 +167,13 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
return items
}, [t, handleClick, application])
const funConfig = useMemo(() => {
return (appRef?.current?.funConfig || { file_type: [] }) as FunConfigForm
}, [appRef])
const handleSaveFunConfig = useCallback((value: FunConfigForm) => {
appRef?.current?.handleSaveFunConfig?.(value)
}, [appRef])
console.log('formatMenuItems', formatMenuItems)
return (
<>
@@ -170,7 +184,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
</div>
<div className="rb:max-w-[100%-80px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{application?.name}</div>
<Dropdown
{source !== 'sharing' && <Dropdown
menu={{ items: formatMenuItems, onClick: handleClick }}
trigger={['click']}
placement="bottomRight"
@@ -178,19 +192,20 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
<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')]"
></div>
</Dropdown>
</Dropdown>}
</div>
<div className="rb:flex rb:justify-center">
<Tabs
activeKey={activeTab}
items={formatTabItems()}
items={formatTabItems}
onChange={handleChangeTab}
className={styles.tabs}
/>
</div>
{application?.type === 'workflow'
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
{/* <FunConfig value={funConfig} refresh={handleSaveFunConfig} /> */}
<Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button>

View File

@@ -0,0 +1,182 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-11 15:42:13
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Radio, InputNumber, Flex, Switch, Row, Col } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import RbModal from '@/components/RbModal';
import type { FunConfigForm } from '../../types'
interface FileUploadSettingModalRef {
handleOpen: (values?: FileUploadSettings) => void;
handleClose: () => void;
}
interface FileUploadSettings extends Omit<FunConfigForm, 'enabled'> {}
interface FileUploadSettingModalProps {
onSave: (values: FileUploadSettings) => void;
}
const fileTypeOptions = [
{
type: 'document',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX',
defaultMaxCount: 1,
defaultMaxSize: 2
},
{
type: 'image',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG',
defaultMaxCount: 1,
defaultMaxSize: 2
},
{
type: 'audio',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
formats: 'MP3, M4A, WAV, AMR, MPGA',
defaultMaxCount: 1,
defaultMaxSize: 2
},
{
type: 'video',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
formats: 'MP4, MOV, MPEG, WEBM',
defaultMaxCount: 1,
defaultMaxSize: 2
},
];
const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadSettingModalProps>(({
onSave,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const values = Form.useWatch([], form)
const handleClose = () => {
setVisible(false);
form.resetFields();
};
const handleOpen = (values?: FileUploadSettings) => {
setVisible(true);
// if (values) {
// form.setFieldsValue(values);
// }
};
const handleSave = async () => {
const values = await form.validateFields();
onSave(values);
handleClose();
};
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('application.settings')}
open={visible}
onCancel={handleClose}
onOk={handleSave}
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={{
uploadType: 'both',
fileTypes: fileTypeOptions.map(opt => ({
type: opt.type,
enabled: false,
maxCount: opt.defaultMaxCount,
maxSize: opt.defaultMaxSize
}))
}}
>
<Form.Item
label={t('application.uploadType')}
name="uploadType"
>
<Radio.Group block buttonStyle="solid">
<Radio.Button value="local">{t('application.local')}</Radio.Button>
<Radio.Button value="url">URL</Radio.Button>
<Radio.Button value="both">{t('application.both')}</Radio.Button>
</Radio.Group>
</Form.Item>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
<Form.Item
name="maxCount"
label={t('application.maxCount')}
>
<InputNumber min={1} max={100} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item label={t('application.supportedTypes')}>
<Form.List name="fileTypes">
{(fields) => (
<Flex vertical gap={12}>
{fields.map((field, index) => {
const option = fileTypeOptions[index];
const isEnabled = values?.fileTypes?.[index]?.enabled;
return (
<div
key={field.key}
className={clsx("rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3", {
'rb:bg-[#f5f7fc]': isEnabled
})}
>
<Row gutter={12}>
<Col flex="36px" className="rb:self-center">
{option.icon}
</Col>
<Col flex="1">
<Flex align="center" justify="space-between">
<Flex vertical>
<div className="rb:font-medium">{t(`application.${option.type}`)}</div>
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats}</div>
</Flex>
<Form.Item name={[field.name, 'enabled']} valuePropName="checked" noStyle>
<Switch />
</Form.Item>
</Flex>
</Col>
</Row>
{isEnabled && (
<Flex align="center" gap={12} className="rb:mt-3! rb:pt-3! rb:border-t rb:border-[#DFE4ED]">
<div>{t('application.singleMaxSize')}: </div>
<Form.Item name={[field.name, 'maxSize']} noStyle>
<InputNumber min={1} max={500} suffix="MB" className="rb:flex-1" />
</Form.Item>
</Flex>
)}
<Form.Item name={[field.name, 'type']} hidden>
<input type="hidden" />
</Form.Item>
</div>
);
})}
</Flex>
)}
</Form.List>
</Form.Item>
</Form>
</RbModal>
);
});
export default FileUploadSettingModal;

View File

@@ -0,0 +1,140 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:20:30
*/
/**
* Copy Application Modal
* Allows users to duplicate an existing application with a new name
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Form, Button, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { FunConfigModalRef } from '../../types'
import RbModal from '@/components/RbModal'
import type { FunConfigForm } from '../../types'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import FileUploadSettingModal from './FileUploadSettingModal'
const FormItem = Form.Item;
interface FunConfigModalProps {
refresh: (value: FunConfigForm) => void;
}
/**
* Modal for copying applications
*/
const FunConfigModal = forwardRef<FunConfigModalRef, FunConfigModalProps>(({
refresh,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<FunConfigForm>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch([], form)
const fileUploadSettingModalRef = useRef<any>(null)
/** Close modal and reset form */
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
/** Open modal */
const handleOpen = (initValue: FunConfigForm) => {
setVisible(true);
form.setFieldsValue(initValue)
};
/** Copy application with new name */
const handleSave = () => {
setVisible(false);
setLoading(true)
const values = form.getFieldsValue()
refresh(values)
}
const handleOpenSettings = () => {
fileUploadSettingModalRef.current?.handleOpen(values)
}
const handleSaveSettings = (settings: any) => {
form.setFieldsValue(settings)
}
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<>
<RbModal
title={t('application.funConfig')}
open={visible}
onCancel={handleClose}
okText={t('common.copy')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Flex vertical gap={12}>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t(`memoryConversation.web_search`)}
name={['web_search', "enabled"]}
/>
</div>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.textTranfer')}
name={['textTranfer', "enabled"]}
desc={t('application.textTranferDesc')}
/>
</div>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.fileUpload')}
name={['fileUpload', "enabled"]}
desc={values?.fileUpload?.enabled ? undefined : t('application.fileUploadDesc')}
/>
{values?.fileUpload?.enabled && values?.fileTypes?.length > 0 ? <>
<div className="rb:grid rb:grid-cols-3 rb:gap-2 rb:text-[12px] rb:text-[#5B6167]">
<div>{t(`application.supportedTypes`)}</div>
<div>{t('application.maxCount')}</div>
<div>{t('application.singleMaxSize')}</div>
</div>
{values?.fileTypes?.filter(item => item.enabled).map(item => (
<div key={item.type} className="rb:grid rb:grid-cols-3 rb:gap-2">
<div>{t(`application.${item.type}`)}</div>
<div>{item.maxCount} {t('application.unix')}</div>
<div>{item.maxSize} MB</div>
</div>
))}
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
</> : null}
<FormItem name="fileTypes" noStyle hidden></FormItem>
<FormItem name="uploadType" noStyle hidden></FormItem>
</div>
</Flex>
</Form>
</RbModal>
<FileUploadSettingModal
ref={fileUploadSettingModalRef}
onSave={handleSaveSettings}
/>
</>
);
});
export default FunConfigModal;

View File

@@ -0,0 +1,50 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:20:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:20:21
*/
import { type FC, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'antd';
import FunConfigModal from './FunConfigModal'
import type { FunConfigModalRef, FunConfigForm } from '../../types'
/** Props for the FunConfig component */
interface FunConfigProps {
/** Current feature configuration values */
value: FunConfigForm;
/** Callback to propagate updated config back to the parent */
refresh: (value: FunConfigForm) => void;
}
const FunConfig: FC<FunConfigProps> = ({
value,
refresh
}) => {
const { t } = useTranslation();
// Ref used to imperatively open the config modal
const funConfigModalRef = useRef<FunConfigModalRef>(null)
/** Open the feature config modal pre-populated with the current values */
const handleFunConfig = () => {
console.log('funConfig', value)
funConfigModalRef.current?.handleOpen(value)
}
return (
<>
{/* Button that triggers the feature configuration modal */}
<Button onClick={handleFunConfig}>{t('application.funConfig')}</Button>
{/* Modal for editing feature settings; calls refresh on save */}
<FunConfigModal
ref={funConfigModalRef}
refresh={refresh}
/>
</>
)
}
export default FunConfig

View File

@@ -1,22 +1,24 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-12 10:23:18
*/
import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import ConfigHeader from './components/ConfigHeader'
import type { AgentRef, ClusterRef, WorkflowRef } from './types'
import type { AgentRef, ClusterRef, WorkflowRef, Config } from './types'
import type { Application } from '@/views/ApplicationManagement/types'
import Agent from './Agent'
import Api from './Api'
import ReleasePage from './ReleasePage'
import Cluster from './Cluster'
import { getApplication } from '@/api/application'
import { getApplication, getApplicationConfig, getMultiAgentConfig, getWorkflowConfig } from '@/api/application'
import Workflow from '@/views/Workflow';
import Statistics from './Statistics'
import TestChat from './TestChat'
import type { WorkflowConfig } from '@/views/Workflow/types';
/**
* Application configuration page component
@@ -25,7 +27,7 @@ import Statistics from './Statistics'
*/
const ApplicationConfig: React.FC = () => {
// Hooks
const { id } = useParams();
const { id, source } = useParams();
// Refs for different application types
const agentRef = useRef<AgentRef>(null)
@@ -36,6 +38,31 @@ const ApplicationConfig: React.FC = () => {
const [application, setApplication] = useState<Application | null>(null);
const [activeTab, setActiveTab] = useState('arrangement');
useEffect(() => {
setActiveTab(source === 'sharing' ? 'test' : 'arrangement')
}, [source])
const [config, setConfig] = useState<Config | WorkflowConfig | null>(null)
useEffect(() => {
if (source === 'sharing' && application?.type) {
getAppConfig()
}
}, [source, application?.type])
const getAppConfig = () => {
if (!id || !source || !application?.type) {
return
}
const request = application?.type === 'agent'
? getApplicationConfig
: application?.type === 'multi_agent'
? getMultiAgentConfig
: getWorkflowConfig
request(id as string).then(res => {
setConfig(res as Config | WorkflowConfig | null)
})
}
/**
* Handle tab change with auto-save for arrangement tab
* @param key - New tab key
@@ -94,6 +121,7 @@ const ApplicationConfig: React.FC = () => {
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
{activeTab === 'test' && <TestChat application={application} config={config} />}
</>
);
};

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:40:30
* @Last Modified time: 2026-03-13 17:01:04
*/
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types'
@@ -77,6 +77,8 @@ export interface Config extends MultiAgentConfig {
/** Last update timestamp */
updated_at: number;
skills?: SkillConfigForm | null;
funConfig?: FunConfigForm;
}
/**
@@ -127,6 +129,8 @@ export interface AgentRef {
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>;
funConfig: Config['funConfig'];
handleSaveFunConfig?: (value: FunConfigForm) => void;
}
/**
@@ -138,6 +142,8 @@ export interface ClusterRef {
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>;
funConfig: Config['funConfig'];
handleSaveFunConfig?: (value: FunConfigForm) => void;
}
/**
@@ -156,6 +162,8 @@ export interface WorkflowRef {
/** Add variable */
addVariable: () => void;
config: WorkflowConfig | null;
funConfig: WorkflowConfig['funConfig'];
handleSaveFunConfig?: (value: FunConfigForm) => void;
}
/**
@@ -400,4 +408,34 @@ export interface StatisticsData {
total_api_calls: number;
/** Total tokens used */
total_tokens: number;
}
export interface FileTypeConfig {
type: string;
enabled: boolean;
maxCount: number;
maxSize: number;
}
export interface FunConfigForm {
enabled: boolean;
fileTypes: FileTypeConfig[]
uploadType: 'local' | 'url' | 'both';
}
/**
* Function config modal ref methods
*/
export interface FunConfigModalRef {
/** Open function config modal */
handleOpen: (value: FunConfigForm) => void;
}
/**
* App sharing modal ref methods
*/
export interface AppSharingModalRef {
handleOpen: () => void;
}
export interface AppSharingForm {
target_workspace_ids: string[];
permission: 'readonly' | 'editable'
}

View File

@@ -0,0 +1,157 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 16:19:37
*/
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, App, Flex, Row, Col, Collapse, Tag } from 'antd';
import clsx from 'clsx';
import type { MySharedOutItem } from './types';
import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application'
const MySharing: React.FC = () => {
const { t } = useTranslation();
const { modal } = App.useApp();
const [data, setData] = useState<MySharedOutItem[]>([])
useEffect(() => { getList() }, [])
const getList = () => {
mySharedOutList().then(res => setData(res as MySharedOutItem[]))
}
/** Group items by target_workspace_id */
const grouped = useMemo(() => {
const map = new Map<string, { workspace: Pick<MySharedOutItem, 'target_workspace_id' | 'target_workspace_name' | 'target_workspace_icon'>, items: MySharedOutItem[] }>();
data.forEach(item => {
if (!map.has(item.target_workspace_id)) {
map.set(item.target_workspace_id, {
workspace: {
target_workspace_id: item.target_workspace_id,
target_workspace_name: item.target_workspace_name,
target_workspace_icon: item.target_workspace_icon,
},
items: [],
});
}
map.get(item.target_workspace_id)!.items.push(item);
});
return Array.from(map.values());
}, [data]);
const handleAllCancel = (workspace: { target_workspace_name: string; target_workspace_id: string; }) => {
modal.confirm({
title: t('application.confirmWorkspaceCancelShareDesc', { workspace: workspace.target_workspace_name }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
cancelSpaceShare(workspace.target_workspace_id)
.then(() => {
getList();
})
}
});
};
const handleCancelOne = (item: MySharedOutItem) => {
modal.confirm({
title: t('application.confirmAppCancelShareDesc', { app: item.source_app_name, workspace: item.target_workspace_name }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
cancelShare(item.source_app_id, item.target_workspace_id)
.then(() => {
getList();
})
}
});
};
return (
<Flex vertical gap={12}>
{grouped.map(({ workspace, items }) => (
<Collapse
key={workspace.target_workspace_id}
defaultActiveKey={[workspace.target_workspace_id]}
items={[{
key: workspace.target_workspace_id,
label: (
<Flex align="center" gap={12}>
{workspace.target_workspace_icon
? <img src={workspace.target_workspace_icon} className="rb:w-8 rb:h-8 rb:rounded-lg rb:object-cover" />
: <div className="rb:w-8 rb:h-8 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{workspace.target_workspace_name[0]}
</div>
}
<span className="rb:font-medium">{workspace.target_workspace_name}</span>
<Tag color="blue">{t('application.appCount', { count: items.length })}</Tag>
</Flex>
),
extra: (
<Button
size="small"
danger
onClick={e => { e.stopPropagation(); handleAllCancel(workspace); }}
>
{t('application.allCancel')}
</Button>
),
children: (
<Row gutter={[12, 12]}>
{items.map(item => (
<Col key={item.id} span={6} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-3! rb:px-4! rb:relative">
<div
className="rb:absolute rb:top-3 rb:right-3 rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/close.svg')]"
onClick={() => handleCancelOne(item)}
/>
<Flex gap={8} align="center">
<div className="rb:size-7 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{item.source_app_name[0]}
</div>
<div className="rb:font-medium">{item.source_app_name}</div>
</Flex>
<Flex vertical gap={4} className="rb:mt-3! rb:text-[12px]!">
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.type')}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': item.source_app_type === 'agent',
'rb:text-[#369F21] rb:font-medium': item.source_app_type === 'multi_agent',
})}>
{t(`application.${item.source_app_type}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.version')}</span>
<span>{item.source_app_version}</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.permission')}</span>
<span className={clsx({
'rb:text-[#369F21] rb:font-medium': item.permission === 'editable',
'rb:text-[#5B6167] rb:font-medium': item.permission === 'readonly',
})}>
{t(`application.${item.permission}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.souceStatus')}</span>
<span>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</span>
</Flex>
</Flex>
</Col>
))}
</Row>
),
}]}
/>
))}
</Flex>
);
};
export default MySharing;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-02 17:48:51
* @Last Modified time: 2026-03-13 17:03:40
*/
/**
* Application Management Page
@@ -10,9 +10,9 @@
* Supports creating, editing, and deleting applications
*/
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App, Select, Space, Dropdown } from 'antd';
import { Button, App, Select, Space, Dropdown, type SegmentedProps, Flex, Form } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom'
@@ -21,12 +21,16 @@ import ApplicationModal, { types } from './components/ApplicationModal';
import type { Application, ApplicationModalRef, Query, UploadWorkflowModalRef } from './types';
import SearchInput from '@/components/SearchInput'
import RbCard from '@/components/RbCard/Card'
import { getApplicationListUrl, deleteApplication } from '@/api/application'
import { getApplicationListUrl, deleteApplication, copyApplication } from '@/api/application'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format';
import UploadWorkflowModal from './components/UploadWorkflowModal'
import UploadModal from './components/UploadModal'
import PageTabs from '@/components/PageTabs'
import MySharing from './MySharing'
const tabKeys = ['apps', 'sharing', 'myShare']
/**
* Application management main component
*/
@@ -34,20 +38,19 @@ const ApplicationManagement: React.FC = () => {
const { t } = useTranslation();
const { modal } = App.useApp();
const [searchParams] = useSearchParams()
const [query, setQuery] = useState<Query>({} as Query);
const applicationModalRef = useRef<ApplicationModalRef>(null);
const scrollListRef = useRef<PageScrollListRef>(null)
const uploadWorkflowModalRef = useRef<UploadWorkflowModalRef>(null);
const uploadModalRef = useRef<UploadWorkflowModalRef>(null);
const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form)
const [activeTab, setActiveTab] = useState('apps');
useEffect(() => {
// Convert URLSearchParams to a plain object for easier access
const data = Object.fromEntries(searchParams)
const { type } = data
setQuery(prev => ({
...prev,
type: type || undefined
}))
form.setFieldValue('type', type || undefined)
}, [searchParams])
/** Refresh application list */
@@ -61,7 +64,11 @@ const ApplicationManagement: React.FC = () => {
}
/** Navigate to application configuration page */
const handleEdit = (item: Application) => {
window.open(`/#/application/config/${item.id}`);
let url = `/#/application/config/${item.id}`
if (item.is_shared) {
url += `/${activeTab}`
}
window.open(url);
}
/** Delete application with confirmation */
const handleDelete = (item: Application) => {
@@ -81,9 +88,6 @@ const ApplicationManagement: React.FC = () => {
}
})
}
const handleChangeType = (value?: string) => {
setQuery(prev => ({...prev, type: value}))
}
const handleImport = () => {
uploadWorkflowModalRef.current?.handleOpen()
@@ -97,90 +101,137 @@ const ApplicationManagement: React.FC = () => {
uploadModalRef.current?.handleOpen()
}
}
const formatTabItems = useMemo(() => {
return tabKeys.map(value => ({
value,
label: t(`application.${value}`),
}))
}, [tabKeys, t])
/** Handle tab change */
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
form.resetFields()
}
const handleCopy = (item: Application) => {
modal.confirm({
title: t('application.confirmCopyDesc', { app: item.name }),
okText: t('common.copy'),
cancelText: t('common.cancel'),
onOk: () => {
copyApplication(item.id)
.then(() => {
setActiveTab('apps')
})
}
});
}
return (
<>
<Row gutter={16} className="rb:mb-4">
<Col span={4}>
<Select
value={query.type}
placeholder={t('application.applicationType')}
options={types.map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
allowClear
className="rb:w-full"
onChange={handleChangeType}
/>
</Col>
<Col span={8}>
<SearchInput
placeholder={t('application.searchPlaceholder')}
onSearch={(value) => setQuery({ search: value })}
style={{width: '100%'}}
/>
</Col>
<Col span={12} className="rb:text-right">
<Space size={12}>
<Dropdown
menu={{ items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
{ key: 'import', label: t('application.import') },
], onClick: handleClick }}
placement="bottomRight"
<Flex justify="space-between" className="rb:mb-3!">
<PageTabs
value={activeTab}
options={formatTabItems}
onChange={handleChangeTab}
/>
<Form
form={form}
initialValues={{}}
>
{activeTab !== 'myShare' &&
<Space size={8}>
<Form.Item name="type" noStyle>
<Select
placeholder={t('application.applicationType')}
options={types.map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
allowClear
className="rb:w-30"
/>
</Form.Item>
<Form.Item name="search" noStyle>
<SearchInput
placeholder={t('application.searchPlaceholder')}
className="rb:w-75!"
/>
</Form.Item>
{activeTab === 'apps' && <>
<Dropdown
menu={{
items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
{ key: 'import', label: t('application.import') },
], onClick: handleClick
}}
placement="bottomRight"
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}>
{t('application.createApplication')}
</Button>
</>}
</Space>
}
</Form>
</Flex>
{(activeTab === 'apps' || activeTab === 'sharing') &&
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={{ ...query, include_shared: activeTab === 'sharing' }}
renderItem={(item) => (
<RbCard
title={item.name}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}>
{t('application.createApplication')}
</Button>
</Space>
</Col>
</Row>
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={query}
renderItem={(item) => (
<RbCard
title={item.name}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
{['type', 'source', 'created_at'].map((key, index) => (
<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({
'rb:text-[#155EEF] rb:font-medium': key === 'type' && item[key] === 'agent',
'rb:text-[#369F21] rb:font-medium': key === 'type' && item[key] === 'multi_agent',
{['type', 'source', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
{key === 'source' && item.is_shared
? t('application.shared')
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`)
}
</span>
</div>
))}
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': key === 'type' && item[key] === 'agent',
'rb:text-[#369F21] rb:font-medium': key === 'type' && item[key] === 'multi_agent',
})}>
{key === 'source' && item.is_shared
? item.source_workspace_name
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`)
}
</span>
</div>
))}
<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>
</RbCard>
)}
/>
{item.is_shared
? <div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost block onClick={() => handleEdit(item)}>{t('common.view')}</Button>
{item.share_permission === 'editable' && <Button type="primary" className="rb:w-[calc(100%-46px)]" onClick={() => handleCopy(item)}>{t('common.copy')}</Button>}
</div>
: <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>
}
</RbCard>
)}
/>
}
{activeTab === 'myShare' && <MySharing />}
<ApplicationModal
ref={applicationModalRef}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:16:03
* @Last Modified time: 2026-03-13 17:01:06
*/
/**
* Type definitions for Application Management
@@ -15,6 +15,7 @@ export interface Query {
/** Search keyword */
search: string;
type?: string;
include_shared?: boolean;
}
/**
@@ -53,6 +54,11 @@ export interface Application {
created_at: number;
/** Last update timestamp */
updated_at: number;
share_permission?: string;
source_workspace_name?: string;
source_workspace_icon?: string;
source_app_version?: string;
source_app_is_active?: boolean;
}
/**
@@ -241,4 +247,20 @@ export interface UploadWorkflowModalRef {
export interface UploadModalRef {
/** Open the upload workflow modal */
handleOpen: () => void;
}
export interface MySharedOutItem {
id: string;
source_app_id: string;
source_workspace_id: string;
target_workspace_id: string;
shared_by: string;
permission: 'readonly' | 'editable';
created_at: number;
updated_at: number;
source_app_name: string;
source_app_type: string;
source_app_version: string;
source_app_is_active: boolean;
target_workspace_name: string;
target_workspace_icon: string;
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-24 17:57:08
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:48:09
* @Last Modified time: 2026-03-12 13:39:24
*/
/*
* Runtime Component
@@ -225,12 +225,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
</div>
)
: <>
{item.error
? <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
{item.error &&
<div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mb-2 rb:-mt-4", getStatus('failed'))}>
<Markdown content={item.error} />
</div>
: renderChild(item.subContent)
}</>
</div>
}
{renderChild(item.subContent)}
</>
)
}]}
/>

View File

@@ -8,7 +8,6 @@ import RbModal from '@/components/RbModal'
interface VariableEditModalProps {
refresh: (values: Variable[]) => void;
variables: Variable[]
}
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({

View File

@@ -60,7 +60,8 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
handleRun,
graphRef,
addVariable,
config
config,
funConfig: config?.funConfig
}))
return (
<div className="rb:h-[calc(100vh-64px)] rb:relative">

View File

@@ -2,6 +2,7 @@
import { Graph } from '@antv/x6';
import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
import type { Variable } from './components/Properties/VariableList/types'
import type { FunConfigForm } from '@/views/ApplicationConfig/types'
export interface NodeConfig {
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
placeholder?: string;
@@ -89,6 +90,8 @@ export interface WorkflowConfig {
is_active: boolean;
created_at: number;
updated_at: number;
funConfig?: FunConfigForm;
}
export interface ChatRef {