mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
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:
parent
bcfa3c5ef5
commit
7268eae7fa
4 changed files with 89 additions and 45 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Check, Copy } from "lucide-react";
|
import { Check, ChevronRight, Copy } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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]);
|
const json = useMemo(() => JSON.stringify(schema, null, 2), [schema]);
|
||||||
return (
|
return (
|
||||||
<details className="group rounded-md border border-border/60">
|
<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">
|
<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">
|
||||||
{title}
|
<span>{title}</span>
|
||||||
|
<ChevronRight className="h-4 w-4 shrink-0 transition-transform group-open:rotate-90" />
|
||||||
</summary>
|
</summary>
|
||||||
<div className="relative border-t border-border/60">
|
<div className="relative border-t border-border/60">
|
||||||
<CopyButton text={json} />
|
<CopyButton text={json} />
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function PlaygroundIndex({ workspaceId }: { workspaceId: number }) {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<History className="h-5 w-5 text-muted-foreground" />
|
<History className="h-5 w-5 text-muted-foreground" />
|
||||||
<div>
|
<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>
|
<p className="text-xs text-muted-foreground">See every API run in this workspace</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AlertTriangle, Coins, Hash, Loader2, Play, Timer, X } from "lucide-react";
|
import {
|
||||||
import Link from "next/link";
|
Check,
|
||||||
|
Coins,
|
||||||
|
Copy,
|
||||||
|
Hash,
|
||||||
|
Info,
|
||||||
|
Loader2,
|
||||||
|
Play,
|
||||||
|
Timer,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useRunStream } from "@/hooks/use-run-stream";
|
import { useRunStream } from "@/hooks/use-run-stream";
|
||||||
import { useScraperCapabilities } from "@/hooks/use-scraper-capabilities";
|
import { useScraperCapabilities } from "@/hooks/use-scraper-capabilities";
|
||||||
import { scrapersApiService } from "@/lib/apis/scrapers-api.service";
|
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 { findVerb } from "@/lib/playground/catalog";
|
||||||
import { formatCost, formatDuration, formatPricing } from "@/lib/playground/format";
|
import { formatCost, formatDuration, formatPricing } from "@/lib/playground/format";
|
||||||
import { buildPayload, initialFormValues, parseSchemaFields } from "@/lib/playground/json-schema";
|
import { buildPayload, initialFormValues, parseSchemaFields } from "@/lib/playground/json-schema";
|
||||||
|
|
@ -58,36 +69,44 @@ function RunStat({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ErrorPanel({ error, workspaceId }: { error: unknown; workspaceId: number }) {
|
function getRunErrorMessage(error: unknown): string {
|
||||||
if (error instanceof AbortedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const status = error instanceof AppError ? error.status : undefined;
|
const status = error instanceof AppError ? error.status : undefined;
|
||||||
|
|
||||||
if (status === 402) {
|
if (status === 402) {
|
||||||
return (
|
return "Insufficient credits. Add credits to run this API.";
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
if (status === 422) {
|
||||||
status === 422
|
return "Invalid input. Check the fields above and try again.";
|
||||||
? "Invalid input. Check the fields above and try again."
|
}
|
||||||
: error instanceof Error && error.message
|
|
||||||
? error.message
|
return error instanceof Error && error.message
|
||||||
: "Something went wrong running this API.";
|
? 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 (
|
return (
|
||||||
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive">
|
<Button
|
||||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
type="button"
|
||||||
<span>{message}</span>
|
variant="ghost"
|
||||||
</div>
|
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 [values, setValues] = useState<Record<string, unknown>>({});
|
||||||
const run = useRunStream(workspaceId);
|
const run = useRunStream(workspaceId);
|
||||||
const isRunning = run.status === "running";
|
const isRunning = run.status === "running";
|
||||||
|
const previousStatusRef = useRef(run.status);
|
||||||
|
const notifiedRunRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// Seed form defaults once the schema is available.
|
// Seed form defaults once the schema is available.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -158,6 +179,29 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
|
||||||
() => (run.detail ? parseJsonlOutput(run.detail.output_text) : null),
|
() => (run.detail ? parseJsonlOutput(run.detail.output_text) : null),
|
||||||
[run.detail]
|
[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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -186,26 +230,26 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<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 className="space-y-5">
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<h1 className="text-xl font-semibold text-foreground md:text-2xl">
|
<EndpointCopyButton endpoint={endpoint} />
|
||||||
{catalogVerb.label} <span className="text-muted-foreground">· {platform}</span>
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
</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">
|
|
||||||
<Coins className="h-3.5 w-3.5" />
|
<Coins className="h-3.5 w-3.5" />
|
||||||
<span className="font-medium tabular-nums text-foreground">
|
<span className="font-medium tabular-nums text-foreground">
|
||||||
{formatPricing(capability.pricing)}
|
{formatPricing(capability.pricing)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
fields={fields}
|
fields={fields}
|
||||||
values={values}
|
values={values}
|
||||||
|
|
@ -230,7 +274,6 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{run.status === "error" && <ErrorPanel error={run.error} workspaceId={workspaceId} />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayou
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "runs",
|
value: "runs",
|
||||||
label: "Runs",
|
label: "API Runs",
|
||||||
href: `${base}/runs`,
|
href: `${base}/runs`,
|
||||||
icon: <History className="h-4 w-4" />,
|
icon: <History className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue