Agent 18: knowledge base
A knowledge base admin: the sources an assistant reads with their health, document counts and a switch for each, live sync with Retry, and a box to test a question and see which sources the answer comes from.
The normal view. Pick a state to see what the block shows while it loads, is empty or fails.
// Ballmac UI: Agent 18. https://ui.ballmac.com/blocks/agent-18
"use client"
import * as React from "react"
import { Loader2, Search } from "lucide-react"
import { CitedAnswer } from "@/components/ballmac/cited-answer"
import type { CitationSource } from "@/components/ballmac/citation"
import { KnowledgeSources, type KnowledgeSource } from "@/components/ballmac/knowledge-sources"
import { Skeleton } from "@/components/ballmac/skeleton"
import { frameProps, StateView, type StateProps } from "@/components/ballmac/state-view"
import { cn } from "@/lib/utils"
type Agent18Answer = { paragraphs: string[]; sources: CitationSource[] }
type Agent18Props = Omit<React.ComponentProps<"section">, "title"> &
StateProps & {
title?: string
description?: string
sources?: KnowledgeSource[]
onAdd?: () => void
onSync?: (id: string) => void | Promise<void>
onToggle?: (id: string, included: boolean) => void | Promise<void>
onRemove?: (id: string) => void | Promise<void>
/** Called with a test question. Return the answer with its sources; reject and the reason is shown. */
onTest?: (question: string) => Promise<Agent18Answer>
}
const items: KnowledgeSource[] = [
{ id: "1", name: "Product docs", kind: "notion", detail: "Workspace: Acme", status: "synced", docs: 1284, lastSync: "12 minutes ago", included: true },
{ id: "2", name: "Support macros", kind: "drive", detail: "Shared drive / Support / Macros", status: "syncing", docs: 342, progress: 0.62, included: true },
{ id: "3", name: "help.acme.com", kind: "web", detail: "Crawled weekly, 3 levels deep", status: "synced", docs: 96, lastSync: "Yesterday", included: true },
{ id: "4", name: "acme/platform", kind: "github", detail: "README and /docs on main", status: "error", docs: 58, lastSync: "3 days ago", error: "The access token expired. Reconnect GitHub and sync again.", included: true },
{ id: "5", name: "#help-questions", kind: "slack", detail: "Last 90 days", status: "paused", docs: 2210, lastSync: "Oct 2", included: false },
]
const sample: Agent18Answer = {
sources: [
{ title: "Refund policy", site: "help.acme.com", url: "https://help.acme.com/refunds", date: "Updated Oct 2", snippet: "If an item does not work as described and we cannot fix it within 14 days of your request, you receive a full refund." },
{ title: "Plans and billing", site: "Product docs", url: "https://docs.acme.com/billing", date: "Oct 11", snippet: "Refunds go back to the original payment method and take 3 to 5 business days to appear." },
],
paragraphs: ["A refund is available when an item does not work as described and cannot be fixed within 14 days [1]. It returns to the original payment method and appears in 3 to 5 business days [2]."],
}
function Skel() {
return (
<div aria-hidden="true" className="grid gap-4">
<Skeleton className="h-14" />
<Skeleton className="h-72" />
</div>
)
}
/**
* A knowledge base admin: the sources an assistant reads, their health and whether answers may use them, and a box to test a question and see which sources the
* answer would come from. Turning a source off and asking again shows what it was contributing.
*/
function Agent18({ title = "Knowledge base", description = "Choose what the assistant reads, keep it fresh, and test a question before customers ask it.", sources = items, onAdd, onSync, onToggle, onRemove, onTest, state = "ready", onRetry, emptyState, errorState, className, ...props }: Agent18Props) {
const id = React.useId()
const [q, setQ] = React.useState("What is the refund policy?")
const [busy, setBusy] = React.useState(false)
const [answer, setAnswer] = React.useState<Agent18Answer | null>(null)
const [error, setError] = React.useState<string | null>(null)
const view = state === "ready" && sources.length === 0 ? "empty" : state
async function test(e: React.FormEvent) {
e.preventDefault()
if (!q.trim() || busy) return
setBusy(true)
setError(null)
try {
setAnswer(onTest ? await onTest(q.trim()) : await new Promise<Agent18Answer>((r) => setTimeout(() => r(sample), 900)))
} catch (err) {
setError(err instanceof Error ? err.message : "The test could not run. Nothing was changed.")
} finally {
setBusy(false)
}
}
return (
<section data-slot="agent-18" data-state={state} aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-4xl px-4 py-8 sm:px-6", className)} {...frameProps(props)}>
<header className="mb-6">
<h1 id={`${id}-title`} className="text-2xl font-semibold tracking-[-0.03em] text-balance">{title}</h1>
<p className="mt-1 text-sm text-muted-foreground text-pretty">{description}</p>
</header>
<StateView state={view} onRetry={onRetry} label="Loading the sources" loading={<Skel />} empty={{ title: "No sources yet", description: "Connect a place the assistant can read and it can start answering from it.", ...emptyState }} error={{ title: "The sources could not be loaded", description: "Nothing was changed. Check your connection and try again.", ...errorState }}>
<div className="grid gap-6">
<KnowledgeSources sources={sources} onAdd={onAdd} onSync={onSync ?? (() => new Promise((r) => setTimeout(r, 1200)))} onToggle={onToggle} onRemove={onRemove} />
<div className="grid gap-3 rounded-xl border bg-card p-5 sm:p-6">
<h2 className="text-sm font-semibold">Test a question</h2>
<form onSubmit={(e) => void test(e)} className="flex flex-wrap gap-2">
<label htmlFor={`${id}-q`} className="sr-only">Question to test</label>
<div className="relative min-w-0 flex-1 basis-60">
<Search aria-hidden="true" className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input id={`${id}-q`} value={q} onChange={(e) => setQ(e.target.value)} className="h-10 w-full rounded-lg border bg-background ps-9 pe-3 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" />
</div>
<button type="submit" disabled={busy || q.trim() === ""} className="inline-flex h-10 items-center gap-2 rounded-lg bg-foreground px-4 text-sm font-medium text-background outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-60">
{busy && <Loader2 aria-hidden="true" className="size-4 animate-spin motion-reduce:animate-none" />}
{busy ? "Answering" : "Test"}
</button>
</form>
{error && <p role="alert" className="rounded-lg border border-destructive/40 px-3 py-2 text-sm">{error}</p>}
{answer && <CitedAnswer paragraphs={answer.paragraphs} sources={answer.sources} className="bg-background" />}
</div>
</div>
</StateView>
</section>
)
}
export { Agent18, type Agent18Answer, type Agent18Props }Installation
$ pnpm dlx shadcn@latest add @ballmac/agent-18Install the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/skeleton @ballmac/state-view @ballmac/citation @ballmac/cited-answer @ballmac/knowledge-sourcesCopy the source into your project.
components/ballmac/blocks/agent-18/agent-18.tsx// Ballmac UI: Agent 18. https://ui.ballmac.com/blocks/agent-18 "use client" import * as React from "react" import { Loader2, Search } from "lucide-react" import { CitedAnswer } from "@/components/ballmac/cited-answer" import type { CitationSource } from "@/components/ballmac/citation" import { KnowledgeSources, type KnowledgeSource } from "@/components/ballmac/knowledge-sources" import { Skeleton } from "@/components/ballmac/skeleton" import { frameProps, StateView, type StateProps } from "@/components/ballmac/state-view" import { cn } from "@/lib/utils" type Agent18Answer = { paragraphs: string[]; sources: CitationSource[] } type Agent18Props = Omit<React.ComponentProps<"section">, "title"> & StateProps & { title?: string description?: string sources?: KnowledgeSource[] onAdd?: () => void onSync?: (id: string) => void | Promise<void> onToggle?: (id: string, included: boolean) => void | Promise<void> onRemove?: (id: string) => void | Promise<void> /** Called with a test question. Return the answer with its sources; reject and the reason is shown. */ onTest?: (question: string) => Promise<Agent18Answer> } const items: KnowledgeSource[] = [ { id: "1", name: "Product docs", kind: "notion", detail: "Workspace: Acme", status: "synced", docs: 1284, lastSync: "12 minutes ago", included: true }, { id: "2", name: "Support macros", kind: "drive", detail: "Shared drive / Support / Macros", status: "syncing", docs: 342, progress: 0.62, included: true }, { id: "3", name: "help.acme.com", kind: "web", detail: "Crawled weekly, 3 levels deep", status: "synced", docs: 96, lastSync: "Yesterday", included: true }, { id: "4", name: "acme/platform", kind: "github", detail: "README and /docs on main", status: "error", docs: 58, lastSync: "3 days ago", error: "The access token expired. Reconnect GitHub and sync again.", included: true }, { id: "5", name: "#help-questions", kind: "slack", detail: "Last 90 days", status: "paused", docs: 2210, lastSync: "Oct 2", included: false }, ] const sample: Agent18Answer = { sources: [ { title: "Refund policy", site: "help.acme.com", url: "https://help.acme.com/refunds", date: "Updated Oct 2", snippet: "If an item does not work as described and we cannot fix it within 14 days of your request, you receive a full refund." }, { title: "Plans and billing", site: "Product docs", url: "https://docs.acme.com/billing", date: "Oct 11", snippet: "Refunds go back to the original payment method and take 3 to 5 business days to appear." }, ], paragraphs: ["A refund is available when an item does not work as described and cannot be fixed within 14 days [1]. It returns to the original payment method and appears in 3 to 5 business days [2]."], } function Skel() { return ( <div aria-hidden="true" className="grid gap-4"> <Skeleton className="h-14" /> <Skeleton className="h-72" /> </div> ) } /** * A knowledge base admin: the sources an assistant reads, their health and whether answers may use them, and a box to test a question and see which sources the * answer would come from. Turning a source off and asking again shows what it was contributing. */ function Agent18({ title = "Knowledge base", description = "Choose what the assistant reads, keep it fresh, and test a question before customers ask it.", sources = items, onAdd, onSync, onToggle, onRemove, onTest, state = "ready", onRetry, emptyState, errorState, className, ...props }: Agent18Props) { const id = React.useId() const [q, setQ] = React.useState("What is the refund policy?") const [busy, setBusy] = React.useState(false) const [answer, setAnswer] = React.useState<Agent18Answer | null>(null) const [error, setError] = React.useState<string | null>(null) const view = state === "ready" && sources.length === 0 ? "empty" : state async function test(e: React.FormEvent) { e.preventDefault() if (!q.trim() || busy) return setBusy(true) setError(null) try { setAnswer(onTest ? await onTest(q.trim()) : await new Promise<Agent18Answer>((r) => setTimeout(() => r(sample), 900))) } catch (err) { setError(err instanceof Error ? err.message : "The test could not run. Nothing was changed.") } finally { setBusy(false) } } return ( <section data-slot="agent-18" data-state={state} aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-4xl px-4 py-8 sm:px-6", className)} {...frameProps(props)}> <header className="mb-6"> <h1 id={`${id}-title`} className="text-2xl font-semibold tracking-[-0.03em] text-balance">{title}</h1> <p className="mt-1 text-sm text-muted-foreground text-pretty">{description}</p> </header> <StateView state={view} onRetry={onRetry} label="Loading the sources" loading={<Skel />} empty={{ title: "No sources yet", description: "Connect a place the assistant can read and it can start answering from it.", ...emptyState }} error={{ title: "The sources could not be loaded", description: "Nothing was changed. Check your connection and try again.", ...errorState }}> <div className="grid gap-6"> <KnowledgeSources sources={sources} onAdd={onAdd} onSync={onSync ?? (() => new Promise((r) => setTimeout(r, 1200)))} onToggle={onToggle} onRemove={onRemove} /> <div className="grid gap-3 rounded-xl border bg-card p-5 sm:p-6"> <h2 className="text-sm font-semibold">Test a question</h2> <form onSubmit={(e) => void test(e)} className="flex flex-wrap gap-2"> <label htmlFor={`${id}-q`} className="sr-only">Question to test</label> <div className="relative min-w-0 flex-1 basis-60"> <Search aria-hidden="true" className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> <input id={`${id}-q`} value={q} onChange={(e) => setQ(e.target.value)} className="h-10 w-full rounded-lg border bg-background ps-9 pe-3 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" /> </div> <button type="submit" disabled={busy || q.trim() === ""} className="inline-flex h-10 items-center gap-2 rounded-lg bg-foreground px-4 text-sm font-medium text-background outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-60"> {busy && <Loader2 aria-hidden="true" className="size-4 animate-spin motion-reduce:animate-none" />} {busy ? "Answering" : "Test"} </button> </form> {error && <p role="alert" className="rounded-lg border border-destructive/40 px-3 py-2 text-sm">{error}</p>} {answer && <CitedAnswer paragraphs={answer.paragraphs} sources={answer.sources} className="bg-background" />} </div> </div> </StateView> </section> ) } export { Agent18, type Agent18Answer, type Agent18Props }Update the import paths to match your project setup.
Usage
"use client"
import { Agent18 } from "@/components/ballmac/blocks/agent-18/agent-18"
export default function Agent18Demo() {
return <Agent18 />
}States
Pass state and the block does the rest: skeletons that match its layout, a clear empty message, an error you can retry, and announcements for screen readers.
Loading
Skeleton sources and test box under the real heading, announced as loading.
state="loading"No sources yet
Says there are no sources yet; emptyState sets the copy.
state="empty"Could not load
An alert saying nothing was changed, with Try again (onRetry). A sync, switch or remove that fails says why and changes nothing.
state="error"
Dependencies
- npm
- lucide-react
Use with AI
Knowledge base. <Agent18 title description sources={KnowledgeSource[]} onAdd onSync onToggle onRemove onTest={(q) => Promise<{ paragraphs, sources }>} />. The test box asks a question and shows the answer with its sources, so a person can turn a source off and ask again to see what it contributed. A rejected onTest shows the reason. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Agent 18: knowledge base (@ballmac/agent-18) to this project with the shadcn MCP, then use it where it fits.
Use it for
- The settings page of an AI product that answers from customer data
- Checking which sources an answer comes from before launch
Not for
- A chat for end users; use agent-16
- Browsing documents; use a file table
Registry JSON: https://ui.ballmac.com/r/agent-18.json
Free to use in personal and commercial projects.
More ai agents blocks
Agent 1: live agent run
One AI agent run as it happens: the task and status, the plan beside it, every note, tool call and approval request in order, approvals that pause the run, the result with copy, and a follow-up box.
Agent 10: console with handoff
An agent console: the conversation with each tool call as a step you can open to see what was asked and what came back, Stop while it works, Enter to send, and a clear Take over and Hand back so a person is never mistaken for the agent.
Agent 11: run workbench
A run workbench: the conversation with tool calls as openable cards, a live plan, a context meter and a steering box to redirect the agent while it works, with Stop, optimistic instructions and honest failures.
Agent 12: mission control
Mission control for a team of agents: spend, tokens, running agents and decisions waiting at the top, the delegation tree below, and an inbox of requests with Approve and Deny that report back honestly.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.