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 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 { 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 { Tool } from '../types';
|
||||||
import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi';
|
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 = () => {
|
export const ToolLibraryPage: React.FC = () => {
|
||||||
const [tools, setTools] = useState<Tool[]>([]);
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -190,6 +203,9 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
||||||
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
||||||
const [toolParameters, setToolParameters] = useState<ToolParameterDraft[]>([]);
|
const [toolParameters, setToolParameters] = useState<ToolParameterDraft[]>([]);
|
||||||
|
const [schemaDrawerOpen, setSchemaDrawerOpen] = useState(false);
|
||||||
|
const [schemaEditorText, setSchemaEditorText] = useState('');
|
||||||
|
const [schemaEditorError, setSchemaEditorError] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const loadTools = async () => {
|
const loadTools = async () => {
|
||||||
@@ -222,6 +238,9 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolHttpHeadersText('{}');
|
setToolHttpHeadersText('{}');
|
||||||
setToolHttpTimeoutMs(10000);
|
setToolHttpTimeoutMs(10000);
|
||||||
setToolParameters([]);
|
setToolParameters([]);
|
||||||
|
setSchemaEditorError('');
|
||||||
|
setSchemaEditorText(JSON.stringify({ type: 'object', properties: {}, required: [] }, null, 2));
|
||||||
|
setSchemaDrawerOpen(false);
|
||||||
setIsToolModalOpen(true);
|
setIsToolModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,9 +258,92 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
||||||
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
||||||
setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults));
|
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);
|
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 filteredTools = tools.filter((tool) => {
|
||||||
const q = searchTerm.toLowerCase();
|
const q = searchTerm.toLowerCase();
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
@@ -506,12 +608,25 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={isToolModalOpen}
|
isOpen={isToolModalOpen}
|
||||||
onClose={() => setIsToolModalOpen(false)}
|
onClose={() => {
|
||||||
|
setIsToolModalOpen(false);
|
||||||
|
setSchemaDrawerOpen(false);
|
||||||
|
setSchemaEditorError('');
|
||||||
|
}}
|
||||||
title={editingTool ? '编辑自定义工具' : '添加自定义工具'}
|
title={editingTool ? '编辑自定义工具' : '添加自定义工具'}
|
||||||
contentClassName="max-w-4xl"
|
contentClassName="max-w-4xl"
|
||||||
footer={
|
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>
|
<Button onClick={handleSaveTool} disabled={saving}>{saving ? '保存中...' : (editingTool ? '保存修改' : '确认添加')}</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -606,9 +721,14 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
<div className="space-y-4 rounded-md border border-white/10 bg-white/5 p-3">
|
<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="flex items-center justify-between">
|
||||||
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
|
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
|
<div className="flex items-center gap-2">
|
||||||
<Plus className="w-3.5 h-3.5 mr-1" /> 添加参数
|
<Button type="button" variant="outline" size="sm" onClick={openSchemaDrawer}>
|
||||||
</Button>
|
Schema 抽屉
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
|
||||||
|
<Plus className="w-3.5 h-3.5 mr-1" /> 添加参数
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{toolParameters.length === 0 ? (
|
{toolParameters.length === 0 ? (
|
||||||
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
|
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
|
||||||
@@ -744,6 +864,39 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user