Merge pull request #322 from SuanmoSuanyangTechnology/feature/skill_zy
feat(web): add skills menu
This commit is contained in:
30
web/src/api/skill.ts
Normal file
30
web/src/api/skill.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:28:44
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:28:44
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { SkillFormData } from '@/views/Skills/types'
|
||||
|
||||
// Get skill list
|
||||
export const getSkillListUrl = '/skills'
|
||||
export const getSkillList = (data?: any) => {
|
||||
return request.get(getSkillListUrl, data)
|
||||
}
|
||||
// Get skill details
|
||||
export const getSkillDetail = (skill_id: string, data?: any) => {
|
||||
return request.get(`/skills/${skill_id}`, data)
|
||||
}
|
||||
// Create skill
|
||||
export const createSkill = (data: SkillFormData) => {
|
||||
return request.post('/skills', data)
|
||||
}
|
||||
// Update skill
|
||||
export const updateSkill = (skill_id: string, data: SkillFormData) => {
|
||||
return request.put(`/skills/${skill_id}`, data)
|
||||
}
|
||||
// Delete skill
|
||||
export const deleteSkill = (skill_id: string) => {
|
||||
return request.delete(`/skills/${skill_id}`)
|
||||
}
|
||||
14
web/src/assets/images/menu/skills.svg
Normal file
14
web/src/assets/images/menu/skills.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>技能点</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页-超级管理员" transform="translate(-46, -456)" fill-rule="nonzero" stroke="#5B6167" stroke-width="1.1">
|
||||
<g id="技能点" transform="translate(46, 456)">
|
||||
<g id="编组-21" transform="translate(1, 1)">
|
||||
<path d="M7,0.55 C8.78106,0.55 10.3935572,1.27197468 11.5607913,2.43920873 C12.7280253,3.60644278 13.45,5.21894 13.45,7 C13.45,8.78106 12.7280253,10.3935572 11.5607913,11.5607913 C10.3935572,12.7280253 8.78106,13.45 7,13.45 C5.21894,13.45 3.60644278,12.7280253 2.43920873,11.5607913 C1.27197468,10.3935572 0.55,8.78106 0.55,7 C0.55,5.21894 1.27197468,3.60644278 2.43920873,2.43920873 C3.60644278,1.27197468 5.21894,0.55 7,0.55 Z" id="路径"></path>
|
||||
<polygon id="路径" stroke-linejoin="round" points="6.66706155 11 6.44510258 7.80088942 4 7.80088942 7.55505896 3 7.55505896 6.20059295 10 6.4285479"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
14
web/src/assets/images/menu/skills_active.svg
Normal file
14
web/src/assets/images/menu/skills_active.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>技能点备份</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页-超级管理员" transform="translate(-22, -456)" fill-rule="nonzero" stroke="#212332" stroke-width="1.1">
|
||||
<g id="技能点备份" transform="translate(22, 456)">
|
||||
<g id="编组-21" transform="translate(1, 1)">
|
||||
<path d="M7,0.55 C8.78106,0.55 10.3935572,1.27197468 11.5607913,2.43920873 C12.7280253,3.60644278 13.45,5.21894 13.45,7 C13.45,8.78106 12.7280253,10.3935572 11.5607913,11.5607913 C10.3935572,12.7280253 8.78106,13.45 7,13.45 C5.21894,13.45 3.60644278,12.7280253 2.43920873,11.5607913 C1.27197468,10.3935572 0.55,8.78106 0.55,7 C0.55,5.21894 1.27197468,3.60644278 2.43920873,2.43920873 C3.60644278,1.27197468 5.21894,0.55 7,0.55 Z" id="路径"></path>
|
||||
<polygon id="路径" stroke-linejoin="round" points="6.66706155 11 6.44510258 7.80088942 4 7.80088942 7.55505896 3 7.55505896 6.20059295 10 6.4285479"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
45
web/src/components/Layout/BasicAuthLayout.tsx
Normal file
45
web/src/components/Layout/BasicAuthLayout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:12:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 14:06:28
|
||||
*/
|
||||
/**
|
||||
* BasicLayout Component
|
||||
*
|
||||
* A minimal layout wrapper that provides:
|
||||
* - User information initialization
|
||||
* - Storage type initialization
|
||||
* - Simple container for child routes without navigation UI
|
||||
*
|
||||
* Used for pages that don't require sidebar/header (e.g., login, public pages).
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useEffect, type FC } from 'react';
|
||||
|
||||
import { useUser } from '@/store/user';
|
||||
|
||||
/**
|
||||
* Basic layout component for pages without navigation UI.
|
||||
* Fetches user info and storage type on mount, then renders child routes.
|
||||
*/
|
||||
const BasicLayout: FC = () => {
|
||||
const { getUserInfo } = useUser();
|
||||
|
||||
// Fetch user information and storage type on component mount
|
||||
useEffect(() => {
|
||||
getUserInfo();
|
||||
}, [getUserInfo]);
|
||||
|
||||
return (
|
||||
<div className="rb:relative rb:h-full rb:w-full">
|
||||
{/* Render child routes without additional UI */}
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default BasicLayout;
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:21:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:21:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 13:49:05
|
||||
*/
|
||||
/**
|
||||
* RbCard Component
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import { Card, Tooltip, Flex } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/** Props interface for RbCard component */
|
||||
@@ -51,6 +51,7 @@ interface RbCardProps {
|
||||
className?: string;
|
||||
/** Click handler */
|
||||
onClick?: () => void;
|
||||
variant?: 'borderL';
|
||||
}
|
||||
|
||||
/** Custom card component with flexible styling and header options */
|
||||
@@ -68,6 +69,7 @@ const RbCard: FC<RbCardProps> = ({
|
||||
bgColor = '#FBFDFF',
|
||||
height = 'auto',
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}) => {
|
||||
/** Calculate body padding based on header type and avatar presence */
|
||||
@@ -82,7 +84,45 @@ const RbCard: FC<RbCardProps> = ({
|
||||
: (headerType === 'border' && !avatarUrl && !avatar) || headerType === 'borderBL'
|
||||
? 'rb:p-[16px_16px_20px_16px]!'
|
||||
: ''
|
||||
|
||||
|
||||
if (variant === 'borderL') {
|
||||
return (
|
||||
<div
|
||||
className="rb:p-[12px_16px] rb:rounded-lg rb:shadow-[inset_4px_0px_0px_0px_#155EEF] rb:border rb:border-[#DFE4ED]"
|
||||
>
|
||||
<Flex justify="space-between" className={`rb:mb-3! ${headerClassName || ''}`}>
|
||||
<Flex vertical gap={4}>
|
||||
<div className="rb:font-medium rb:leading-5.5">
|
||||
{typeof title === 'function' ? title() : title ?
|
||||
<div className="rb:flex rb:items-center">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
|
||||
: avatar ? avatar : null
|
||||
}
|
||||
<div className={
|
||||
clsx(
|
||||
{
|
||||
'rb:max-w-full': !avatarUrl && !avatar,
|
||||
'rb:max-w-[calc(100%-60px)]': avatarUrl || avatar,
|
||||
}
|
||||
)
|
||||
}>
|
||||
<div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div>
|
||||
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
{subTitle && <div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{subTitle}</div>}
|
||||
</Flex>
|
||||
{extra}
|
||||
</Flex>
|
||||
<div className={bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-0!'}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
{...props}
|
||||
@@ -126,7 +166,7 @@ const RbCard: FC<RbCardProps> = ({
|
||||
},
|
||||
headerClassName,
|
||||
),
|
||||
body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-[0]!',
|
||||
body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-0!',
|
||||
}}
|
||||
style={{
|
||||
background: bgColor,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:25:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:25:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 13:49:16
|
||||
*/
|
||||
/**
|
||||
* SiderMenu Component
|
||||
@@ -67,6 +67,8 @@ import ontologyIcon from '@/assets/images/menu/ontology.svg'
|
||||
import ontologyActiveIcon from '@/assets/images/menu/ontology_active.svg'
|
||||
import promptIcon from '@/assets/images/menu/prompt.svg'
|
||||
import promptActiveIcon from '@/assets/images/menu/prompt_active.svg'
|
||||
import skillsIcon from '@/assets/images/menu/skills.svg'
|
||||
import skillsActiveIcon from '@/assets/images/menu/skills_active.svg'
|
||||
|
||||
/** Icon path mapping table for menu items (normal and active states) */
|
||||
const iconPathMap: Record<string, string> = {
|
||||
@@ -102,6 +104,8 @@ const iconPathMap: Record<string, string> = {
|
||||
'ontologyActive': ontologyActiveIcon,
|
||||
'prompt': promptIcon,
|
||||
'promptActive': promptActiveIcon,
|
||||
'skills': skillsIcon,
|
||||
'skillsActive': skillsActiveIcon,
|
||||
};
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
@@ -115,6 +115,7 @@ export const en = {
|
||||
spaceConfig: 'Space Configuration',
|
||||
ontology: 'Ontology Engineering',
|
||||
prompt: 'Prompt Engineering',
|
||||
skills: 'Skill Library',
|
||||
},
|
||||
dashboard: {
|
||||
total_models: 'Available Models',
|
||||
@@ -1248,6 +1249,22 @@ export const en = {
|
||||
daily_new_users: 'Daily New Users',
|
||||
daily_api_calls: 'Daily API Calls',
|
||||
daily_tokens: 'Token Consumption',
|
||||
|
||||
skill: 'Skill Configuration',
|
||||
skillTitle: 'Configure Agent skills and matching modes',
|
||||
skillHelp: 'Help Center',
|
||||
addSkill: 'Add Skill',
|
||||
dynamicBindingSkill: 'Dynamic Optional Skills',
|
||||
dynamicBindingSkill_subTitle: 'Skill pool that Agent can automatically match based on tasks',
|
||||
dynamicBindingSkill_empty: 'No dynamic skills configured yet, click the button above to add or enable "Allow access to all skills"',
|
||||
chooseSkill: 'Choose Skill',
|
||||
allSkill: 'Allow access to all skills',
|
||||
allSkillIntro: 'Access to all skills enabled, Agent will automatically match optimal skills based on tasks',
|
||||
executeProcessPreview: 'Execution Process Preview',
|
||||
receiveTask: 'Receive Task',
|
||||
analyTask: 'Analyze Task Intent',
|
||||
dynamicMatchSkill: 'Dynamic Match Skill',
|
||||
executeTask: 'Execute Task',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -2494,5 +2511,28 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
initialInput: 'Original Input',
|
||||
saveTitle: 'Title',
|
||||
},
|
||||
skills: {
|
||||
searchPlaceholder: 'Search skills',
|
||||
create: 'Add Skill',
|
||||
mainfest: 'Define Encapsulation Container',
|
||||
name: 'Skill Name',
|
||||
description: 'Brief Description',
|
||||
descriptionPlaceholder: 'Describe the intent and purpose of the skill...',
|
||||
keywords: 'Keywords',
|
||||
promptConfiguration: 'Inject Experience Logic',
|
||||
aiPrompt: 'AI Experience Refinement',
|
||||
prompt_type: 'System Instructions / Expert Knowledge',
|
||||
promptPlaceholder: 'Enter system instructions or expert knowledge...',
|
||||
save: 'Save',
|
||||
AIPromptAssistant: 'AI Experience Refinement',
|
||||
model: 'Model',
|
||||
promptChatEmpty: 'No conversation content available',
|
||||
you: 'You',
|
||||
ai: 'AI Assistant',
|
||||
promptChatPlaceholder: 'Describe your requirements...',
|
||||
conversationOptimizationPrompt: 'Refined Content',
|
||||
apply: 'Apply',
|
||||
tools: 'Tools',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -114,6 +114,7 @@ export const zh = {
|
||||
spaceConfig: '空间配置',
|
||||
ontology: '本体工程',
|
||||
prompt: '提示词工程',
|
||||
skills: '技能库',
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: '首页',
|
||||
@@ -667,6 +668,22 @@ export const zh = {
|
||||
daily_new_users: '新增用户数',
|
||||
daily_api_calls: '调用次数',
|
||||
daily_tokens: 'Token消耗',
|
||||
|
||||
skill: '技能配置',
|
||||
skillTitle: '配置 Agent 可使用的技能及匹配模式',
|
||||
skillHelp: '帮助中心',
|
||||
addSkill: '添加技能',
|
||||
dynamicBindingSkill: '动态可选技能',
|
||||
dynamicBindingSkill_subTitle: 'Agent 可根据任务自动匹配的技能池',
|
||||
dynamicBindingSkill_empty: '暂未配置动态技能,点击上方按钮添加或开启"允许访问所有技能"',
|
||||
chooseSkill: '选择技能',
|
||||
allSkill: '允许访问所有技能',
|
||||
allSkillIntro: '已开启访问所有技能,Agent 将根据任务自动匹配最优技能',
|
||||
executeProcessPreview: '执行流程预览',
|
||||
receiveTask: '收到任务',
|
||||
analyTask: '分析任务意图',
|
||||
dynamicMatchSkill: '动态匹配技能',
|
||||
executeTask: '执行任务',
|
||||
},
|
||||
role: {
|
||||
roleManagement: '角色管理',
|
||||
@@ -2583,5 +2600,28 @@ export const zh = {
|
||||
initialInput: '原始输入',
|
||||
saveTitle: '标题',
|
||||
},
|
||||
skills: {
|
||||
searchPlaceholder: '搜索技能',
|
||||
create: '添加技能',
|
||||
mainfest: '定义封装容器',
|
||||
name: '技能名称',
|
||||
description: '简要描述',
|
||||
descriptionPlaceholder: '描述技能的意图和用途...',
|
||||
keywords: '关键词',
|
||||
promptConfiguration: '注入经验逻辑',
|
||||
aiPrompt: 'AI 经验提炼',
|
||||
prompt_type: '系统指令 / 专家知识',
|
||||
promptPlaceholder: '输入系统指令或专家知识...',
|
||||
save: '保存',
|
||||
AIPromptAssistant: 'AI 经验提炼',
|
||||
model: '模型',
|
||||
promptChatEmpty: '目前没有对话内容',
|
||||
you: '你',
|
||||
ai: 'AI 助手',
|
||||
promptChatPlaceholder: '描述你的需求...',
|
||||
conversationOptimizationPrompt: '提炼内容',
|
||||
apply: '应用',
|
||||
tools: '工具',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -51,6 +51,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
|
||||
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
|
||||
NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
|
||||
BasicAuthLayout: lazy(() => import('@/components/Layout/BasicAuthLayout')),
|
||||
/** View components */
|
||||
Index: lazy(() => import('@/views/Index')),
|
||||
Home: lazy(() => import('@/views/Home')),
|
||||
@@ -87,6 +88,8 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
Ontology: lazy(() => import('@/views/Ontology')),
|
||||
OntologyDetail: lazy(() => import('@/views/Ontology/pages/Detail')),
|
||||
Prompt: lazy(() => import('@/views/Prompt')),
|
||||
Skills: lazy(() => import('@/views/Skills')),
|
||||
SkillConfig: lazy(() => import('@/views/Skills/pages/SkillConfig')),
|
||||
Jump: lazy(() => import('@/views/JumpPage')),
|
||||
Login: lazy(() => import('@/views/Login')),
|
||||
InviteRegister: lazy(() => import('@/views/InviteRegister')),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "/pricing", "element": "Pricing" },
|
||||
{ "path": "/order-pay", "element": "OrderPayment" },
|
||||
{ "path": "/orders", "element": "OrderHistory" },
|
||||
{ "path": "/skills", "element": "Skills" },
|
||||
{ "path": "/no-permission", "element": "NoPermission" }
|
||||
]
|
||||
},
|
||||
@@ -50,6 +51,13 @@
|
||||
{ "path": "/ontology/:id", "element": "OntologyDetail" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"element": "BasicAuthLayout",
|
||||
"children": [
|
||||
{ "path": "/skills/add", "element": "SkillConfig" },
|
||||
{ "path": "/skills/config/:id", "element": "SkillConfig" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"element": "NoAuthLayout",
|
||||
"children": [
|
||||
|
||||
@@ -52,6 +52,21 @@
|
||||
"sort": 0,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"parent": 0,
|
||||
"code": "skills",
|
||||
"label": "技能库",
|
||||
"i18nKey": "menu.skills",
|
||||
"path": "/skills",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"parent": 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:29:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 20:16:45
|
||||
*/
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
@@ -39,6 +39,8 @@ import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
||||
import AiPromptModal from './components/AiPromptModal'
|
||||
import ToolList from './components/ToolList/ToolList'
|
||||
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
|
||||
import SkillList from './components/Skill'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
|
||||
/**
|
||||
* Description wrapper component
|
||||
@@ -164,6 +166,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
setLoading(true)
|
||||
getApplicationConfig(id as string).then(res => {
|
||||
const response = res as Config
|
||||
const { skills } = response
|
||||
let allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
|
||||
let allTools = Array.isArray(response.tools) ? response.tools : []
|
||||
const memoryContent = response.memory?.memory_content
|
||||
const parsedMemoryContent = memoryContent === null || memoryContent === ''
|
||||
@@ -175,6 +179,10 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
memory: {
|
||||
...response.memory,
|
||||
memory_content: parsedMemoryContent
|
||||
},
|
||||
skills: {
|
||||
...skills,
|
||||
skill_ids: allSkills
|
||||
}
|
||||
})
|
||||
setData({
|
||||
@@ -252,7 +260,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
*/
|
||||
const handleSave = (flag = true) => {
|
||||
if (!isSave || !data) return Promise.resolve()
|
||||
const { memory, knowledge_retrieval, tools, ...rest } = values
|
||||
const { memory, knowledge_retrieval, tools, skills, ...rest } = values
|
||||
const { knowledge_bases = [], ...knowledgeRest } = knowledge_retrieval || {}
|
||||
const { memory_content } = memory || {}
|
||||
// Get other necessary properties of memory from original data
|
||||
@@ -278,7 +286,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
tool_id: vo.tool_id,
|
||||
operation: vo.operation,
|
||||
enabled: vo.enabled
|
||||
}))
|
||||
})),
|
||||
skills: {
|
||||
...skills,
|
||||
skill_ids: (skills?.skill_ids as Skill[])?.map(vo => vo.id)
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -438,12 +450,16 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Form.Item name="variables">
|
||||
<Form.Item name="variables" noStyle>
|
||||
<VariableList />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item name="skills" noStyle>
|
||||
<SkillList />
|
||||
</Form.Item>
|
||||
|
||||
{/* Tool Configuration */}
|
||||
<Form.Item name="tools">
|
||||
<Form.Item name="tools" noStyle>
|
||||
<ToolList />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:44
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:26:44
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:31:12
|
||||
*/
|
||||
/**
|
||||
* AI Prompt Assistant Modal
|
||||
@@ -38,7 +38,8 @@ interface AiPromptModalProps {
|
||||
/** Callback to refresh prompt with optimized value */
|
||||
refresh: (value: string) => void;
|
||||
/** Default model to pre-select */
|
||||
defaultModel: ModelListItem | null;
|
||||
defaultModel?: ModelListItem | null;
|
||||
source?: 'app' | 'skills'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ interface AiPromptModalProps {
|
||||
const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
refresh,
|
||||
defaultModel,
|
||||
source = 'application'
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
@@ -92,11 +94,11 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
const handleSend = () => {
|
||||
if (!promptSession) return
|
||||
if (!values.model_id) {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('application.model') }))
|
||||
message.warning(t('common.selectPlaceholder', { title: t(`${source}.model`) }))
|
||||
return
|
||||
}
|
||||
if (!values.message) {
|
||||
message.warning(t('application.promptChatPlaceholder'))
|
||||
message.warning(t(`${source}.promptChatPlaceholder`))
|
||||
return
|
||||
}
|
||||
const messageContent = values.message
|
||||
@@ -144,15 +146,10 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
}
|
||||
})
|
||||
};
|
||||
updatePromptMessages(promptSession, values, handleStreamMessage)
|
||||
// .then(res => {
|
||||
// const response = res as { prompt: string; desc: string; variables: string[] }
|
||||
// form.setFieldsValue({ current_prompt: response.prompt })
|
||||
// setChatList(prev => {
|
||||
// return [...prev, { role: 'assistant', content: response.desc }]
|
||||
// })
|
||||
// setVariables(response.variables)
|
||||
// })
|
||||
updatePromptMessages(promptSession, {
|
||||
...values,
|
||||
skill: source === 'skills'
|
||||
}, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -192,7 +189,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
console.log(values)
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.AIPromptAssistant')}
|
||||
title={t(`${source}.AIPromptAssistant`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
@@ -202,7 +199,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
<div className="rb:grid rb:grid-cols-2 rb:border-t rb:border-t-[#EBEBEB]">
|
||||
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-6 rb:pt-3">
|
||||
<Form.Item
|
||||
label={t('application.model')}
|
||||
label={t(`${source}.model`)}
|
||||
name="model_id"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
@@ -219,18 +216,18 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
<ChatContent
|
||||
classNames="rb:h-100.5 rb:px-[16px] rb:py-[20px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]"
|
||||
contentClassNames="rb:max-w-[260px]!"
|
||||
empty={<Empty url={ConversationEmptyIcon} title={t('application.promptChatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chatList || []}
|
||||
streamLoading={false}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('application.you') : t('application.ai')}
|
||||
labelFormat={(item) => item.role === 'user' ? t(`${source}.you`) : t(`${source}.ai`)}
|
||||
/>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:py-4">
|
||||
<Form.Item name="message" className="rb:mb-0!" style={{ width: 'calc(100% - 54px)' }}>
|
||||
<Input
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('application.promptChatPlaceholder')}
|
||||
placeholder={t(`${source}.promptChatPlaceholder`)}
|
||||
onPressEnter={handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -242,12 +239,12 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
|
||||
<div className="rb:pl-6 rb:pt-3">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t('application.conversationOptimizationPrompt')}></Form.Item>
|
||||
</Col>
|
||||
<Col span={12} className="rb:text-right">
|
||||
<Button onClick={handleAdd}>+ {t('application.addVariable')}</Button>
|
||||
<Col span={source === 'application' ? 12 : 24}>
|
||||
<Form.Item label={t(`${source}.conversationOptimizationPrompt`)}></Form.Item>
|
||||
</Col>
|
||||
{source === 'application' && <Col span={12} className="rb:text-right">
|
||||
<Button onClick={handleAdd}>+ {t(`${source}.addVariable`)}</Button>
|
||||
</Col>}
|
||||
</Row>
|
||||
<Form.Item name="current_prompt">
|
||||
<Editor
|
||||
@@ -258,7 +255,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
</Form.Item>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
||||
<Button block disabled={!values?.current_prompt} onClick={handleCopy}>{t('common.copy')}</Button>
|
||||
<Button type="primary" block disabled={!values?.current_prompt} onClick={handleApply}>{t('application.apply')}</Button>
|
||||
<Button type="primary" block disabled={!values?.current_prompt} onClick={handleApply}>{t(`${source}.apply`)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:27:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 13:50:47
|
||||
*/
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
@@ -20,6 +20,7 @@ interface CardProps {
|
||||
children: ReactNode;
|
||||
/** Extra content in header */
|
||||
extra?: ReactNode;
|
||||
variant?: 'borderL';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,14 +32,16 @@ const Card: FC<CardProps> = ({
|
||||
subTitle,
|
||||
children,
|
||||
extra,
|
||||
variant
|
||||
}) => {
|
||||
return (
|
||||
<RbCard
|
||||
title={title}
|
||||
subTitle={subTitle}
|
||||
extra={extra}
|
||||
variant={variant}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]! rb:before:h-[19px]"
|
||||
headerClassName={variant ? '' : "rb:before:bg-[#155EEF]! rb:before:h-[19px]"}
|
||||
>
|
||||
{children}
|
||||
</RbCard>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:45:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:45:08
|
||||
*/
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Space, List, Flex, Tooltip } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { SkillModalRef } from './types'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getSkillList } from '@/api/skill'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
/**
|
||||
* Props for SkillListModal Component
|
||||
*/
|
||||
interface SkillModalProps {
|
||||
/** Callback function to refresh parent component with selected skills */
|
||||
refresh: (rows: Skill[], type: 'skill') => void;
|
||||
/** Array of already selected skills to exclude from selection */
|
||||
selectedList: Skill[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill List Modal Component
|
||||
*
|
||||
* A modal dialog for selecting skills from a searchable list.
|
||||
* Features:
|
||||
* - Search functionality to filter skills by keywords
|
||||
* - Grid layout displaying skill cards with icons and descriptions
|
||||
* - Multi-select capability with visual feedback
|
||||
* - Excludes already selected skills from the list
|
||||
* - Displays skill name initial as avatar when no icon available
|
||||
*
|
||||
* @param refresh - Callback to update parent with selected skills
|
||||
* @param selectedList - Currently selected skills to filter out
|
||||
* @param ref - Forwarded ref exposing handleOpen and handleClose methods
|
||||
*/
|
||||
const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
refresh,
|
||||
selectedList
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [list, setList] = useState<Skill[]>([])
|
||||
const [filterList, setFilterList] = useState<Skill[]>([])
|
||||
const [query, setQuery] = useState<{keywords?: string}>({})
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [selectedRows, setSelectedRows] = useState<Skill[]>([])
|
||||
|
||||
/**
|
||||
* Closes the modal and resets all state
|
||||
* Clears search query, selected IDs, and selected rows
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setQuery({})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the modal and resets selection state
|
||||
* Clears any previous selections when reopening
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
setQuery({})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect: Fetch skill list when modal is visible or search query changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getList()
|
||||
}
|
||||
}, [query.keywords, visible])
|
||||
|
||||
/**
|
||||
* Fetches the skill list from API with current search parameters
|
||||
* Sorts by creation date in descending order
|
||||
*/
|
||||
const getList = () => {
|
||||
getSkillList({
|
||||
...query,
|
||||
pagesize: 100,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as { items: Skill[] }
|
||||
setList(response.items || [])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves selected skills and closes modal
|
||||
* Passes selected skills to parent component via refresh callback
|
||||
*/
|
||||
const handleSave = () => {
|
||||
refresh(selectedRows, 'skill')
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes methods to parent component via ref
|
||||
* Allows parent to programmatically open/close the modal
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
/**
|
||||
* Handles search input changes and resets selection
|
||||
* Clears current selections when search query changes
|
||||
* @param value - Search keyword
|
||||
*/
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({keywords: value})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles skill selection state
|
||||
* Adds skill to selection if not selected, removes if already selected
|
||||
* @param item - Skill to select/deselect
|
||||
*/
|
||||
const handleSelect = (item: Skill) => {
|
||||
const index = selectedIds.indexOf(item.id)
|
||||
if (index === -1) {
|
||||
// Add to selection
|
||||
setSelectedIds([...selectedIds, item.id])
|
||||
setSelectedRows([...selectedRows, item])
|
||||
} else {
|
||||
// Remove from selection
|
||||
setSelectedIds(selectedIds.filter(id => id !== item.id))
|
||||
setSelectedRows(selectedRows.filter(row => row.id !== item.id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effect: Filter out already selected skills from the display list
|
||||
* Updates filterList whenever list or selectedList changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (list.length && selectedList.length) {
|
||||
const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
|
||||
setFilterList([...unSelectedList])
|
||||
} else if (list.length) {
|
||||
setFilterList([...list])
|
||||
}
|
||||
}, [list, selectedList])
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.chooseSkill')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
width={1000}
|
||||
>
|
||||
<Space size={24} direction="vertical" className="rb:w-full">
|
||||
{/* Search input for filtering skills */}
|
||||
<SearchInput
|
||||
placeholder={t('skills.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{/* Display empty state or skill grid */}
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
: <List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
dataSource={filterList}
|
||||
renderItem={(item: Skill) => (
|
||||
<List.Item>
|
||||
{/* Skill card with selection state styling */}
|
||||
<div key={item.id} className={clsx("rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": selectedIds.includes(item.id),
|
||||
"rb:border-[#DFE4ED] rb:text-[#212332]": !selectedIds.includes(item.id),
|
||||
})} onClick={() => handleSelect(item)}>
|
||||
<Flex>
|
||||
{/* Skill avatar showing first letter of name */}
|
||||
<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>
|
||||
{/* Skill name and description */}
|
||||
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
|
||||
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.description}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default SkillListModal;
|
||||
161
web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx
Normal file
161
web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:43:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 11:10:01
|
||||
*/
|
||||
import { type FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Form, Flex, Tooltip, Checkbox } from 'antd'
|
||||
import { CloseOutlined, CheckCircleFilled } from '@ant-design/icons'
|
||||
|
||||
import type {
|
||||
SkillConfigForm,
|
||||
SkillModalRef,
|
||||
} from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import SkillListModal from './SkillListModal'
|
||||
import Card from '../Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
|
||||
/**
|
||||
* Props for SkillsItem Component
|
||||
*/
|
||||
interface SkillsItemProps {
|
||||
/** Title displayed in the card header */
|
||||
title: string;
|
||||
/** Form field path for nested form structure */
|
||||
parentName: string[];
|
||||
/** Whether to show "Allow all skills" checkbox option */
|
||||
supportAll?: boolean;
|
||||
/** Message displayed when no skills are configured */
|
||||
emptyTitle: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skills Item Component
|
||||
*
|
||||
* Displays and manages a list of configured skills with the following features:
|
||||
* - Add new skills via modal selection
|
||||
* - Optional "Allow all skills" toggle
|
||||
* - Display skill cards with icons and descriptions
|
||||
* - Remove individual skills
|
||||
* - Empty state when no skills configured
|
||||
* - Alert message when all skills are enabled
|
||||
*
|
||||
* @param title - Card header title
|
||||
* @param parentName - Form field path array for nested structure
|
||||
* @param supportAll - Enable "Allow all skills" checkbox
|
||||
* @param emptyTitle - Empty state message
|
||||
*/
|
||||
const SkillsItem: FC<SkillsItemProps> = ({
|
||||
title,
|
||||
parentName,
|
||||
supportAll = false,
|
||||
emptyTitle
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const skillModalRef = useRef<SkillModalRef>(null)
|
||||
const form = Form.useFormInstance()
|
||||
const allSkills = Form.useWatch([...parentName, 'all_skills'], form)
|
||||
|
||||
/**
|
||||
* Opens the skill selection modal
|
||||
*/
|
||||
const handleAddSkill = () => {
|
||||
skillModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates form with newly selected skills
|
||||
* Merges new selections with existing skills, avoiding duplicates
|
||||
* @param values - Array of newly selected skills
|
||||
*/
|
||||
const refresh = (values: SkillConfigForm['skill_ids']) => {
|
||||
const currentSkills = form.getFieldValue([...parentName, 'skill_ids']) || []
|
||||
const newSkills = values?.filter(v => !currentSkills?.find((s: Skill) => s.id === (v as Skill).id)) || []
|
||||
form.setFieldValue([...parentName, 'skill_ids'], [...currentSkills, ...newSkills])
|
||||
}
|
||||
|
||||
/**
|
||||
* Effect: Clear skill list when "all skills" is enabled
|
||||
*/
|
||||
useEffect(() => {
|
||||
form.setFieldValue([...parentName, 'skill_ids'], [])
|
||||
}, [allSkills])
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
extra={
|
||||
<Space>
|
||||
{/* "Allow all skills" checkbox - only shown if supportAll is true */}
|
||||
{supportAll && <Form.Item name={[...parentName, 'all_skills']} valuePropName="checked" noStyle>
|
||||
<Checkbox>{t('application.allSkill')}</Checkbox>
|
||||
</Form.Item>}
|
||||
{/* Add skill button - disabled when all skills are enabled */}
|
||||
<Button disabled={allSkills} style={{ padding: '0 8px', height: '24px' }} onClick={handleAddSkill}>+ {t('application.addSkill')}</Button>
|
||||
</Space>
|
||||
}
|
||||
variant="borderL"
|
||||
>
|
||||
{/* Show alert when all skills enabled, otherwise show skill list */}
|
||||
{allSkills
|
||||
? <RbAlert color="green" icon={<CheckCircleFilled />}>{t('application.allSkillIntro')}</RbAlert>
|
||||
: <>
|
||||
<Form.List name={[...parentName, 'skill_ids']}>
|
||||
{(fields, { remove }) => (
|
||||
fields.length === 0 ? (
|
||||
/* Empty state when no skills configured */
|
||||
<Empty size={88} subTitle={emptyTitle} />
|
||||
) : (
|
||||
/* Render list of configured skills */
|
||||
<Space direction="vertical" size={12} className="rb:w-full">
|
||||
{fields.map((field) => {
|
||||
const skill = form.getFieldValue([...parentName, 'skill_ids', field.name])
|
||||
return (
|
||||
/* Individual skill card */
|
||||
<div key={field.key} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<Flex className="rb:flex-1 rb:max-w-[calc(100%-186px)]!">
|
||||
{/* Skill icon or fallback initial */}
|
||||
{skill.icon
|
||||
? <img src={skill.icon} className="rb:mr-3.25 rb:size-12 rb:rounded-lg" />
|
||||
: <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]">
|
||||
{skill.name?.[0]}
|
||||
</div>
|
||||
}
|
||||
{/* Skill name and description */}
|
||||
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
|
||||
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{skill.name}</div>
|
||||
<Tooltip title={skill.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{skill.description}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Flex>
|
||||
<Space size={16} align="center">
|
||||
{/* Remove skill button */}
|
||||
<CloseOutlined
|
||||
className="rb:cursor-pointer rb:text-[#5B6167] hover:rb:text-[#155EEF]"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
)
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
}
|
||||
{/* Skill selection modal */}
|
||||
<SkillListModal
|
||||
ref={skillModalRef}
|
||||
selectedList={form.getFieldValue([...parentName, 'skill_ids']) || []}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default SkillsItem
|
||||
151
web/src/views/ApplicationConfig/components/Skill/index.tsx
Normal file
151
web/src/views/ApplicationConfig/components/Skill/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:42:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:42:56
|
||||
*/
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Switch, Form, Flex } from 'antd'
|
||||
|
||||
import type {
|
||||
SkillConfigForm,
|
||||
} from './types'
|
||||
import Card from '../Card'
|
||||
import SkillsItem from './SkillsItem'
|
||||
import { getSkillList } from '@/api/skill'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
|
||||
/**
|
||||
* Process flow steps for skill execution
|
||||
* Defines the sequential steps in the skill execution workflow
|
||||
*/
|
||||
const processObj = [
|
||||
'receiveTask', // Step 1: Receive task
|
||||
'analyTask', // Step 2: Analyze task intent
|
||||
'dynamicMatchSkill', // Step 3: Dynamically match appropriate skill
|
||||
'executeTask' // Step 4: Execute the task
|
||||
]
|
||||
|
||||
/**
|
||||
* Skill Configuration Component
|
||||
*
|
||||
* Main component for managing agent skill configuration including:
|
||||
* - Enabling/disabling skill functionality
|
||||
* - Configuring dynamic skill binding
|
||||
* - Displaying skill execution process flow
|
||||
* - Managing skill selection and assignment
|
||||
*
|
||||
* @param value - Current skill configuration values
|
||||
* @param onChange - Callback function when configuration changes
|
||||
*/
|
||||
const Skill: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigForm) => void}> = () => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const skillConfig = Form.useWatch(['skills'], form)
|
||||
|
||||
/**
|
||||
* Effect: Fetch and populate skill details for skills without names
|
||||
* Ensures all selected skills have complete information by fetching from API
|
||||
*/
|
||||
useEffect(() => {
|
||||
const { skill_ids = [] } = skillConfig || {}
|
||||
|
||||
// Filter skills that don't have name property
|
||||
const skillsWithoutName = skill_ids.filter((vo: Skill) => !vo.name)
|
||||
|
||||
if (skillsWithoutName.length > 0) {
|
||||
getSkillList({ page: 1, pagesize: 100 })
|
||||
.then(res => {
|
||||
const response = res as { items: Skill[] }
|
||||
// Create a map of skill ID to skill object for quick lookup
|
||||
const map = response.items.reduce((prev: any, curr: any) => {
|
||||
prev[curr.id] = curr
|
||||
return prev
|
||||
}, {})
|
||||
|
||||
// Merge fetched skill details with existing skill IDs
|
||||
const newSkillIds = skill_ids.map((vo: any) => {
|
||||
return {
|
||||
...vo,
|
||||
...map[vo.id]
|
||||
}
|
||||
})
|
||||
|
||||
form.setFieldValue(['skills', 'skill_ids'], newSkillIds)
|
||||
})
|
||||
}
|
||||
|
||||
}, [skillConfig?.skill_ids])
|
||||
|
||||
/**
|
||||
* Effect: Reset skill configuration when skill functionality is disabled
|
||||
* Clears all_skills flag and skill_ids array when enabled is set to false
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!skillConfig?.enabled) {
|
||||
form.setFieldValue('skills', {
|
||||
...skillConfig,
|
||||
all_skills: false,
|
||||
skill_ids: []
|
||||
})
|
||||
}
|
||||
}, [skillConfig?.enabled])
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<>
|
||||
{t('application.skill')}
|
||||
<span className="rb:font-regular rb:text-[12px] rb:text-[#5B6167]"> ({t('application.skillTitle')})</span>
|
||||
</>}
|
||||
extra={
|
||||
<Space>
|
||||
{/* Help button for skill configuration guidance */}
|
||||
<Button style={{ padding: '0 8px', height: '24px' }}>{t('application.skillHelp')}</Button>
|
||||
{/* Toggle switch to enable/disable skill functionality */}
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
name={['skills', 'enabled']}
|
||||
noStyle
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* Render skill configuration UI only when enabled */}
|
||||
{skillConfig?.enabled && <Flex vertical gap={12}>
|
||||
{/* Dynamic skill binding configuration section */}
|
||||
<Form.Item noStyle>
|
||||
<SkillsItem
|
||||
title={t('application.dynamicBindingSkill')}
|
||||
parentName={['skills']}
|
||||
supportAll={true}
|
||||
emptyTitle={t('application.dynamicBindingSkill_empty')}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* Execution process preview card showing workflow steps */}
|
||||
<Card
|
||||
title={t('application.executeProcessPreview')}
|
||||
variant="borderL"
|
||||
>
|
||||
<Flex align="center" gap={8} className="rb:text-[12px]">
|
||||
{/* Render each step in the process flow with numbered badges */}
|
||||
{processObj.map((key, index) => (<>
|
||||
<Flex align="center" gap={8} className="rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:bg-white rb:p-2!">
|
||||
{/* Step number badge */}
|
||||
<div className="rb:size-4 rb:rounded-full rb:bg-[#155EEF] rb:text-white rb:flex rb:items-center rb:justify-center rb:font-medium">{index + 1}</div>
|
||||
{/* Step label */}
|
||||
<span>{t(`application.${key}`)}</span>
|
||||
</Flex>
|
||||
{/* Arrow separator between steps (except after last step) */}
|
||||
{index !== processObj.length - 1 && <div className="rb:text-[#8C9196]">→</div>}
|
||||
</>))}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default Skill
|
||||
53
web/src/views/ApplicationConfig/components/Skill/types.ts
Normal file
53
web/src/views/ApplicationConfig/components/Skill/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:43:09
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:43:09
|
||||
*/
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
|
||||
/**
|
||||
* Skill Configuration Form Data Structure
|
||||
* Used to manage skill configuration settings in the application
|
||||
*/
|
||||
export interface SkillConfigForm {
|
||||
/** Whether skill configuration is enabled */
|
||||
enabled?: boolean;
|
||||
/** Whether all skills are accessible */
|
||||
all_skills?: boolean;
|
||||
/** Array of selected skill IDs or full skill objects */
|
||||
skill_ids?: Skill[] | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill Binding Mode Types
|
||||
* Defines different strategies for skill assignment
|
||||
*/
|
||||
export type SkillMode = 'staticBinding' | 'dynamicBinding' | 'mixedMode'
|
||||
|
||||
/**
|
||||
* Skill Configuration Modal Reference Interface
|
||||
* Provides methods to control the skill configuration modal
|
||||
*/
|
||||
export interface SkillConfigModalRef {
|
||||
/** Opens the modal with optional initial data */
|
||||
handleOpen: (data: SkillConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill Global Configuration Modal Reference Interface
|
||||
* Provides methods to control the global skill configuration modal
|
||||
*/
|
||||
export interface SkillGlobalConfigModalRef {
|
||||
/** Opens the global configuration modal */
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill Selection Modal Reference Interface
|
||||
* Provides methods to control the skill selection modal
|
||||
*/
|
||||
export interface SkillModalRef {
|
||||
/** Opens the modal with optional skill configuration array */
|
||||
handleOpen: (config?: SkillConfigForm[]) => void;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
|
||||
<Card
|
||||
title={t('application.toolConfiguration')}
|
||||
extra={
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}>+{t('application.addTool')}</Button>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}>+ {t('application.addTool')}</Button>
|
||||
}
|
||||
>
|
||||
{toolList.length === 0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:29:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:31:10
|
||||
*/
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
@@ -10,6 +10,7 @@ import type { ToolOption } from './components/ToolList/types'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import type { GraphRef } from '@/views/Workflow/types';
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
import type { SkillConfigForm } from './components/Skill/types'
|
||||
|
||||
/**
|
||||
* Model configuration parameters
|
||||
@@ -75,6 +76,7 @@ export interface Config extends MultiAgentConfig {
|
||||
created_at: number;
|
||||
/** Last update timestamp */
|
||||
updated_at: number;
|
||||
skills?: SkillConfigForm | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,6 +355,7 @@ export interface AiPromptForm {
|
||||
message?: string;
|
||||
/** Current prompt */
|
||||
current_prompt?: string;
|
||||
skill?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
192
web/src/views/Skills/components/ToolList/ToolList.tsx
Normal file
192
web/src/views/Skills/components/ToolList/ToolList.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:51:22
|
||||
*/
|
||||
/**
|
||||
* Tool List Component
|
||||
* Manages tool configurations for the application
|
||||
* Allows adding, removing, and enabling/disabling tools
|
||||
*/
|
||||
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, List } from 'antd'
|
||||
|
||||
import Card from '@/views/ApplicationConfig/components/Card'
|
||||
import type {
|
||||
ToolModalRef,
|
||||
ToolOption
|
||||
} from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import ToolModal from './ToolModal'
|
||||
import { getToolMethods, getToolDetail } from '@/api/tools'
|
||||
|
||||
/**
|
||||
* Tool List Component Props
|
||||
*/
|
||||
interface ToolListProps {
|
||||
/** Current tool configurations */
|
||||
value?: ToolOption[];
|
||||
/** Callback when tools change */
|
||||
onChange?: (config: ToolOption[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool list management component
|
||||
* @param value - Current tool configurations
|
||||
* @param onChange - Callback when tools change
|
||||
*/
|
||||
const ToolList: FC<ToolListProps> = ({value, onChange}) => {
|
||||
const { t } = useTranslation()
|
||||
const toolModalRef = useRef<ToolModalRef>(null)
|
||||
const [toolList, setToolList] = useState<ToolOption[]>([])
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const processedData = value.map(async (item) => {
|
||||
// Skip if tool already has label (already processed)
|
||||
if (!item.label && item.tool_id) {
|
||||
try {
|
||||
// Fetch tool details and methods in parallel
|
||||
const [toolDetail, methods] = await Promise.all([
|
||||
getToolDetail(item.tool_id),
|
||||
getToolMethods(item.tool_id)
|
||||
])
|
||||
|
||||
// Process based on tool type
|
||||
switch ((toolDetail as any).tool_type) {
|
||||
case 'mcp':
|
||||
// MCP tools: Find method by operation name
|
||||
const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: mcpFilterItem?.description,
|
||||
method_id: mcpFilterItem?.method_id,
|
||||
value: mcpFilterItem?.name,
|
||||
description: mcpFilterItem?.description,
|
||||
parameters: mcpFilterItem?.parameters
|
||||
}
|
||||
break
|
||||
case 'builtin':
|
||||
// Builtin tools: Handle single or multiple methods
|
||||
if ((methods as any[]).length > 1) {
|
||||
const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: builtinFilterItem?.description,
|
||||
method_id: builtinFilterItem?.method_id,
|
||||
value: builtinFilterItem?.name,
|
||||
description: builtinFilterItem?.description,
|
||||
parameters: builtinFilterItem?.parameters
|
||||
}
|
||||
}
|
||||
// Single method: Use first method
|
||||
return {
|
||||
...item,
|
||||
label: (methods as any[])[0]?.description,
|
||||
method_id: (methods as any[])[0]?.method_id,
|
||||
value: (methods as any[])[0]?.name,
|
||||
description: (methods as any[])[0]?.description,
|
||||
parameters: (methods as any[])[0]?.parameters
|
||||
}
|
||||
break
|
||||
default:
|
||||
// Custom tools: Find method by method_id
|
||||
const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: customFilterItem?.name,
|
||||
method_id: customFilterItem?.method_id,
|
||||
value: customFilterItem?.name,
|
||||
description: customFilterItem?.description,
|
||||
parameters: customFilterItem?.parameters
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Return original item if fetch fails
|
||||
return item
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
// Wait for all tools to be processed
|
||||
Promise.all(processedData).then(setToolList)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
/**
|
||||
* Opens the tool selection modal
|
||||
*/
|
||||
const handleAddTool = () => {
|
||||
toolModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new tool to the list
|
||||
* Updates both local state and parent component
|
||||
* @param tool - Tool to add
|
||||
*/
|
||||
const updateTools = (tool: ToolOption) => {
|
||||
const list = [...toolList, tool]
|
||||
setToolList(list)
|
||||
onChange && onChange(list)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tool from the list by index
|
||||
* Updates both local state and parent component
|
||||
* @param index - Index of tool to remove
|
||||
*/
|
||||
const handleDeleteTool = (index: number) => {
|
||||
const list = toolList.filter((_item, idx) => idx !== index)
|
||||
setToolList([...list])
|
||||
onChange && onChange(list)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('application.toolConfiguration')}
|
||||
extra={
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}>
|
||||
+ {t('application.addTool')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* Show empty state or tool list */}
|
||||
{toolList.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={toolList}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
{/* Tool card with delete button */}
|
||||
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
{/* Tool label/description */}
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.label}
|
||||
</div>
|
||||
<Space size={12}>
|
||||
{/* Delete button with hover effect */}
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteTool(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{/* Tool selection modal */}
|
||||
<ToolModal
|
||||
ref={toolModalRef}
|
||||
refresh={updateTools}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default ToolList
|
||||
209
web/src/views/Skills/components/ToolList/ToolModal.tsx
Normal file
209
web/src/views/Skills/components/ToolList/ToolModal.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:06
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:52:13
|
||||
*/
|
||||
/**
|
||||
* Tool Selection Modal
|
||||
* Provides cascading selection of tools by type, tool, and method
|
||||
* Supports MCP, builtin, and custom tool types
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Cascader, type CascaderProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ToolModalRef, ToolOption } from './types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getToolMethods, getTools } from '@/api/tools'
|
||||
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Component Props
|
||||
*/
|
||||
interface ToolModalProps {
|
||||
/** Callback to add selected tool to parent component */
|
||||
refresh: (tool: ToolOption) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for selecting tools
|
||||
*/
|
||||
const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
/**
|
||||
* Initial cascader options for tool types
|
||||
* Level 1: Tool type selection (MCP, Builtin, Custom)
|
||||
*/
|
||||
const [optionList, setOptionList] = useState<ToolOption[]>([
|
||||
{ value: 'mcp', label: t('tool.mcp'), isLeaf: false },
|
||||
{ value: 'builtin', label: t('tool.inner'), isLeaf: false },
|
||||
{ value: 'custom', label: t('tool.custom'), isLeaf: false },
|
||||
])
|
||||
|
||||
/**
|
||||
* Stores the complete selection path
|
||||
* [0] = Tool type, [1] = Specific tool, [2] = Tool method
|
||||
*/
|
||||
const [selectdTools, setSelectedTools] = useState<ToolOption[]>([])
|
||||
|
||||
/**
|
||||
* Closes the modal and resets all state
|
||||
* Clears form, loading state, and selections
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setSelectedTools([])
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the modal and resets state
|
||||
* Clears any previous selections
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
setSelectedTools([])
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the selected tool and closes modal
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
setLoading(false)
|
||||
let operation: any = undefined
|
||||
|
||||
// Determine operation based on tool type
|
||||
if (selectdTools[0].value === 'mcp' ||
|
||||
(selectdTools[0].value === 'builtin' &&
|
||||
selectdTools[1]?.children &&
|
||||
selectdTools[1].children.length > 1)) {
|
||||
// MCP or builtin with multiple methods: use method name
|
||||
operation = selectdTools[2].value
|
||||
} else if (selectdTools[0].value === 'custom') {
|
||||
// Custom tools: use method_id
|
||||
operation = selectdTools[2].method_id
|
||||
}
|
||||
|
||||
// Construct tool object
|
||||
const tool = {
|
||||
...selectdTools[2],
|
||||
// Custom tools use label, others use description
|
||||
label: selectdTools[0].value === 'custom' ? selectdTools[2].label : selectdTools[2].description,
|
||||
tool_id: selectdTools[1].value as string,
|
||||
enabled: true
|
||||
}
|
||||
|
||||
// Add operation if determined
|
||||
if (operation) {
|
||||
tool.operation = operation
|
||||
}
|
||||
|
||||
refresh(tool)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically loads cascader options based on selection
|
||||
*/
|
||||
const loadData = (selectedOptions: ToolOption[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
|
||||
if (selectedOptions.length === 1) {
|
||||
// Level 1 selected: Load tools of this type
|
||||
getTools({ tool_type: targetOption.value as ToolType })
|
||||
.then(res => {
|
||||
const response = res as ToolItem[]
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.id,
|
||||
label: vo.name,
|
||||
isLeaf: response.length === 0,
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
} else {
|
||||
// Level 2 selected: Load methods for this tool
|
||||
getToolMethods(targetOption.value as string)
|
||||
.then(res => {
|
||||
const response = res as Array<{ method_id: string; name: string }>
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.name,
|
||||
label: vo.name,
|
||||
description: vo.description,
|
||||
isLeaf: true,
|
||||
method_id: vo.method_id,
|
||||
parameters: vo.parameters
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles cascader selection change
|
||||
*/
|
||||
const handleChange: CascaderProps<ToolOption>['onChange'] = (_value, selectedOptions) => {
|
||||
console.log('selectedOptions', selectedOptions)
|
||||
setSelectedTools(selectedOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t(`application.addTool`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="agent_id"
|
||||
label={t('application.tool')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') },
|
||||
]}
|
||||
>
|
||||
{/* Three-level cascading selector */}
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
changeOnSelect={false}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ToolModal;
|
||||
67
web/src/views/Skills/components/ToolList/types.ts
Normal file
67
web/src/views/Skills/components/ToolList/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:26:10
|
||||
*/
|
||||
/**
|
||||
* Type definitions for tool configuration in application settings
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tool option for cascader selection
|
||||
*/
|
||||
export interface ToolOption {
|
||||
/** Option value */
|
||||
value?: string | number | null;
|
||||
/** Display label */
|
||||
label?: React.ReactNode;
|
||||
/** Tool description */
|
||||
description?: string;
|
||||
/** Child options for nested selection */
|
||||
children?: ToolOption[];
|
||||
/** Whether this is a leaf node */
|
||||
isLeaf?: boolean;
|
||||
/** Method ID for API operations */
|
||||
method_id?: string;
|
||||
/** Operation name */
|
||||
operation?: string;
|
||||
/** Method parameters */
|
||||
parameters?: Parameter[];
|
||||
/** Tool ID */
|
||||
tool_id?: string;
|
||||
/** Whether tool is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter definition for tool methods
|
||||
*/
|
||||
export interface Parameter {
|
||||
/** Parameter name */
|
||||
name: string;
|
||||
/** Parameter data type */
|
||||
type: string;
|
||||
/** Parameter description */
|
||||
description: string;
|
||||
/** Whether parameter is required */
|
||||
required: boolean;
|
||||
/** Default value */
|
||||
default: any;
|
||||
/** Enum values if applicable */
|
||||
enum: null | string[];
|
||||
/** Minimum value for numeric types */
|
||||
minimum: number;
|
||||
/** Maximum value for numeric types */
|
||||
maximum: number;
|
||||
/** Regex pattern for validation */
|
||||
pattern: null | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal ref for tool selection
|
||||
*/
|
||||
export interface ToolModalRef {
|
||||
/** Open tool selection modal */
|
||||
handleOpen: () => void;
|
||||
}
|
||||
85
web/src/views/Skills/index.tsx
Normal file
85
web/src/views/Skills/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:43:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:43:49
|
||||
*/
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { Skill } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getSkillListUrl } from '@/api/skill'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
|
||||
/**
|
||||
* Skills List Page Component
|
||||
*
|
||||
* Main page for displaying and managing skills.
|
||||
* Features:
|
||||
* - Grid layout of skill cards
|
||||
* - Infinite scroll pagination
|
||||
* - Create new skills
|
||||
* - Navigate to skill configuration
|
||||
* - Display skill name and description
|
||||
*
|
||||
* @returns Skills list page with grid of skill cards
|
||||
*/
|
||||
const Skills: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
|
||||
/**
|
||||
* Navigate to create new skill page
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
navigate('/skills/add')
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to skill configuration page
|
||||
* @param item - Skill to view/edit
|
||||
*/
|
||||
const handleView = (item: Skill) => {
|
||||
navigate(`/skills/config/${item.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Create skill button */}
|
||||
<div className="rb:text-right rb:mb-4">
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
+ {t('skills.create')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Infinite scroll skill list */}
|
||||
<PageScrollList<Skill>
|
||||
ref={scrollListRef}
|
||||
url={getSkillListUrl}
|
||||
query={{ is_active: true, type: 'service' }}
|
||||
column={3}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<RbCard
|
||||
title={item.name}
|
||||
avatar={<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF] rb:mr-2">{item.name[0]}</div>}
|
||||
className="rb:cursor-pointer"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
{/* Skill description with tooltip */}
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.description}</div>
|
||||
</Tooltip>
|
||||
</RbCard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Skills;
|
||||
218
web/src/views/Skills/pages/SkillConfig.tsx
Normal file
218
web/src/views/Skills/pages/SkillConfig.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:44:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:56:28
|
||||
*/
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Form, Input, Button, Space, Select, App } from 'antd'
|
||||
|
||||
import Card from '@/views/ApplicationConfig/components/Card'
|
||||
import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
||||
import AiPromptModal from '@/views/ApplicationConfig/components/AiPromptModal'
|
||||
import ToolList from '../components/ToolList/ToolList'
|
||||
import type { AiPromptModalRef } from '@/views/ApplicationConfig/types'
|
||||
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
||||
import type { SkillFormData } from '../types'
|
||||
import { getSkillDetail, createSkill, updateSkill } from '@/api/skill'
|
||||
|
||||
/**
|
||||
* Skill Configuration Page Component
|
||||
*
|
||||
* Page for creating and editing skills with the following sections:
|
||||
* - Manifest: Basic skill information (name, description, keywords)
|
||||
* - Prompt Configuration: AI instructions with AI assistant
|
||||
* - Tool Configuration: Associated tools for the skill
|
||||
*
|
||||
* Features:
|
||||
* - Create new skills or edit existing ones
|
||||
* - AI-powered prompt generation
|
||||
* - Tool selection and management
|
||||
* - Form validation
|
||||
* - Auto-save functionality
|
||||
*
|
||||
* @returns Skill configuration form page
|
||||
*/
|
||||
const SkillConfig: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams()
|
||||
const { message } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [form] = Form.useForm<SkillFormData>();
|
||||
|
||||
/**
|
||||
* Effect: Load skill data if editing existing skill
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getConfig()
|
||||
} else {
|
||||
// Initialize default config for new skill
|
||||
form.setFieldsValue({
|
||||
config: {
|
||||
enabled: false,
|
||||
keywords: []
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [id])
|
||||
|
||||
/**
|
||||
* Fetch skill configuration from API
|
||||
*/
|
||||
const getConfig = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getSkillDetail(id)
|
||||
.then(res => {
|
||||
form.setFieldsValue(res as SkillFormData)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const aiPromptModalRef = useRef<AiPromptModalRef>(null)
|
||||
|
||||
/**
|
||||
* Open AI prompt generation modal
|
||||
*/
|
||||
const handlePrompt = () => {
|
||||
aiPromptModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update prompt field with AI-generated content
|
||||
* @param value - Generated prompt text
|
||||
*/
|
||||
const updatePrompt = (value: string) => {
|
||||
form.setFieldValue('prompt', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back to skills list
|
||||
*/
|
||||
const handleBack = () => {
|
||||
navigate('/skills')
|
||||
};
|
||||
|
||||
/**
|
||||
* Save skill configuration
|
||||
* Validates form and calls create or update API
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const { tools, ...rest } = values;
|
||||
// Format tools data for API
|
||||
const formData = {
|
||||
...rest,
|
||||
tools: tools?.map((item: any) => ({
|
||||
tool_id: item.tool_id,
|
||||
operation: item.operation
|
||||
}))
|
||||
}
|
||||
setLoading(true)
|
||||
// Choose create or update based on whether id exists
|
||||
const request = id ? updateSkill(id, formData) : createSkill(formData)
|
||||
request
|
||||
.then(() => {
|
||||
message.success(id ? t('common.saveSuccess') : t('common.createSuccess'))
|
||||
handleBack()
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
{/* Back button */}
|
||||
<div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}>
|
||||
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' />
|
||||
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{/* Manifest Section: Basic skill information */}
|
||||
<Card title={t('skills.mainfest')}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('skills.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('skills.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('skills.descriptionPlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['config', 'keywords']}
|
||||
label={t('skills.keywords')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.keywords') }) }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* Prompt Configuration Section: AI instructions */}
|
||||
<Card title={t('skills.promptConfiguration')}
|
||||
extra={
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handlePrompt}>
|
||||
<img src={aiPrompt} className="rb:size-5" />
|
||||
{t('skills.aiPrompt')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
name="prompt"
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('skills.promptPlaceholder')}
|
||||
styles={{
|
||||
textarea: {
|
||||
minHeight: '200px',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* Tool Configuration Section */}
|
||||
<Form.Item
|
||||
name="tools"
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('skills.tools') }) }]}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<ToolList />
|
||||
</Form.Item>
|
||||
|
||||
{/* Save button */}
|
||||
<Button type="primary" block disabled={loading} onClick={handleSave}>{t('skills.save')}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
{/* AI Prompt Generation Modal */}
|
||||
<AiPromptModal
|
||||
ref={aiPromptModalRef}
|
||||
refresh={updatePrompt}
|
||||
source="skills"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SkillConfig;
|
||||
49
web/src/views/Skills/types.ts
Normal file
49
web/src/views/Skills/types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:49:35
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:49:35
|
||||
*/
|
||||
/**
|
||||
* Skill Form Data Structure
|
||||
* Defines the data structure for creating and updating skills
|
||||
*/
|
||||
export interface SkillFormData {
|
||||
/** Skill name */
|
||||
name: string;
|
||||
/** Skill description */
|
||||
description: string;
|
||||
/** Array of tools associated with this skill */
|
||||
tools: Array<{
|
||||
/** Tool identifier */
|
||||
tool_id: string;
|
||||
}>;
|
||||
/** Skill configuration settings */
|
||||
config: {
|
||||
/** Keywords for skill matching and discovery */
|
||||
keywords: string[];
|
||||
/** Whether the skill is enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
/** AI prompt/instructions for the skill */
|
||||
prompt: string;
|
||||
/** Whether the skill is active */
|
||||
is_active: boolean;
|
||||
/** Whether the skill is publicly accessible */
|
||||
is_public: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete Skill Data Structure
|
||||
* Extends SkillFormData with system-generated fields
|
||||
*/
|
||||
export interface Skill extends SkillFormData {
|
||||
/** Unique skill identifier */
|
||||
id: string;
|
||||
/** Tenant/organization identifier */
|
||||
tenant_id: string;
|
||||
/** Creation timestamp */
|
||||
created_at: number;
|
||||
/** Last update timestamp */
|
||||
updated_at: number;
|
||||
}
|
||||
Reference in New Issue
Block a user