feat: surface validation feedback in playground UI

This commit is contained in:
CREDO23 2026-07-23 18:52:50 +02:00
parent 1ad8efb658
commit 15962d205f
4 changed files with 63 additions and 5 deletions

View file

@ -12,6 +12,7 @@ import { useScraperCapabilities } from "@/hooks/use-scraper-capabilities";
import { scrapersApiService } from "@/lib/apis/scrapers-api.service";
import { AppError } from "@/lib/error";
import { findVerb } from "@/lib/playground/catalog";
import { fieldErrorsFromError } from "@/lib/playground/field-errors";
import { formatCost, formatDuration, formatPricing } from "@/lib/playground/format";
import { buildPayload, initialFormValues, parseSchemaFields } from "@/lib/playground/json-schema";
import {
@ -19,6 +20,7 @@ import {
getAmazonFieldOptions,
hasAmazonFranceValue,
} from "@/lib/playground/platform-overrides/amazon";
import { urlFieldWarnings } from "@/lib/playground/url-hints";
import { ApiReference } from "./api-reference";
import { OutputViewer } from "./output-viewer";
import { RunProgressPanel } from "./run-progress-panel";
@ -73,7 +75,12 @@ function getRunErrorMessage(error: unknown): string {
}
if (status === 422) {
return "Invalid input. Check the fields above and try again.";
if (Object.keys(fieldErrorsFromError(error)).length > 0) {
return "Invalid input. Check the highlighted fields.";
}
return error instanceof Error && error.message
? error.message
: "Invalid input. Check the fields above and try again.";
}
return error instanceof Error && error.message
@ -123,6 +130,7 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
const fields = useMemo(() => parseSchemaFields(capability?.input_schema), [capability]);
const [values, setValues] = useState<Record<string, unknown>>({});
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const run = useRunStream(workspaceId);
const isRunning = run.status === "running";
const previousStatusRef = useRef(run.status);
@ -164,9 +172,15 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
const handleChange = (name: string, value: unknown) => {
setValues((prev) => ({ ...prev, [name]: value }));
setFieldErrors((prev) => {
if (!(name in prev)) return prev;
const { [name]: _cleared, ...rest } = prev;
return rest;
});
};
const handleRun = useCallback(() => {
setFieldErrors({});
const payload = buildPayload(fields, values);
void run.start(platform, verb, payload);
}, [fields, values, platform, verb, run]);
@ -178,6 +192,7 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
const endpoint = `POST /workspaces/${workspaceId}/scrapers/${platform}/${verb}`;
const isAmazonScrape = platform === "amazon" && verb === "scrape";
const hasAmazonFranceUrl = useMemo(() => hasAmazonFranceValue(values), [values]);
const fieldWarnings = useMemo(() => urlFieldWarnings(platform, values), [platform, values]);
useEffect(() => {
const previousStatus = previousStatusRef.current;
@ -194,6 +209,7 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
}
if (run.status === "error") {
setFieldErrors(fieldErrorsFromError(run.error));
const key = `${run.runId ?? "run"}:error`;
if (notifiedRunRef.current === key) return;
notifiedRunRef.current = key;
@ -268,6 +284,8 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
onChange={handleChange}
disabled={isRunning}
getFieldOptions={isAmazonScrape ? getAmazonFieldOptions : undefined}
fieldErrors={fieldErrors}
fieldWarnings={fieldWarnings}
/>
{isAmazonScrape ? <AmazonMarketplaceHint showFranceWarning={hasAmazonFranceUrl} /> : null}

View file

@ -32,6 +32,8 @@ interface SchemaFormProps {
getFieldOptions?: FieldOptionsResolver;
/** Field names flagged by a 422 response, shown with error styling. */
fieldErrors?: Record<string, string>;
/** Client-side, non-blocking URL warnings keyed by field name. */
fieldWarnings?: Record<string, string>;
}
function FieldControl({
@ -147,6 +149,7 @@ function FieldRow({
onChange,
disabled,
error,
warning,
options,
}: {
field: FormField;
@ -154,6 +157,7 @@ function FieldRow({
onChange: (value: unknown) => void;
disabled?: boolean;
error?: string;
warning?: string;
options?: FieldOption[];
}) {
return (
@ -179,6 +183,7 @@ function FieldRow({
options={options}
/>
{error && <p className="text-xs text-destructive">{error}</p>}
{!error && warning && <p className="text-xs text-amber-600 dark:text-amber-500">{warning}</p>}
</div>
);
}
@ -190,6 +195,7 @@ export function SchemaForm({
disabled,
getFieldOptions,
fieldErrors,
fieldWarnings,
}: SchemaFormProps) {
const [showAdvanced, setShowAdvanced] = useState(false);
@ -209,6 +215,7 @@ export function SchemaForm({
onChange={(value) => onChange(field.name, value)}
disabled={disabled}
error={fieldErrors?.[field.name]}
warning={fieldWarnings?.[field.name]}
options={getFieldOptions?.(field)}
/>
))}
@ -239,6 +246,7 @@ export function SchemaForm({
onChange={(value) => onChange(field.name, value)}
disabled={disabled}
error={fieldErrors?.[field.name]}
warning={fieldWarnings?.[field.name]}
options={getFieldOptions?.(field)}
/>
))}