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
79 changes: 79 additions & 0 deletions client/app/api/multiple-ai/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { streamChat } from "@/lib/ai/stream";
import { prisma } from "@/lib/db";
import { ChatRequestSchema } from "@/lib/ai/schemas";

export const dynamic = "force-dynamic";

export async function POST(req: Request) {
try {
const body = await req.json();

// Validate request schema
const parseResult = ChatRequestSchema.safeParse(body);
if (!parseResult.success) {
return new Response(JSON.stringify({ error: "Invalid request payload", details: parseResult.error.format() }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

const { chatId, messages, modelId, mode } = parseResult.data;

// Call streamChat helper from client/lib/ai/stream.ts
// We pass our custom onFinish handler to persist the assistant response
const startTime = Date.now();
const result = streamChat({
chatId,
messages,
modelId,
mode,
onFinish: async ({ text, usage }: any) => {
try {
const durationMs = Date.now() - startTime;
await prisma.message.create({
data: {
role: "assistant",
content: text,
model: modelId,
mode: mode,
promptTokens: usage?.promptTokens,
completionTokens: usage?.completionTokens,
durationMs,
chatId,
},
});

// Automatically update the chat title if it's "New Chat"
const chat = await prisma.chat.findUnique({
where: { id: chatId },
select: { title: true, messages: { take: 1, orderBy: { createdAt: "asc" } } },
});

if (chat && chat.title === "New Chat") {
const firstUserMsg = chat.messages[0]?.content || text;
const generatedTitle = firstUserMsg.trim().slice(0, 40) + (firstUserMsg.length > 40 ? "..." : "");
await prisma.chat.update({
where: { id: chatId },
data: { title: generatedTitle || "New Chat" },
});
}
} catch (dbError) {
console.error("Failed to save assistant message to database:", dbError);
}
}
});

return result.toTextStreamResponse();
} catch (error) {
console.error("Stream route error:", error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "An error occurred during streaming",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}
53 changes: 53 additions & 0 deletions client/app/multiple-ai/[chatId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import { notFound, redirect } from "next/navigation";
import { getChat } from "../actions";
import { ChatContainer } from "../components/ChatContainer";
import { ChatMode } from "@/constants/modes";

interface ChatPageProps {
params: Promise<{ chatId: string }>;
}

export default async function ChatSessionPage({ params }: ChatPageProps) {
const { chatId } = await params;
const chat = await getChat(chatId);

if (!chat) {
redirect("/multiple-ai");
}

// Formatting messages for type compatibility
const initialMessages = chat.messages.map((m) => ({
id: m.id,
role: m.role as any,
content: m.content,
model: m.model,
mode: m.mode,
promptTokens: m.promptTokens,
completionTokens: m.completionTokens,
durationMs: m.durationMs,
createdAt: m.createdAt,
}));

// Resolve initial model/mode based on the last assistant message, or default to chat mode settings
let initialModelId = "gemini-2.0-flash";
let initialMode: ChatMode = (chat.mode as ChatMode) || "chat";

// Search backwards to find the last configured model and mode
for (let i = chat.messages.length - 1; i >= 0; i--) {
const msg = chat.messages[i];
if (msg.model) {
initialModelId = msg.model;
break;
}
}

return (
<ChatContainer
chatId={chatId}
initialMessages={initialMessages}
initialModelId={initialModelId}
initialMode={initialMode}
/>
);
}
102 changes: 102 additions & 0 deletions client/app/multiple-ai/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use server";

import { prisma } from "@/lib/db";
import { ChatMode, MessageRole } from "@/lib/generated/prisma/enums";
import { revalidatePath } from "next/cache";

export async function getChats() {
try {
return await prisma.chat.findMany({
orderBy: {
createdAt: "desc",
},
include: {
messages: {
take: 1,
orderBy: {
createdAt: "asc",
},
},
},
});
} catch (error) {
console.error("Error fetching chats:", error);
return [];
}
}

export async function getChat(id: string) {
try {
return await prisma.chat.findUnique({
where: { id },
include: {
messages: {
orderBy: {
createdAt: "asc",
},
},
},
});
} catch (error) {
console.error(`Error fetching chat ${id}:`, error);
return null;
}
}

export async function createChat(mode: ChatMode = "chat") {
try {
const chat = await prisma.chat.create({
data: {
title: "New Chat",
mode,
},
});
revalidatePath("/multiple-ai");
return chat;
} catch (error) {
console.error("Error creating chat:", error);
throw new Error("Could not create chat session.");
}
}

export async function deleteChat(id: string) {
try {
await prisma.chat.delete({
where: { id },
});
revalidatePath("/multiple-ai");
return { success: true };
} catch (error) {
console.error(`Error deleting chat ${id}:`, error);
throw new Error("Could not delete chat session.");
}
}

export async function saveUserMessage({
chatId,
content,
modelId,
mode,
}: {
chatId: string;
content: string;
modelId: string;
mode: ChatMode;
}) {
try {
const message = await prisma.message.create({
data: {
role: "user" as MessageRole,
content,
model: modelId,
mode,
chatId,
},
});
revalidatePath(`/multiple-ai/${chatId}`);
return message;
} catch (error) {
console.error("Error saving user message:", error);
throw new Error("Could not save message.");
}
}
Loading