Files
AI-VideoAssistant/web/pages/ToolLibrary.tsx

698 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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" />,
};
type ToolParameterType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array';
type ToolParameterDraft = {
id: string;
key: string;
type: ToolParameterType;
required: boolean;
description: string;
hasDefault: boolean;
defaultValueText: string;
};
const TOOL_PARAMETER_TYPES: ToolParameterType[] = ['string', 'number', 'integer', 'boolean', 'object', 'array'];
const newParameterDraft = (): ToolParameterDraft => ({
id: `param_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
key: '',
type: 'string',
required: false,
description: '',
hasDefault: false,
defaultValueText: '',
});
const schemaTypeOrDefault = (value: any): ToolParameterType =>
TOOL_PARAMETER_TYPES.includes(value as ToolParameterType) ? (value as ToolParameterType) : 'string';
const stringifyDefaultValue = (value: any): string => {
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch {
return '';
}
}
return String(value);
};
const parseDefaultValueByType = (type: ToolParameterType, valueText: string): { value?: any; error?: string } => {
const text = valueText.trim();
if (type === 'string') return { value: valueText };
if (type === 'number') {
const n = Number(text);
if (!Number.isFinite(n)) return { error: `参数默认值不是合法 number: ${valueText}` };
return { value: n };
}
if (type === 'integer') {
const n = Number(text);
if (!Number.isInteger(n)) return { error: `参数默认值不是合法 integer: ${valueText}` };
return { value: n };
}
if (type === 'boolean') {
const lowered = text.toLowerCase();
if (lowered === 'true') return { value: true };
if (lowered === 'false') return { value: false };
return { error: `参数默认值不是合法 boolean(true/false): ${valueText}` };
}
if (type === 'object' || type === 'array') {
try {
const parsed = JSON.parse(text || (type === 'array' ? '[]' : '{}'));
if (type === 'object' && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))) {
return { error: `参数默认值不是合法 object JSON: ${valueText}` };
}
if (type === 'array' && !Array.isArray(parsed)) {
return { error: `参数默认值不是合法 array JSON: ${valueText}` };
}
return { value: parsed };
} catch {
return { error: `参数默认值不是合法 JSON: ${valueText}` };
}
}
return { value: valueText };
};
const draftsFromSchema = (schema: Record<string, any> | undefined, defaults: Record<string, any> | undefined): ToolParameterDraft[] => {
const safeSchema = schema && typeof schema === 'object' ? schema : {};
const safeDefaults = defaults && typeof defaults === 'object' ? defaults : {};
const properties = safeSchema.properties && typeof safeSchema.properties === 'object' ? safeSchema.properties : {};
const requiredSet = new Set<string>(Array.isArray(safeSchema.required) ? safeSchema.required.map((item) => String(item)) : []);
const drafts: ToolParameterDraft[] = Object.entries(properties).map(([key, spec]) => {
const typedSpec: Record<string, any> = spec && typeof spec === 'object' ? (spec as Record<string, any>) : {};
const hasDefault = Object.prototype.hasOwnProperty.call(safeDefaults, key);
return {
id: `param_${key}_${Math.random().toString(16).slice(2, 8)}`,
key,
type: schemaTypeOrDefault(typedSpec.type),
required: requiredSet.has(key),
description: String(typedSpec.description || ''),
hasDefault,
defaultValueText: hasDefault ? stringifyDefaultValue(safeDefaults[key]) : '',
};
});
for (const [key, value] of Object.entries(safeDefaults)) {
if (drafts.some((item) => item.key === key)) continue;
drafts.push({
id: `param_${key}_${Math.random().toString(16).slice(2, 8)}`,
key,
type: typeof value === 'number' && Number.isInteger(value) ? 'integer' : (typeof value as ToolParameterType) || 'string',
required: false,
description: '',
hasDefault: true,
defaultValueText: stringifyDefaultValue(value),
});
}
return drafts;
};
const buildToolParameterConfig = (drafts: ToolParameterDraft[]): { schema: Record<string, any>; defaults: Record<string, any>; error?: string } => {
const properties: Record<string, any> = {};
const required: string[] = [];
const defaults: Record<string, any> = {};
const seen = new Set<string>();
for (const draft of drafts) {
const key = draft.key.trim();
if (!key) continue;
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
return { schema: {}, defaults: {}, error: `参数标识不合法: ${key}` };
}
if (seen.has(key)) {
return { schema: {}, defaults: {}, error: `参数标识重复: ${key}` };
}
seen.add(key);
properties[key] = {
type: draft.type,
...(draft.description.trim() ? { description: draft.description.trim() } : {}),
};
if (draft.required) required.push(key);
if (draft.hasDefault) {
const parsed = parseDefaultValueByType(draft.type, draft.defaultValueText);
if (parsed.error) {
return { schema: {}, defaults: {}, error: parsed.error };
}
defaults[key] = parsed.value;
}
}
return {
schema: {
type: 'object',
properties,
required,
},
defaults,
};
};
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 [toolParameters, setToolParameters] = useState<ToolParameterDraft[]>([]);
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);
setToolParameters([]);
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);
setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults));
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 addToolParameter = () => {
setToolParameters((prev) => [...prev, newParameterDraft()]);
};
const updateToolParameter = (parameterId: string, patch: Partial<ToolParameterDraft>) => {
setToolParameters((prev) => prev.map((item) => (item.id === parameterId ? { ...item, ...patch } : item)));
};
const removeToolParameter = (parameterId: string) => {
setToolParameters((prev) => prev.filter((item) => item.id !== parameterId));
};
const handleSaveTool = async () => {
if (!toolName.trim()) {
alert('请填写工具名称');
return;
}
try {
setSaving(true);
let parsedHeaders: Record<string, string> = {};
const parameterConfig = buildToolParameterConfig(toolParameters);
if (parameterConfig.error) {
alert(parameterConfig.error);
setSaving(false);
return;
}
const parsedParameterSchema = parameterConfig.schema;
const parsedParameterDefaults = parameterConfig.defaults;
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 max-h-[68vh] overflow-y-auto pr-1">
<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="flex items-center justify-between">
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
<Plus className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
{toolParameters.length === 0 ? (
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
ElevenLabs JSON Schema
</div>
) : (
<div className="space-y-3">
{toolParameters.map((param) => (
<div key={param.id} className="rounded-md border border-white/10 bg-black/20 p-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
<div className="md:col-span-2 space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Identifier</label>
<Input
value={param.key}
onChange={(e) => updateToolParameter(param.id, { key: e.target.value })}
placeholder="city"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Type</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={param.type}
onChange={(e) => updateToolParameter(param.id, { type: e.target.value as ToolParameterType })}
>
{TOOL_PARAMETER_TYPES.map((type) => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<label className="flex items-center gap-2 text-xs text-muted-foreground md:mt-7">
<input
type="checkbox"
checked={param.required}
onChange={(e) => updateToolParameter(param.id, { required: e.target.checked })}
/>
</label>
<div className="flex items-end justify-end md:mt-0">
<button
type="button"
onClick={() => removeToolParameter(param.id)}
className="h-9 px-3 rounded-md border border-white/10 text-xs text-muted-foreground hover:text-destructive hover:border-destructive/40"
>
</button>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Description</label>
<Input
value={param.description}
onChange={(e) => updateToolParameter(param.id, { description: e.target.value })}
placeholder="Parameter description for model"
/>
</div>
<div className="space-y-1.5">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
checked={param.hasDefault}
onChange={(e) => updateToolParameter(param.id, { hasDefault: e.target.checked })}
/>
</label>
{param.hasDefault && (
<Input
value={param.defaultValueText}
onChange={(e) => updateToolParameter(param.id, { defaultValueText: e.target.value })}
placeholder={param.type === 'object' || param.type === 'array' ? 'JSON value' : 'Default value'}
/>
)}
</div>
</div>
))}
</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>
);
};