feat: v1 — persistent shell, model gateway, artifact improvements (#1462)
This commit is contained in:
parent
3651670fb9
commit
f9652b452a
161 changed files with 5166 additions and 8009 deletions
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
import { generateText, type UIMessage } from "ai";
|
||||
import { cookies } from "next/headers";
|
||||
import type { VisibilityType } from "@/components/visibility-selector";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import type { VisibilityType } from "@/components/chat/visibility-selector";
|
||||
import { titleModel } from "@/lib/ai/models";
|
||||
import { titlePrompt } from "@/lib/ai/prompts";
|
||||
import { getTitleModel } from "@/lib/ai/providers";
|
||||
import {
|
||||
deleteMessagesByChatIdAfterTimestamp,
|
||||
getChatById,
|
||||
getMessageById,
|
||||
updateChatVisibilityById,
|
||||
} from "@/lib/db/queries";
|
||||
|
|
@ -26,6 +29,9 @@ export async function generateTitleFromUserMessage({
|
|||
model: getTitleModel(),
|
||||
system: titlePrompt,
|
||||
prompt: getTextFromMessage(message),
|
||||
providerOptions: {
|
||||
gateway: { order: titleModel.gatewayOrder },
|
||||
},
|
||||
});
|
||||
return text
|
||||
.replace(/^[#*"\s]+/, "")
|
||||
|
|
@ -34,7 +40,20 @@ export async function generateTitleFromUserMessage({
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
@ -49,5 +68,15 @@ export async function updateChatVisibility({
|
|||
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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth);
|
||||
|
|
@ -10,15 +10,21 @@ import {
|
|||
import { checkBotId } from "botid/server";
|
||||
import { after } from "next/server";
|
||||
import { createResumableStreamContext } from "resumable-stream";
|
||||
import { auth, type UserType } from "@/app/(auth)/auth";
|
||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||
import { allowedModelIds } from "@/lib/ai/models";
|
||||
import {
|
||||
allowedModelIds,
|
||||
chatModels,
|
||||
DEFAULT_CHAT_MODEL,
|
||||
getCapabilities,
|
||||
} from "@/lib/ai/models";
|
||||
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
||||
import { getLanguageModel } from "@/lib/ai/providers";
|
||||
import { createDocument } from "@/lib/ai/tools/create-document";
|
||||
import { editDocument } from "@/lib/ai/tools/edit-document";
|
||||
import { getWeather } from "@/lib/ai/tools/get-weather";
|
||||
import { requestSuggestions } from "@/lib/ai/tools/request-suggestions";
|
||||
import { updateDocument } from "@/lib/ai/tools/update-document";
|
||||
import { getSession, getUserType, type UserType } from "@/lib/auth";
|
||||
import { isProductionEnvironment } from "@/lib/constants";
|
||||
import {
|
||||
createStreamId,
|
||||
|
|
@ -67,20 +73,20 @@ export async function POST(request: Request) {
|
|||
|
||||
const [, session] = await Promise.all([
|
||||
checkBotId().catch(() => null),
|
||||
getSession(),
|
||||
auth(),
|
||||
]);
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
if (!allowedModelIds.has(selectedChatModel)) {
|
||||
return new ChatbotError("bad_request:api").toResponse();
|
||||
}
|
||||
const chatModel = allowedModelIds.has(selectedChatModel)
|
||||
? selectedChatModel
|
||||
: DEFAULT_CHAT_MODEL;
|
||||
|
||||
await checkIpRateLimit(ipAddress(request));
|
||||
|
||||
const userType: UserType = getUserType(session.user);
|
||||
const userType: UserType = session.user.type;
|
||||
|
||||
const messageCount = await getMessageCountByUserId({
|
||||
id: session.user.id,
|
||||
|
|
@ -101,9 +107,7 @@ export async function POST(request: Request) {
|
|||
if (chat.userId !== session.user.id) {
|
||||
return new ChatbotError("forbidden:chat").toResponse();
|
||||
}
|
||||
if (!isToolApprovalFlow) {
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
}
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
} else if (message?.role === "user") {
|
||||
await saveChat({
|
||||
id,
|
||||
|
|
@ -114,9 +118,43 @@ export async function POST(request: Request) {
|
|||
titlePromise = generateTitleFromUserMessage({ message });
|
||||
}
|
||||
|
||||
const uiMessages = isToolApprovalFlow
|
||||
? (messages as ChatMessage[])
|
||||
: [...convertToUIMessages(messagesFromDb), message as ChatMessage];
|
||||
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 { longitude, latitude, city, country } = geolocation(request);
|
||||
|
||||
|
|
@ -142,10 +180,11 @@ export async function POST(request: Request) {
|
|||
});
|
||||
}
|
||||
|
||||
const isReasoningModel =
|
||||
selectedChatModel.endsWith("-thinking") ||
|
||||
(selectedChatModel.includes("reasoning") &&
|
||||
!selectedChatModel.includes("non-reasoning"));
|
||||
const modelConfig = chatModels.find((m) => m.id === chatModel);
|
||||
const modelCapabilities = await getCapabilities();
|
||||
const capabilities = modelCapabilities[chatModel];
|
||||
const isReasoningModel = capabilities?.reasoning === true;
|
||||
const supportsTools = capabilities?.tools === true;
|
||||
|
||||
const modelMessages = await convertToModelMessages(uiMessages);
|
||||
|
||||
|
|
@ -153,30 +192,46 @@ export async function POST(request: Request) {
|
|||
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
|
||||
execute: async ({ writer: dataStream }) => {
|
||||
const result = streamText({
|
||||
model: getLanguageModel(selectedChatModel),
|
||||
system: systemPrompt({ selectedChatModel, requestHints }),
|
||||
model: getLanguageModel(chatModel),
|
||||
system: systemPrompt({ requestHints, supportsTools }),
|
||||
messages: modelMessages,
|
||||
stopWhen: stepCountIs(5),
|
||||
experimental_activeTools: isReasoningModel
|
||||
? []
|
||||
: [
|
||||
"getWeather",
|
||||
"createDocument",
|
||||
"updateDocument",
|
||||
"requestSuggestions",
|
||||
],
|
||||
providerOptions: isReasoningModel
|
||||
? {
|
||||
anthropic: {
|
||||
thinking: { type: "enabled", budgetTokens: 10_000 },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
experimental_activeTools:
|
||||
isReasoningModel && !supportsTools
|
||||
? []
|
||||
: [
|
||||
"getWeather",
|
||||
"createDocument",
|
||||
"editDocument",
|
||||
"updateDocument",
|
||||
"requestSuggestions",
|
||||
],
|
||||
providerOptions: {
|
||||
...(modelConfig?.gatewayOrder && {
|
||||
gateway: { order: modelConfig.gatewayOrder },
|
||||
}),
|
||||
...(modelConfig?.reasoningEffort && {
|
||||
openai: { reasoningEffort: modelConfig.reasoningEffort },
|
||||
}),
|
||||
},
|
||||
tools: {
|
||||
getWeather,
|
||||
createDocument: createDocument({ session, dataStream }),
|
||||
updateDocument: updateDocument({ session, dataStream }),
|
||||
requestSuggestions: requestSuggestions({ session, dataStream }),
|
||||
createDocument: createDocument({
|
||||
session,
|
||||
dataStream,
|
||||
modelId: chatModel,
|
||||
}),
|
||||
editDocument: editDocument({ dataStream, session }),
|
||||
updateDocument: updateDocument({
|
||||
session,
|
||||
dataStream,
|
||||
modelId: chatModel,
|
||||
}),
|
||||
requestSuggestions: requestSuggestions({
|
||||
session,
|
||||
dataStream,
|
||||
modelId: chatModel,
|
||||
}),
|
||||
},
|
||||
experimental_telemetry: {
|
||||
isEnabled: isProductionEnvironment,
|
||||
|
|
@ -262,7 +317,7 @@ export async function POST(request: Request) {
|
|||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore redis errors
|
||||
/* non-critical */
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -295,7 +350,7 @@ export async function DELETE(request: Request) {
|
|||
return new ChatbotError("bad_request:api").toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
|
|
|
|||
|
|
@ -20,18 +20,16 @@ const userMessageSchema = z.object({
|
|||
parts: z.array(partSchema),
|
||||
});
|
||||
|
||||
// For tool approval flows, we accept all messages (more permissive schema)
|
||||
const messageSchema = z.object({
|
||||
const toolApprovalMessageSchema = z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
parts: z.array(z.any()),
|
||||
role: z.enum(["user", "assistant"]),
|
||||
parts: z.array(z.record(z.unknown())),
|
||||
});
|
||||
|
||||
export const postRequestBodySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
// Either a single new message or all messages (for tool approvals)
|
||||
message: userMessageSchema.optional(),
|
||||
messages: z.array(messageSchema).optional(),
|
||||
messages: z.array(toolApprovalMessageSchema).optional(),
|
||||
selectedChatModel: z.string(),
|
||||
selectedVisibilityType: z.enum(["public", "private"]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
import type { ArtifactKind } from "@/components/artifact";
|
||||
import { getSession } from "@/lib/auth";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import type { ArtifactKind } from "@/components/chat/artifact";
|
||||
import {
|
||||
deleteDocumentsByIdAfterTimestamp,
|
||||
getDocumentsById,
|
||||
saveDocument,
|
||||
updateDocumentContent,
|
||||
} from "@/lib/db/queries";
|
||||
import { ChatbotError } from "@/lib/errors";
|
||||
|
||||
const documentSchema = z.object({
|
||||
content: z.string(),
|
||||
title: z.string(),
|
||||
kind: z.enum(["text", "code", "image", "sheet"]),
|
||||
isManualEdit: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get("id");
|
||||
|
|
@ -18,7 +27,7 @@ export async function GET(request: Request) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:document").toResponse();
|
||||
|
|
@ -50,18 +59,29 @@ export async function POST(request: Request) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("not_found:document").toResponse();
|
||||
}
|
||||
|
||||
const {
|
||||
content,
|
||||
title,
|
||||
kind,
|
||||
}: { content: string; title: string; kind: ArtifactKind } =
|
||||
await request.json();
|
||||
let content: string;
|
||||
let title: string;
|
||||
let kind: ArtifactKind;
|
||||
let isManualEdit: boolean | undefined;
|
||||
|
||||
try {
|
||||
const parsed = documentSchema.parse(await request.json());
|
||||
content = parsed.content;
|
||||
title = parsed.title;
|
||||
kind = parsed.kind;
|
||||
isManualEdit = parsed.isManualEdit;
|
||||
} catch {
|
||||
return new ChatbotError(
|
||||
"bad_request:api",
|
||||
"Invalid request body."
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
const documents = await getDocumentsById({ id });
|
||||
|
||||
|
|
@ -73,6 +93,11 @@ export async function POST(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isManualEdit && documents.length > 0) {
|
||||
const result = await updateDocumentContent({ id, content });
|
||||
return Response.json(result, { status: 200 });
|
||||
}
|
||||
|
||||
const document = await saveDocument({
|
||||
id,
|
||||
content,
|
||||
|
|
@ -103,7 +128,7 @@ export async function DELETE(request: Request) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:document").toResponse();
|
||||
|
|
@ -117,9 +142,18 @@ export async function DELETE(request: Request) {
|
|||
return new ChatbotError("forbidden:document").toResponse();
|
||||
}
|
||||
|
||||
const parsedTimestamp = new Date(timestamp);
|
||||
|
||||
if (Number.isNaN(parsedTimestamp.getTime())) {
|
||||
return new ChatbotError(
|
||||
"bad_request:api",
|
||||
"Invalid timestamp."
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({
|
||||
id,
|
||||
timestamp: new Date(timestamp),
|
||||
timestamp: parsedTimestamp,
|
||||
});
|
||||
|
||||
return Response.json(documentsDeleted, { status: 200 });
|
||||
|
|
|
|||
|
|
@ -2,23 +2,21 @@ import { put } from "@vercel/blob";
|
|||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getSession } from "@/lib/auth";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
|
||||
// Use Blob instead of File since File is not available in Node.js environment
|
||||
const FileSchema = z.object({
|
||||
file: z
|
||||
.instanceof(Blob)
|
||||
.refine((file) => file.size <= 5 * 1024 * 1024, {
|
||||
message: "File size should be less than 5MB",
|
||||
})
|
||||
// Update the file type based on the kind of files you want to accept
|
||||
.refine((file) => ["image/jpeg", "image/png"].includes(file.type), {
|
||||
message: "File type should be JPEG or PNG",
|
||||
}),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
|
@ -46,12 +44,12 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: errorMessage }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get filename from formData since Blob doesn't have name property
|
||||
const filename = (formData.get("file") as File).name;
|
||||
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
try {
|
||||
const data = await put(`${filename}`, fileBuffer, {
|
||||
const data = await put(`${safeName}`, fileBuffer, {
|
||||
access: "public",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import type { NextRequest } from "next/server";
|
||||
import { getSession } from "@/lib/auth";
|
||||
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 = Number.parseInt(searchParams.get("limit") || "10", 10);
|
||||
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");
|
||||
|
||||
|
|
@ -17,7 +20,7 @@ export async function GET(request: NextRequest) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
|
|
@ -34,7 +37,7 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
export async function DELETE() {
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:chat").toResponse();
|
||||
|
|
|
|||
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,
|
||||
});
|
||||
}
|
||||
20
app/(chat)/api/models/route.ts
Normal file
20
app/(chat)/api/models/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { getAllGatewayModels, getCapabilities, isDemo } from "@/lib/ai/models";
|
||||
|
||||
export async function GET() {
|
||||
const headers = {
|
||||
"Cache-Control": "public, max-age=86400, s-maxage=86400",
|
||||
};
|
||||
|
||||
const curatedCapabilities = await getCapabilities();
|
||||
|
||||
if (isDemo) {
|
||||
const models = await getAllGatewayModels();
|
||||
const capabilities = Object.fromEntries(
|
||||
models.map((m) => [m.id, curatedCapabilities[m.id] ?? m.capabilities])
|
||||
);
|
||||
|
||||
return Response.json({ capabilities, models }, { headers });
|
||||
}
|
||||
|
||||
return Response.json(curatedCapabilities, { headers });
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { getSession } from "@/lib/auth";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getSuggestionsByDocumentId } from "@/lib/db/queries";
|
||||
import { ChatbotError } from "@/lib/errors";
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ export async function GET(request: Request) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:suggestions").toResponse();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { getSession } from "@/lib/auth";
|
||||
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");
|
||||
|
|
@ -13,7 +20,7 @@ export async function GET(request: Request) {
|
|||
).toResponse();
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:vote").toResponse();
|
||||
|
|
@ -35,21 +42,23 @@ export async function GET(request: Request) {
|
|||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
const {
|
||||
chatId,
|
||||
messageId,
|
||||
type,
|
||||
}: { chatId: string; messageId: string; type: "up" | "down" } =
|
||||
await request.json();
|
||||
let chatId: string;
|
||||
let messageId: string;
|
||||
let type: "up" | "down";
|
||||
|
||||
if (!chatId || !messageId || !type) {
|
||||
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 getSession();
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return new ChatbotError("unauthorized:vote").toResponse();
|
||||
|
|
|
|||
|
|
@ -1,81 +1,3 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { Chat } from "@/components/chat";
|
||||
import { DataStreamHandler } from "@/components/data-stream-handler";
|
||||
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
|
||||
import { getSession } from "@/lib/auth";
|
||||
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
|
||||
import { convertToUIMessages } from "@/lib/utils";
|
||||
|
||||
export default function Page(props: { params: Promise<{ id: string }> }) {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex h-dvh" />}>
|
||||
<ChatPage params={props.params} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function ChatPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const chat = await getChatById({ id });
|
||||
|
||||
if (!chat) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
if (!session) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (chat.visibility === "private") {
|
||||
if (!session.user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (session.user.id !== chat.userId) {
|
||||
return notFound();
|
||||
}
|
||||
}
|
||||
|
||||
const messagesFromDb = await getMessagesByChatId({
|
||||
id,
|
||||
});
|
||||
|
||||
const uiMessages = convertToUIMessages(messagesFromDb);
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const chatModelFromCookie = cookieStore.get("chat-model");
|
||||
|
||||
if (!chatModelFromCookie) {
|
||||
return (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={true}
|
||||
id={chat.id}
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialMessages={uiMessages}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={true}
|
||||
id={chat.id}
|
||||
initialChatModel={chatModelFromCookie.value}
|
||||
initialMessages={uiMessages}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
);
|
||||
export default function Page() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,53 @@
|
|||
import { cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import { Suspense } from "react";
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { DataStreamProvider } from "@/components/data-stream-provider";
|
||||
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 { getSession } from "@/lib/auth";
|
||||
import { ActiveChatProvider } from "@/hooks/use-active-chat";
|
||||
import { auth } from "../(auth)/auth";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
|
||||
strategy="beforeInteractive"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
<DataStreamProvider>
|
||||
<Suspense fallback={<div className="flex h-dvh" />}>
|
||||
<SidebarWrapper>{children}</SidebarWrapper>
|
||||
<Suspense fallback={<div className="flex h-dvh bg-sidebar" />}>
|
||||
<SidebarShell>{children}</SidebarShell>
|
||||
</Suspense>
|
||||
</DataStreamProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function SidebarWrapper({ children }: { children: React.ReactNode }) {
|
||||
const [session, cookieStore] = await Promise.all([getSession(), cookies()]);
|
||||
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>{children}</SidebarInset>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,3 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { Suspense } from "react";
|
||||
import { Chat } from "@/components/chat";
|
||||
import { DataStreamHandler } from "@/components/data-stream-handler";
|
||||
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex h-dvh" />}>
|
||||
<NewChatPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function NewChatPage() {
|
||||
const cookieStore = await cookies();
|
||||
const modelIdFromCookie = cookieStore.get("chat-model");
|
||||
const id = generateUUID();
|
||||
|
||||
if (!modelIdFromCookie) {
|
||||
return (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={false}
|
||||
id={id}
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialMessages={[]}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
key={id}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={false}
|
||||
id={id}
|
||||
initialChatModel={modelIdFromCookie.value}
|
||||
initialMessages={[]}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
key={id}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue