Skip to content

Commit f946351

Browse files
Merge remote-tracking branch 'origin/main' into feat/mcp
# Conflicts: # lib/db/queries.ts # pnpm-lock.yaml
2 parents ab4990b + ccbc649 commit f946351

File tree

13 files changed

+1226
-954
lines changed

13 files changed

+1226
-954
lines changed

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
import { auth } from '@/app/(auth)/auth';
2+
import { NextRequest } from 'next/server';
23
import { getChatsByUserId } from '@/lib/db/queries';
34

4-
export async function GET() {
5+
export async function GET(request: NextRequest) {
6+
const { searchParams } = request.nextUrl;
7+
8+
const limit = parseInt(searchParams.get('limit') || '10');
9+
const startingAfter = searchParams.get('starting_after');
10+
const endingBefore = searchParams.get('ending_before');
11+
12+
if (startingAfter && endingBefore) {
13+
return Response.json(
14+
'Only one of starting_after or ending_before can be provided!',
15+
{ status: 400 },
16+
);
17+
}
18+
519
const session = await auth();
620

7-
if (!session || !session.user) {
21+
if (!session?.user?.id) {
822
return Response.json('Unauthorized!', { status: 401 });
923
}
1024

11-
// biome-ignore lint: Forbidden non-null assertion.
12-
const chats = await getChatsByUserId({ id: session.user.id! });
13-
return Response.json(chats);
25+
try {
26+
const chats = await getChatsByUserId({
27+
id: session.user.id,
28+
limit,
29+
startingAfter,
30+
endingBefore,
31+
});
32+
33+
return Response.json(chats);
34+
} catch (_) {
35+
return Response.json('Failed to fetch chats!', { status: 500 });
36+
}
1437
}

components/chat-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { PlusIcon, VercelIcon } from './icons';
1111
import { useSidebar } from './ui/sidebar';
1212
import { memo } from 'react';
1313
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
14-
import { VisibilityType, VisibilitySelector } from './visibility-selector';
14+
import { type VisibilityType, VisibilitySelector } from './visibility-selector';
1515

1616
function PureChatHeader({
1717
chatId,

components/chat.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { fetcher, generateUUID } from '@/lib/utils';
1010
import { Artifact } from './artifact';
1111
import { MultimodalInput } from './multimodal-input';
1212
import { Messages } from './messages';
13-
import { VisibilityType } from './visibility-selector';
13+
import type { VisibilityType } from './visibility-selector';
1414
import { useArtifactSelector } from '@/hooks/use-artifact';
1515
import { toast } from 'sonner';
16+
import { unstable_serialize } from 'swr/infinite';
17+
import { getChatHistoryPaginationKey } from './sidebar-history';
1618

1719
export function Chat({
1820
id,
@@ -47,7 +49,7 @@ export function Chat({
4749
sendExtraMessageFields: true,
4850
generateId: generateUUID,
4951
onFinish: () => {
50-
mutate('/api/history');
52+
mutate(unstable_serialize(getChatHistoryPaginationKey));
5153
},
5254
onError: () => {
5355
toast.error('An error occured, please try again!');

components/greeting.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { motion } from 'framer-motion';
2+
3+
export const Greeting = () => {
4+
return (
5+
<div
6+
key="overview"
7+
className="max-w-3xl mx-auto md:mt-20 px-8 size-full flex flex-col justify-center"
8+
>
9+
<motion.div
10+
initial={{ opacity: 0, y: 10 }}
11+
animate={{ opacity: 1, y: 0 }}
12+
exit={{ opacity: 0, y: 10 }}
13+
transition={{ delay: 0.5 }}
14+
className="text-2xl font-semibold"
15+
>
16+
Hello there!
17+
</motion.div>
18+
<motion.div
19+
initial={{ opacity: 0, y: 10 }}
20+
animate={{ opacity: 1, y: 0 }}
21+
exit={{ opacity: 0, y: 10 }}
22+
transition={{ delay: 0.6 }}
23+
className="text-2xl text-zinc-500"
24+
>
25+
How can I help you today?
26+
</motion.div>
27+
</div>
28+
);
29+
};

components/messages.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { UIMessage } from 'ai';
1+
import type { UIMessage } from 'ai';
22
import { PreviewMessage, ThinkingMessage } from './message';
33
import { useScrollToBottom } from './use-scroll-to-bottom';
4-
import { Overview } from './overview';
4+
import { Greeting } from './greeting';
55
import { memo } from 'react';
6-
import { Vote } from '@/lib/db/schema';
6+
import type { Vote } from '@/lib/db/schema';
77
import equal from 'fast-deep-equal';
8-
import { UseChatHelpers } from '@ai-sdk/react';
8+
import type { UseChatHelpers } from '@ai-sdk/react';
99

1010
interface MessagesProps {
1111
chatId: string;
@@ -35,7 +35,7 @@ function PureMessages({
3535
ref={messagesContainerRef}
3636
className="flex flex-col min-w-0 gap-6 flex-1 overflow-y-scroll pt-4"
3737
>
38-
{messages.length === 0 && <Overview />}
38+
{messages.length === 0 && <Greeting />}
3939

4040
{messages.map((message, index) => (
4141
<PreviewMessage

components/multimodal-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import type { Attachment, Message, UIMessage } from 'ai';
3+
import type { Attachment, UIMessage } from 'ai';
44
import cx from 'classnames';
55
import type React from 'react';
66
import {
@@ -22,7 +22,7 @@ import { Button } from './ui/button';
2222
import { Textarea } from './ui/textarea';
2323
import { SuggestedActions } from './suggested-actions';
2424
import equal from 'fast-deep-equal';
25-
import { UseChatHelpers } from '@ai-sdk/react';
25+
import type { UseChatHelpers } from '@ai-sdk/react';
2626

2727
function PureMultimodalInput({
2828
chatId,

components/overview.tsx

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Chat } from '@/lib/db/schema';
2+
import {
3+
SidebarMenuAction,
4+
SidebarMenuButton,
5+
SidebarMenuItem,
6+
} from './ui/sidebar';
7+
import Link from 'next/link';
8+
import {
9+
DropdownMenu,
10+
DropdownMenuContent,
11+
DropdownMenuItem,
12+
DropdownMenuPortal,
13+
DropdownMenuSub,
14+
DropdownMenuSubContent,
15+
DropdownMenuSubTrigger,
16+
DropdownMenuTrigger,
17+
} from './ui/dropdown-menu';
18+
import {
19+
CheckCircleFillIcon,
20+
GlobeIcon,
21+
LockIcon,
22+
MoreHorizontalIcon,
23+
ShareIcon,
24+
TrashIcon,
25+
} from './icons';
26+
import { memo } from 'react';
27+
import { useChatVisibility } from '@/hooks/use-chat-visibility';
28+
29+
const PureChatItem = ({
30+
chat,
31+
isActive,
32+
onDelete,
33+
setOpenMobile,
34+
}: {
35+
chat: Chat;
36+
isActive: boolean;
37+
onDelete: (chatId: string) => void;
38+
setOpenMobile: (open: boolean) => void;
39+
}) => {
40+
const { visibilityType, setVisibilityType } = useChatVisibility({
41+
chatId: chat.id,
42+
initialVisibility: chat.visibility,
43+
});
44+
45+
return (
46+
<SidebarMenuItem>
47+
<SidebarMenuButton asChild isActive={isActive}>
48+
<Link href={`/chat/${chat.id}`} onClick={() => setOpenMobile(false)}>
49+
<span>{chat.title}</span>
50+
</Link>
51+
</SidebarMenuButton>
52+
53+
<DropdownMenu modal={true}>
54+
<DropdownMenuTrigger asChild>
55+
<SidebarMenuAction
56+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground mr-0.5"
57+
showOnHover={!isActive}
58+
>
59+
<MoreHorizontalIcon />
60+
<span className="sr-only">More</span>
61+
</SidebarMenuAction>
62+
</DropdownMenuTrigger>
63+
64+
<DropdownMenuContent side="bottom" align="end">
65+
<DropdownMenuSub>
66+
<DropdownMenuSubTrigger className="cursor-pointer">
67+
<ShareIcon />
68+
<span>Share</span>
69+
</DropdownMenuSubTrigger>
70+
<DropdownMenuPortal>
71+
<DropdownMenuSubContent>
72+
<DropdownMenuItem
73+
className="cursor-pointer flex-row justify-between"
74+
onClick={() => {
75+
setVisibilityType('private');
76+
}}
77+
>
78+
<div className="flex flex-row gap-2 items-center">
79+
<LockIcon size={12} />
80+
<span>Private</span>
81+
</div>
82+
{visibilityType === 'private' ? (
83+
<CheckCircleFillIcon />
84+
) : null}
85+
</DropdownMenuItem>
86+
<DropdownMenuItem
87+
className="cursor-pointer flex-row justify-between"
88+
onClick={() => {
89+
setVisibilityType('public');
90+
}}
91+
>
92+
<div className="flex flex-row gap-2 items-center">
93+
<GlobeIcon />
94+
<span>Public</span>
95+
</div>
96+
{visibilityType === 'public' ? <CheckCircleFillIcon /> : null}
97+
</DropdownMenuItem>
98+
</DropdownMenuSubContent>
99+
</DropdownMenuPortal>
100+
</DropdownMenuSub>
101+
102+
<DropdownMenuItem
103+
className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive dark:text-red-500"
104+
onSelect={() => onDelete(chat.id)}
105+
>
106+
<TrashIcon />
107+
<span>Delete</span>
108+
</DropdownMenuItem>
109+
</DropdownMenuContent>
110+
</DropdownMenu>
111+
</SidebarMenuItem>
112+
);
113+
};
114+
115+
export const ChatItem = memo(PureChatItem, (prevProps, nextProps) => {
116+
if (prevProps.isActive !== nextProps.isActive) return false;
117+
return true;
118+
});

0 commit comments

Comments
 (0)