Implement schema editor functionality in ToolLibrary, allowing users to manage tool parameters with JSON schema validation. Add a drawer for schema editing, enhance state management for schema-related errors, and integrate schema defaults into tool parameter configuration. Update UI to include a button for opening the schema drawer.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
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 { Button, Input, Badge, Dialog, Drawer } from '../components/UI';
|
||||
import { Tool } from '../types';
|
||||
import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi';
|
||||
|
||||
@@ -170,6 +170,19 @@ const buildToolParameterConfig = (drafts: ToolParameterDraft[]): { schema: Recor
|
||||
};
|
||||
};
|
||||
|
||||
const defaultsFromSchema = (schema: Record<string, any>): Record<string, any> => {
|
||||
const defaults: Record<string, any> = {};
|
||||
const properties = schema?.properties;
|
||||
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) return defaults;
|
||||
for (const [key, spec] of Object.entries(properties)) {
|
||||
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
|
||||
if (Object.prototype.hasOwnProperty.call(spec, 'default')) {
|
||||
defaults[key] = (spec as Record<string, any>).default;
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
export const ToolLibraryPage: React.FC = () => {
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -190,6 +203,9 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
||||
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
||||
const [toolParameters, setToolParameters] = useState<ToolParameterDraft[]>([]);
|
||||
const [schemaDrawerOpen, setSchemaDrawerOpen] = useState(false);
|
||||
const [schemaEditorText, setSchemaEditorText] = useState('');
|
||||
const [schemaEditorError, setSchemaEditorError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadTools = async () => {
|
||||
@@ -222,6 +238,9 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
setToolHttpHeadersText('{}');
|
||||
setToolHttpTimeoutMs(10000);
|
||||
setToolParameters([]);
|
||||
setSchemaEditorError('');
|
||||
setSchemaEditorText(JSON.stringify({ type: 'object', properties: {}, required: [] }, null, 2));
|
||||
setSchemaDrawerOpen(false);
|
||||
setIsToolModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -239,9 +258,92 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
||||
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
||||
setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults));
|
||||
setSchemaEditorError('');
|
||||
setSchemaEditorText(
|
||||
JSON.stringify(
|
||||
(tool.parameterSchema && typeof tool.parameterSchema === 'object')
|
||||
? tool.parameterSchema
|
||||
: { type: 'object', properties: {}, required: [] },
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
setSchemaDrawerOpen(false);
|
||||
setIsToolModalOpen(true);
|
||||
};
|
||||
|
||||
const openSchemaDrawer = () => {
|
||||
const parameterConfig = buildToolParameterConfig(toolParameters);
|
||||
if (!parameterConfig.error) {
|
||||
setSchemaEditorText(JSON.stringify(parameterConfig.schema, null, 2));
|
||||
} else if (editingTool?.parameterSchema && typeof editingTool.parameterSchema === 'object') {
|
||||
setSchemaEditorText(JSON.stringify(editingTool.parameterSchema, null, 2));
|
||||
} else if (!schemaEditorText.trim()) {
|
||||
setSchemaEditorText(JSON.stringify({ type: 'object', properties: {}, required: [] }, null, 2));
|
||||
}
|
||||
setSchemaEditorError('');
|
||||
setSchemaDrawerOpen(true);
|
||||
};
|
||||
|
||||
const formatSchemaEditor = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(schemaEditorText || '{}');
|
||||
setSchemaEditorText(JSON.stringify(parsed, null, 2));
|
||||
setSchemaEditorError('');
|
||||
} catch {
|
||||
setSchemaEditorError('Schema 不是合法 JSON,无法格式化。');
|
||||
}
|
||||
};
|
||||
|
||||
const applySchemaEditor = () => {
|
||||
let parsedSchema: Record<string, any>;
|
||||
try {
|
||||
const parsed = JSON.parse(schemaEditorText || '{}');
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
setSchemaEditorError('Schema 必须是 JSON 对象。');
|
||||
return;
|
||||
}
|
||||
parsedSchema = parsed as Record<string, any>;
|
||||
} catch {
|
||||
setSchemaEditorError('Schema 不是合法 JSON。');
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedSchema.type !== undefined && parsedSchema.type !== 'object') {
|
||||
setSchemaEditorError("Schema.type 必须为 'object'。");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
parsedSchema.properties !== undefined
|
||||
&& (!parsedSchema.properties || typeof parsedSchema.properties !== 'object' || Array.isArray(parsedSchema.properties))
|
||||
) {
|
||||
setSchemaEditorError('Schema.properties 必须是对象。');
|
||||
return;
|
||||
}
|
||||
if (parsedSchema.required !== undefined && !Array.isArray(parsedSchema.required)) {
|
||||
setSchemaEditorError('Schema.required 必须是数组。');
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSchema: Record<string, any> = {
|
||||
...parsedSchema,
|
||||
type: 'object',
|
||||
properties: parsedSchema.properties && typeof parsedSchema.properties === 'object' && !Array.isArray(parsedSchema.properties)
|
||||
? parsedSchema.properties
|
||||
: {},
|
||||
required: Array.isArray(parsedSchema.required) ? parsedSchema.required : [],
|
||||
};
|
||||
|
||||
const currentParameterConfig = buildToolParameterConfig(toolParameters);
|
||||
const currentDefaults = currentParameterConfig.error ? {} : currentParameterConfig.defaults;
|
||||
const mergedDefaults = { ...currentDefaults, ...defaultsFromSchema(normalizedSchema) };
|
||||
|
||||
setToolParameters(draftsFromSchema(normalizedSchema, mergedDefaults));
|
||||
setSchemaEditorError('');
|
||||
setSchemaEditorText(JSON.stringify(normalizedSchema, null, 2));
|
||||
setSchemaDrawerOpen(false);
|
||||
};
|
||||
|
||||
const filteredTools = tools.filter((tool) => {
|
||||
const q = searchTerm.toLowerCase();
|
||||
const matchesSearch =
|
||||
@@ -506,12 +608,25 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
|
||||
<Dialog
|
||||
isOpen={isToolModalOpen}
|
||||
onClose={() => setIsToolModalOpen(false)}
|
||||
onClose={() => {
|
||||
setIsToolModalOpen(false);
|
||||
setSchemaDrawerOpen(false);
|
||||
setSchemaEditorError('');
|
||||
}}
|
||||
title={editingTool ? '编辑自定义工具' : '添加自定义工具'}
|
||||
contentClassName="max-w-4xl"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsToolModalOpen(false)}>取消</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsToolModalOpen(false);
|
||||
setSchemaDrawerOpen(false);
|
||||
setSchemaEditorError('');
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSaveTool} disabled={saving}>{saving ? '保存中...' : (editingTool ? '保存修改' : '确认添加')}</Button>
|
||||
</>
|
||||
}
|
||||
@@ -606,10 +721,15 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={openSchemaDrawer}>
|
||||
Schema 抽屉
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
|
||||
<Plus className="w-3.5 h-3.5 mr-1" /> 添加参数
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{toolParameters.length === 0 ? (
|
||||
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
|
||||
当前无参数。可以逐个添加参数,无需直接编辑 JSON Schema。
|
||||
@@ -744,6 +864,39 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Drawer
|
||||
isOpen={schemaDrawerOpen}
|
||||
onClose={() => {
|
||||
setSchemaDrawerOpen(false);
|
||||
setSchemaEditorError('');
|
||||
}}
|
||||
title="Tool Schema Editor"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
可在此粘贴/编辑 JSON Schema。点击「应用到参数表单」后会同步到上方参数编辑区。
|
||||
</p>
|
||||
<textarea
|
||||
className="flex min-h-[58vh] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white font-mono"
|
||||
value={schemaEditorText}
|
||||
onChange={(e) => {
|
||||
setSchemaEditorText(e.target.value);
|
||||
if (schemaEditorError) setSchemaEditorError('');
|
||||
}}
|
||||
placeholder='{"type":"object","properties":{},"required":[]}'
|
||||
/>
|
||||
{schemaEditorError && (
|
||||
<div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
||||
{schemaEditorError}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" onClick={() => setSchemaDrawerOpen(false)}>关闭</Button>
|
||||
<Button variant="outline" onClick={formatSchemaEditor}>格式化</Button>
|
||||
<Button onClick={applySchemaEditor}>应用到参数表单</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user