feat(capabilities): add documentation URL to capabilities and enhance Reddit scrape capability

- Introduced a `docs_url` field to the `Capability` and `CapabilitySummary` classes for better documentation access.
- Updated the `REDDIT_SCRAPE` capability to include a specific documentation link.
- Enhanced the PlaygroundRunner component to display the documentation link when available, improving user guidance.
This commit is contained in:
Anish Sarkar 2026-07-08 03:35:32 +05:30
parent 7268eae7fa
commit 3b9c806c90
9 changed files with 51 additions and 62 deletions

View file

@ -71,6 +71,7 @@ class CapabilitySummary(BaseModel):
name: str
description: str
docs_url: str | None = None
input_schema: dict
output_schema: dict
# Empty list = free (billing disabled or an unmetered verb).
@ -145,6 +146,7 @@ def _register_capabilities_list(
CapabilitySummary(
name=capability.name,
description=capability.description,
docs_url=capability.docs_url,
input_schema=capability.input_schema.model_json_schema(),
output_schema=capability.output_schema.model_json_schema(),
),

View file

@ -62,3 +62,4 @@ class Capability:
output_schema: type[BaseModel]
executor: Executor
billing_unit: BillingUnit | None
docs_url: str | None = None

View file

@ -10,16 +10,14 @@ from app.capabilities.reddit.scrape.schemas import ScrapeInput, ScrapeOutput
REDDIT_SCRAPE = Capability(
name="reddit.scrape",
description=(
"Scrape public Reddit data. Give it Reddit URLs (post, subreddit, or "
"user) and/or search terms, and it returns structured items — posts "
"(title, body, score, comment count, subreddit, author), their comments, "
"and community/user metadata. Use search_queries (optionally scoped to a "
"community) to discover posts, or urls to pull a known post/subreddit/user."
"Scrape public Reddit posts, comments, and metadata. Use urls or "
"search_queries."
),
input_schema=ScrapeInput,
output_schema=ScrapeOutput,
executor=build_scrape_executor(),
billing_unit=BillingUnit.REDDIT_ITEM,
docs_url="/docs/connectors/native/reddit",
)
register_capability(REDDIT_SCRAPE)

View file

@ -22,10 +22,10 @@ function CopyButton({ text }: { text: string }) {
variant="ghost"
size="sm"
onClick={copy}
className="absolute right-2 top-2 h-7 gap-1.5 px-2 text-xs"
aria-label={copied ? "Copied" : "Copy"}
className="absolute right-2 top-2 h-7 w-7 p-0"
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? "Copied" : "Copy"}
</Button>
);
}
@ -91,11 +91,7 @@ export function ApiReference({
<div>
<h2 className="text-base font-semibold">API reference</h2>
<p className="mt-1 text-sm text-muted-foreground">
Call this API from your own project. Create a key in{" "}
<span className="font-medium text-foreground">User settings</span> and enable API access
for this workspace, then send it as a{" "}
<code className="rounded bg-muted/40 px-1 py-0.5 text-xs">Authorization: Bearer</code>{" "}
header.
Create an API key, enable API access for this workspace, then use the examples below to call this endpoint.
</p>
</div>

View file

@ -3,6 +3,7 @@
import { Check, Copy, Download } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Table,
TableBody,
@ -12,7 +13,6 @@ import {
TableRow,
} from "@/components/ui/table";
import { downloadCsv, rowsToCsv } from "@/lib/playground/csv";
import { cn } from "@/lib/utils";
const MAX_TABLE_ROWS = 200;
@ -109,34 +109,12 @@ export function OutputViewer({ data, filenameBase }: { data: unknown; filenameBa
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="inline-flex rounded-md border border-border/60 p-0.5">
{items && (
<button
type="button"
onClick={() => setView("table")}
className={cn(
"rounded px-2.5 py-1 text-xs font-medium transition-colors",
view === "table"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
Table
</button>
)}
<button
type="button"
onClick={() => setView("json")}
className={cn(
"rounded px-2.5 py-1 text-xs font-medium transition-colors",
view === "json"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
JSON
</button>
</div>
<Tabs value={view} onValueChange={(value) => setView(value as "table" | "json")}>
<TabsList className="h-auto">
{items && <TabsTrigger value="table">Table</TabsTrigger>}
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-1">
{items && items.length > 0 && (
<Button
@ -150,9 +128,15 @@ export function OutputViewer({ data, filenameBase }: { data: unknown; filenameBa
Export CSV
</Button>
)}
<Button type="button" variant="ghost" size="sm" onClick={copy} className="gap-1.5">
<Button
type="button"
variant="ghost"
size="sm"
onClick={copy}
aria-label={copied ? "Copied JSON" : "Copy JSON"}
className="h-8 w-8 p-0"
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? "Copied" : "Copy JSON"}
</Button>
</div>
</div>
@ -168,7 +152,7 @@ export function OutputViewer({ data, filenameBase }: { data: unknown; filenameBa
<ResultTable items={items} />
{truncated && (
<p className="text-xs text-muted-foreground">
Showing first {MAX_TABLE_ROWS} of {items.length} items. Use Copy JSON for the full
Showing first {MAX_TABLE_ROWS} of {items.length} items. Switch to JSON for the full
output.
</p>
)}

View file

@ -2,15 +2,13 @@
import {
Check,
Coins,
Copy,
Hash,
Info,
Loader2,
Play,
Coins,
Timer,
X,
} from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert";
@ -235,7 +233,21 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
<Alert>
<Info />
<AlertDescription>
<p>{capability.description}</p>
<p>
{capability.description}
{capability.docs_url ? (
<>
{" "}
<Link
href={capability.docs_url}
className="font-medium text-foreground underline-offset-4 hover:underline"
>
Read docs
</Link>
.
</>
) : null}
</p>
</AlertDescription>
</Alert>
)}
@ -243,8 +255,8 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
<div className="space-y-5">
<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" />
<div className="text-xs text-muted-foreground">
<span>Pricing: </span>
<span className="font-medium tabular-nums text-foreground">
{formatPricing(capability.pricing)}
</span>
@ -258,17 +270,12 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
/>
<div className="flex items-center gap-2">
<Button type="button" onClick={handleRun} disabled={isRunning} className="gap-1.5">
{isRunning ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
Run
<Button type="button" onClick={handleRun} disabled={isRunning} className="relative">
<span className={isRunning ? "opacity-0" : ""}>Run</span>
{isRunning && <Spinner size="sm" className="absolute" />}
</Button>
{isRunning && (
<Button type="button" variant="outline" onClick={run.cancel} className="gap-1.5">
<X className="h-4 w-4" />
<Button type="button" variant="secondary" onClick={run.cancel}>
Cancel
</Button>
)}

View file

@ -90,6 +90,6 @@ function getSelectedLabel(
const group = groups.find((item) => item.items.some((child) => child.value === activeValue));
const child = group?.items.find((item) => item.value === activeValue);
if (group && child) return `${group.label} / ${child.label}`;
if (group && child) return `${group.label}: ${child.label}`;
return "API Playground";
}

View file

@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-popover data-[state=active]:text-popover-foreground data-[state=active]:shadow-sm",
className
)}
{...props}

View file

@ -15,6 +15,7 @@ export const scraperPricingMeter = z.object({
export const scraperCapability = z.object({
name: z.string(),
description: z.string(),
docs_url: z.string().nullable().optional(),
input_schema: z.record(z.string(), z.unknown()),
// Optional so a backend that predates output schemas degrades to just not
// showing the output-schema block instead of failing the whole fetch.