Skip to content

Commit 0d65f38

Browse files
committed
add openrouter ai chat
1 parent a7fa7d9 commit 0d65f38

11 files changed

Lines changed: 934 additions & 7 deletions

File tree

.source/source.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ var docs = defineDocs({
4040
title: z.string().optional().default(""),
4141
full: z.boolean().optional()
4242
})
43+
},
44+
mdxOptions: {
45+
includeProcessedMarkdown: true
4346
}
4447
});
4548
var source_config_default = defineConfig({

app/api/chat/route.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
2+
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from 'ai';
3+
import { z } from 'zod';
4+
import { source } from '@/lib/source';
5+
import { Document, type DocumentData } from 'flexsearch';
6+
7+
interface CustomDocument extends DocumentData {
8+
url: string;
9+
title: string;
10+
description: string;
11+
content: string;
12+
}
13+
14+
const searchServer = createSearchServer();
15+
16+
async function createSearchServer() {
17+
const search = new Document<CustomDocument>({
18+
document: {
19+
id: 'url',
20+
index: ['title', 'description', 'content'],
21+
store: true,
22+
},
23+
});
24+
25+
const docs = await chunkedAll(
26+
source.getPages().map(async (page) => {
27+
if (!('getText' in page.data)) return null;
28+
29+
return {
30+
title: page.data.title,
31+
description: page.data.description,
32+
url: page.url,
33+
content: await page.data.getText('raw'),
34+
} as CustomDocument;
35+
}),
36+
);
37+
38+
for (const doc of docs) {
39+
if (doc) search.add(doc);
40+
}
41+
42+
return search;
43+
}
44+
45+
async function chunkedAll<O>(promises: Promise<O>[]): Promise<O[]> {
46+
const SIZE = 50;
47+
const out: O[] = [];
48+
for (let i = 0; i < promises.length; i += SIZE) {
49+
out.push(...(await Promise.all(promises.slice(i, i + SIZE))));
50+
}
51+
return out;
52+
}
53+
54+
const openrouter = createOpenRouter({
55+
apiKey: process.env.OPENROUTER_API_KEY,
56+
});
57+
58+
const systemPrompt = [
59+
'You are an AI assistant for a documentation site.',
60+
'Use the `search` tool to retrieve relevant docs context before answering when needed.',
61+
'The `search` tool returns raw JSON results from documentation. Use those results to ground your answer and cite sources as markdown links using the document `url` field when available.',
62+
'If you cannot find the answer in search results, say you do not know and suggest a better search query.',
63+
].join('\n');
64+
65+
export async function POST(req: Request) {
66+
const reqJson: { messages?: UIMessage[] } = await req.json();
67+
68+
const result = streamText({
69+
model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'),
70+
stopWhen: stepCountIs(5),
71+
tools: {
72+
search: searchTool,
73+
},
74+
messages: [
75+
{ role: 'system', content: systemPrompt },
76+
...(await convertToModelMessages(reqJson.messages ?? [])),
77+
],
78+
toolChoice: 'auto',
79+
});
80+
81+
return result.toUIMessageStreamResponse();
82+
}
83+
84+
const searchTool = tool({
85+
description: 'Search the docs content and return raw JSON results.',
86+
inputSchema: z.object({
87+
query: z.string(),
88+
limit: z.number().int().min(1).max(100).default(10),
89+
}),
90+
async execute({ query, limit }) {
91+
const search = await searchServer;
92+
return await search.searchAsync(query, { limit, merge: true, enrich: true });
93+
},
94+
});
95+
96+
export type SearchTool = typeof searchTool;

app/layout.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { RootProvider } from 'fumadocs-ui/provider/next';
22
import { AlgoliaDialog } from '@/components/search';
3+
import { AISearch, AISearchPanel, AISearchTrigger } from '@/components/ai/search';
4+
import { MessageCircleIcon } from 'lucide-react';
35
import type { ReactNode } from 'react';
46
import './globals.css';
57

@@ -8,7 +10,17 @@ export default function Layout({ children }: { children: ReactNode }) {
810
<html lang="en" suppressHydrationWarning>
911
<body className="flex min-h-screen flex-col">
1012
<RootProvider search={{ SearchDialog: AlgoliaDialog }}>
11-
{children}
13+
<AISearch>
14+
<AISearchPanel />
15+
<AISearchTrigger
16+
position="float"
17+
className="fixed bottom-4 end-4 z-20 flex items-center gap-2 rounded-2xl bg-fd-secondary px-4 py-2 text-sm text-fd-secondary-foreground shadow-lg"
18+
>
19+
<MessageCircleIcon className="size-4" />
20+
Ask AI
21+
</AISearchTrigger>
22+
{children}
23+
</AISearch>
1224
</RootProvider>
1325
</body>
1426
</html>

cli.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "node_modules/@fumadocs/cli/dist/schema/default.json",
3+
"aliases": {
4+
"uiDir": "./components/ui",
5+
"componentsDir": "./components",
6+
"blockDir": "./components",
7+
"cssDir": "./styles",
8+
"libDir": "./lib"
9+
},
10+
"baseDir": "",
11+
"uiLibrary": "radix-ui",
12+
"commands": {}
13+
}

0 commit comments

Comments
 (0)