Refactor to use hooks (#436)
This commit is contained in:
parent
124efca9a1
commit
cb60f8b143
139 changed files with 8871 additions and 8726 deletions
94
app/(chat)/api/chat/route.ts
Normal file
94
app/(chat)/api/chat/route.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { convertToCoreMessages, Message, streamText } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
import { customModel } from "@/ai";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { deleteChatById, getChatById, saveChat } from "@/db/queries";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { id, messages }: { id: string; messages: Array<Message> } =
|
||||
await request.json();
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const coreMessages = convertToCoreMessages(messages);
|
||||
|
||||
const result = await streamText({
|
||||
model: customModel,
|
||||
system:
|
||||
"you are a friendly assistant! keep your responses concise and helpful.",
|
||||
messages: coreMessages,
|
||||
maxSteps: 5,
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get the current weather at a location",
|
||||
parameters: z.object({
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
}),
|
||||
execute: async ({ latitude, longitude }) => {
|
||||
const response = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`,
|
||||
);
|
||||
|
||||
const weatherData = await response.json();
|
||||
return weatherData;
|
||||
},
|
||||
},
|
||||
},
|
||||
onFinish: async ({ responseMessages }) => {
|
||||
if (session.user && session.user.id) {
|
||||
try {
|
||||
await saveChat({
|
||||
id,
|
||||
messages: [...coreMessages, ...responseMessages],
|
||||
userId: session.user.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to save chat");
|
||||
}
|
||||
}
|
||||
},
|
||||
experimental_telemetry: {
|
||||
isEnabled: true,
|
||||
functionId: "stream-text",
|
||||
},
|
||||
});
|
||||
|
||||
return result.toDataStreamResponse({});
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session || !session.user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const chat = await getChatById({ id });
|
||||
|
||||
if (chat.userId !== session.user.id) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
await deleteChatById({ id });
|
||||
|
||||
return new Response("Chat deleted", { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response("An error occurred while processing your request", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
69
app/(chat)/api/files/upload/route.ts
Normal file
69
app/(chat)/api/files/upload/route.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { put } from "@vercel/blob";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
|
||||
const FileSchema = z.object({
|
||||
file: z
|
||||
.instanceof(File)
|
||||
.refine((file) => file.size <= 5 * 1024 * 1024, {
|
||||
message: "File size should be less than 5MB",
|
||||
})
|
||||
.refine(
|
||||
(file) =>
|
||||
["image/jpeg", "image/png", "application/pdf"].includes(file.type),
|
||||
{
|
||||
message: "File type should be JPEG, PNG, or PDF",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (request.body === null) {
|
||||
return new Response("Request body is empty", { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validatedFile = FileSchema.safeParse({ file });
|
||||
|
||||
if (!validatedFile.success) {
|
||||
const errorMessage = validatedFile.error.errors
|
||||
.map((error) => error.message)
|
||||
.join(", ");
|
||||
|
||||
return NextResponse.json({ error: errorMessage }, { status: 400 });
|
||||
}
|
||||
|
||||
const filename = file.name;
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
try {
|
||||
const data = await put(`${filename}`, fileBuffer, {
|
||||
access: "public",
|
||||
});
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to process request" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
13
app/(chat)/api/history/route.ts
Normal file
13
app/(chat)/api/history/route.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getChatsByUserId } from "@/db/queries";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session || !session.user) {
|
||||
return Response.json("Unauthorized!", { status: 401 });
|
||||
}
|
||||
|
||||
const chats = await getChatsByUserId({ id: session.user.id! });
|
||||
return Response.json(chats);
|
||||
}
|
||||
|
|
@ -1,65 +1,108 @@
|
|||
import { type Metadata } from 'next'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import { CoreMessage, CoreToolMessage, Message, ToolInvocation } from "ai";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { auth } from '@/auth'
|
||||
import { getChat, getMissingKeys } from '@/app/actions'
|
||||
import { Chat } from '@/components/chat'
|
||||
import { AI } from '@/lib/chat/actions'
|
||||
import { Session } from '@/lib/types'
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { Chat as PreviewChat } from "@/components/custom/chat";
|
||||
import { getChatById } from "@/db/queries";
|
||||
import { Chat } from "@/db/schema";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
|
||||
export interface ChatPageProps {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
function addToolMessageToChat({
|
||||
toolMessage,
|
||||
messages,
|
||||
}: {
|
||||
toolMessage: CoreToolMessage;
|
||||
messages: Array<Message>;
|
||||
}): Array<Message> {
|
||||
return messages.map((message) => {
|
||||
if (message.toolInvocations) {
|
||||
return {
|
||||
...message,
|
||||
toolInvocations: message.toolInvocations.map((toolInvocation) => {
|
||||
const toolResult = toolMessage.content.find(
|
||||
(tool) => tool.toolCallId === toolInvocation.toolCallId,
|
||||
);
|
||||
|
||||
export async function generateMetadata({
|
||||
params
|
||||
}: ChatPageProps): Promise<Metadata> {
|
||||
const session = await auth()
|
||||
if (toolResult) {
|
||||
return {
|
||||
...toolInvocation,
|
||||
state: "result",
|
||||
result: toolResult.result,
|
||||
};
|
||||
}
|
||||
|
||||
if (!session?.user) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const chat = await getChat(params.id, session.user.id)
|
||||
|
||||
if (!chat || 'error' in chat) {
|
||||
redirect('/')
|
||||
} else {
|
||||
return {
|
||||
title: chat?.title.toString().slice(0, 50) ?? 'Chat'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ChatPage({ params }: ChatPageProps) {
|
||||
const session = (await auth()) as Session
|
||||
const missingKeys = await getMissingKeys()
|
||||
|
||||
if (!session?.user) {
|
||||
redirect(`/login?next=/chat/${params.id}`)
|
||||
}
|
||||
|
||||
const userId = session.user.id as string
|
||||
const chat = await getChat(params.id, userId)
|
||||
|
||||
if (!chat || 'error' in chat) {
|
||||
redirect('/')
|
||||
} else {
|
||||
if (chat?.userId !== session?.user?.id) {
|
||||
notFound()
|
||||
return toolInvocation;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<AI initialAIState={{ chatId: chat.id, messages: chat.messages }}>
|
||||
<Chat
|
||||
id={chat.id}
|
||||
session={session}
|
||||
initialMessages={chat.messages}
|
||||
missingKeys={missingKeys}
|
||||
/>
|
||||
</AI>
|
||||
)
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
function convertToUIMessages(messages: Array<CoreMessage>): Array<Message> {
|
||||
return messages.reduce((chatMessages: Array<Message>, message) => {
|
||||
if (message.role === "tool") {
|
||||
return addToolMessageToChat({
|
||||
toolMessage: message as CoreToolMessage,
|
||||
messages: chatMessages,
|
||||
});
|
||||
}
|
||||
|
||||
let textContent = "";
|
||||
let toolInvocations: Array<ToolInvocation> = [];
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
textContent = message.content;
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const content of message.content) {
|
||||
if (content.type === "text") {
|
||||
textContent += content.text;
|
||||
} else if (content.type === "tool-call") {
|
||||
toolInvocations.push({
|
||||
state: "call",
|
||||
toolCallId: content.toolCallId,
|
||||
toolName: content.toolName,
|
||||
args: content.args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMessages.push({
|
||||
id: generateUUID(),
|
||||
role: message.role,
|
||||
content: textContent,
|
||||
toolInvocations,
|
||||
});
|
||||
|
||||
return chatMessages;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: any }) {
|
||||
const { id } = params;
|
||||
const chatFromDb = await getChatById({ id });
|
||||
|
||||
if (!chatFromDb) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// type casting
|
||||
const chat: Chat = {
|
||||
...chatFromDb,
|
||||
messages: convertToUIMessages(chatFromDb.messages as Array<CoreMessage>),
|
||||
};
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session || !session.user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (session.user.id !== chat.userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <PreviewChat id={chat.id} initialMessages={chat.messages} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { SidebarDesktop } from '@/components/sidebar-desktop'
|
||||
|
||||
interface ChatLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default async function ChatLayout({ children }: ChatLayoutProps) {
|
||||
return (
|
||||
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
||||
<SidebarDesktop />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
app/(chat)/opengraph-image.png
Normal file
BIN
app/(chat)/opengraph-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
|
|
@ -1,22 +1,7 @@
|
|||
import { nanoid } from '@/lib/utils'
|
||||
import { Chat } from '@/components/chat'
|
||||
import { AI } from '@/lib/chat/actions'
|
||||
import { auth } from '@/auth'
|
||||
import { Session } from '@/lib/types'
|
||||
import { getMissingKeys } from '@/app/actions'
|
||||
import { Chat } from "@/components/custom/chat";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next.js AI Chatbot'
|
||||
}
|
||||
|
||||
export default async function IndexPage() {
|
||||
const id = nanoid()
|
||||
const session = (await auth()) as Session
|
||||
const missingKeys = await getMissingKeys()
|
||||
|
||||
return (
|
||||
<AI initialAIState={{ chatId: id, messages: [] }}>
|
||||
<Chat id={id} session={session} missingKeys={missingKeys} />
|
||||
</AI>
|
||||
)
|
||||
export default async function Page() {
|
||||
const id = generateUUID();
|
||||
return <Chat key={id} id={id} initialMessages={[]} />;
|
||||
}
|
||||
|
|
|
|||
BIN
app/(chat)/twitter-image.png
Normal file
BIN
app/(chat)/twitter-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Loading…
Add table
Add a link
Reference in a new issue