e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import { getLLMText } from "@/lib/get-llm-text";
|
|
import { source, examples as examplesSource } from "@/lib/source";
|
|
import { tool, zodSchema } from "ai";
|
|
import type * as PageTree from "fumadocs-core/page-tree";
|
|
import z from "zod";
|
|
|
|
const DOCS_PATH_ERROR = "Only local docs paths are supported";
|
|
|
|
function normalizeSegment(name: string): string {
|
|
return name.toLowerCase().replace(/\s+/g, "-");
|
|
}
|
|
|
|
function findFolderByPath(
|
|
tree: PageTree.Root,
|
|
path: string,
|
|
): PageTree.Folder | undefined {
|
|
const segments = path.split("/").filter(Boolean);
|
|
let currentFolder: PageTree.Folder | undefined;
|
|
let children: PageTree.Node[] = tree.children;
|
|
|
|
for (const segment of segments) {
|
|
const folder = children.find(
|
|
(node): node is PageTree.Folder =>
|
|
node.type === "folder" &&
|
|
normalizeSegment(typeof node.name === "string" ? node.name : "") ===
|
|
normalizeSegment(segment),
|
|
);
|
|
if (!folder) return undefined;
|
|
currentFolder = folder;
|
|
children = folder.children;
|
|
}
|
|
|
|
return currentFolder;
|
|
}
|
|
|
|
function normalizeDocPath(slugOrUrl: string, routeUrl: string): string {
|
|
const raw = slugOrUrl.trim();
|
|
if (!raw) {
|
|
throw new Error("Slug/path is required");
|
|
}
|
|
|
|
const current = new URL(routeUrl);
|
|
const isAbsoluteUrl = /^https?:\/\//i.test(raw);
|
|
|
|
if (!isAbsoluteUrl) {
|
|
const cleaned = raw.replace(/^\/+/, "").replace(/^docs\//, "");
|
|
if (!cleaned || cleaned.includes("..")) {
|
|
throw new Error(DOCS_PATH_ERROR);
|
|
}
|
|
return cleaned;
|
|
}
|
|
|
|
const resolved = new URL(raw);
|
|
if (resolved.origin !== current.origin) {
|
|
throw new Error(DOCS_PATH_ERROR);
|
|
}
|
|
|
|
const cleaned = resolved.pathname.replace(/^\/+/, "").replace(/^docs\//, "");
|
|
if (!cleaned || cleaned.includes("..")) {
|
|
throw new Error(DOCS_PATH_ERROR);
|
|
}
|
|
|
|
return cleaned;
|
|
}
|
|
|
|
function descriptionFromNode(node: PageTree.Node) {
|
|
const description = (node as { description?: unknown }).description;
|
|
return typeof description === "string" && description.trim()
|
|
? { description }
|
|
: {};
|
|
}
|
|
|
|
const flatNodes = (nodes: PageTree.Node[]) =>
|
|
nodes.flatMap((node) => {
|
|
switch (node.type) {
|
|
case "page":
|
|
return {
|
|
type: "page",
|
|
title: node.name,
|
|
url: node.url,
|
|
...descriptionFromNode(node),
|
|
};
|
|
case "folder":
|
|
return {
|
|
type: "folder",
|
|
name: node.name,
|
|
...(node.index ? { url: node.index.url } : {}),
|
|
...descriptionFromNode(node),
|
|
};
|
|
default:
|
|
return [];
|
|
}
|
|
});
|
|
|
|
export function createDocsTools({ routeUrl }: { routeUrl: string }) {
|
|
return {
|
|
listDocs: tool({
|
|
description:
|
|
"List documentation pages. Use with no path for root categories, or specify path to browse a section.",
|
|
inputSchema: zodSchema(
|
|
z.object({
|
|
path: z
|
|
.string()
|
|
.optional()
|
|
.describe("Subpath to browse. Omit to list root categories first."),
|
|
}),
|
|
),
|
|
execute: async ({ path }) => {
|
|
const pageTree = source.pageTree;
|
|
|
|
if (!path) {
|
|
return [
|
|
...flatNodes(
|
|
pageTree.children.filter(
|
|
(node): node is PageTree.Folder => node.type === "folder",
|
|
),
|
|
),
|
|
{
|
|
type: "folder",
|
|
name: "examples",
|
|
description:
|
|
"Examples of app types users can build with assistant-ui, showing instructions, recommended patterns, and UI structure.",
|
|
},
|
|
];
|
|
}
|
|
|
|
const segments = path.split("/").filter(Boolean);
|
|
if (segments[0] === "examples") {
|
|
const rest = segments.slice(1).join("/");
|
|
const target = rest
|
|
? findFolderByPath(examplesSource.pageTree, rest)
|
|
: examplesSource.pageTree;
|
|
if (!target) return { error: "Path not found" };
|
|
return flatNodes(target.children);
|
|
}
|
|
|
|
const targetFolder = findFolderByPath(pageTree, path);
|
|
if (!targetFolder) return { error: "Path not found" };
|
|
return flatNodes(targetFolder.children);
|
|
},
|
|
}),
|
|
readDoc: tool({
|
|
description: "Read full content of a documentation page.",
|
|
inputSchema: zodSchema(
|
|
z.object({
|
|
slugOrUrl: z
|
|
.string()
|
|
.describe(
|
|
"Page slug (e.g., 'examples/ai-sdk', 'ui/thread') or URL",
|
|
),
|
|
}),
|
|
),
|
|
execute: async ({ slugOrUrl }) => {
|
|
let normalized: string;
|
|
try {
|
|
normalized = normalizeDocPath(slugOrUrl, routeUrl);
|
|
} catch (error) {
|
|
return {
|
|
error: error instanceof Error ? error.message : "Invalid docs path",
|
|
};
|
|
}
|
|
|
|
const slugs = normalized.split("/").filter(Boolean);
|
|
const isExample = slugs[0] === "examples";
|
|
const docSource = isExample ? examplesSource : source;
|
|
const docSlugs = isExample ? slugs.slice(1) : slugs;
|
|
|
|
const page = docSource.getPage(docSlugs);
|
|
if (!page) return { error: `Page not found: ${slugOrUrl}` };
|
|
const content = await getLLMText(page);
|
|
return { title: page.data.title, url: page.url, content };
|
|
},
|
|
}),
|
|
};
|
|
}
|