feat(ui): common_components/check_openapi_schema to shadcn

- @tremor TextInput + antd Input/Input.TextArea/InputNumber/Tooltip
  + @ant-design InfoCircleOutlined \u2192 shadcn Input/Textarea/Tooltip +
  NumericalInput shared helper + lucide Info.
- antd Select preserved for enum-backed fields.
- text-red-500 \u2192 text-destructive / text-gray-500 \u2192 text-muted-
  foreground.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 16:33:34 +00:00
parent 7d31cc2ecc
commit aa7c219fc4
No known key found for this signature in database

View file

@ -1,8 +1,15 @@
import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Select } from "antd";
import { TextInput } from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Tooltip } from "antd";
import { Form, Select } from "antd";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import NumericalInput from "@/components/shared/numerical_input";
import { getOpenAPISchema } from "../networking";
import { formatLabel } from "@/utils/textUtils";
@ -25,24 +32,25 @@ interface OpenAPISchema {
interface SchemaFormFieldsProps {
schemaComponent: string;
excludedFields?: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: any;
overrideLabels?: { [key: string]: string };
overrideTooltips?: { [key: string]: string };
customValidation?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: (rule: any, value: any) => Promise<void>;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValues?: { [key: string]: any };
}
// Define which fields should be parsed as JSON
export const jsonFields = ["metadata", "config", "enforced_params", "aliases"];
// Helper function to determine if a field should be treated as JSON
const isJSONField = (key: string, property: SchemaProperty): boolean => {
return jsonFields.includes(key) || property.format === "json";
};
// Helper function to validate JSON input
const validateJSON = (value: string): boolean => {
if (!value) return true;
try {
@ -53,8 +61,11 @@ const validateJSON = (value: string): boolean => {
}
};
const getFieldHelp = (key: string, property: SchemaProperty, type: string): string => {
// Default help text based on type
const getFieldHelp = (
key: string,
property: SchemaProperty,
type: string,
): string => {
const defaultHelp =
{
string: "Text input",
@ -63,28 +74,28 @@ const getFieldHelp = (key: string, property: SchemaProperty, type: string): stri
boolean: "True/False value",
}[type] || "Text input";
// Specific field help text
const specificHelp: { [key: string]: string } = {
max_budget: "Enter maximum budget in USD (e.g., 100.50)",
budget_duration: "Select a time period for budget reset",
tpm_limit: "Enter maximum tokens per minute (whole number)",
rpm_limit: "Enter maximum requests per minute (whole number)",
duration: "Enter duration (e.g., 30s, 24h, 7d)",
metadata: 'Enter JSON object with key-value pairs\nExample: {"team": "research", "project": "nlp"}',
metadata:
'Enter JSON object with key-value pairs\nExample: {"team": "research", "project": "nlp"}',
config: 'Enter configuration as JSON object\nExample: {"setting": "value"}',
permissions: "Enter comma-separated permission strings",
enforced_params: 'Enter parameters as JSON object\nExample: {"param": "value"}',
enforced_params:
'Enter parameters as JSON object\nExample: {"param": "value"}',
blocked: "Enter true/false or specific block conditions",
aliases: 'Enter aliases as JSON object\nExample: {"alias1": "value1", "alias2": "value2"}',
aliases:
'Enter aliases as JSON object\nExample: {"alias1": "value1", "alias2": "value2"}',
models: "Select one or more model names",
key_alias: "Enter a unique identifier for this key",
tags: "Enter comma-separated tag strings",
};
// Get specific help text or use default based on type
const helpText = specificHelp[key] || defaultHelp;
// Add format requirements for special cases
if (isJSONField(key, property)) {
return `${helpText}\nMust be valid JSON format`;
}
@ -105,7 +116,8 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
customValidation = {},
defaultValues = {},
}) => {
const [schemaProperties, setSchemaProperties] = useState<OpenAPISchema | null>(null);
const [schemaProperties, setSchemaProperties] =
useState<OpenAPISchema | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@ -120,9 +132,13 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
setSchemaProperties(componentSchema);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const defaultFormValues: { [key: string]: any } = {};
Object.keys(componentSchema.properties)
.filter((key) => !excludedFields.includes(key) && defaultValues[key] !== undefined)
.filter(
(key) =>
!excludedFields.includes(key) && defaultValues[key] !== undefined,
)
.forEach((key) => {
defaultFormValues[key] = defaultValues[key];
});
@ -130,11 +146,15 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
form.setFieldsValue(defaultFormValues);
} catch (error) {
console.error("Schema fetch error:", error);
setError(error instanceof Error ? error.message : "Failed to fetch schema");
setError(
error instanceof Error ? error.message : "Failed to fetch schema",
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
};
fetchOpenAPISchema();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schemaComponent, form, excludedFields]);
const getPropertyType = (property: SchemaProperty): string => {
@ -156,7 +176,8 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
const label = overrideLabels[key] || property.title || formatLabel(key);
const tooltip = overrideTooltips[key] || property.description;
const rules = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rules: any[] = [];
if (isRequired) {
rules.push({ required: true, message: `${label} is required` });
}
@ -165,6 +186,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
}
if (isJSONField(key, property)) {
rules.push({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
validator: async (_: any, value: string) => {
if (value && !validateJSON(value)) {
throw new Error("Please enter valid JSON");
@ -176,9 +198,14 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
const formLabel = tooltip ? (
<span>
{label}{" "}
<Tooltip title={tooltip}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-1 h-3 w-3 inline text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">{tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
) : (
label
@ -186,7 +213,9 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
let inputComponent;
if (isJSONField(key, property)) {
inputComponent = <Input.TextArea rows={4} placeholder="Enter as JSON" className="font-mono" />;
inputComponent = (
<Textarea rows={4} placeholder="Enter as JSON" className="font-mono" />
);
} else if (property.enum) {
inputComponent = (
<Select>
@ -198,11 +227,16 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
</Select>
);
} else if (type === "number" || type === "integer") {
inputComponent = <InputNumber style={{ width: "100%" }} precision={type === "integer" ? 0 : undefined} />;
inputComponent = (
<NumericalInput
style={{ width: "100%" }}
precision={type === "integer" ? 0 : undefined}
/>
);
} else if (key === "duration") {
inputComponent = <TextInput placeholder="eg: 30s, 30h, 30d" />;
inputComponent = <Input placeholder="eg: 30s, 30h, 30d" />;
} else {
inputComponent = <TextInput placeholder={tooltip || ""} />;
inputComponent = <Input placeholder={tooltip || ""} />;
}
return (
@ -213,7 +247,11 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
className="mt-8"
rules={rules}
initialValue={defaultValues[key]}
help={<div className="text-xs text-gray-500">{getFieldHelp(key, property, type)}</div>}
help={
<div className="text-xs text-muted-foreground">
{getFieldHelp(key, property, type)}
</div>
}
>
{inputComponent}
</Form.Item>
@ -221,7 +259,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
};
if (error) {
return <div className="text-red-500">Error: {error}</div>;
return <div className="text-destructive">Error: {error}</div>;
}
if (!schemaProperties?.properties) {