191 lines
9.2 KiB
TypeScript
191 lines
9.2 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, X, Box } from 'lucide-react';
|
|
import { Button, Input, Badge, Dialog } from '../components/UI';
|
|
import { mockTools } from '../services/mockData';
|
|
import { Tool } from '../types';
|
|
|
|
// Map icon strings to React Nodes
|
|
const iconMap: Record<string, React.ReactNode> = {
|
|
Camera: <Camera className="w-5 h-5" />,
|
|
CameraOff: <CameraOff className="w-5 h-5" />,
|
|
Image: <Image className="w-5 h-5" />,
|
|
Images: <Images className="w-5 h-5" />,
|
|
CloudSun: <CloudSun className="w-5 h-5" />,
|
|
Calendar: <Calendar className="w-5 h-5" />,
|
|
TrendingUp: <TrendingUp className="w-5 h-5" />,
|
|
Coins: <Coins className="w-5 h-5" />,
|
|
Terminal: <Terminal className="w-5 h-5" />,
|
|
Globe: <Globe className="w-5 h-5" />,
|
|
Wrench: <Wrench className="w-5 h-5" />,
|
|
};
|
|
|
|
export const ToolLibraryPage: React.FC = () => {
|
|
const [tools, setTools] = useState<Tool[]>(mockTools);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [categoryFilter, setCategoryFilter] = useState<'all' | 'system' | 'query'>('all');
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
|
|
// New Tool Form
|
|
const [newToolName, setNewToolName] = useState('');
|
|
const [newToolDesc, setNewToolDesc] = useState('');
|
|
const [newToolCategory, setNewToolCategory] = useState<'system' | 'query'>('system');
|
|
|
|
const filteredTools = tools.filter(tool => {
|
|
const matchesSearch = tool.name.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesCategory = categoryFilter === 'all' || tool.category === categoryFilter;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
const handleAddTool = () => {
|
|
if (!newToolName.trim()) return;
|
|
const newTool: Tool = {
|
|
id: `custom_${Date.now()}`,
|
|
name: newToolName,
|
|
description: newToolDesc,
|
|
category: newToolCategory,
|
|
icon: newToolCategory === 'system' ? 'Terminal' : 'Globe',
|
|
isCustom: true
|
|
};
|
|
setTools([...tools, newTool]);
|
|
setIsAddModalOpen(false);
|
|
setNewToolName('');
|
|
setNewToolDesc('');
|
|
};
|
|
|
|
const handleDeleteTool = (e: React.MouseEvent, id: string) => {
|
|
e.stopPropagation();
|
|
if (confirm('确认删除该工具吗?')) {
|
|
setTools(prev => prev.filter(t => t.id !== id));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold tracking-tight text-white">工具库</h1>
|
|
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
|
<Plus className="mr-2 h-4 w-4" /> 添加工具
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
|
<div className="relative col-span-1 md:col-span-2">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="搜索工具名称..."
|
|
className="pl-9 border-0 bg-white/5"
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
<select
|
|
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
|
value={categoryFilter}
|
|
onChange={(e) => setCategoryFilter(e.target.value as any)}
|
|
>
|
|
<option value="all">所有类型</option>
|
|
<option value="system">系统指令 (System)</option>
|
|
<option value="query">信息查询 (Query)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredTools.map(tool => (
|
|
<div
|
|
key={tool.id}
|
|
className={`p-5 rounded-xl border transition-all relative group flex items-start space-x-4 bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10 hover:shadow-lg`}
|
|
>
|
|
<div className={`p-3 rounded-lg shrink-0 transition-colors ${tool.category === 'system' ? 'bg-primary/10 text-primary' : 'bg-blue-500/10 text-blue-400'}`}>
|
|
{iconMap[tool.icon] || <Box className="w-5 h-5" />}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-base font-bold text-white">{tool.name}</span>
|
|
{tool.isCustom && <Badge variant="outline" className="text-[9px] h-4 px-1">CUSTOM</Badge>}
|
|
</div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge variant="outline" className={`text-[10px] border-0 px-0 ${tool.category === 'system' ? 'text-primary' : 'text-blue-400'}`}>
|
|
{tool.category === 'system' ? 'SYSTEM' : 'QUERY'}
|
|
</Badge>
|
|
<span className="text-[10px] text-muted-foreground font-mono opacity-50">ID: {tool.id}</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed opacity-80">{tool.description}</p>
|
|
</div>
|
|
|
|
{tool.isCustom && (
|
|
<div className="absolute top-3 right-3 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={(e) => handleDeleteTool(e, tool.id)}
|
|
className="p-1.5 rounded-md hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{filteredTools.length === 0 && (
|
|
<div className="col-span-full py-12 flex flex-col items-center justify-center text-muted-foreground opacity-50">
|
|
<Wrench className="w-12 h-12 mb-4 stroke-1" />
|
|
<p>未找到相关工具</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Dialog
|
|
isOpen={isAddModalOpen}
|
|
onClose={() => setIsAddModalOpen(false)}
|
|
title="添加自定义工具"
|
|
footer={
|
|
<>
|
|
<Button variant="ghost" onClick={() => setIsAddModalOpen(false)}>取消</Button>
|
|
<Button onClick={handleAddTool}>确认添加</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>
|
|
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
|
<button
|
|
onClick={() => setNewToolCategory('system')}
|
|
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${newToolCategory === 'system' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
|
>
|
|
<Terminal className="w-3.5 h-3.5 mr-2" /> 系统指令
|
|
</button>
|
|
<button
|
|
onClick={() => setNewToolCategory('query')}
|
|
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${newToolCategory === 'query' ? 'bg-blue-500 text-white shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
|
>
|
|
<Globe className="w-3.5 h-3.5 mr-2" /> 信息查询
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}; |