Skip to content

Commit 8d4af15

Browse files
committed
improve ai chat
1 parent 2b0ad1f commit 8d4af15

3 files changed

Lines changed: 50 additions & 84 deletions

File tree

app/api/chat/route.ts

Lines changed: 48 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,48 @@
11
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
22
import { convertToModelMessages, streamText, tool, type UIMessage } from 'ai';
33
import { z } from 'zod';
4-
import { source } from '@/lib/source';
5-
import { Document, type DocumentData } from 'flexsearch';
6-
import apiEndpoints from '@/lib/generated/api-endpoints.json';
7-
8-
interface CustomDocument extends DocumentData {
9-
url: string;
10-
title: string;
11-
description: string;
12-
content: string;
4+
import { liteClient } from 'algoliasearch/lite';
5+
6+
const algolia = liteClient(
7+
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
8+
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!,
9+
);
10+
11+
const STOP_WORDS = new Set([
12+
'a','an','the','is','are','was','were','be','been','being','have','has','had',
13+
'do','does','did','will','would','could','should','may','might','shall','can',
14+
'i','you','he','she','it','we','they','what','which','who','how','when','where',
15+
'why','and','or','but','not','in','on','at','to','for','of','with','by','from',
16+
'this','that','these','those','my','your','his','her','its','our','their',
17+
]);
18+
19+
function buildSearchQuery(text: string): string {
20+
return text
21+
.toLowerCase()
22+
.replace(/[?!.,;:]/g, '')
23+
.split(/\s+/)
24+
.filter((w) => w.length > 1 && !STOP_WORDS.has(w))
25+
.join(' ');
1326
}
1427

15-
const searchServer = createSearchServer();
16-
17-
async function createSearchServer() {
18-
const search = new Document<CustomDocument>({
19-
document: {
20-
id: 'url',
21-
index: ['title', 'description', 'content'],
22-
store: true,
23-
},
28+
async function runSearch(query: string, limit = 8) {
29+
const searchQuery = buildSearchQuery(query);
30+
const { results } = await algolia.search({
31+
requests: [
32+
{
33+
indexName: process.env.NEXT_PUBLIC_ALGOLIA_INDEX!,
34+
query: searchQuery,
35+
hitsPerPage: limit,
36+
},
37+
],
2438
});
2539

26-
const docs = await chunkedAll(
27-
source.getPages().map(async (page) => {
28-
if (!('getText' in page.data)) return null;
29-
30-
return {
31-
title: page.data.title,
32-
description: page.data.description,
33-
url: page.url,
34-
content: await page.data.getText('processed'),
35-
} as CustomDocument;
36-
}),
37-
);
38-
39-
for (const doc of docs) {
40-
if (doc) search.add(doc);
41-
}
42-
43-
for (const ep of apiEndpoints) {
44-
search.add(ep as CustomDocument);
45-
}
46-
47-
return search;
48-
}
49-
50-
async function chunkedAll<O>(promises: Promise<O>[]): Promise<O[]> {
51-
const SIZE = 50;
52-
const out: O[] = [];
53-
for (let i = 0; i < promises.length; i += SIZE) {
54-
out.push(...(await Promise.all(promises.slice(i, i + SIZE))));
55-
}
56-
return out;
57-
}
58-
59-
async function runSearch(query: string, limit = 8): Promise<CustomDocument[]> {
60-
const search = await searchServer;
61-
const results = await search.searchAsync(query, { limit, merge: true, enrich: true });
62-
return (results as any[])
63-
.flatMap((r) => r.result ?? [])
64-
.map((d) => ({
65-
...d.doc,
66-
content: d.doc?.content?.slice(0, 1200) ?? '',
67-
}))
68-
.filter((d) => d.url);
40+
return ((results[0] as any).hits ?? []).map((hit: any) => ({
41+
url: hit.url ?? hit.objectID,
42+
title: hit.title ?? '',
43+
section: hit.section ?? '',
44+
content: hit.content ?? '',
45+
}));
6946
}
7047

7148
const openrouter = createOpenRouter({
@@ -86,7 +63,6 @@ export async function POST(req: Request) {
8663
const reqJson: { messages?: UIMessage[] } = await req.json();
8764
const messages = reqJson.messages ?? [];
8865

89-
// Extract latest user question and search server-side — don't rely on model to call tools
9066
const lastUserText = messages
9167
.filter((m) => m.role === 'user')
9268
.at(-1)
@@ -99,11 +75,14 @@ export async function POST(req: Request) {
9975
const contextBlock =
10076
docs.length > 0
10177
? 'Relevant documentation:\n\n' +
102-
docs.map((d) => `### [${d.title}](${d.url})\n${d.description ? d.description + '\n' : ''}${d.content}`).join('\n\n---\n\n')
103-
: 'No relevant documentation found.';
78+
docs
79+
.map((d: any) => `### [${d.title}${d.section ? ` — ${d.section}` : ''}](${d.url})\n${d.content}`)
80+
.join('\n\n---\n\n')
81+
: 'No relevant documentation found for this query.';
10482

10583
const result = streamText({
10684
model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'),
85+
maxOutputTokens: 1024,
10786
messages: [
10887
{ role: 'system', content: `${systemPrompt}\n\n${contextBlock}` },
10988
...(await convertToModelMessages(messages)),
@@ -113,15 +92,12 @@ export async function POST(req: Request) {
11392
return result.toUIMessageStreamResponse();
11493
}
11594

116-
// Keep tool definition so the UI type import still works
95+
// Kept for UI type compatibility
11796
const searchTool = tool({
118-
description: 'Search the docs content and return raw JSON results.',
119-
inputSchema: z.object({
120-
query: z.string(),
121-
limit: z.number().int().min(1).max(20).default(8),
122-
}),
123-
async execute({ query, limit }) {
124-
return runSearch(query, limit);
97+
description: 'Search the docs.',
98+
inputSchema: z.object({ query: z.string() }),
99+
async execute({ query }) {
100+
return runSearch(query);
125101
},
126102
});
127103

components/ai/search.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
'use client';
2-
import { usePathname } from 'next/navigation';
32
import {
43
type ComponentProps,
54
createContext,
@@ -292,19 +291,10 @@ function Message({ message, ...props }: { message: UIMessage } & ComponentProps<
292291

293292
export function AISearch({ children }: { children: ReactNode }) {
294293
const [open, setOpen] = useState(false);
295-
const pathname = usePathname();
296-
const pathnameRef = useRef(pathname);
297-
pathnameRef.current = pathname;
298294

299295
const chat = useChat({
300296
id: 'search',
301-
transport: new DefaultChatTransport({
302-
api: '/api/chat',
303-
prepareSendMessagesRequest: (options) => ({
304-
...options,
305-
body: { ...options.body, pageUrl: pathnameRef.current },
306-
}),
307-
}),
297+
transport: new DefaultChatTransport({ api: '/api/chat' }),
308298
});
309299

310300
return (

tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)