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 ai/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateObject } from "ai";
import { z } from "zod";
import { z } from 'zod/v3';

import { geminiFlashModel } from ".";

Expand Down
4 changes: 2 additions & 2 deletions ai/custom-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Experimental_LanguageModelV1Middleware } from "ai";
import { LanguageModelV2Middleware } from "@ai-sdk/provider";

export const customMiddleware: Experimental_LanguageModelV1Middleware = {};
export const customMiddleware: LanguageModelV2Middleware = {};
2 changes: 1 addition & 1 deletion ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { google } from "@ai-sdk/google";
import { experimental_wrapLanguageModel as wrapLanguageModel } from "ai";
import { wrapLanguageModel as wrapLanguageModel } from "ai";

import { customMiddleware } from "./custom-middleware";

Expand Down
2 changes: 1 addition & 1 deletion app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";

import { z } from "zod";
import { z } from 'zod/v3';

import { createUser, getUser } from "@/db/queries";

Expand Down
54 changes: 32 additions & 22 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { convertToCoreMessages, Message, streamText } from "ai";
import { z } from "zod";
import { convertToModelMessages, UIMessage, streamText, generateId } from "ai";
import { z } from 'zod/v3';

import { geminiProModel } from "@/ai";
import {
Expand All @@ -16,19 +16,21 @@ import {
getReservationById,
saveChat,
} from "@/db/queries";
import { convertV5MessageToV4 } from "@/lib/convert-messages";
import { generateUUID } from "@/lib/utils";

export async function POST(request: Request) {
const { id, messages }: { id: string; messages: Array<Message> } =
const { id, messages }: { id: string; messages: Array<UIMessage> } =
await request.json();

const session = await auth();

const coreMessages = convertToCoreMessages(messages).filter(
(message) => message.content.length > 0,
// In v5, content is an array of parts, so we filter based on parts length
const coreMessages = convertToModelMessages(messages).filter(
(message) => Array.isArray(message.content) && message.content.length > 0,
);

const result = await streamText({
const result = streamText({
model: geminiProModel,
system: `\n
- you help users book flights!
Expand All @@ -53,7 +55,7 @@ export async function POST(request: Request) {
tools: {
getWeather: {
description: "Get the current weather at a location",
parameters: z.object({
inputSchema: z.object({
latitude: z.number().describe("Latitude coordinate"),
longitude: z.number().describe("Longitude coordinate"),
}),
Expand All @@ -68,7 +70,7 @@ export async function POST(request: Request) {
},
displayFlightStatus: {
description: "Display the status of a flight",
parameters: z.object({
inputSchema: z.object({
flightNumber: z.string().describe("Flight number"),
date: z.string().describe("Date of the flight"),
}),
Expand All @@ -83,7 +85,7 @@ export async function POST(request: Request) {
},
searchFlights: {
description: "Search for flights based on the given parameters",
parameters: z.object({
inputSchema: z.object({
origin: z.string().describe("Origin airport or city"),
destination: z.string().describe("Destination airport or city"),
}),
Expand All @@ -92,13 +94,14 @@ export async function POST(request: Request) {
origin,
destination,
});
console.log(results);

return results;
},
},
selectSeats: {
description: "Select seats for a flight",
parameters: z.object({
inputSchema: z.object({
flightNumber: z.string().describe("Flight number"),
}),
execute: async ({ flightNumber }) => {
Expand All @@ -108,7 +111,7 @@ export async function POST(request: Request) {
},
createReservation: {
description: "Display pending reservation details",
parameters: z.object({
inputSchema: z.object({
seats: z.string().array().describe("Array of selected seat numbers"),
flightNumber: z.string().describe("Flight number"),
departure: z.object({
Expand Down Expand Up @@ -151,7 +154,7 @@ export async function POST(request: Request) {
authorizePayment: {
description:
"User will enter credentials to authorize payment, wait for user to repond when they are done",
parameters: z.object({
inputSchema: z.object({
reservationId: z
.string()
.describe("Unique identifier for the reservation"),
Expand All @@ -162,7 +165,7 @@ export async function POST(request: Request) {
},
verifyPayment: {
description: "Verify payment status",
parameters: z.object({
inputSchema: z.object({
reservationId: z
.string()
.describe("Unique identifier for the reservation"),
Expand All @@ -179,7 +182,7 @@ export async function POST(request: Request) {
},
displayBoardingPass: {
description: "Display a boarding pass",
parameters: z.object({
inputSchema: z.object({
reservationId: z
.string()
.describe("Unique identifier for the reservation"),
Expand Down Expand Up @@ -210,26 +213,33 @@ export async function POST(request: Request) {
},
},
},
onFinish: async ({ responseMessages }) => {
experimental_telemetry: {
isEnabled: true,
functionId: "stream-text",
},
});

return result.toUIMessageStreamResponse({
originalMessages: messages,
generateMessageId: () => generateId(),
onFinish: async ({ messages: allMessages }) => {
if (session && session.user && session.user.id) {
try {
// Convert v5 messages to v4 format for database storage
const v4Messages = allMessages.map((msg: any) => convertV5MessageToV4(msg));

// Save v4 messages directly (they're already in the right format for database)
await saveChat({
id,
messages: [...coreMessages, ...responseMessages],
messages: v4Messages as any,
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) {
Expand Down
2 changes: 1 addition & 1 deletion app/(chat)/api/files/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
import { z } from "zod";
import { z } from 'zod/v3';

// import { auth } from "@/app/(auth)/auth";

Expand Down
7 changes: 3 additions & 4 deletions app/(chat)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { CoreMessage } from "ai";
import { notFound } from "next/navigation";

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 { convertToUIMessages } from "@/lib/utils";
import { convertV4MessageToV5 } from "@/lib/convert-messages";

export default async function Page({ params }: { params: any }) {
const { id } = params;
Expand All @@ -15,10 +14,10 @@ export default async function Page({ params }: { params: any }) {
notFound();
}

// type casting and converting messages to UI messages
// Convert v4 messages from database to v5 format for application use
const chat: Chat = {
...chatFromDb,
messages: convertToUIMessages(chatFromDb.messages as Array<CoreMessage>),
messages: (chatFromDb.messages as any[]).map((msg: any, index: number) => convertV4MessageToV5(msg, index)),
};

const session = await auth();
Expand Down
90 changes: 71 additions & 19 deletions components/custom/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client";

import { Attachment, Message } from "ai";
import { useChat } from "ai/react";
import { useChat } from '@ai-sdk/react';
import { UIMessage, DefaultChatTransport } from "ai";
import { User } from "next-auth";
import { useState } from "react";
import { toast } from "sonner";

import { Message as PreviewMessage } from "@/components/custom/message";
import { useScrollToBottom } from "@/components/custom/use-scroll-to-bottom";
import { Attachment } from "@/lib/types";

import { MultimodalInput } from "./multimodal-input";
import { Overview } from "./overview";
Expand All @@ -18,26 +19,48 @@ export function Chat({
user,
}: {
id: string;
initialMessages: Array<Message>;
initialMessages: Array<UIMessage>;
user: User | undefined;
}) {
const { messages, handleSubmit, input, setInput, append, isLoading, stop } =
const [input, setInput] = useState('');
const {
messages,
sendMessage,
status,
stop
} =
useChat({
id,
body: { id },
initialMessages,
maxSteps: 10,
transport: new DefaultChatTransport({
api: "/api/chat",
body: { id },
}),
messages: initialMessages,
onFinish: () => {
if (user) {
window.history.replaceState({}, "", `/chat/${id}`);
}
},
onError: (error) => {
onError: (error: Error) => {
if (error.message === "Too many requests") {
toast.error("Too many requests. Please try again later!");
}
},
});

const isLoading = status === "streaming";

const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
};

const append = async (message: UIMessage): Promise<string | null | undefined> => {
await sendMessage(message);
return null;
};

const [messagesContainerRef, messagesEndRef] =
useScrollToBottom<HTMLDivElement>();
Expand All @@ -53,16 +76,45 @@ export function Chat({
>
{messages.length === 0 && <Overview />}

{messages.map((message) => (
<PreviewMessage
key={message.id}
chatId={id}
role={message.role}
content={message.content}
attachments={message.experimental_attachments}
toolInvocations={message.toolInvocations}
/>
))}
{messages.map((message: UIMessage) => {
// Extract text content from parts
const textContent = message.parts
?.filter((part: any) => part.type === "text")
.map((part: any) => part.text)
.join("");

// Extract file attachments from parts
const fileAttachments = message.parts
?.filter((part: any) => part.type === "file")
.map((part: any) => ({
url: part.url,
name: part.name || "",
contentType: part.mediaType || "",
})) || [];

// Extract tool invocations from parts (v5 format) - cast to any for compatibility
const toolInvocations: any[] = message.parts
?.filter((part: any) => part.type?.startsWith("tool-"))
.map((part: any) => ({
...part,
// Add v4 compatibility properties for Message component
state: part.state === "output-available" ? "result" : part.state,
toolName: part.toolName || part.type?.replace("tool-", ""),
args: part.input,
result: part.output,
})) || [];

return (
<PreviewMessage
key={message.id}
chatId={id}
role={message.role}
content={textContent || ""}
attachments={fileAttachments}
toolInvocations={toolInvocations}
/>
);
})}

<div
ref={messagesEndRef}
Expand Down
6 changes: 4 additions & 2 deletions components/custom/message.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"use client";

import { Attachment, ToolInvocation } from "ai";
import { UIToolInvocation } from "ai";
import { motion } from "framer-motion";
import { ReactNode } from "react";

import { Attachment } from "@/lib/types";

import { BotIcon, InfoIcon, UserIcon } from "./icons";
import { Markdown } from "./markdown";
import { PreviewAttachment } from "./preview-attachment";
Expand All @@ -26,7 +28,7 @@ export const Message = ({
chatId: string;
role: string;
content: string | ReactNode;
toolInvocations: Array<ToolInvocation> | undefined;
toolInvocations: Array<any> | undefined;
attachments?: Array<Attachment>;
}) => {
return (
Expand Down
Loading