Tool config using db

This commit is contained in:
Xin Wang
2026-02-09 15:07:51 +08:00
parent cd68ebe306
commit 1b83b58d48

View File

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