29 lines
3.8 KiB
TypeScript
29 lines
3.8 KiB
TypeScript
import { FormEvent, useMemo, useState } from "react";
|
|
import type { Capability } from "../../protocol/model";
|
|
|
|
interface SchemaProperty { type?: unknown; title?: unknown; description?: unknown; enum?: unknown; default?: unknown; minimum?: unknown; maximum?: unknown; maxLength?: unknown; }
|
|
|
|
function properties(schema: Capability["input_schema"]): Record<string, SchemaProperty> {
|
|
const value = schema.properties;
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, SchemaProperty> : {};
|
|
}
|
|
|
|
export function OperationForm({ capability, disabled, onSubmit }: { capability: Capability; disabled?: boolean; onSubmit: (parameters: Readonly<Record<string, unknown>>) => void }) {
|
|
const fields = useMemo(() => properties(capability.input_schema), [capability]);
|
|
const required = new Set(Array.isArray(capability.input_schema.required) ? capability.input_schema.required.filter((value): value is string => typeof value === "string") : []);
|
|
const [values, setValues] = useState<Record<string, unknown>>(() => Object.fromEntries(Object.entries(fields).filter(([, field]) => field.default !== undefined).map(([name, field]) => [name, field.default])));
|
|
const unsupported = Object.entries(fields).filter(([, field]) => !Array.isArray(field.enum) && ![undefined, "string", "boolean", "number", "integer"].includes(field.type as string | undefined)).map(([name]) => name);
|
|
const submit = (event: FormEvent) => { event.preventDefault(); if (unsupported.length === 0) onSubmit(values); };
|
|
const controls = Object.entries(fields).map(([name, field]) => {
|
|
const label = typeof field.title === "string" ? field.title : name;
|
|
const common = { id: `operation-${name}`, disabled, required: required.has(name) };
|
|
const enumValues = Array.isArray(field.enum) && field.enum.every((item) => ["string", "number"].includes(typeof item)) ? field.enum as readonly (string | number)[] : undefined;
|
|
if (enumValues) return <select {...common} value={String(values[name] ?? "")} onChange={(e) => setValues({ ...values, [name]: enumValues.find((item) => String(item) === e.currentTarget.value) })}><option value="">请选择</option>{enumValues.map((item) => <option key={String(item)} value={String(item)}>{String(item)}</option>)}</select>;
|
|
if (field.type === "boolean") return <input {...common} type="checkbox" checked={Boolean(values[name])} onChange={(e) => setValues({ ...values, [name]: e.currentTarget.checked })} />;
|
|
if (field.type === "number" || field.type === "integer") return <input {...common} type="number" min={typeof field.minimum === "number" ? field.minimum : undefined} max={typeof field.maximum === "number" ? field.maximum : undefined} value={String(values[name] ?? "")} onChange={(e) => setValues({ ...values, [name]: e.currentTarget.value === "" ? undefined : Number(e.currentTarget.value) })} />;
|
|
if (field.type === "string" || field.type === undefined) return <input {...common} type="text" maxLength={typeof field.maxLength === "number" ? field.maxLength : undefined} value={String(values[name] ?? "")} onChange={(e) => setValues({ ...values, [name]: e.currentTarget.value })} />;
|
|
return <p role="alert">字段 {name} 使用不支持的 Schema 类型</p>;
|
|
});
|
|
return <form className="operation-form" onSubmit={submit}><h3>{capability.name}</h3>{Object.keys(fields).length === 0 && <p className="empty-state">此操作无需参数</p>}{Object.entries(fields).map(([name, field], index) => <label key={name} htmlFor={`operation-${name}`}><span>{typeof field.title === "string" ? field.title : name}{required.has(name) ? " *" : ""}</span>{controls[index]}{typeof field.description === "string" && <small>{field.description}</small>}</label>)}<button className="primary" disabled={disabled || unsupported.length > 0} type="submit">检查并确认</button></form>;
|
|
}
|