Initial commit: EGBE chatbot template
Stripped from vercel/chatbot (Apache 2.0): - Dropped @vercel/* packages and AI Gateway - Removed artifacts feature (code/text/sheet/image side panel) - Switched AI provider to @ai-sdk/openai-compatible -> EGBE LiteLLM - Replaced Vercel Blob upload with data URLs - Dropped Redis resumable streams and rate limiter (in-memory now) - Added Dockerfile (Next.js standalone) + entrypoint that runs migrations - Wired DATABASE_URL, EGBE_AI_API_URL/KEY, NEXT_PUBLIC_BASE_URL for app-deploy.sh
This commit is contained in:
commit
3e21c2334c
129 changed files with 21913 additions and 0 deletions
78
app/(chat)/actions.ts
Normal file
78
app/(chat)/actions.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"use server";
|
||||
|
||||
import { generateText, type UIMessage } from "ai";
|
||||
import { cookies } from "next/headers";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import type { VisibilityType } from "@/components/chat/visibility-selector";
|
||||
import { titlePrompt } from "@/lib/ai/prompts";
|
||||
import { getTitleModel } from "@/lib/ai/providers";
|
||||
import {
|
||||
deleteMessagesByChatIdAfterTimestamp,
|
||||
getChatById,
|
||||
getMessageById,
|
||||
updateChatVisibilityById,
|
||||
} from "@/lib/db/queries";
|
||||
import { getTextFromMessage } from "@/lib/utils";
|
||||
|
||||
export async function saveChatModelAsCookie(model: string) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set("chat-model", model);
|
||||
}
|
||||
|
||||
export async function generateTitleFromUserMessage({
|
||||
message,
|
||||
}: {
|
||||
message: UIMessage;
|
||||
}) {
|
||||
const { text } = await generateText({
|
||||
model: getTitleModel(),
|
||||
system: titlePrompt,
|
||||
prompt: getTextFromMessage(message),
|
||||
});
|
||||
return text
|
||||
.replace(/^[#*"\s]+/, "")
|
||||
.replace(/["]+$/, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export async function deleteTrailingMessages({ id }: { id: string }) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
const [message] = await getMessageById({ id });
|
||||
if (!message) {
|
||||
throw new Error("Message not found");
|
||||
}
|
||||
|
||||
const chat = await getChatById({ id: message.chatId });
|
||||
if (!chat || chat.userId !== session.user.id) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await deleteMessagesByChatIdAfterTimestamp({
|
||||
chatId: message.chatId,
|
||||
timestamp: message.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateChatVisibility({
|
||||
chatId,
|
||||
visibility,
|
||||
}: {
|
||||
chatId: string;
|
||||
visibility: VisibilityType;
|
||||
}) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
const chat = await getChatById({ id: chatId });
|
||||
if (!chat || chat.userId !== session.user.id) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await updateChatVisibilityById({ chatId, visibility });
|
||||
}
|
||||
3
app/(chat)/api/chat/[id]/stream/route.ts
Normal file
3
app/(chat)/api/chat/[id]/stream/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function GET() {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
272
app/(chat)/api/chat/route.ts
Normal file
272
app/(chat)/api/chat/route.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import {
|
||||
convertToModelMessages,
|
||||
createUIMessageStream,
|
||||
createUIMessageStreamResponse,
|
||||
stepCountIs,
|
||||
streamText,
|
||||
} from "ai";
|
||||
import { auth, type UserType } from "@/app/(auth)/auth";
|
||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||
import {
|
||||
allowedModelIds,
|
||||
DEFAULT_CHAT_MODEL,
|
||||
modelCapabilities,
|
||||
} from "@/lib/ai/models";
|
||||
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
||||
import { getLanguageModel } from "@/lib/ai/providers";
|
||||
import { getWeather } from "@/lib/ai/tools/get-weather";
|
||||
import {
|
||||
deleteChatById,
|
||||
getChatById,
|
||||
getMessageCountByUserId,
|
||||
getMessagesByChatId,
|
||||
saveChat,
|
||||
saveMessages,
|
||||
updateChatTitleById,
|
||||
updateMessage,
|
||||
} from "@/lib/db/queries";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import { ChatbotError } from "@/lib/errors";
|
||||
import { checkIpRateLimit } from "@/lib/ratelimit";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { convertToUIMessages, generateUUID } from "@/lib/utils";
|
||||
import { generateTitleFromUserMessage } from "../../actions";
|
||||
import { type PostRequestBody, postRequestBodySchema } from "./schema";
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
function getClientIp(request: Request): string | undefined {
|
||||
const fwd = request.headers.get("x-forwarded-for");
|
||||
if (fwd) {
|
||||
return fwd.split(",")[0]?.trim();
|
||||
}
|
||||
return request.headers.get("x-real-ip") ?? undefined;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let requestBody: PostRequestBody;
|
||||
|
||||
try {
|
||||
const json = await request.json();
|
||||
requestBody = postRequestBodySchema.parse(json);
|
||||
} catch (_) {
|
||||
return new ChatbotError("bad_request:api").toResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, message, messages, selectedChatModel, selectedVisibilityType } =
|
||||
requestBody;
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
const chatModel = allowedModelIds.has(selectedChatModel)
|
||||
? selectedChatModel
|
||||
: DEFAULT_CHAT_MODEL;
|
||||
|
||||
await checkIpRateLimit(getClientIp(request));
|
||||
|
||||
const userType: UserType = session.user.type;
|
||||
|
||||
const messageCount = await getMessageCountByUserId({
|
||||
id: session.user.id,
|
||||
differenceInHours: 1,
|
||||
});
|
||||
|
||||
if (messageCount > entitlementsByUserType[userType].maxMessagesPerHour) {
|
||||
return new ChatbotError("rate_limit:chat").toResponse();
|
||||
}
|
||||
|
||||
const isToolApprovalFlow = Boolean(messages);
|
||||
|
||||
const chat = await getChatById({ id });
|
||||
let messagesFromDb: DBMessage[] = [];
|
||||
let titlePromise: Promise<string> | null = null;
|
||||
|
||||
if (chat) {
|
||||
if (chat.userId !== session.user.id) {
|
||||
return new ChatbotError("forbidden:chat").toResponse();
|
||||
}
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
} else if (message?.role === "user") {
|
||||
await saveChat({
|
||||
id,
|
||||
userId: session.user.id,
|
||||
title: "New chat",
|
||||
visibility: selectedVisibilityType,
|
||||
});
|
||||
titlePromise = generateTitleFromUserMessage({ message });
|
||||
}
|
||||
|
||||
let uiMessages: ChatMessage[];
|
||||
|
||||
if (isToolApprovalFlow && messages) {
|
||||
const dbMessages = convertToUIMessages(messagesFromDb);
|
||||
const approvalStates = new Map(
|
||||
messages.flatMap(
|
||||
(m) =>
|
||||
m.parts
|
||||
?.filter(
|
||||
(p: Record<string, unknown>) =>
|
||||
p.state === "approval-responded" ||
|
||||
p.state === "output-denied"
|
||||
)
|
||||
.map((p: Record<string, unknown>) => [
|
||||
String(p.toolCallId ?? ""),
|
||||
p,
|
||||
]) ?? []
|
||||
)
|
||||
);
|
||||
uiMessages = dbMessages.map((msg) => ({
|
||||
...msg,
|
||||
parts: msg.parts.map((part) => {
|
||||
if (
|
||||
"toolCallId" in part &&
|
||||
approvalStates.has(String(part.toolCallId))
|
||||
) {
|
||||
return { ...part, ...approvalStates.get(String(part.toolCallId)) };
|
||||
}
|
||||
return part;
|
||||
}),
|
||||
})) as ChatMessage[];
|
||||
} else {
|
||||
uiMessages = [
|
||||
...convertToUIMessages(messagesFromDb),
|
||||
message as ChatMessage,
|
||||
];
|
||||
}
|
||||
|
||||
const requestHints: RequestHints = {
|
||||
city: request.headers.get("x-vercel-ip-city") ?? undefined,
|
||||
country: request.headers.get("x-vercel-ip-country") ?? undefined,
|
||||
};
|
||||
|
||||
if (message?.role === "user") {
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
chatId: id,
|
||||
id: message.id,
|
||||
role: "user",
|
||||
parts: message.parts,
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const capabilities = modelCapabilities[chatModel];
|
||||
const isReasoningModel = capabilities?.reasoning === true;
|
||||
const supportsTools = capabilities?.tools === true;
|
||||
|
||||
const modelMessages = await convertToModelMessages(uiMessages);
|
||||
|
||||
const stream = createUIMessageStream({
|
||||
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
|
||||
execute: async ({ writer: dataStream }) => {
|
||||
const result = streamText({
|
||||
model: getLanguageModel(chatModel),
|
||||
system: systemPrompt({ requestHints, supportsTools }),
|
||||
messages: modelMessages,
|
||||
stopWhen: stepCountIs(5),
|
||||
experimental_activeTools:
|
||||
isReasoningModel && !supportsTools ? [] : ["getWeather"],
|
||||
tools: {
|
||||
getWeather,
|
||||
},
|
||||
});
|
||||
|
||||
dataStream.merge(
|
||||
result.toUIMessageStream({ sendReasoning: isReasoningModel })
|
||||
);
|
||||
|
||||
if (titlePromise) {
|
||||
try {
|
||||
const title = await titlePromise;
|
||||
dataStream.write({ type: "data-chat-title", data: title });
|
||||
updateChatTitleById({ chatId: id, title });
|
||||
} catch (_) {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
},
|
||||
generateId: generateUUID,
|
||||
onFinish: async ({ messages: finishedMessages }) => {
|
||||
if (isToolApprovalFlow) {
|
||||
for (const finishedMsg of finishedMessages) {
|
||||
const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id);
|
||||
if (existingMsg) {
|
||||
await updateMessage({
|
||||
id: finishedMsg.id,
|
||||
parts: finishedMsg.parts,
|
||||
});
|
||||
} else {
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
id: finishedMsg.id,
|
||||
role: finishedMsg.role,
|
||||
parts: finishedMsg.parts,
|
||||
createdAt: new Date(),
|
||||
attachments: [],
|
||||
chatId: id,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (finishedMessages.length > 0) {
|
||||
await saveMessages({
|
||||
messages: finishedMessages.map((currentMessage) => ({
|
||||
id: currentMessage.id,
|
||||
role: currentMessage.role,
|
||||
parts: currentMessage.parts,
|
||||
createdAt: new Date(),
|
||||
attachments: [],
|
||||
chatId: id,
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => "Oops, an error occurred!",
|
||||
});
|
||||
|
||||
return createUIMessageStreamResponse({ stream });
|
||||
} catch (error) {
|
||||
if (error instanceof ChatbotError) {
|
||||
return error.toResponse();
|
||||
}
|
||||
|
||||
console.error("Unhandled error in chat API:", error);
|
||||
return new ChatbotError("offline:chat").toResponse();
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
return new ChatbotError("bad_request:api").toResponse();
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
const chat = await getChatById({ id });
|
||||
|
||||
if (chat?.userId !== session.user.id) {
|
||||
return new ChatbotError("forbidden:chat").toResponse();
|
||||
}
|
||||
|
||||
const deletedChat = await deleteChatById({ id });
|
||||
|
||||
return Response.json(deletedChat, { status: 200 });
|
||||
}
|
||||
37
app/(chat)/api/chat/schema.ts
Normal file
37
app/(chat)/api/chat/schema.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const textPartSchema = z.object({
|
||||
type: z.enum(["text"]),
|
||||
text: z.string().min(1).max(2000),
|
||||
});
|
||||
|
||||
const filePartSchema = z.object({
|
||||
type: z.enum(["file"]),
|
||||
mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif"]),
|
||||
name: z.string().min(1).max(100),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const partSchema = z.union([textPartSchema, filePartSchema]);
|
||||
|
||||
const userMessageSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.enum(["user"]),
|
||||
parts: z.array(partSchema),
|
||||
});
|
||||
|
||||
const toolApprovalMessageSchema = z.object({
|
||||
id: z.string(),
|
||||
role: z.enum(["user", "assistant"]),
|
||||
parts: z.array(z.record(z.unknown())),
|
||||
});
|
||||
|
||||
export const postRequestBodySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
message: userMessageSchema.optional(),
|
||||
messages: z.array(toolApprovalMessageSchema).optional(),
|
||||
selectedChatModel: z.string(),
|
||||
selectedVisibilityType: z.enum(["public", "private"]),
|
||||
});
|
||||
|
||||
export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
|
||||
64
app/(chat)/api/files/upload/route.ts
Normal file
64
app/(chat)/api/files/upload/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
||||
|
||||
const FileSchema = z.object({
|
||||
file: z
|
||||
.instanceof(Blob)
|
||||
.refine((file) => file.size <= MAX_FILE_SIZE, {
|
||||
message: "File size should be less than 5MB",
|
||||
})
|
||||
.refine((file) => ALLOWED_TYPES.includes(file.type), {
|
||||
message: "File type should be JPEG, PNG, WebP, or GIF",
|
||||
}),
|
||||
});
|
||||
|
||||
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 Blob;
|
||||
|
||||
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 = (formData.get("file") as File).name;
|
||||
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const dataUrl = `data:${file.type};base64,${buffer.toString("base64")}`;
|
||||
|
||||
return NextResponse.json({
|
||||
url: dataUrl,
|
||||
pathname: safeName,
|
||||
contentType: file.type,
|
||||
});
|
||||
} catch (_error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to process request" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
49
app/(chat)/api/history/route.ts
Normal file
49
app/(chat)/api/history/route.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { NextRequest } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { deleteAllChatsByUserId, getChatsByUserId } from "@/lib/db/queries";
|
||||
import { ChatbotError } from "@/lib/errors";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl;
|
||||
|
||||
const limit = Math.min(
|
||||
Math.max(Number.parseInt(searchParams.get("limit") || "10", 10), 1),
|
||||
50
|
||||
);
|
||||
const startingAfter = searchParams.get("starting_after");
|
||||
const endingBefore = searchParams.get("ending_before");
|
||||
|
||||
if (startingAfter && endingBefore) {
|
||||
return new ChatbotError(
|
||||
"bad_request:api",
|
||||
"Only one of starting_after or ending_before can be provided."
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
const chats = await getChatsByUserId({
|
||||
id: session.user.id,
|
||||
limit,
|
||||
startingAfter,
|
||||
endingBefore,
|
||||
});
|
||||
|
||||
return Response.json(chats);
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
const result = await deleteAllChatsByUserId({ userId: session.user.id });
|
||||
|
||||
return Response.json(result, { status: 200 });
|
||||
}
|
||||
43
app/(chat)/api/messages/route.ts
Normal file
43
app/(chat)/api/messages/route.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
|
||||
import { convertToUIMessages } from "@/lib/utils";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const chatId = searchParams.get("chatId");
|
||||
|
||||
if (!chatId) {
|
||||
return Response.json({ error: "chatId required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const [session, chat, messages] = await Promise.all([
|
||||
auth(),
|
||||
getChatById({ id: chatId }),
|
||||
getMessagesByChatId({ id: chatId }),
|
||||
]);
|
||||
|
||||
if (!chat) {
|
||||
return Response.json({
|
||||
messages: [],
|
||||
visibility: "private",
|
||||
userId: null,
|
||||
isReadonly: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
chat.visibility === "private" &&
|
||||
(!session?.user || session.user.id !== chat.userId)
|
||||
) {
|
||||
return Response.json({ error: "forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const isReadonly = !session?.user || session.user.id !== chat.userId;
|
||||
|
||||
return Response.json({
|
||||
messages: convertToUIMessages(messages),
|
||||
visibility: chat.visibility,
|
||||
userId: chat.userId,
|
||||
isReadonly,
|
||||
});
|
||||
}
|
||||
9
app/(chat)/api/models/route.ts
Normal file
9
app/(chat)/api/models/route.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { modelCapabilities } from "@/lib/ai/models";
|
||||
|
||||
export function GET() {
|
||||
return Response.json(modelCapabilities, {
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=86400, s-maxage=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
84
app/(chat)/api/vote/route.ts
Normal file
84
app/(chat)/api/vote/route.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries";
|
||||
import { ChatbotError } from "@/lib/errors";
|
||||
|
||||
const voteSchema = z.object({
|
||||
chatId: z.string(),
|
||||
messageId: z.string(),
|
||||
type: z.enum(["up", "down"]),
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const chatId = searchParams.get("chatId");
|
||||
|
||||
if (!chatId) {
|
||||
return new ChatbotError(
|
||||
"bad_request:api",
|
||||
"Parameter chatId is required."
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:vote").toResponse();
|
||||
}
|
||||
|
||||
const chat = await getChatById({ id: chatId });
|
||||
|
||||
if (!chat) {
|
||||
return new ChatbotError("not_found:chat").toResponse();
|
||||
}
|
||||
|
||||
if (chat.userId !== session.user.id) {
|
||||
return new ChatbotError("forbidden:vote").toResponse();
|
||||
}
|
||||
|
||||
const votes = await getVotesByChatId({ id: chatId });
|
||||
|
||||
return Response.json(votes, { status: 200 });
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
let chatId: string;
|
||||
let messageId: string;
|
||||
let type: "up" | "down";
|
||||
|
||||
try {
|
||||
const parsed = voteSchema.parse(await request.json());
|
||||
chatId = parsed.chatId;
|
||||
messageId = parsed.messageId;
|
||||
type = parsed.type;
|
||||
} catch {
|
||||
return new ChatbotError(
|
||||
"bad_request:api",
|
||||
"Parameters chatId, messageId, and type are required."
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:vote").toResponse();
|
||||
}
|
||||
|
||||
const chat = await getChatById({ id: chatId });
|
||||
|
||||
if (!chat) {
|
||||
return new ChatbotError("not_found:vote").toResponse();
|
||||
}
|
||||
|
||||
if (chat.userId !== session.user.id) {
|
||||
return new ChatbotError("forbidden:vote").toResponse();
|
||||
}
|
||||
|
||||
await voteMessage({
|
||||
chatId,
|
||||
messageId,
|
||||
type,
|
||||
});
|
||||
|
||||
return new Response("Message voted", { status: 200 });
|
||||
}
|
||||
3
app/(chat)/chat/[id]/page.tsx
Normal file
3
app/(chat)/chat/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return null;
|
||||
}
|
||||
46
app/(chat)/layout.tsx
Normal file
46
app/(chat)/layout.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { Suspense } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { AppSidebar } from "@/components/chat/app-sidebar";
|
||||
import { DataStreamProvider } from "@/components/chat/data-stream-provider";
|
||||
import { ChatShell } from "@/components/chat/shell";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { ActiveChatProvider } from "@/hooks/use-active-chat";
|
||||
import { auth } from "../(auth)/auth";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DataStreamProvider>
|
||||
<Suspense fallback={<div className="flex h-dvh bg-sidebar" />}>
|
||||
<SidebarShell>{children}</SidebarShell>
|
||||
</Suspense>
|
||||
</DataStreamProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function SidebarShell({ children }: { children: React.ReactNode }) {
|
||||
const [session, cookieStore] = await Promise.all([auth(), cookies()]);
|
||||
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={!isCollapsed}>
|
||||
<AppSidebar user={session?.user} />
|
||||
<SidebarInset>
|
||||
<Toaster
|
||||
position="top-center"
|
||||
theme="system"
|
||||
toastOptions={{
|
||||
className:
|
||||
"!bg-card !text-foreground !border-border/50 !shadow-[var(--shadow-float)]",
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<div className="flex h-dvh" />}>
|
||||
<ActiveChatProvider>
|
||||
<ChatShell />
|
||||
</ActiveChatProvider>
|
||||
</Suspense>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
BIN
app/(chat)/opengraph-image.png
Normal file
BIN
app/(chat)/opengraph-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
3
app/(chat)/page.tsx
Normal file
3
app/(chat)/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return null;
|
||||
}
|
||||
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