| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import { readFile } from "fs/promises";
- import { join } from "path";
- import { convertToModelMessages, stepCountIs, streamText } from "ai";
- import type { ModelMessage, UIMessage } from "ai";
- import { createBashTool } from "bash-tool";
- import { headers } from "next/headers";
- import { allDocsPages } from "@/lib/docs-navigation";
- import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
- import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
- export const maxDuration = 60;
- const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
- const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render, a library for AI-generated UI with guardrails.
- GitHub repository: https://github.com/vercel-labs/json-render
- Documentation: https://json-render.dev/docs
- npm packages: @json-render/core, @json-render/react, @json-render/next, @json-render/ink, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml
- Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, next, ink, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
- You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
- When answering questions:
- - Use the bash tool to list files (ls /workspace/docs/) or search for content (grep -r "keyword" /workspace/docs/)
- - Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/docs/index.md")
- - Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
- - Always base your answers on the actual documentation content
- - Be concise and accurate
- - If the docs don't cover a topic, say so honestly
- - Do NOT include source references or file paths in your response
- - Do NOT use emojis in your responses`;
- async function loadDocsFiles(): Promise<Record<string, string>> {
- const files: Record<string, string> = {};
- const results = await Promise.allSettled(
- allDocsPages.map(async (page) => {
- const slug =
- page.href === "/docs" ? "" : page.href.replace(/^\/docs\/?/, "");
- const filePath = slug
- ? join(
- process.cwd(),
- "app",
- "(main)",
- "docs",
- ...slug.split("/"),
- "page.mdx",
- )
- : join(process.cwd(), "app", "(main)", "docs", "page.mdx");
- const raw = await readFile(filePath, "utf-8");
- const md = mdxToCleanMarkdown(raw);
- const fileName = slug ? `/docs/${slug}.md` : "/docs/index.md";
- return { fileName, md };
- }),
- );
- for (const result of results) {
- if (result.status === "fulfilled") {
- files[result.value.fileName] = result.value.md;
- }
- }
- return files;
- }
- function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
- if (messages.length === 0) return messages;
- return messages.map((message, index) => {
- if (index === messages.length - 1) {
- return {
- ...message,
- providerOptions: {
- ...message.providerOptions,
- anthropic: { cacheControl: { type: "ephemeral" } },
- },
- };
- }
- return message;
- });
- }
- export async function POST(req: Request) {
- const headersList = await headers();
- const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
- const [minuteResult, dailyResult] = await Promise.all([
- minuteRateLimit.limit(ip),
- dailyRateLimit.limit(ip),
- ]);
- if (!minuteResult.success || !dailyResult.success) {
- const isMinuteLimit = !minuteResult.success;
- return new Response(
- JSON.stringify({
- error: "Rate limit exceeded",
- message: isMinuteLimit
- ? "Too many requests. Please wait a moment before trying again."
- : "Daily limit reached. Please try again tomorrow.",
- }),
- {
- status: 429,
- headers: { "Content-Type": "application/json" },
- },
- );
- }
- const { messages }: { messages: UIMessage[] } = await req.json();
- const docsFiles = await loadDocsFiles();
- const {
- tools: { bash, readFile },
- } = await createBashTool({ files: docsFiles });
- const result = streamText({
- model: DEFAULT_MODEL,
- system: SYSTEM_PROMPT,
- messages: await convertToModelMessages(messages),
- stopWhen: stepCountIs(5),
- tools: {
- bash,
- readFile,
- },
- prepareStep: ({ messages: stepMessages }) => ({
- messages: addCacheControl(stepMessages),
- }),
- });
- return result.toUIMessageStreamResponse();
- }
|