Files
wehub-resource-sync 542cfa195c
CI / Frontend build (push) Failing after 9m6s
CI / Plugin validate (push) Failing after 9m27s
CI / Python lint (push) Failing after 16m1s
CI / Tests (push) Successful in 18m0s
Deploy / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:27 +08:00

531 lines
20 KiB
TypeScript

"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { ApiPlayground } from "@/components/ApiPlayground"
interface Field {
name: string
type: string
required?: boolean
description?: string
children?: Field[]
}
interface Endpoint {
id: string
method: "GET" | "POST"
path: string
summary: string
description: string
requestFields?: Field[]
responseFields?: Field[]
curlPrefix: string
defaultBody?: string
defaultParams?: string
buildPath?: (body: string, params: string) => string
}
const endpoints: Endpoint[] = [
{
id: "search",
method: "POST",
path: "/search",
summary: "Search for visually similar tiles",
description:
"Submit one or more queries (text, image, or embedding) and retrieve the top matching tiles. Each hit returns article_id, tile_index, and chunk_index — pass these to GET /tile/... to fetch the actual screenshot.",
requestFields: [
{ name: "queries", type: "Query[]", required: true, description: "Array of query objects", children: [
{ name: "text", type: "string", description: "Text search query" },
{ name: "image", type: "string", description: "Base64-encoded image" },
{ name: "embedding", type: "number[]", description: "Pre-computed embedding vector" },
]},
{ name: "n_docs", type: "number", description: "Number of results to return (default: 20)" },
{ name: "nprobe", type: "number", description: "FAISS nprobe override" },
{ name: "min_tile_height", type: "number", description: "Filter out tiles shorter than this" },
{ name: "articles_only", type: "boolean", description: "Drop Wikipedia meta pages (Portal:, List_of_, disambiguation, …) — keeps real articles" },
{ name: "instruction", type: "string", description: "Custom embedding instruction" },
],
responseFields: [
{ name: "results", type: "QueryResult[]", required: true, description: "One result per query", children: [
{ name: "hits", type: "Hit[]", required: true, description: "Ranked list of matches", children: [
{ name: "score", type: "number", required: true, description: "Cosine similarity" },
{ name: "vector_id", type: "number", required: true, description: "FAISS vector index" },
{ name: "article_id", type: "number", required: true, description: "Wikipedia article ID" },
{ name: "tile_index", type: "number", required: true, description: "Which 8192px tile" },
{ name: "chunk_index", type: "number", required: true, description: "Which 1024px chunk within tile" },
{ name: "y_offset", type: "number", required: true, description: "Y position in page (px)" },
{ name: "tile_height", type: "number", required: true, description: "Chunk height (px)" },
{ name: "path", type: "string", required: true, description: "Tile file path on server" },
{ name: "url", type: "string", required: true, description: "Wikipedia article slug" },
]},
]},
],
curlPrefix: `curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json"`,
defaultBody: JSON.stringify({ queries: [{ text: "solar system" }], n_docs: 5 }, null, 2),
},
{
id: "tile",
method: "GET",
path: "/tile/{article_id}/{tile_index}/{chunk_index}",
summary: "Retrieve a tile image",
description:
"Fetches the screenshot image for a /search hit — pass the article_id, tile_index, and chunk_index straight from a search result. Returns PNG binary data; embed it with an <img> tag. The example below is the top of the Albert Einstein article.",
requestFields: [
{ name: "article_id", type: "number", required: true, description: "Wikipedia article ID" },
{ name: "tile_index", type: "number", required: true, description: "Which 8192px tile (0-based)" },
{ name: "chunk_index", type: "number", required: true, description: "Which 1024px chunk within tile (0-based)" },
],
responseFields: [
{ name: "(binary)", type: "image/png", required: true, description: "PNG image data" },
],
curlPrefix: `curl https://pixelrag.ai/api/tile`,
defaultParams: "article_id=698618&tile_index=0&chunk_index=0",
buildPath: (_body, params) => {
const p = new URLSearchParams(params)
return `/tile/${p.get("article_id") || "698618"}/${p.get("tile_index") || "0"}/${p.get("chunk_index") || "0"}`
},
},
{
id: "status",
method: "GET",
path: "/status",
summary: "Get index status and configuration",
description:
"Returns metadata about the FAISS index including vector count, dimension, model, and configuration.",
responseFields: [
{ name: "total_vectors", type: "number", required: true, description: "Total indexed vectors" },
{ name: "dimension", type: "number", required: true, description: "Embedding dimension" },
{ name: "nlist", type: "number", required: true, description: "IVF cluster count" },
{ name: "nprobe", type: "number", required: true, description: "Search probe count" },
{ name: "model", type: "string", required: true, description: "Embedding model name" },
{ name: "index_dir", type: "string", required: true, description: "Index directory path" },
{ name: "tiles_dir", type: "string", required: true, description: "Tiles directory path" },
{ name: "index_built_at", type: "string", required: true, description: "ISO 8601 timestamp" },
{ name: "index_size_bytes", type: "number", required: true, description: "FAISS index file size" },
{ name: "metadata_size_bytes", type: "number", required: true, description: "Metadata NPZ size" },
],
curlPrefix: `curl https://pixelrag.ai/api/status`,
},
{
id: "health",
method: "GET",
path: "/health",
summary: "Health check",
description:
"Returns {\"status\": \"ok\"} when the server is running.",
responseFields: [
{ name: "status", type: "string", required: true, description: "Always \"ok\"" },
],
curlPrefix: `curl https://pixelrag.ai/api/health`,
},
]
export default function DocsPage() {
const initialId = typeof window !== "undefined" ? window.location.hash.slice(1) : ""
const [activeId, setActiveId] = React.useState<string>(
endpoints.find((e) => e.id === initialId)?.id ?? "overview"
)
const active = endpoints.find((e) => e.id === activeId)
function selectEndpoint(id: string) {
setActiveId(id)
window.history.replaceState(null, "", `#${id}`)
}
return (
<div className="mx-auto flex max-w-6xl flex-col gap-0 px-4 py-8 sm:px-6 md:flex-row md:py-10">
{/* Sidebar */}
<aside className="hidden w-56 shrink-0 pr-6 md:block">
<nav className="mb-6 space-y-1">
<button
onClick={() => selectEndpoint("overview")}
className={cn(
"flex w-full items-center rounded-md px-2.5 py-1.5 text-left text-sm transition-colors",
activeId === "overview"
? "bg-muted font-medium text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
)}
>
Overview
</button>
</nav>
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Endpoints
</h2>
<nav className="space-y-1">
{endpoints.map((ep) => (
<button
key={ep.id}
onClick={() => selectEndpoint(ep.id)}
className={cn(
"flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors",
activeId === ep.id
? "bg-muted font-medium text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
)}
>
<MethodBadge method={ep.method} />
<span className="truncate font-mono text-xs">{ep.path}</span>
</button>
))}
</nav>
</aside>
{/* Mobile endpoint selector */}
<div className="mb-6 md:hidden">
<select
value={activeId}
onChange={(e) => selectEndpoint(e.target.value)}
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
>
<option value="overview">Overview</option>
{endpoints.map((ep) => (
<option key={ep.id} value={ep.id}>
{ep.method} {ep.path}
</option>
))}
</select>
</div>
{/* Main content */}
<div className="min-w-0 flex-1">
{active ? (
<>
<div className="flex items-center gap-3">
<MethodBadge method={active.method} />
<h1 className="font-mono text-lg font-semibold">{active.path}</h1>
</div>
<p className="mt-1 text-sm text-muted-foreground">{active.summary}</p>
<div className="mt-6 space-y-6">
{/* Description */}
<p className="text-sm leading-relaxed text-foreground/80">
{active.description}
</p>
{/* Try it — most useful, put first */}
<ApiPlayground
key={active.id}
method={active.method}
path={active.path}
curlPrefix={active.curlPrefix}
defaultBody={active.defaultBody}
defaultParams={active.defaultParams}
buildPath={active.buildPath}
/>
{/* Schema — Request + Response side by side when both exist */}
{active.requestFields ? (
<div className="grid gap-4 lg:grid-cols-2">
<Section title="Request">
<FieldTable fields={active.requestFields} />
</Section>
{active.responseFields && (
<Section title="Response">
<FieldTable fields={active.responseFields} />
</Section>
)}
</div>
) : active.responseFields ? (
<Section title="Response">
<FieldTable fields={active.responseFields} />
</Section>
) : null}
</div>
</>
) : (
<OverviewSection onSelect={selectEndpoint} />
)}
</div>
</div>
)
}
function Code({ children }: { children: React.ReactNode }) {
return (
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em] text-foreground">
{children}
</code>
)
}
// Minimal, dependency-free shell highlighter for the curl examples — colors
// comments, commands, flags, quoted strings, and URLs.
function ShellBlock({ code }: { code: string }) {
const TOKEN =
/(#.*)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(https?:\/\/[^\s'"]+)|(\bcurl\b)|(\s-{1,2}[A-Za-z][\w-]*)/g
const nodes: React.ReactNode[] = []
let last = 0
let m: RegExpExecArray | null
let key = 0
while ((m = TOKEN.exec(code)) !== null) {
if (m.index > last) nodes.push(code.slice(last, m.index))
const [full, comment, str, url, cmd] = m
const cls = comment
? "italic text-muted-foreground/60"
: str
? "text-green-600 dark:text-green-400"
: url
? "text-blue-600 dark:text-blue-400"
: cmd
? "font-medium text-purple-600 dark:text-purple-400"
: "text-amber-600 dark:text-amber-400" // flag
nodes.push(
<span key={key++} className={cls}>
{full}
</span>
)
last = m.index + full.length
}
if (last < code.length) nodes.push(code.slice(last))
return (
<pre className="mt-3 overflow-x-auto rounded-xl border border-border/60 bg-card p-4 text-xs leading-relaxed text-foreground/80">
<code>{nodes}</code>
</pre>
)
}
function FlowStep({
n,
method,
path,
title,
onSelect,
children,
}: {
n: number
method: "GET" | "POST"
path: string
title: string
onSelect: () => void
children: React.ReactNode
}) {
return (
<button
onClick={onSelect}
className="flex w-full gap-4 rounded-xl border border-border/60 bg-card/40 p-4 text-left transition-colors hover:border-border hover:bg-muted/40"
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
{n}
</span>
<div className="min-w-0">
<div className="flex items-center gap-2">
<MethodBadge method={method} />
<code className="truncate font-mono text-xs text-foreground">{path}</code>
</div>
<p className="mt-2 text-sm font-medium text-foreground">{title}</p>
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">{children}</p>
</div>
</button>
)
}
function OverviewSection({ onSelect }: { onSelect: (id: string) => void }) {
return (
<div>
<h1 className="text-2xl font-semibold tracking-tight">API Overview</h1>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-muted-foreground">
PixelRAG indexes 8.28M Wikipedia articles as{" "}
<strong className="font-medium text-foreground">screenshot tiles</strong> and retrieves over
the images directly. Using the API is a two-step flow: search for the tiles that match your
query, then fetch each tile&apos;s screenshot.
</p>
<div className="mt-8 space-y-3">
<FlowStep
n={1}
method="POST"
path="/search"
title="Find matching tiles"
onSelect={() => onSelect("search")}
>
Send a natural-language question or an image. Each hit comes back with{" "}
<Code>article_id</Code>, <Code>tile_index</Code>, and <Code>chunk_index</Code>.
</FlowStep>
<FlowStep
n={2}
method="GET"
path="/tile/{article_id}/{tile_index}/{chunk_index}"
title="Fetch the screenshot"
onSelect={() => onSelect("tile")}
>
Pass those three IDs straight from a search hit to get the PNG embed it with an{" "}
<Code>&lt;img&gt;</Code> tag.
</FlowStep>
</div>
<h2 className="mt-10 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Example
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Search, then fetch the top hit&apos;s screenshot:
</p>
<ShellBlock
code={`# 1 — search with a text query
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d '{"queries": [{"text": "How does photosynthesis work?"}], "n_docs": 5}'
# → hits[0] = { "article_id": 5878188, "tile_index": 1, "chunk_index": 0, ... }
# 2 — fetch that hit's screenshot
curl https://pixelrag.ai/api/tile/5878188/1/0 --output tile.png
# 3 — or search with an image: base64-encode any local file inline
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d "{\\"queries\\": [{\\"image\\": \\"$(base64 < photo.jpg | tr -d '\\n')\\"}], \\"n_docs\\": 5}"
# 4 — hybrid: combine text + image in one query (joint multimodal retrieval)
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d "{\\"queries\\": [{\\"text\\": \\"impressionist oil painting\\", \\"image\\": \\"$(base64 < photo.jpg | tr -d '\\n')\\"}], \\"n_docs\\": 5}"`}
/>
<h2 className="mt-10 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Base URL
</h2>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-muted-foreground">
<Code>https://pixelrag.ai/api</Code> — or query the index server directly at{" "}
<Code>https://api.pixelrag.ai</Code>.
</p>
<h2 className="mt-10 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Endpoints
</h2>
<div className="mt-3 space-y-1.5">
{endpoints.map((ep) => (
<button
key={ep.id}
onClick={() => onSelect(ep.id)}
className="flex w-full items-center gap-3 rounded-lg border border-border/40 px-3 py-2 text-left text-sm transition-colors hover:border-border hover:bg-muted/40"
>
<MethodBadge method={ep.method} />
<code className="shrink-0 font-mono text-xs text-foreground">{ep.path}</code>
<span className="truncate text-xs text-muted-foreground">{ep.summary}</span>
</button>
))}
</div>
<h2 className="mt-10 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Connect your own agent
</h2>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-muted-foreground">
The search API is a plain HTTP endpoint &mdash; wire it into any agent framework
(Claude tool-use, OpenAI function calling, LangChain, custom harness) as a tool.
A typical agent loop: <strong className="font-medium text-foreground">search</strong> with
text and/or an image, <strong className="font-medium text-foreground">fetch tiles</strong> to
read the screenshots, then answer from what they show.
</p>
<ShellBlock
code={`# Text-only search
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d '{"queries": [{"text": "When was the Eiffel Tower built?"}], "n_docs": 5}'
# Image-only search (visual similarity)
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d "{\\"queries\\": [{\\"image\\": \\"$(base64 < photo.jpg | tr -d '\\n')\\"}], \\"n_docs\\": 5}"
# Joint image + text search (best for "what is this X?")
curl -X POST https://pixelrag.ai/api/search \\
-H "Content-Type: application/json" \\
-d "{\\"queries\\": [{\\"image\\": \\"$(base64 < photo.jpg | tr -d '\\n')\\", \\"text\\": \\"landmark building\\"}], \\"n_docs\\": 5}"
# Fetch a tile screenshot from the results
curl https://pixelrag.ai/api/tile/698618/0/0 --output tile.png`}
/>
</div>
)
}
function MethodBadge({ method }: { method: "GET" | "POST" }) {
return (
<Badge
variant="secondary"
className={cn(
"shrink-0 font-mono text-[0.6rem] font-bold uppercase",
method === "POST"
? "bg-green-500/15 text-green-700 dark:bg-green-500/20 dark:text-green-400"
: "bg-blue-500/15 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400"
)}
>
{method}
</Badge>
)
}
function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
return (
<div>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{title}
</h3>
{children}
</div>
)
}
function TypeBadge({ type }: { type: string }) {
const color = type.includes("[]")
? "text-amber-400 bg-amber-400/10"
: type === "number"
? "text-blue-400 bg-blue-400/10"
: type === "string" || type.startsWith("image/")
? "text-green-400 bg-green-400/10"
: "text-purple-400 bg-purple-400/10"
return (
<span className={cn("rounded px-1.5 py-0.5 font-mono text-[10px] font-medium", color)}>
{type}
</span>
)
}
function FieldRow({ field, depth = 0 }: { field: Field; depth?: number }) {
return (
<>
<div
className="flex items-start gap-3 border-b border-border/30 px-3 py-2.5 text-xs"
style={{ paddingLeft: `${12 + depth * 16}px` }}
>
<div className="flex shrink-0 items-center gap-2" style={{ minWidth: "120px" }}>
<span className="font-mono font-medium text-foreground">{field.name}</span>
{!field.required && (
<span className="text-[10px] italic text-muted-foreground/60">optional</span>
)}
</div>
{field.description && (
<span className="min-w-0 flex-1 truncate text-muted-foreground" title={field.description}>
{field.description}
</span>
)}
<TypeBadge type={field.type} />
</div>
{field.children?.map((child) => (
<FieldRow key={child.name} field={child} depth={depth + 1} />
))}
</>
)
}
function FieldTable({ fields }: { fields: Field[] }) {
return (
<div className="overflow-hidden rounded-xl border border-border/60 bg-card">
{fields.map((field) => (
<FieldRow key={field.name} field={field} />
))}
</div>
)
}