Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(chat)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { generateText, type UIMessage } from "ai";
import { cookies } from "next/headers";
import type { VisibilityType } from "@/components/visibility-selector";
import { myProvider } from "@/lib/ai/providers";
import { titlePrompt } from "@/lib/ai/prompts";
import { myProvider } from "@/lib/ai/providers";
import {
deleteMessagesByChatIdAfterTimestamp,
getMessageById,
Expand Down
71 changes: 71 additions & 0 deletions app/(chat)/api/chat/webllm-save/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { getChatById, saveChat, saveMessages } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import { generateTitleFromUserMessage } from "../../../actions";

const webllmSaveSchema = z.object({
chatId: z.string().uuid(),
messages: z.array(
z.object({
id: z.string().uuid(),
role: z.enum(["user", "assistant"]),
parts: z.array(z.any()),
createdAt: z.string().or(z.date()).optional(),
})
),
visibility: z.enum(["public", "private"]).optional().default("private"),
});

export async function POST(request: Request) {
try {
const json = await request.json();
const { chatId, messages, visibility } = webllmSaveSchema.parse(json);

const session = await auth();

if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
}

const chat = await getChatById({ id: chatId });

if (!chat) {
const userMessage = messages.find((m) => m.role === "user");
if (userMessage) {
const title = await generateTitleFromUserMessage({
message: userMessage as any,
});

await saveChat({
id: chatId,
userId: session.user.id,
title,
visibility,
});
}
} else if (chat.userId !== session.user.id) {
return new ChatSDKError("forbidden:chat").toResponse();
}

await saveMessages({
messages: messages.map((msg) => ({
id: msg.id,
chatId,
role: msg.role,
parts: msg.parts,
attachments: [],
createdAt: msg.createdAt ? new Date(msg.createdAt) : new Date(),
})),
});

return Response.json({ success: true });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}

console.error("Error saving WebLLM messages:", error);
return new ChatSDKError("bad_request:api").toResponse();
}
}
2 changes: 1 addition & 1 deletion app/(chat)/api/history/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NextRequest } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { getChatsByUserId, deleteAllChatsByUserId } from "@/lib/db/queries";
import { deleteAllChatsByUserId, getChatsByUserId } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";

