feat(web): skill toolList add is_active

This commit is contained in:
zhaoying
2026-04-01 13:33:16 +08:00
parent d3cd66fc6e
commit e77a1a92fd
4 changed files with 130 additions and 116 deletions

View File

@@ -19,6 +19,7 @@
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { useEffect, type FC } from 'react'; import { useEffect, type FC } from 'react';
import { Layout } from 'antd';
import { useUser } from '@/store/user'; import { useUser } from '@/store/user';
@@ -35,10 +36,10 @@ const BasicAuthLayout: FC = () => {
}, [getUserInfo]); }, [getUserInfo]);
return ( return (
<div className="rb:relative rb:min-h-screen rb:w-screen"> <Layout className="rb:min-h-screen!">
{/* Render child routes without additional UI */} {/* Render child routes without additional UI */}
<Outlet /> <Outlet />
</div> </Layout>
) )
}; };

View File

@@ -12,7 +12,7 @@
import { type FC, useRef, useState, useEffect } from 'react' import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, List } from 'antd' import { Space, Button, Flex } from 'antd'
import Card from '@/views/ApplicationConfig/components/Card' import Card from '@/views/ApplicationConfig/components/Card'
import type { import type {
@@ -22,6 +22,7 @@ import type {
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import ToolModal from './ToolModal' import ToolModal from './ToolModal'
import { getToolMethods, getToolDetail } from '@/api/tools' import { getToolMethods, getToolDetail } from '@/api/tools'
import Tag from '@/components/Tag'
/** /**
* Tool List Component Props * Tool List Component Props
@@ -61,6 +62,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation) const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
return { return {
...item, ...item,
is_active: (toolDetail as any).is_active,
label: mcpFilterItem?.description, label: mcpFilterItem?.description,
method_id: mcpFilterItem?.method_id, method_id: mcpFilterItem?.method_id,
value: mcpFilterItem?.name, value: mcpFilterItem?.name,
@@ -74,6 +76,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation) const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
return { return {
...item, ...item,
is_active: (toolDetail as any).is_active,
label: builtinFilterItem?.description, label: builtinFilterItem?.description,
method_id: builtinFilterItem?.method_id, method_id: builtinFilterItem?.method_id,
value: builtinFilterItem?.name, value: builtinFilterItem?.name,
@@ -84,6 +87,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
// Single method: Use first method // Single method: Use first method
return { return {
...item, ...item,
is_active: (toolDetail as any).is_active,
label: (methods as any[])[0]?.description, label: (methods as any[])[0]?.description,
method_id: (methods as any[])[0]?.method_id, method_id: (methods as any[])[0]?.method_id,
value: (methods as any[])[0]?.name, value: (methods as any[])[0]?.name,
@@ -96,6 +100,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation) const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
return { return {
...item, ...item,
is_active: (toolDetail as any).is_active,
label: customFilterItem?.name, label: customFilterItem?.name,
method_id: customFilterItem?.method_id, method_id: customFilterItem?.method_id,
value: customFilterItem?.name, value: customFilterItem?.name,
@@ -129,7 +134,10 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
* @param tool - Tool to add * @param tool - Tool to add
*/ */
const updateTools = (tool: ToolOption) => { const updateTools = (tool: ToolOption) => {
const list = [...toolList, tool] const list = [...toolList, {
...tool,
is_active: true,
}]
setToolList(list) setToolList(list)
onChange && onChange(list) onChange && onChange(list)
} }
@@ -149,39 +157,32 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => {
<Card <Card
title={t('application.toolConfiguration')} title={t('application.toolConfiguration')}
extra={ extra={
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}> <Button className="rb:h-6! rb:py-0! rb:px-2! rb:rounded-md! rb:text-[#21233" onClick={handleAddTool}>+ {t('application.addTool')}</Button>
+ {t('application.addTool')}
</Button>
} }
> >
{/* Show empty state or tool list */}
{toolList.length === 0 {toolList.length === 0
? <Empty size={88} /> ? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div>
: : <Flex vertical gap={12}>
<List {toolList.map((item, index) => (
grid={{ gutter: 12, column: 1 }} <Flex key={index} align="center" justify="space-between" className="rb:py-2.5! rb:pl-4! rb:pr-3! rb-border rb:rounded-lg">
dataSource={toolList} <div>
renderItem={(item, index) => ( <div className="rb:font-medium rb:leading-4">
<List.Item> {item.label}
{/* 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> </div>
</List.Item> <Tag color={item.is_active ? 'success' : 'error'} className="rb:mt-1">
)} {item.is_active ? t('common.enable') : t('common.deleted')}
/> </Tag>
</div>
<Space size={12}>
<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>
</Flex>
))}
</Flex>
} }
{/* Tool selection modal */}
<ToolModal <ToolModal
ref={toolModalRef} ref={toolModalRef}
refresh={updateTools} refresh={updateTools}

View File

@@ -32,6 +32,7 @@ export interface ToolOption {
tool_id?: string; tool_id?: string;
/** Whether tool is enabled */ /** Whether tool is enabled */
enabled?: boolean; enabled?: boolean;
is_active?: boolean;
} }
/** /**

View File

@@ -7,17 +7,16 @@
import { type FC, useEffect, useRef, useState } from "react"; import { type FC, useEffect, useRef, useState } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Form, Input, Button, Space, Select, App } from 'antd' import { Form, Input, Button, Space, Select, App, Flex } from 'antd'
import Card from '@/views/ApplicationConfig/components/Card' import Card from '@/views/ApplicationConfig/components/Card'
import aiPrompt from '@/assets/images/application/aiPrompt.png'
import AiPromptModal from '@/views/ApplicationConfig/components/AiPromptModal' import AiPromptModal from '@/views/ApplicationConfig/components/AiPromptModal'
import ToolList from '../components/ToolList/ToolList' import ToolList from '../components/ToolList/ToolList'
import type { AiPromptModalRef } from '@/views/ApplicationConfig/types' import type { AiPromptModalRef } from '@/views/ApplicationConfig/types'
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
import type { SkillFormData } from '../types' import type { SkillFormData } from '../types'
import { getSkillDetail, createSkill, updateSkill } from '@/api/skill' import { getSkillDetail, createSkill, updateSkill } from '@/api/skill'
import { stringRegExp } from '@/utils/validator'; import { stringRegExp } from '@/utils/validator';
import PageHeader from '@/components/Layout/PageHeader'
/** /**
* Skill Configuration Page Component * Skill Configuration Page Component
@@ -43,6 +42,7 @@ const SkillConfig: FC = () => {
const { message } = App.useApp() const { message } = App.useApp()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [form] = Form.useForm<SkillFormData>(); const [form] = Form.useForm<SkillFormData>();
const [data, setData] = useState<SkillFormData | null>(null)
/** /**
* Effect: Load skill data if editing existing skill * Effect: Load skill data if editing existing skill
@@ -70,6 +70,7 @@ const SkillConfig: FC = () => {
getSkillDetail(id) getSkillDetail(id)
.then(res => { .then(res => {
form.setFieldsValue(res as SkillFormData) form.setFieldsValue(res as SkillFormData)
setData(res as SkillFormData)
}) })
.finally(() => { .finally(() => {
setLoading(false) setLoading(false)
@@ -131,93 +132,103 @@ const SkillConfig: FC = () => {
} }
return ( return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto"> <Flex vertical className="rb:h-screen!">
{/* Back button */} <PageHeader
<div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}> title={data?.name}
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' /> extra={
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span> <Flex gap={12} align="center">
</div> {/* Save button */}
<Button type="primary" className="rb:px-2! rb:gap-0.5!" disabled={loading} onClick={handleSave}>{t('skills.save')}</Button>
<Button
className="rb:px-2! rb:gap-0.5!"
icon={<div className="rb:bg-[url('@/assets/images/workflow/return.svg')] rb:size-4 rb:bg-cover"></div>}
onClick={handleBack}
>
{t('common.return')}
</Button>
</Flex>
}
/>
<div className="rb:w-250 rb:my-3 rb:mx-auto rb:flex-1 rb:overflow-y-auto">
<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') }) },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="description"
label={t('skills.description')}
rules={[{ max: 500 }]}
>
<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>
<Form form={form} layout="vertical"> {/* Prompt Configuration Section: AI instructions */}
<Space size={16} direction="vertical" className="rb:w-full"> <Card title={t('skills.promptConfiguration')}
{/* Manifest Section: Basic skill information */} extra={
<Card title={t('skills.mainfest')}> <Button style={{ padding: '0 8px', height: '24px' }} onClick={handlePrompt}>
<Form.Item <div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/application/aiPrompt.png')] rb:mr-1!" />
name="name" {t('skills.aiPrompt')}
label={t('skills.name')} </Button>
rules={[ }
{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
> >
<Input placeholder={t('common.pleaseEnter')} /> <Form.Item
</Form.Item> name="prompt"
<Form.Item className="rb:mb-0!"
name="description" >
label={t('skills.description')} <Input.TextArea
rules={[{ max: 500 }]} placeholder={t('skills.promptPlaceholder')}
> styles={{
<Input.TextArea placeholder={t('skills.descriptionPlaceholder')} /> textarea: {
</Form.Item> minHeight: '200px',
<Form.Item borderRadius: '8px'
name={['config', 'keywords']} },
label={t('skills.keywords')} }}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.keywords') }) }]} />
> </Form.Item>
<Select </Card>
mode="tags"
placeholder={t('common.pleaseEnter')}
/>
</Form.Item>
</Card>
{/* Prompt Configuration Section: AI instructions */} {/* Tool Configuration Section */}
<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 <Form.Item
name="prompt" name="tools"
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('skills.tools') }) }]}
className="rb:mb-0!" className="rb:mb-0!"
> >
<Input.TextArea <ToolList />
placeholder={t('skills.promptPlaceholder')}
styles={{
textarea: {
minHeight: '200px',
borderRadius: '8px'
},
}}
/>
</Form.Item> </Form.Item>
</Card>
{/* Tool Configuration Section */} </Space>
<Form.Item </Form>
name="tools"
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('skills.tools') }) }]}
className="rb:mb-0!"
>
<ToolList />
</Form.Item>
{/* Save button */} {/* AI Prompt Generation Modal */}
<Button type="primary" block disabled={loading} onClick={handleSave}>{t('skills.save')}</Button> <AiPromptModal
</Space> ref={aiPromptModalRef}
</Form> refresh={updatePrompt}
source="skills"
{/* AI Prompt Generation Modal */} />
<AiPromptModal </div>
ref={aiPromptModalRef} </Flex>
refresh={updatePrompt}
source="skills"
/>
</div>
) )
} }