feat(web): add skills menu

This commit is contained in:
zhaoying
2026-02-05 10:53:16 +08:00
parent 161da723b9
commit 60231ec88d
26 changed files with 1722 additions and 47 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -0,0 +1,161 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-05 10:43:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 10:43:03
*/
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.desciption}>
<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.desciption}</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

View 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

View 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;
}

View File

@@ -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