refactor(playground): update terminology and enhance UI components for API runs

- Changed label from "Runs" to "API Runs" in PlaygroundLayoutShell and PlaygroundIndex for clarity.
- Improved the API reference UI by adding a ChevronRight icon for expandable sections and enhancing the summary layout.
- Introduced an EndpointCopyButton component for easier copying of API endpoint details, improving user experience.
This commit is contained in:
Anish Sarkar 2026-07-08 03:08:39 +05:30
parent bcfa3c5ef5
commit 7268eae7fa
4 changed files with 89 additions and 45 deletions

View file

@ -1,6 +1,6 @@
"use client";
import { Check, Copy } from "lucide-react";
import { Check, ChevronRight, Copy } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -45,8 +45,9 @@ function SchemaBlock({ title, schema }: { title: string; schema: Record<string,
const json = useMemo(() => JSON.stringify(schema, null, 2), [schema]);
return (
<details className="group rounded-md border border-border/60">
<summary className="cursor-pointer select-none px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
{title}
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground [&::-webkit-details-marker]:hidden">
<span>{title}</span>
<ChevronRight className="h-4 w-4 shrink-0 transition-transform group-open:rotate-90" />
</summary>
<div className="relative border-t border-border/60">
<CopyButton text={json} />

View file

@ -45,7 +45,7 @@ export function PlaygroundIndex({ workspaceId }: { workspaceId: number }) {
<div className="flex items-center gap-3">
<History className="h-5 w-5 text-muted-foreground" />
<div>
<p className="text-sm font-medium">Runs</p>
<p className="text-sm font-medium">API Runs</p>
<p className="text-xs text-muted-foreground">See every API run in this workspace</p>
</div>
</div>

View file

@ -1,14 +1,25 @@
"use client";
import { AlertTriangle, Coins, Hash, Loader2, Play, Timer, X } from "lucide-react";
import Link from "next/link";
import {
Check,
Coins,
Copy,
Hash,
Info,
Loader2,
Play,
Timer,
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { useRunStream } from "@/hooks/use-run-stream";
import { useScraperCapabilities } from "@/hooks/use-scraper-capabilities";
import { scrapersApiService } from "@/lib/apis/scrapers-api.service";
import { AbortedError, AppError } from "@/lib/error";
import { AppError } from "@/lib/error";
import { findVerb } from "@/lib/playground/catalog";
import { formatCost, formatDuration, formatPricing } from "@/lib/playground/format";
import { buildPayload, initialFormValues, parseSchemaFields } from "@/lib/playground/json-schema";
@ -58,36 +69,44 @@ function RunStat({
);
}
function ErrorPanel({ error, workspaceId }: { error: unknown; workspaceId: number }) {
if (error instanceof AbortedError) {
return null;
}
function getRunErrorMessage(error: unknown): string {
const status = error instanceof AppError ? error.status : undefined;
if (status === 402) {
return (
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm">
<p className="font-medium text-destructive">Insufficient credits</p>
<p className="mt-1 text-muted-foreground">You don't have enough credits to run this API.</p>
<Button asChild size="sm" variant="outline" className="mt-3">
<Link href={`/dashboard/${workspaceId}/buy-more`}>Buy credits</Link>
</Button>
</div>
);
return "Insufficient credits. Add credits to run this API.";
}
const message =
status === 422
? "Invalid input. Check the fields above and try again."
: error instanceof Error && error.message
? error.message
: "Something went wrong running this API.";
if (status === 422) {
return "Invalid input. Check the fields above and try again.";
}
return error instanceof Error && error.message
? error.message
: "Something went wrong running this API.";
}
function EndpointCopyButton({ endpoint }: { endpoint: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(endpoint).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
return (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{message}</span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleCopy}
className="h-auto justify-start gap-2 rounded bg-muted/40 px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
>
<code>{endpoint}</code>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
<span className="sr-only">{copied ? "Copied endpoint" : "Copy endpoint"}</span>
</Button>
);
}
@ -110,6 +129,8 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
const [values, setValues] = useState<Record<string, unknown>>({});
const run = useRunStream(workspaceId);
const isRunning = run.status === "running";
const previousStatusRef = useRef(run.status);
const notifiedRunRef = useRef<string | null>(null);
// Seed form defaults once the schema is available.
useEffect(() => {
@ -158,6 +179,29 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
() => (run.detail ? parseJsonlOutput(run.detail.output_text) : null),
[run.detail]
);
const endpoint = `POST /workspaces/${workspaceId}/scrapers/${platform}/${verb}`;
useEffect(() => {
const previousStatus = previousStatusRef.current;
previousStatusRef.current = run.status;
if (previousStatus !== "running") return;
if (run.status === "success") {
const key = `${run.runId ?? "run"}:success`;
if (notifiedRunRef.current === key) return;
notifiedRunRef.current = key;
toast.success("API run completed.");
return;
}
if (run.status === "error") {
const key = `${run.runId ?? "run"}:error`;
if (notifiedRunRef.current === key) return;
notifiedRunRef.current = key;
toast.error(getRunErrorMessage(run.error));
}
}, [run.status, run.runId, run.error]);
if (isLoading) {
return (
@ -186,26 +230,26 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
return (
<div className="space-y-10">
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-6">
{capability.description && (
<Alert>
<Info />
<AlertDescription>
<p>{capability.description}</p>
</AlertDescription>
</Alert>
)}
<div className="space-y-5">
<div>
<h1 className="text-xl font-semibold text-foreground md:text-2xl">
{catalogVerb.label} <span className="text-muted-foreground">· {platform}</span>
</h1>
{capability.description && (
<p className="mt-1 text-sm text-muted-foreground">{capability.description}</p>
)}
<code className="mt-2 inline-block rounded bg-muted/40 px-1.5 py-0.5 text-xs text-muted-foreground">
POST /workspaces/{workspaceId}/scrapers/{platform}/{verb}
</code>
<div className="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
<div className="space-y-2">
<EndpointCopyButton endpoint={endpoint} />
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Coins className="h-3.5 w-3.5" />
<span className="font-medium tabular-nums text-foreground">
{formatPricing(capability.pricing)}
</span>
</div>
</div>
<SchemaForm
fields={fields}
values={values}
@ -230,7 +274,6 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
)}
</div>
{run.status === "error" && <ErrorPanel error={run.error} workspaceId={workspaceId} />}
</div>
<div className="space-y-3">

View file

@ -30,7 +30,7 @@ export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayou
},
{
value: "runs",
label: "Runs",
label: "API Runs",
href: `${base}/runs`,
icon: <History className="h-4 w-4" />,
},