Tool config using db
This commit is contained in:
@@ -2,17 +2,8 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Database, Server, Zap, ExternalLink, Key, BrainCircuit, Ear, Book, Filter } from 'lucide-react';
|
||||
import { Button, Input, Badge, Drawer, Dialog } from '../components/UI';
|
||||
import { ASRModel, Assistant, KnowledgeBase, LLMModel, TabValue, Voice } from '../types';
|
||||
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchVoices, updateAssistant as updateAssistantApi } from '../services/backendApi';
|
||||
|
||||
interface ToolItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
desc: string;
|
||||
category: 'system' | 'query';
|
||||
isCustom?: boolean;
|
||||
}
|
||||
import { ASRModel, Assistant, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
|
||||
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, updateAssistant as updateAssistantApi } from '../services/backendApi';
|
||||
|
||||
const isSiliconflowVendor = (vendor?: string) => {
|
||||
const normalized = String(vendor || '').trim().toLowerCase();
|
||||
@@ -42,12 +33,31 @@ const resolveRuntimeTtsVoice = (selectedVoiceId: string, voice: Voice) => {
|
||||
return explicitKey || buildSiliconflowVoiceKey(selectedVoiceId, voice.model);
|
||||
};
|
||||
|
||||
const renderToolIcon = (icon: string) => {
|
||||
const className = 'w-4 h-4';
|
||||
const map: Record<string, React.ReactNode> = {
|
||||
Camera: <Camera className={className} />,
|
||||
CameraOff: <CameraOff className={className} />,
|
||||
Image: <Image className={className} />,
|
||||
Images: <Images className={className} />,
|
||||
CloudSun: <CloudSun className={className} />,
|
||||
Calendar: <Calendar className={className} />,
|
||||
TrendingUp: <TrendingUp className={className} />,
|
||||
Coins: <Coins className={className} />,
|
||||
Terminal: <Terminal className={className} />,
|
||||
Globe: <Globe className={className} />,
|
||||
Wrench: <Wrench className={className} />,
|
||||
};
|
||||
return map[icon] || <Wrench className={className} />;
|
||||
};
|
||||
|
||||
export const AssistantsPage: React.FC = () => {
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [voices, setVoices] = useState<Voice[]>([]);
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
||||
const [asrModels, setAsrModels] = useState<ASRModel[]>([]);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabValue>(TabValue.GLOBAL);
|
||||
@@ -58,16 +68,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
|
||||
const [publishTab, setPublishTab] = useState<'web' | 'api'>('web');
|
||||
|
||||
// Custom Tools State
|
||||
const [customTools, setCustomTools] = useState<ToolItem[]>([]);
|
||||
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]);
|
||||
const [isAddToolModalOpen, setIsAddToolModalOpen] = useState(false);
|
||||
const [addingToCategory, setAddingToCategory] = useState<'system' | 'query'>('system');
|
||||
|
||||
// New Tool Form State
|
||||
const [newToolName, setNewToolName] = useState('');
|
||||
const [newToolDesc, setNewToolDesc] = useState('');
|
||||
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
@@ -83,18 +83,20 @@ export const AssistantsPage: React.FC = () => {
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [assistantList, voiceList, kbList, llmList, asrList] = await Promise.all([
|
||||
const [assistantList, voiceList, kbList, llmList, asrList, toolList] = await Promise.all([
|
||||
fetchAssistants(),
|
||||
fetchVoices(),
|
||||
fetchKnowledgeBases(),
|
||||
fetchLLMModels(),
|
||||
fetchASRModels(),
|
||||
fetchTools(),
|
||||
]);
|
||||
setAssistants(assistantList);
|
||||
setVoices(voiceList);
|
||||
setKnowledgeBases(kbList);
|
||||
setLlmModels(llmList);
|
||||
setAsrModels(asrList);
|
||||
setTools(toolList);
|
||||
if (assistantList.length > 0) {
|
||||
setSelectedId(assistantList[0].id);
|
||||
}
|
||||
@@ -209,18 +211,10 @@ export const AssistantsPage: React.FC = () => {
|
||||
updateAssistant('tools', newTools);
|
||||
};
|
||||
|
||||
const deleteTool = (e: React.MouseEvent, toolId: string) => {
|
||||
const removeImportedTool = (e: React.MouseEvent, tool: Tool) => {
|
||||
e.stopPropagation();
|
||||
setAssistants(prev => prev.map(a => ({
|
||||
...a,
|
||||
tools: a.tools?.filter(id => id !== toolId) || []
|
||||
})));
|
||||
|
||||
if (customTools.some(t => t.id === toolId)) {
|
||||
setCustomTools(prev => prev.filter(t => t.id !== toolId));
|
||||
} else {
|
||||
setHiddenToolIds(prev => [...prev, toolId]);
|
||||
}
|
||||
if (!selectedAssistant) return;
|
||||
updateAssistant('tools', (selectedAssistant.tools || []).filter((id) => id !== tool.id));
|
||||
};
|
||||
|
||||
const addHotword = () => {
|
||||
@@ -236,44 +230,8 @@ export const AssistantsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCustomTool = () => {
|
||||
if (!newToolName.trim()) return;
|
||||
const newTool: ToolItem = {
|
||||
id: `custom_${Date.now()}`,
|
||||
name: newToolName,
|
||||
desc: newToolDesc,
|
||||
category: addingToCategory,
|
||||
icon: addingToCategory === 'system' ? <Terminal className="w-4 h-4" /> : <Globe className="w-4 h-4" />,
|
||||
isCustom: true
|
||||
};
|
||||
setCustomTools([...customTools, newTool]);
|
||||
setIsAddToolModalOpen(false);
|
||||
setNewToolName('');
|
||||
setNewToolDesc('');
|
||||
};
|
||||
|
||||
const openAddToolModal = (e: React.MouseEvent, cat: 'system' | 'query') => {
|
||||
e.stopPropagation();
|
||||
setAddingToCategory(cat);
|
||||
setIsAddToolModalOpen(true);
|
||||
};
|
||||
|
||||
const baseSystemTools: ToolItem[] = [
|
||||
{ id: 'cam_open', name: '打开相机', icon: <Camera className="w-4 h-4" />, desc: '允许 AI 开启摄像头流', category: 'system' },
|
||||
{ id: 'cam_close', name: '关闭相机', icon: <CameraOff className="w-4 h-4" />, desc: '允许 AI 停止摄像头流', category: 'system' },
|
||||
{ id: 'take_photo', name: '拍照', icon: <Image className="w-4 h-4" />, desc: 'AI 触发单张拍摄', category: 'system' },
|
||||
{ id: 'burst_3', name: '连拍三张', icon: <Images className="w-4 h-4" />, desc: 'AI 触发快速连拍', category: 'system' },
|
||||
];
|
||||
|
||||
const baseQueryTools: ToolItem[] = [
|
||||
{ id: 'q_weather', name: '天气查询', icon: <CloudSun className="w-4 h-4" />, desc: '查询实时及未来天气', category: 'query' },
|
||||
{ id: 'q_calendar', name: '日历查询', icon: <Calendar className="w-4 h-4" />, desc: '查询日程及节假日信息', category: 'query' },
|
||||
{ id: 'q_stock', name: '股价查询', icon: <TrendingUp className="w-4 h-4" />, desc: '查询股票实时行情', category: 'query' },
|
||||
{ id: 'q_exchange', name: '汇率查询', icon: <Coins className="w-4 h-4" />, desc: '查询多国货币汇率', category: 'query' },
|
||||
];
|
||||
|
||||
const systemTools = [...baseSystemTools, ...customTools.filter(t => t.category === 'system')].filter(t => !hiddenToolIds.includes(t.id));
|
||||
const queryTools = [...baseQueryTools, ...customTools.filter(t => t.category === 'query')].filter(t => !hiddenToolIds.includes(t.id));
|
||||
const systemTools = tools.filter((t) => t.enabled !== false && t.category === 'system');
|
||||
const queryTools = tools.filter((t) => t.enabled !== false && t.category === 'query');
|
||||
|
||||
const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt';
|
||||
const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode;
|
||||
@@ -766,12 +724,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
<h3 className="text-[10px] font-black flex items-center text-primary uppercase tracking-[0.2em]">
|
||||
<Wrench className="w-3.5 h-3.5 mr-2" /> 系统指令
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => openAddToolModal(e, 'system')}
|
||||
className="p-1 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{systemTools.map(tool => (
|
||||
@@ -781,7 +733,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative flex items-start space-x-3 ${selectedAssistant.tools?.includes(tool.id) ? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.1)]' : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'}`}
|
||||
>
|
||||
<div className={`p-2 rounded-lg shrink-0 transition-colors ${selectedAssistant.tools?.includes(tool.id) ? 'bg-primary text-primary-foreground' : 'bg-white/5 text-muted-foreground'}`}>
|
||||
{tool.icon}
|
||||
{renderToolIcon(tool.icon)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
@@ -790,15 +742,17 @@ export const AssistantsPage: React.FC = () => {
|
||||
{selectedAssistant.tools?.includes(tool.id) && <div className="w-1.5 h-1.5 bg-white rounded-full"></div>}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => deleteTool(e, tool.id)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
title="删除工具"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
{selectedAssistant.tools?.includes(tool.id) && (
|
||||
<button
|
||||
onClick={(e) => removeImportedTool(e, tool)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
title="从当前小助手移除"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -809,12 +763,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
<h3 className="text-[10px] font-black flex items-center text-blue-400 uppercase tracking-[0.2em]">
|
||||
<TrendingUp className="w-3.5 h-3.5 mr-2" /> 信息查询
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => openAddToolModal(e, 'query')}
|
||||
className="p-1 rounded-full bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{queryTools.map(tool => (
|
||||
@@ -824,7 +772,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative flex items-start space-x-3 ${selectedAssistant.tools?.includes(tool.id) ? 'bg-blue-500/10 border-blue-500/40 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'}`}
|
||||
>
|
||||
<div className={`p-2 rounded-lg shrink-0 transition-colors ${selectedAssistant.tools?.includes(tool.id) ? 'bg-blue-500 text-white' : 'bg-white/5 text-muted-foreground'}`}>
|
||||
{tool.icon}
|
||||
{renderToolIcon(tool.icon)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
@@ -833,22 +781,24 @@ export const AssistantsPage: React.FC = () => {
|
||||
{selectedAssistant.tools?.includes(tool.id) && <div className="w-1.5 h-1.5 bg-white rounded-full"></div>}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => deleteTool(e, tool.id)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
title="删除工具"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
{selectedAssistant.tools?.includes(tool.id) && (
|
||||
<button
|
||||
onClick={(e) => removeImportedTool(e, tool)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
title="从当前小助手移除"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 border border-white/5 rounded-xl text-[10px] text-muted-foreground flex items-center gap-3">
|
||||
<Rocket className="w-4 h-4 text-primary shrink-0" />
|
||||
<span>提示:启用工具后,AI 将能在对话中自动识别并调用相关功能以协助用户。</span>
|
||||
<span>提示:此处仅导入工具库中的已有工具。移除仅对当前小助手生效,不会删除工具库。</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -968,44 +918,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add Custom Tool Modal */}
|
||||
<Dialog
|
||||
isOpen={isAddToolModalOpen}
|
||||
onClose={() => setIsAddToolModalOpen(false)}
|
||||
title={addingToCategory === 'system' ? '添加自定义系统指令' : '添加自定义信息查询'}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsAddToolModalOpen(false)}>取消</Button>
|
||||
<Button onClick={handleAddCustomTool}>确认添加</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具名称</label>
|
||||
<Input
|
||||
value={newToolName}
|
||||
onChange={e => setNewToolName(e.target.value)}
|
||||
placeholder="例如: 智能家居控制"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具描述 (给 AI 的说明)</label>
|
||||
<textarea
|
||||
className="flex min-h-[100px] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white"
|
||||
value={newToolDesc}
|
||||
onChange={e => setNewToolDesc(e.target.value)}
|
||||
placeholder="描述该工具的功能,以及 AI 应该在什么情况下调用它..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3 bg-primary/5 border border-primary/20 rounded-lg text-[10px] text-muted-foreground flex items-start gap-2">
|
||||
<Wrench className="w-3.5 h-3.5 text-primary shrink-0 mt-0.5" />
|
||||
<p>自定义工具将通过其名称 and 描述告知 AI 它的用途。您可以在后续的工作流中进一步定义 these 工具的具体行为逻辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
isOpen={!!deleteId}
|
||||
|
||||
Reference in New Issue
Block a user