Skip to content

Commit 2fa9291

Browse files
feat: add chat search implementation
1 parent 9628c54 commit 2fa9291

11 files changed

+1221
-56
lines changed

app/(chat)/api/search/route.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { auth } from "@/app/(auth)/auth";
2+
import { searchChatsByUserId } from "@/lib/db/queries";
3+
import { groupChatsByDate } from "@/lib/utils";
4+
5+
function transformSearchResults(searchResults: any[], query: string) {
6+
return searchResults.map((result) => {
7+
let preview = result.preview;
8+
let contextPreview = "";
9+
10+
try {
11+
// NOTE: user messages stored in our DB are plain string & tool call results are stored as JSON.
12+
// TODO: As tool call results have different schemas in the DB, we only show no preview available for now
13+
if (result.role !== "user") {
14+
preview = "No preview available";
15+
16+
// LLM responses are stored under the "text" key
17+
if (result.role === "assistant") {
18+
const previewData = JSON.parse(result.preview);
19+
20+
if (previewData[0].text) {
21+
preview = previewData[0].text;
22+
}
23+
}
24+
}
25+
26+
// Generate a context preview with 50 characters before and after the query match
27+
if (preview !== "No preview available") {
28+
const sanitizedQuery = query.toLowerCase();
29+
const lowerPreview = preview.toLowerCase();
30+
const matchIndex = lowerPreview.indexOf(sanitizedQuery);
31+
32+
// Calculate start and end indices for the context window
33+
if (matchIndex !== -1) {
34+
const startIndex = Math.max(0, matchIndex - 50);
35+
const endIndex = Math.min(
36+
preview.length,
37+
matchIndex + sanitizedQuery.length + 50
38+
);
39+
40+
contextPreview = preview.substring(startIndex, endIndex);
41+
42+
// Add ellipsis if we're not showing from the beginning or to the end
43+
if (startIndex > 0) {
44+
contextPreview = "..." + contextPreview;
45+
}
46+
if (endIndex < preview.length) {
47+
contextPreview += "...";
48+
}
49+
preview = contextPreview;
50+
} else {
51+
// If for some reason the query isn't found in the preview, fallback to showing the first part
52+
preview =
53+
preview?.length > 100 ? preview?.slice(0, 100) + "..." : preview;
54+
}
55+
}
56+
} catch (e: any) {
57+
preview = "No preview available";
58+
}
59+
60+
return {
61+
id: result.id,
62+
title: result.title || "Untitled",
63+
// TODO: Strip any markdown formatting from the preview
64+
preview,
65+
createdAt: new Date(result.createdAt),
66+
role: result.role,
67+
userId: result.userId,
68+
visibility: result.visibility,
69+
};
70+
});
71+
}
72+
73+
export async function GET(request: Request) {
74+
const session = await auth();
75+
76+
if (!session || !session.user || !session.user.id) {
77+
return Response.json("Unauthorized!", { status: 401 });
78+
}
79+
80+
const { searchParams } = new URL(request.url);
81+
const query = searchParams.get("q")?.trim?.();
82+
83+
if (!query) {
84+
return Response.json(
85+
{ error: "Search query is required" },
86+
{ status: 400 }
87+
);
88+
}
89+
90+
const searchResults = await searchChatsByUserId({
91+
userId: session.user.id,
92+
query,
93+
});
94+
95+
const transformedResults = transformSearchResults(searchResults, query);
96+
const groupedResults = groupChatsByDate(transformedResults);
97+
98+
return Response.json(groupedResults);
99+
}

components/chat-header.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@ import { useSidebar } from './ui/sidebar';
1212
import { memo } from 'react';
1313
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
1414
import { VisibilityType, VisibilitySelector } from './visibility-selector';
15+
import { Search } from 'lucide-react';
1516

1617
function PureChatHeader({
1718
chatId,
1819
selectedModelId,
1920
selectedVisibilityType,
2021
isReadonly,
22+
setIsSearchOpen,
2123
}: {
2224
chatId: string;
2325
selectedModelId: string;
2426
selectedVisibilityType: VisibilityType;
2527
isReadonly: boolean;
28+
setIsSearchOpen: (open: boolean) => void;
2629
}) {
2730
const router = useRouter();
2831
const { open } = useSidebar();
@@ -43,6 +46,7 @@ function PureChatHeader({
4346
router.push('/');
4447
router.refresh();
4548
}}
49+
aria-label="New Chat"
4650
>
4751
<PlusIcon />
4852
<span className="md:sr-only">New Chat</span>
@@ -52,6 +56,24 @@ function PureChatHeader({
5256
</Tooltip>
5357
)}
5458

59+
<Tooltip>
60+
<TooltipTrigger asChild>
61+
<Button
62+
variant="outline"
63+
className="order-1 md:px-2 md:h-fit md:ml-0"
64+
onClick={() => setIsSearchOpen(true)}
65+
aria-label="Search"
66+
>
67+
<Search />
68+
</Button>
69+
</TooltipTrigger>
70+
<TooltipContent>
71+
Search (
72+
{navigator?.userAgent?.toLowerCase().includes("mac") ? "⌘" : "Ctrl"} +
73+
K)
74+
</TooltipContent>
75+
</Tooltip>
76+
5577
{!isReadonly && (
5678
<ModelSelector
5779
selectedModelId={selectedModelId}

0 commit comments

Comments
 (0)