512 lines
23 KiB
TypeScript
512 lines
23 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, Box, Volume2 } from 'lucide-react';
|
|
import { Button, Input, Badge, Dialog } from '../components/UI';
|
|
import { Tool } from '../types';
|
|
import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi';
|
|
|
|
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" />,
|
|
Box: <Box className="w-5 h-5" />,
|
|
Volume2: <Volume2 className="w-5 h-5" />,
|
|
};
|
|
|
|
const DEFAULT_PARAMETER_SCHEMA_TEXT = JSON.stringify(
|
|
{ type: 'object', properties: {}, required: [] },
|
|
null,
|
|
2
|
|
);
|
|
|
|
export const ToolLibraryPage: React.FC = () => {
|
|
const [tools, setTools] = useState<Tool[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [categoryFilter, setCategoryFilter] = useState<'all' | 'system' | 'query'>('all');
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isToolModalOpen, setIsToolModalOpen] = useState(false);
|
|
const [editingTool, setEditingTool] = useState<Tool | null>(null);
|
|
|
|
const [toolName, setToolName] = useState('');
|
|
const [toolDesc, setToolDesc] = useState('');
|
|
const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system');
|
|
const [toolIcon, setToolIcon] = useState('Wrench');
|
|
const [toolEnabled, setToolEnabled] = useState(true);
|
|
const [toolHttpMethod, setToolHttpMethod] = useState<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>('GET');
|
|
const [toolHttpUrl, setToolHttpUrl] = useState('');
|
|
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
|
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
|
const [toolParameterSchemaText, setToolParameterSchemaText] = useState(DEFAULT_PARAMETER_SCHEMA_TEXT);
|
|
const [toolParameterDefaultsText, setToolParameterDefaultsText] = useState('{}');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const loadTools = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
setTools(await fetchTools());
|
|
} catch (error) {
|
|
console.error(error);
|
|
setTools([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadTools();
|
|
}, []);
|
|
|
|
const openAdd = () => {
|
|
setEditingTool(null);
|
|
setToolName('');
|
|
setToolDesc('');
|
|
setToolCategory('system');
|
|
setToolIcon('Wrench');
|
|
setToolEnabled(true);
|
|
setToolHttpMethod('GET');
|
|
setToolHttpUrl('');
|
|
setToolHttpHeadersText('{}');
|
|
setToolHttpTimeoutMs(10000);
|
|
setToolParameterSchemaText(DEFAULT_PARAMETER_SCHEMA_TEXT);
|
|
setToolParameterDefaultsText('{}');
|
|
setIsToolModalOpen(true);
|
|
};
|
|
|
|
const openEdit = (tool: Tool) => {
|
|
setEditingTool(tool);
|
|
setToolName(tool.name);
|
|
setToolDesc(tool.description || '');
|
|
setToolCategory(tool.category);
|
|
setToolIcon(tool.icon || 'Wrench');
|
|
setToolEnabled(tool.enabled ?? true);
|
|
setToolHttpMethod((tool.httpMethod || 'GET') as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE');
|
|
setToolHttpUrl(tool.httpUrl || '');
|
|
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
|
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
|
setToolParameterSchemaText(JSON.stringify(tool.parameterSchema || { type: 'object', properties: {}, required: [] }, null, 2));
|
|
setToolParameterDefaultsText(JSON.stringify(tool.parameterDefaults || {}, null, 2));
|
|
setIsToolModalOpen(true);
|
|
};
|
|
|
|
const filteredTools = tools.filter((tool) => {
|
|
const q = searchTerm.toLowerCase();
|
|
const matchesSearch =
|
|
tool.name.toLowerCase().includes(q) ||
|
|
(tool.description || '').toLowerCase().includes(q) ||
|
|
tool.id.toLowerCase().includes(q);
|
|
const matchesCategory = categoryFilter === 'all' || tool.category === categoryFilter;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
const systemTools = filteredTools.filter((tool) => tool.category === 'system');
|
|
const queryTools = filteredTools.filter((tool) => tool.category === 'query');
|
|
|
|
const renderToolCard = (tool: Tool) => (
|
|
<div
|
|
key={tool.id}
|
|
className="p-5 rounded-xl border transition-all relative bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10 hover:shadow-lg"
|
|
>
|
|
<div className="flex items-start space-x-4">
|
|
<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 gap-2">
|
|
<span className="text-base font-bold text-white truncate">{tool.name}</span>
|
|
{tool.isSystem ? <Badge variant="outline" className="text-[9px] h-4 px-1">SYSTEM</Badge> : <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 truncate">ID: {tool.id}</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed opacity-80">{tool.description}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-3 border-t border-white/10 flex items-center justify-between">
|
|
<span className="text-[11px] text-muted-foreground">system/query 仅表示执行类型</span>
|
|
<div className="flex space-x-1">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openEdit(tool);
|
|
}}
|
|
title="编辑工具"
|
|
className="p-1.5 rounded-md transition-colors hover:bg-primary/20 text-muted-foreground hover:text-primary"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => handleDeleteTool(e, tool)}
|
|
title="删除工具"
|
|
className="p-1.5 rounded-md transition-colors hover:bg-destructive/20 text-muted-foreground hover:text-destructive"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const handleSaveTool = async () => {
|
|
if (!toolName.trim()) {
|
|
alert('请填写工具名称');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSaving(true);
|
|
let parsedHeaders: Record<string, string> = {};
|
|
let parsedParameterSchema: Record<string, any> = {};
|
|
let parsedParameterDefaults: Record<string, any> = {};
|
|
|
|
try {
|
|
const parsed = JSON.parse(toolParameterSchemaText || '{}');
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new Error('schema must be object');
|
|
}
|
|
parsedParameterSchema = parsed as Record<string, any>;
|
|
} catch {
|
|
alert('参数 Schema 必须是合法 JSON 对象');
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
if (parsedParameterSchema.type && parsedParameterSchema.type !== 'object') {
|
|
alert("参数 Schema 的 type 必须是 'object'");
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
if (!parsedParameterSchema.type) parsedParameterSchema.type = 'object';
|
|
if (!parsedParameterSchema.properties || typeof parsedParameterSchema.properties !== 'object' || Array.isArray(parsedParameterSchema.properties)) {
|
|
parsedParameterSchema.properties = {};
|
|
}
|
|
if (!Array.isArray(parsedParameterSchema.required)) {
|
|
parsedParameterSchema.required = [];
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(toolParameterDefaultsText || '{}');
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new Error('defaults must be object');
|
|
}
|
|
parsedParameterDefaults = parsed as Record<string, any>;
|
|
} catch {
|
|
alert('参数默认值必须是合法 JSON 对象');
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
|
|
if (toolCategory === 'query') {
|
|
if (
|
|
!toolHttpUrl.trim()
|
|
&& editingTool?.id !== 'calculator'
|
|
&& editingTool?.id !== 'code_interpreter'
|
|
&& editingTool?.id !== 'current_time'
|
|
) {
|
|
alert('信息查询工具请填写 HTTP URL');
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(toolHttpHeadersText || '{}');
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
parsedHeaders = parsed as Record<string, string>;
|
|
} else {
|
|
throw new Error('headers must be object');
|
|
}
|
|
} catch {
|
|
alert('HTTP Headers 必须是合法 JSON 对象');
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (editingTool) {
|
|
const updated = await updateTool(editingTool.id, {
|
|
name: toolName.trim(),
|
|
description: toolDesc,
|
|
category: toolCategory,
|
|
icon: toolIcon,
|
|
httpMethod: toolHttpMethod,
|
|
httpUrl: toolHttpUrl.trim(),
|
|
httpHeaders: parsedHeaders,
|
|
httpTimeoutMs: toolHttpTimeoutMs,
|
|
parameterSchema: parsedParameterSchema,
|
|
parameterDefaults: parsedParameterDefaults,
|
|
enabled: toolEnabled,
|
|
});
|
|
setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item)));
|
|
} else {
|
|
const created = await createTool({
|
|
name: toolName.trim(),
|
|
description: toolDesc,
|
|
category: toolCategory,
|
|
icon: toolIcon,
|
|
httpMethod: toolHttpMethod,
|
|
httpUrl: toolHttpUrl.trim(),
|
|
httpHeaders: parsedHeaders,
|
|
httpTimeoutMs: toolHttpTimeoutMs,
|
|
parameterSchema: parsedParameterSchema,
|
|
parameterDefaults: parsedParameterDefaults,
|
|
enabled: toolEnabled,
|
|
});
|
|
setTools((prev) => [created, ...prev]);
|
|
}
|
|
setIsToolModalOpen(false);
|
|
} catch (error: any) {
|
|
alert(error?.message || '保存工具失败');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteTool = async (e: React.MouseEvent, tool: Tool) => {
|
|
e.stopPropagation();
|
|
if (!confirm('确认删除该工具吗?')) return;
|
|
|
|
try {
|
|
await deleteTool(tool.id);
|
|
setTools((prev) => prev.filter((item) => item.id !== tool.id));
|
|
} catch (error: any) {
|
|
alert(error?.message || '删除失败');
|
|
}
|
|
};
|
|
|
|
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={openAdd} 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 'all' | 'system' | 'query')}
|
|
>
|
|
<option value="all">所有类型</option>
|
|
<option value="system">系统指令 (System)</option>
|
|
<option value="query">信息查询 (Query)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="py-12 flex flex-col items-center justify-center text-muted-foreground opacity-70">
|
|
<Wrench className="w-12 h-12 mb-4 stroke-1 animate-pulse" />
|
|
<p>加载中...</p>
|
|
</div>
|
|
) : filteredTools.length === 0 ? (
|
|
<div className="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 className="space-y-8">
|
|
{(categoryFilter === 'all' || categoryFilter === 'system') && (
|
|
<section className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-sm font-black uppercase tracking-wider text-primary">System Command</h2>
|
|
<Badge variant="outline" className="text-[10px]">{systemTools.length} tools</Badge>
|
|
</div>
|
|
{systemTools.length === 0 ? (
|
|
<div className="rounded-lg border border-white/10 bg-black/20 p-4 text-xs text-muted-foreground">当前筛选条件下无系统指令工具。</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{systemTools.map(renderToolCard)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{(categoryFilter === 'all' || categoryFilter === 'query') && (
|
|
<section className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-sm font-black uppercase tracking-wider text-blue-300">Information Query</h2>
|
|
<Badge variant="outline" className="text-[10px]">{queryTools.length} tools</Badge>
|
|
</div>
|
|
{queryTools.length === 0 ? (
|
|
<div className="rounded-lg border border-white/10 bg-black/20 p-4 text-xs text-muted-foreground">当前筛选条件下无信息查询工具。</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{queryTools.map(renderToolCard)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Dialog
|
|
isOpen={isToolModalOpen}
|
|
onClose={() => setIsToolModalOpen(false)}
|
|
title={editingTool ? '编辑自定义工具' : '添加自定义工具'}
|
|
footer={
|
|
<>
|
|
<Button variant="ghost" onClick={() => setIsToolModalOpen(false)}>取消</Button>
|
|
<Button onClick={handleSaveTool} disabled={saving}>{saving ? '保存中...' : (editingTool ? '保存修改' : '确认添加')}</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={() => {
|
|
setToolCategory('system');
|
|
if (!toolIcon) setToolIcon('Terminal');
|
|
}}
|
|
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${toolCategory === '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={() => {
|
|
setToolCategory('query');
|
|
if (!toolIcon) setToolIcon('Globe');
|
|
}}
|
|
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${toolCategory === '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">图标 (Icon)</label>
|
|
<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={toolIcon}
|
|
onChange={(e) => setToolIcon(e.target.value)}
|
|
>
|
|
{Object.keys(iconMap).map((icon) => (
|
|
<option key={icon} value={icon}>{icon}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具名称</label>
|
|
<Input
|
|
value={toolName}
|
|
onChange={(e) => setToolName(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={toolDesc}
|
|
onChange={(e) => setToolDesc(e.target.value)}
|
|
placeholder="描述该工具的功能,以及 AI 应该在什么情况下调用它..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-4 rounded-md border border-white/10 bg-white/5 p-3">
|
|
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Schema (JSON Schema)</label>
|
|
<textarea
|
|
className="flex min-h-[110px] w-full rounded-md border border-white/10 bg-black/20 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 font-mono"
|
|
value={toolParameterSchemaText}
|
|
onChange={(e) => setToolParameterSchemaText(e.target.value)}
|
|
placeholder={DEFAULT_PARAMETER_SCHEMA_TEXT}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Default Args (JSON)</label>
|
|
<textarea
|
|
className="flex min-h-[90px] w-full rounded-md border border-white/10 bg-black/20 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 font-mono"
|
|
value={toolParameterDefaultsText}
|
|
onChange={(e) => setToolParameterDefaultsText(e.target.value)}
|
|
placeholder='{"step": 1}'
|
|
/>
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
支持系统指令和信息查询两类工具。Default Args 会在模型未传值时自动补齐。
|
|
</p>
|
|
</div>
|
|
|
|
{toolCategory === 'query' && (
|
|
<div className="space-y-4 rounded-md border border-blue-500/20 bg-blue-500/5 p-3">
|
|
<div className="text-[10px] font-black uppercase tracking-widest text-blue-300">HTTP Request Config</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Method</label>
|
|
<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={toolHttpMethod}
|
|
onChange={(e) => setToolHttpMethod(e.target.value as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')}
|
|
>
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="PATCH">PATCH</option>
|
|
<option value="DELETE">DELETE</option>
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1.5 md:col-span-2">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">URL</label>
|
|
<Input value={toolHttpUrl} onChange={(e) => setToolHttpUrl(e.target.value)} placeholder="https://api.example.com/endpoint" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Headers (JSON)</label>
|
|
<textarea
|
|
className="flex min-h-[90px] 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 font-mono"
|
|
value={toolHttpHeadersText}
|
|
onChange={(e) => setToolHttpHeadersText(e.target.value)}
|
|
placeholder='{"Authorization":"Bearer ..."}'
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Timeout (ms)</label>
|
|
<Input
|
|
type="number"
|
|
min={1000}
|
|
value={toolHttpTimeoutMs}
|
|
onChange={(e) => setToolHttpTimeoutMs(Math.max(1000, Number(e.target.value || 10000)))}
|
|
/>
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Query tools send model arguments as request params for GET/DELETE, and JSON body for POST/PUT/PATCH.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<label className="flex items-center space-x-2 text-xs text-muted-foreground">
|
|
<input type="checkbox" checked={toolEnabled} onChange={(e) => setToolEnabled(e.target.checked)} />
|
|
<span>启用该工具</span>
|
|
</label>
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|