export async function GET(request: NextRequest) {
Expand Down
6 changes: 3 additions & 3 deletions app/(chat)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";

import { auth } from "@/app/(auth)/auth";
import { Chat } from "@/components/chat";
import { ChatWrapper } from "@/components/chat-wrapper";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
Expand Down Expand Up @@ -45,7 +45,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
if (!chatModelFromCookie) {
return (
<>
<Chat
<ChatWrapper
autoResume={true}
id={chat.id}
initialChatModel={DEFAULT_CHAT_MODEL}
Expand All @@ -61,7 +61,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {

return (
<>
<Chat
<ChatWrapper
autoResume={true}
id={chat.id}
initialChatModel={chatModelFromCookie.value}
Expand Down
6 changes: 3 additions & 3 deletions app/(chat)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Chat } from "@/components/chat";
import { ChatWrapper } from "@/components/chat-wrapper";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { generateUUID } from "@/lib/utils";
Expand All @@ -21,7 +21,7 @@ export default async function Page() {
if (!modelIdFromCookie) {
return (
<>
<Chat
<ChatWrapper
autoResume={false}
id={id}
initialChatModel={DEFAULT_CHAT_MODEL}
Expand All @@ -37,7 +37,7 @@ export default async function Page() {

return (
<>
<Chat
<ChatWrapper
autoResume={false}
id={id}
initialChatModel={modelIdFromCookie.value}
Expand Down
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@source '../node_modules/streamdown/dist/index.js';

/* Add KaTeX CSS for math rendering */
@import 'katex/dist/katex.min.css';
@import "katex/dist/katex.min.css";

/* custom variant for setting dark mode programmatically */
@custom-variant dark (&:is(.dark, .dark *));
Expand Down
16 changes: 11 additions & 5 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { toast } from "sonner";
import { useSWRConfig } from "swr";
import { unstable_serialize } from "swr/infinite";
import { PlusIcon, TrashIcon } from "@/components/icons";
import { SidebarHistory, getChatHistoryPaginationKey } from "@/components/sidebar-history";
import {
getChatHistoryPaginationKey,
SidebarHistory,
} from "@/components/sidebar-history";
import { SidebarUserNav } from "@/components/sidebar-user-nav";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -19,7 +22,6 @@ import {
SidebarMenu,
useSidebar,
} from "@/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import {
AlertDialog,
AlertDialogAction,
Expand All @@ -30,6 +32,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";

export function AppSidebar({ user }: { user: User | undefined }) {
const router = useRouter();
Expand Down Expand Up @@ -118,13 +121,16 @@ export function AppSidebar({ user }: { user: User | undefined }) {
<SidebarFooter>{user && <SidebarUserNav user={user} />}</SidebarFooter>
</Sidebar>

<AlertDialog onOpenChange={setShowDeleteAllDialog} open={showDeleteAllDialog}>
<AlertDialog
onOpenChange={setShowDeleteAllDialog}
open={showDeleteAllDialog}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete all chats?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all your
chats and remove them from our servers.
This action cannot be undone. This will permanently delete all
your chats and remove them from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
Expand Down
62 changes: 62 additions & 0 deletions components/chat-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { useState } from "react";
import { isWebLLMModel } from "@/lib/ai/models";
import type { ChatMessage } from "@/lib/types";
import type { AppUsage } from "@/lib/usage";
import { Chat } from "./chat";
import type { VisibilityType } from "./visibility-selector";
import { WebLLMChat } from "./webllm-chat";

export function ChatWrapper({
id,
initialMessages,
initialChatModel,
initialVisibilityType,
isReadonly,
autoResume,
initialLastContext,
}: {
id: string;
initialMessages: ChatMessage[];
initialChatModel: string;
initialVisibilityType: VisibilityType;
isReadonly: boolean;
autoResume: boolean;
initialLastContext?: AppUsage;
}) {
const [currentModelId, setCurrentModelId] = useState(initialChatModel);
const [messagesForSwitch] = useState<ChatMessage[]>(initialMessages);

const isWebLLM = isWebLLMModel(currentModelId);

const handleModelChange = (modelId: string) => {
setCurrentModelId(modelId);
};

if (isWebLLM) {
return (
<WebLLMChat
id={id}
initialChatModel={currentModelId}
initialMessages={messagesForSwitch}
initialVisibilityType={initialVisibilityType}
isReadonly={isReadonly}
onModelChange={handleModelChange}
/>
);
}

return (
<Chat
autoResume={autoResume}
id={id}
initialChatModel={currentModelId}
initialLastContext={initialLastContext}
initialMessages={messagesForSwitch}
initialVisibilityType={initialVisibilityType}
isReadonly={isReadonly}
onModelChange={handleModelChange}
/>
);
}
9 changes: 8 additions & 1 deletion components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function Chat({
isReadonly,
autoResume,
initialLastContext,
onModelChange,
}: {
id: string;
initialMessages: ChatMessage[];
Expand All @@ -49,6 +50,7 @@ export function Chat({
isReadonly: boolean;
autoResume: boolean;
initialLastContext?: AppUsage;
onModelChange?: (modelId: string) => void;
}) {
const { visibilityType } = useChatVisibility({
chatId: id,
Expand All @@ -68,6 +70,11 @@ export function Chat({
currentModelIdRef.current = currentModelId;
}, [currentModelId]);

const handleModelChange = (modelId: string) => {
setCurrentModelId(modelId);
onModelChange?.(modelId);
};

const {
messages,
setMessages,
Expand Down Expand Up @@ -182,7 +189,7 @@ export function Chat({
chatId={id}
input={input}
messages={messages}
onModelChange={setCurrentModelId}
onModelChange={handleModelChange}
selectedModelId={currentModelId}
selectedVisibilityType={visibilityType}
sendMessage={sendMessage}
Expand Down
2 changes: 1 addition & 1 deletion components/data-stream-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { artifactDefinitions } from "./artifact";
import { useDataStream } from "./data-stream-provider";

export function DataStreamHandler() {
const { dataStream,setDataStream } = useDataStream();
const { dataStream, setDataStream } = useDataStream();

const { artifact, setArtifact, setMetadata } = useArtifact();

Expand Down
2 changes: 1 addition & 1 deletion components/elements/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
className={cn(
"inline-flex select-none items-center gap-1 rounded-md text-sm",
"cursor-pointer bg-background text-foreground",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none ring-offset-background",
"outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
type="button"
Expand Down
5 changes: 1 addition & 4 deletions components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,9 @@ export const ThinkingMessage = () => {
</div>

<div className="flex w-full flex-col gap-2 md:gap-4">
<div className="p-0 text-muted-foreground text-sm">
Thinking...
</div>
<div className="p-0 text-muted-foreground text-sm">Thinking...</div>
</div>
</div>
</motion.div>
);
};

Loading