feat: surface playground run errors inline and persist toasts

This commit is contained in:
CREDO23 2026-07-23 19:39:15 +02:00
parent 55676cfe94
commit 4414932346
2 changed files with 32 additions and 3 deletions

View file

@ -209,11 +209,21 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
}
if (run.status === "error") {
setFieldErrors(fieldErrorsFromError(run.error));
const nextFieldErrors = fieldErrorsFromError(run.error);
setFieldErrors(nextFieldErrors);
const key = `${run.runId ?? "run"}:error`;
if (notifiedRunRef.current === key) return;
notifiedRunRef.current = key;
toast.error(getRunErrorMessage(run.error));
// Field-level failures are shown inline (the form reveals + focuses the
// first one), so only toast global failures that have no field to anchor to.
// Keep run errors until dismissed — they're actionable and shouldn't
// vanish before the user reads them.
if (Object.keys(nextFieldErrors).length === 0) {
toast.error(getRunErrorMessage(run.error), {
duration: Number.POSITIVE_INFINITY,
closeButton: true,
});
}
}
}, [run.status, run.runId, run.error]);

View file

@ -1,7 +1,7 @@
"use client";
import { ChevronDown } from "lucide-react";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -205,6 +205,25 @@ export function SchemaForm({
return { primary: primaryFields, advanced: advancedFields };
}, [fields]);
// First invalid field in display order; drives reveal + focus below.
const firstErrorName = useMemo(
() => (fieldErrors ? fields.find((f) => fieldErrors[f.name])?.name : undefined),
[fields, fieldErrors]
);
// Reveal the section holding an invalid field, then move focus to it, so an
// error is never left hidden inside the collapsed "Advanced" group.
useEffect(() => {
if (!firstErrorName) return;
if (advanced.some((f) => f.name === firstErrorName)) setShowAdvanced(true);
const raf = requestAnimationFrame(() => {
const el = document.getElementById(`field-${firstErrorName}`);
el?.focus();
el?.scrollIntoView({ block: "center", behavior: "smooth" });
});
return () => cancelAnimationFrame(raf);
}, [firstErrorName, advanced]);
return (
<div className="space-y-5">
{primary.map((field) => (