feat(web): add skills menu
This commit is contained in:
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;
|
||||
220
web/src/views/Skills/pages/SkillConfig.tsx
Normal file
220
web/src/views/Skills/pages/SkillConfig.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:44:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:52:29
|
||||
*/
|
||||
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, Skill } 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>();
|
||||
const [data, setData] = useState<Skill>({} as Skill)
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
setData(res as Skill)
|
||||
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) => {
|
||||
console.log(values, data)
|
||||
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
|
||||
}))
|
||||
}
|
||||
// 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