From 4fa39447cb316c0f21a0c20b9a244e25d99672ac Mon Sep 17 00:00:00 2001 From: matthewrgourd Date: Sat, 4 Apr 2026 18:32:23 +0100 Subject: [PATCH 1/4] Add anonymous widget-chat API endpoint with CORS for doc-platform embedding --- app/(chat)/api/chat/route.ts | 9 +++-- app/api/widget-chat/route.ts | 70 ++++++++++++++++++++++++++++++++++++ lib/ai/docs.ts | 20 +++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 app/api/widget-chat/route.ts create mode 100644 lib/ai/docs.ts diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index ac52197..00f220e 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -25,6 +25,7 @@ import { editDocument } from "@/lib/ai/tools/edit-document"; import { getWeather } from "@/lib/ai/tools/get-weather"; import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; import { updateDocument } from "@/lib/ai/tools/update-document"; +import { fetchDocsContent } from "@/lib/ai/docs"; import { isProductionEnvironment } from "@/lib/constants"; import { createStreamId, @@ -47,6 +48,7 @@ import { type PostRequestBody, postRequestBodySchema } from "./schema"; export const maxDuration = 60; + function getStreamContext() { try { return createResumableStreamContext({ waitUntil: after }); @@ -186,14 +188,17 @@ export async function POST(request: Request) { const isReasoningModel = capabilities?.reasoning === true; const supportsTools = capabilities?.tools === true; - const modelMessages = await convertToModelMessages(uiMessages); + const [modelMessages, docsContent] = await Promise.all([ + convertToModelMessages(uiMessages), + fetchDocsContent(), + ]); const stream = createUIMessageStream({ originalMessages: isToolApprovalFlow ? uiMessages : undefined, execute: async ({ writer: dataStream }) => { const result = streamText({ model: getLanguageModel(chatModel), - system: systemPrompt({ requestHints, supportsTools }), + system: systemPrompt({ requestHints, supportsTools, docsContent }), messages: modelMessages, stopWhen: stepCountIs(5), experimental_activeTools: diff --git a/app/api/widget-chat/route.ts b/app/api/widget-chat/route.ts new file mode 100644 index 0000000..9c45a0e --- /dev/null +++ b/app/api/widget-chat/route.ts @@ -0,0 +1,70 @@ +import { streamText } from "ai"; +import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; +import { fetchDocsContent } from "@/lib/ai/docs"; +import { regularPrompt } from "@/lib/ai/prompts"; +import { getLanguageModel } from "@/lib/ai/providers"; + +export const maxDuration = 30; + +const ALLOWED_ORIGINS = [ + "https://www.devdocify.com", + "https://devdocify.com", +]; + +function corsHeaders(origin: string | null): Record { + const allowed = + (origin && ALLOWED_ORIGINS.includes(origin)) || + origin?.startsWith("http://localhost"); + return { + "Access-Control-Allow-Origin": allowed + ? (origin ?? ALLOWED_ORIGINS[0]) + : ALLOWED_ORIGINS[0], + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; +} + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 204, + headers: corsHeaders(request.headers.get("origin")), + }); +} + +export async function POST(request: Request) { + const origin = request.headers.get("origin"); + const headers = corsHeaders(origin); + + let messages: { role: string; content: string }[]; + try { + const body = await request.json(); + messages = body.messages; + if (!Array.isArray(messages) || messages.length === 0) { + return new Response("Bad request", { status: 400, headers }); + } + } catch { + return new Response("Bad request", { status: 400, headers }); + } + + const docsContent = await fetchDocsContent(); + const docsSection = docsContent + ? `\n\n## DevDocify Documentation\n\n${docsContent}` + : ""; + const system = `${regularPrompt}${docsSection}`; + + const result = streamText({ + model: getLanguageModel(DEFAULT_CHAT_MODEL), + system, + messages: messages as any, + }); + + const textResponse = result.toTextStreamResponse(); + const responseHeaders = new Headers(textResponse.headers); + for (const [k, v] of Object.entries(headers)) { + responseHeaders.set(k, v); + } + return new Response(textResponse.body, { + status: textResponse.status, + headers: responseHeaders, + }); +} diff --git a/lib/ai/docs.ts b/lib/ai/docs.ts new file mode 100644 index 0000000..667bfda --- /dev/null +++ b/lib/ai/docs.ts @@ -0,0 +1,20 @@ +const LLMS_TXT_URL = "https://www.devdocify.com/llms.txt"; +const CACHE_TTL = 60 * 60 * 1000; // 1 hour + +let cache: { content: string; fetchedAt: number } | null = null; + +export async function fetchDocsContent(): Promise { + const now = Date.now(); + if (cache && now - cache.fetchedAt < CACHE_TTL) { + return cache.content; + } + try { + const res = await fetch(LLMS_TXT_URL, { next: { revalidate: 3600 } }); + if (!res.ok) return undefined; + const content = await res.text(); + cache = { content, fetchedAt: now }; + return content; + } catch { + return undefined; + } +} From ff50e17607b84d75f7265c49a11a33dc2049fd7f Mon Sep 17 00:00:00 2001 From: matthewrgourd Date: Sat, 4 Apr 2026 18:37:16 +0100 Subject: [PATCH 2/4] Add anonymous widget-chat streaming endpoint and allow iframe embedding from devdocify.com --- lib/ai/prompts.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index 77d0d28..95f34e0 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -44,7 +44,7 @@ CRITICAL RULES: - ONLY when the user explicitly asks for suggestions on an existing document `; -export const regularPrompt = `You are a helpful assistant. Keep responses concise and direct. +export const regularPrompt = `You are a helpful assistant for DevDocify, a documentation platform. Answer questions about DevDocify's features, configuration, and usage using the provided documentation. For questions unrelated to DevDocify, you can still help as a general assistant. Keep responses concise and direct. When asked to write, create, or build something, do it immediately. Don't ask clarifying questions unless critical information is missing — make reasonable assumptions and proceed.`; @@ -66,17 +66,22 @@ About the origin of user's request: export const systemPrompt = ({ requestHints, supportsTools, + docsContent, }: { requestHints: RequestHints; supportsTools: boolean; + docsContent?: string; }) => { const requestPrompt = getRequestPromptFromHints(requestHints); + const docsSection = docsContent + ? `\n\n## DevDocify Documentation\n\n${docsContent}` + : ""; if (!supportsTools) { - return `${regularPrompt}\n\n${requestPrompt}`; + return `${regularPrompt}${docsSection}\n\n${requestPrompt}`; } - return `${regularPrompt}\n\n${requestPrompt}\n\n${artifactsPrompt}`; + return `${regularPrompt}${docsSection}\n\n${requestPrompt}\n\n${artifactsPrompt}`; }; export const codePrompt = ` From 6018f348ba14ecddf8e7a6483ccb5f7d2e68a526 Mon Sep 17 00:00:00 2001 From: matthewrgourd Date: Sat, 4 Apr 2026 18:42:58 +0100 Subject: [PATCH 3/4] Fix Biome lint errors in widget-chat route and docs utility --- app/api/widget-chat/route.ts | 11 ++++------- lib/ai/docs.ts | 4 +++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/api/widget-chat/route.ts b/app/api/widget-chat/route.ts index 9c45a0e..1253ac2 100644 --- a/app/api/widget-chat/route.ts +++ b/app/api/widget-chat/route.ts @@ -1,19 +1,16 @@ import { streamText } from "ai"; -import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; import { fetchDocsContent } from "@/lib/ai/docs"; +import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; import { regularPrompt } from "@/lib/ai/prompts"; import { getLanguageModel } from "@/lib/ai/providers"; export const maxDuration = 30; -const ALLOWED_ORIGINS = [ - "https://www.devdocify.com", - "https://devdocify.com", -]; +const ALLOWED_ORIGINS = ["https://www.devdocify.com", "https://devdocify.com"]; function corsHeaders(origin: string | null): Record { const allowed = - (origin && ALLOWED_ORIGINS.includes(origin)) || + (origin !== null && ALLOWED_ORIGINS.includes(origin)) || origin?.startsWith("http://localhost"); return { "Access-Control-Allow-Origin": allowed @@ -24,7 +21,7 @@ function corsHeaders(origin: string | null): Record { }; } -export async function OPTIONS(request: Request) { +export function OPTIONS(request: Request) { return new Response(null, { status: 204, headers: corsHeaders(request.headers.get("origin")), diff --git a/lib/ai/docs.ts b/lib/ai/docs.ts index 667bfda..d206e01 100644 --- a/lib/ai/docs.ts +++ b/lib/ai/docs.ts @@ -10,7 +10,9 @@ export async function fetchDocsContent(): Promise { } try { const res = await fetch(LLMS_TXT_URL, { next: { revalidate: 3600 } }); - if (!res.ok) return undefined; + if (!res.ok) { + return undefined; + } const content = await res.text(); cache = { content, fetchedAt: now }; return content; From 9c63de9042e06333bcd84bc8d92c89a89e3d1442 Mon Sep 17 00:00:00 2001 From: matthewrgourd Date: Sat, 4 Apr 2026 18:45:23 +0100 Subject: [PATCH 4/4] Fix import order in chat route --- app/(chat)/api/chat/route.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 00f220e..95e137a 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -11,6 +11,7 @@ import { checkBotId } from "botid/server"; import { after } from "next/server"; import { createResumableStreamContext } from "resumable-stream"; import { auth, type UserType } from "@/app/(auth)/auth"; +import { fetchDocsContent } from "@/lib/ai/docs"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; import { allowedModelIds, @@ -25,7 +26,6 @@ import { editDocument } from "@/lib/ai/tools/edit-document"; import { getWeather } from "@/lib/ai/tools/get-weather"; import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; import { updateDocument } from "@/lib/ai/tools/update-document"; -import { fetchDocsContent } from "@/lib/ai/docs"; import { isProductionEnvironment } from "@/lib/constants"; import { createStreamId, @@ -48,7 +48,6 @@ import { type PostRequestBody, postRequestBodySchema } from "./schema"; export const maxDuration = 60; - function getStreamContext() { try { return createResumableStreamContext({ waitUntil: after });