diff --git a/web/src/api/skill.ts b/web/src/api/skill.ts new file mode 100644 index 00000000..47e77d86 --- /dev/null +++ b/web/src/api/skill.ts @@ -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}`) +} \ No newline at end of file diff --git a/web/src/assets/images/menu/skills.svg b/web/src/assets/images/menu/skills.svg new file mode 100644 index 00000000..ac121d1e --- /dev/null +++ b/web/src/assets/images/menu/skills.svg @@ -0,0 +1,14 @@ + + + 技能点 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menu/skills_active.svg b/web/src/assets/images/menu/skills_active.svg new file mode 100644 index 00000000..789b5586 --- /dev/null +++ b/web/src/assets/images/menu/skills_active.svg @@ -0,0 +1,14 @@ + + + 技能点备份 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/Layout/BasicAuthLayout.tsx b/web/src/components/Layout/BasicAuthLayout.tsx new file mode 100644 index 00000000..a73f6c69 --- /dev/null +++ b/web/src/components/Layout/BasicAuthLayout.tsx @@ -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 ( +
+ {/* Render child routes without additional UI */} + +
+ ) +}; + +export default BasicLayout; \ No newline at end of file diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx index 85b569df..896dc201 100644 --- a/web/src/components/RbCard/Card.tsx +++ b/web/src/components/RbCard/Card.tsx @@ -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 = ({ bgColor = '#FBFDFF', height = 'auto', className, + variant, ...props }) => { /** Calculate body padding based on header type and avatar presence */ @@ -82,7 +84,45 @@ const RbCard: FC = ({ : (headerType === 'border' && !avatarUrl && !avatar) || headerType === 'borderBL' ? 'rb:p-[16px_16px_20px_16px]!' : '' - + + if (variant === 'borderL') { + return ( +
+ + +
+ {typeof title === 'function' ? title() : title ? +
+ {avatarUrl + ? + : avatar ? avatar : null + } +
+
{title}
+ {subTitle &&
{subTitle}
} +
+
: null + } +
+ {subTitle &&
{subTitle}
} +
+ {extra} +
+
+ {children} +
+
+ ) + } return ( = ({ }, headerClassName, ), - body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-[0]!', + body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-0!', }} style={{ background: bgColor, diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index 82ea8c6e..21202aa0 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -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 = { @@ -102,6 +104,8 @@ const iconPathMap: Record = { 'ontologyActive': ontologyActiveIcon, 'prompt': promptIcon, 'promptActive': promptActiveIcon, + 'skills': skillsIcon, + 'skillsActive': skillsActiveIcon, }; const { Sider } = Layout; diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 94a90a56..1c0a8f2f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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', + }, }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 30b4bbd2..8566a399 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -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: '工具', + }, }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 13799d2a..42e0106a 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -51,6 +51,7 @@ const componentMap: Record>> = 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>> = 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')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index 476766e0..ea137bd4 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -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": [ diff --git a/web/src/store/menu.json b/web/src/store/menu.json index d264e061..4f53ab50 100644 --- a/web/src/store/menu.json +++ b/web/src/store/menu.json @@ -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, diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index c9d959d1..0bfd4ba7 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -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((_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((_props, ref) => { memory: { ...response.memory, memory_content: parsedMemoryContent + }, + skills: { + ...skills, + skill_ids: allSkills } }) setData({ @@ -252,7 +260,7 @@ const Agent = forwardRef((_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((_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((_props, ref) => { - + - + + + + + {/* Tool Configuration */} - + diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx index c35a2a0f..6a4a50b1 100644 --- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx +++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx @@ -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(({ refresh, defaultModel, + source = 'application' }, ref) => { const { t } = useTranslation(); const { message } = App.useApp() @@ -92,11 +94,11 @@ const AiPromptModal = forwardRef(({ 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(({ } }) }; - 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(({ console.log(values) return ( (({
@@ -219,18 +216,18 @@ const AiPromptModal = forwardRef(({ } + empty={} 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`)} />
@@ -242,12 +239,12 @@ const AiPromptModal = forwardRef(({
- - - - - + + + {source === 'application' && + + } (({
- +
diff --git a/web/src/views/ApplicationConfig/components/Card.tsx b/web/src/views/ApplicationConfig/components/Card.tsx index 7437b8f0..bf67d970 100644 --- a/web/src/views/ApplicationConfig/components/Card.tsx +++ b/web/src/views/ApplicationConfig/components/Card.tsx @@ -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 = ({ subTitle, children, extra, + variant }) => { return ( {children} diff --git a/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx new file mode 100644 index 00000000..8220878e --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx @@ -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(({ + refresh, + selectedList +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [list, setList] = useState([]) + const [filterList, setFilterList] = useState([]) + const [query, setQuery] = useState<{keywords?: string}>({}) + const [selectedIds, setSelectedIds] = useState([]) + const [selectedRows, setSelectedRows] = useState([]) + + /** + * 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 ( + <> + + + {/* Search input for filtering skills */} + + {/* Display empty state or skill grid */} + {filterList.length === 0 + ? + : ( + + {/* Skill card with selection state styling */} +
handleSelect(item)}> + + {/* Skill avatar showing first letter of name */} +
+ {item.name[0]} +
+ {/* Skill name and description */} +
+
{item.name}
+ +
{item.description}
+
+
+
+
+
+ )} + /> + } +
+
+ + ); +}); + +export default SkillListModal; diff --git a/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx b/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx new file mode 100644 index 00000000..35dee5ec --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx @@ -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 = ({ + title, + parentName, + supportAll = false, + emptyTitle +}) => { + const { t } = useTranslation() + const skillModalRef = useRef(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 ( + + {/* "Allow all skills" checkbox - only shown if supportAll is true */} + {supportAll && + {t('application.allSkill')} + } + {/* Add skill button - disabled when all skills are enabled */} + + + } + variant="borderL" + > + {/* Show alert when all skills enabled, otherwise show skill list */} + {allSkills + ? }>{t('application.allSkillIntro')} + : <> + + {(fields, { remove }) => ( + fields.length === 0 ? ( + /* Empty state when no skills configured */ + + ) : ( + /* Render list of configured skills */ + + {fields.map((field) => { + const skill = form.getFieldValue([...parentName, 'skill_ids', field.name]) + return ( + /* Individual skill card */ +
+ + {/* Skill icon or fallback initial */} + {skill.icon + ? + :
+ {skill.name?.[0]} +
+ } + {/* Skill name and description */} +
+
{skill.name}
+ +
{skill.description}
+
+
+
+ + {/* Remove skill button */} + remove(field.name)} + /> + +
+ ) + })} +
+ ) + )} +
+ + } + {/* Skill selection modal */} + +
+ ) +} +export default SkillsItem \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Skill/index.tsx b/web/src/views/ApplicationConfig/components/Skill/index.tsx new file mode 100644 index 00000000..1a8dcc6d --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Skill/index.tsx @@ -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 ( + + {t('application.skill')} + ({t('application.skillTitle')}) + } + extra={ + + {/* Help button for skill configuration guidance */} + + {/* Toggle switch to enable/disable skill functionality */} + + + + + } + > + {/* Render skill configuration UI only when enabled */} + {skillConfig?.enabled && + {/* Dynamic skill binding configuration section */} + + + + {/* Execution process preview card showing workflow steps */} + + + {/* Render each step in the process flow with numbered badges */} + {processObj.map((key, index) => (<> + + {/* Step number badge */} +
{index + 1}
+ {/* Step label */} + {t(`application.${key}`)} +
+ {/* Arrow separator between steps (except after last step) */} + {index !== processObj.length - 1 &&
} + ))} +
+
+
} +
+ ) +} +export default Skill \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Skill/types.ts b/web/src/views/ApplicationConfig/components/Skill/types.ts new file mode 100644 index 00000000..a520663c --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Skill/types.ts @@ -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; +} \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx index f46dd3f1..1a093b6e 100644 --- a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx +++ b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx @@ -131,7 +131,7 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) => +{t('application.addTool')} + } > {toolList.length === 0 diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index e1a97afa..fc799b91 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -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; } /** diff --git a/web/src/views/Skills/components/ToolList/ToolList.tsx b/web/src/views/Skills/components/ToolList/ToolList.tsx new file mode 100644 index 00000000..785e0a54 --- /dev/null +++ b/web/src/views/Skills/components/ToolList/ToolList.tsx @@ -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 = ({value, onChange}) => { + const { t } = useTranslation() + const toolModalRef = useRef(null) + const [toolList, setToolList] = useState([]) + 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 ( + + + {t('application.addTool')} + + } + > + {/* Show empty state or tool list */} + {toolList.length === 0 + ? + : + ( + + {/* Tool card with delete button */} +
+ {/* Tool label/description */} +
+ {item.label} +
+ + {/* Delete button with hover effect */} +
handleDeleteTool(index)} + >
+
+
+
+ )} + /> + } + {/* Tool selection modal */} + +
+ ) +} +export default ToolList diff --git a/web/src/views/Skills/components/ToolList/ToolModal.tsx b/web/src/views/Skills/components/ToolList/ToolModal.tsx new file mode 100644 index 00000000..e3dcfd34 --- /dev/null +++ b/web/src/views/Skills/components/ToolList/ToolModal.tsx @@ -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(({ + 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([ + { 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([]) + + /** + * 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['onChange'] = (_value, selectedOptions) => { + console.log('selectedOptions', selectedOptions) + setSelectedTools(selectedOptions) + } + + /** + * Exposes methods to parent component via ref + */ + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+ + {/* Three-level cascading selector */} + + +
+
+ ); +}); + +export default ToolModal; diff --git a/web/src/views/Skills/components/ToolList/types.ts b/web/src/views/Skills/components/ToolList/types.ts new file mode 100644 index 00000000..b0380f6a --- /dev/null +++ b/web/src/views/Skills/components/ToolList/types.ts @@ -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; +} \ No newline at end of file diff --git a/web/src/views/Skills/index.tsx b/web/src/views/Skills/index.tsx new file mode 100644 index 00000000..1522a3c8 --- /dev/null +++ b/web/src/views/Skills/index.tsx @@ -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(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 */} +
+ +
+ + {/* Infinite scroll skill list */} + + ref={scrollListRef} + url={getSkillListUrl} + query={{ is_active: true, type: 'service' }} + column={3} + renderItem={(item) => { + return ( + {item.name[0]}
} + className="rb:cursor-pointer" + onClick={() => handleView(item)} + > + {/* Skill description with tooltip */} + +
{item.description}
+
+ + ); + }} + /> + + ); +}; + +export default Skills; diff --git a/web/src/views/Skills/pages/SkillConfig.tsx b/web/src/views/Skills/pages/SkillConfig.tsx new file mode 100644 index 00000000..6e12e72d --- /dev/null +++ b/web/src/views/Skills/pages/SkillConfig.tsx @@ -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(); + + /** + * 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(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 ( +
+ {/* Back button */} +
+ exit + {t('common.exit')} +
+ +
+ + {/* Manifest Section: Basic skill information */} + + + + + + + + +