fix: title generation + ai sdk upgrade (#1392)
This commit is contained in:
parent
f19d3d4071
commit
9d5d8a3ea7
38 changed files with 1422 additions and 2360 deletions
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
|
|
@ -31,5 +31,23 @@
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
"source.organizeImports.biome": "explicit"
|
"source.organizeImports.biome": "explicit"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[mdx]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
// biome-ignore lint/performance/noBarrelFile: "Required"
|
|
||||||
export { GET, POST } from "@/app/(auth)/auth";
|
export { GET, POST } from "@/app/(auth)/auth";
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ declare module "next-auth" {
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required"
|
|
||||||
interface User {
|
interface User {
|
||||||
id?: string;
|
id?: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,15 @@ export async function generateTitleFromUserMessage({
|
||||||
}: {
|
}: {
|
||||||
message: UIMessage;
|
message: UIMessage;
|
||||||
}) {
|
}) {
|
||||||
const { text: title } = await generateText({
|
const { text } = await generateText({
|
||||||
model: getTitleModel(),
|
model: getTitleModel(),
|
||||||
system: titlePrompt,
|
system: titlePrompt,
|
||||||
prompt: getTextFromMessage(message),
|
prompt: getTextFromMessage(message),
|
||||||
});
|
});
|
||||||
|
return text
|
||||||
return title;
|
.replace(/^[#*"\s]+/, "")
|
||||||
|
.replace(/["]+$/, "")
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTrailingMessages({ id }: { id: string }) {
|
export async function deleteTrailingMessages({ id }: { id: string }) {
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,3 @@
|
||||||
import { createUIMessageStream, JsonToSseTransformStream } from "ai";
|
export function GET() {
|
||||||
import { differenceInSeconds } from "date-fns";
|
|
||||||
import { auth } from "@/app/(auth)/auth";
|
|
||||||
import {
|
|
||||||
getChatById,
|
|
||||||
getMessagesByChatId,
|
|
||||||
getStreamIdsByChatId,
|
|
||||||
} from "@/lib/db/queries";
|
|
||||||
import type { Chat } from "@/lib/db/schema";
|
|
||||||
import { ChatSDKError } from "@/lib/errors";
|
|
||||||
import type { ChatMessage } from "@/lib/types";
|
|
||||||
import { getStreamContext } from "../../route";
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
_: Request,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
const { id: chatId } = await params;
|
|
||||||
|
|
||||||
const streamContext = getStreamContext();
|
|
||||||
const resumeRequestedAt = new Date();
|
|
||||||
|
|
||||||
if (!streamContext) {
|
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatId) {
|
|
||||||
return new ChatSDKError("bad_request:api").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
return new ChatSDKError("unauthorized:chat").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
let chat: Chat | null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
chat = await getChatById({ id: chatId });
|
|
||||||
} catch {
|
|
||||||
return new ChatSDKError("not_found:chat").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
return new ChatSDKError("not_found:chat").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chat.visibility === "private" && chat.userId !== session.user.id) {
|
|
||||||
return new ChatSDKError("forbidden:chat").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamIds = await getStreamIdsByChatId({ chatId });
|
|
||||||
|
|
||||||
if (!streamIds.length) {
|
|
||||||
return new ChatSDKError("not_found:stream").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentStreamId = streamIds.at(-1);
|
|
||||||
|
|
||||||
if (!recentStreamId) {
|
|
||||||
return new ChatSDKError("not_found:stream").toResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyDataStream = createUIMessageStream<ChatMessage>({
|
|
||||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: "Needs to exist"
|
|
||||||
execute: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = await streamContext.resumableStream(recentStreamId, () =>
|
|
||||||
emptyDataStream.pipeThrough(new JsonToSseTransformStream())
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* For when the generation is streaming during SSR
|
|
||||||
* but the resumable stream has concluded at this point.
|
|
||||||
*/
|
|
||||||
if (!stream) {
|
|
||||||
const messages = await getMessagesByChatId({ id: chatId });
|
|
||||||
const mostRecentMessage = messages.at(-1);
|
|
||||||
|
|
||||||
if (!mostRecentMessage) {
|
|
||||||
return new Response(emptyDataStream, { status: 200 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostRecentMessage.role !== "assistant") {
|
|
||||||
return new Response(emptyDataStream, { status: 200 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageCreatedAt = new Date(mostRecentMessage.createdAt);
|
|
||||||
|
|
||||||
if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) {
|
|
||||||
return new Response(emptyDataStream, { status: 200 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const restoredStream = createUIMessageStream<ChatMessage>({
|
|
||||||
execute: ({ writer }) => {
|
|
||||||
writer.write({
|
|
||||||
type: "data-appendMessage",
|
|
||||||
data: JSON.stringify(mostRecentMessage),
|
|
||||||
transient: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
restoredStream.pipeThrough(new JsonToSseTransformStream()),
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(stream, { status: 200 });
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@ import { geolocation } from "@vercel/functions";
|
||||||
import {
|
import {
|
||||||
convertToModelMessages,
|
convertToModelMessages,
|
||||||
createUIMessageStream,
|
createUIMessageStream,
|
||||||
JsonToSseTransformStream,
|
createUIMessageStreamResponse,
|
||||||
smoothStream,
|
generateId,
|
||||||
stepCountIs,
|
stepCountIs,
|
||||||
streamText,
|
streamText,
|
||||||
} from "ai";
|
} from "ai";
|
||||||
import { after } from "next/server";
|
import { after } from "next/server";
|
||||||
import {
|
import { createResumableStreamContext } from "resumable-stream";
|
||||||
createResumableStreamContext,
|
|
||||||
type ResumableStreamContext,
|
|
||||||
} from "resumable-stream";
|
|
||||||
import { auth, type UserType } from "@/app/(auth)/auth";
|
import { auth, type UserType } from "@/app/(auth)/auth";
|
||||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||||
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
||||||
|
|
@ -41,27 +38,15 @@ import { type PostRequestBody, postRequestBodySchema } from "./schema";
|
||||||
|
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
let globalStreamContext: ResumableStreamContext | null = null;
|
function getStreamContext() {
|
||||||
|
|
||||||
export function getStreamContext() {
|
|
||||||
if (!globalStreamContext) {
|
|
||||||
try {
|
try {
|
||||||
globalStreamContext = createResumableStreamContext({
|
return createResumableStreamContext({ waitUntil: after });
|
||||||
waitUntil: after,
|
} catch (_) {
|
||||||
});
|
return null;
|
||||||
} catch (error: any) {
|
|
||||||
if (error.message.includes("REDIS_URL")) {
|
|
||||||
console.log(
|
|
||||||
" > Resumable streams are disabled due to missing REDIS_URL"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return globalStreamContext;
|
export { getStreamContext };
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
let requestBody: PostRequestBody;
|
let requestBody: PostRequestBody;
|
||||||
|
|
@ -94,7 +79,6 @@ export async function POST(request: Request) {
|
||||||
return new ChatSDKError("rate_limit:chat").toResponse();
|
return new ChatSDKError("rate_limit:chat").toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a tool approval flow (all messages sent)
|
|
||||||
const isToolApprovalFlow = Boolean(messages);
|
const isToolApprovalFlow = Boolean(messages);
|
||||||
|
|
||||||
const chat = await getChatById({ id });
|
const chat = await getChatById({ id });
|
||||||
|
|
@ -105,24 +89,19 @@ export async function POST(request: Request) {
|
||||||
if (chat.userId !== session.user.id) {
|
if (chat.userId !== session.user.id) {
|
||||||
return new ChatSDKError("forbidden:chat").toResponse();
|
return new ChatSDKError("forbidden:chat").toResponse();
|
||||||
}
|
}
|
||||||
// Only fetch messages if chat already exists and not tool approval
|
|
||||||
if (!isToolApprovalFlow) {
|
if (!isToolApprovalFlow) {
|
||||||
messagesFromDb = await getMessagesByChatId({ id });
|
messagesFromDb = await getMessagesByChatId({ id });
|
||||||
}
|
}
|
||||||
} else if (message?.role === "user") {
|
} else if (message?.role === "user") {
|
||||||
// Save chat immediately with placeholder title
|
|
||||||
await saveChat({
|
await saveChat({
|
||||||
id,
|
id,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
title: "New chat",
|
title: "New chat",
|
||||||
visibility: selectedVisibilityType,
|
visibility: selectedVisibilityType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start title generation in parallel (don't await)
|
|
||||||
titlePromise = generateTitleFromUserMessage({ message });
|
titlePromise = generateTitleFromUserMessage({ message });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use all messages for tool approval, otherwise DB messages + new message
|
|
||||||
const uiMessages = isToolApprovalFlow
|
const uiMessages = isToolApprovalFlow
|
||||||
? (messages as ChatMessage[])
|
? (messages as ChatMessage[])
|
||||||
: [...convertToUIMessages(messagesFromDb), message as ChatMessage];
|
: [...convertToUIMessages(messagesFromDb), message as ChatMessage];
|
||||||
|
|
@ -136,7 +115,6 @@ export async function POST(request: Request) {
|
||||||
country,
|
country,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only save user messages to the database (not tool approval responses)
|
|
||||||
if (message?.role === "user") {
|
if (message?.role === "user") {
|
||||||
await saveMessages({
|
await saveMessages({
|
||||||
messages: [
|
messages: [
|
||||||
|
|
@ -152,29 +130,19 @@ export async function POST(request: Request) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamId = generateUUID();
|
|
||||||
await createStreamId({ streamId, chatId: id });
|
|
||||||
|
|
||||||
const stream = createUIMessageStream({
|
|
||||||
// Pass original messages for tool approval continuation
|
|
||||||
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
|
|
||||||
execute: async ({ writer: dataStream }) => {
|
|
||||||
// Handle title generation in parallel
|
|
||||||
if (titlePromise) {
|
|
||||||
titlePromise.then((title) => {
|
|
||||||
updateChatTitleById({ chatId: id, title });
|
|
||||||
dataStream.write({ type: "data-chat-title", data: title });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReasoningModel =
|
const isReasoningModel =
|
||||||
selectedChatModel.includes("reasoning") ||
|
selectedChatModel.includes("reasoning") ||
|
||||||
selectedChatModel.includes("thinking");
|
selectedChatModel.includes("thinking");
|
||||||
|
|
||||||
|
const modelMessages = await convertToModelMessages(uiMessages);
|
||||||
|
|
||||||
|
const stream = createUIMessageStream({
|
||||||
|
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
|
||||||
|
execute: async ({ writer: dataStream }) => {
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: getLanguageModel(selectedChatModel),
|
model: getLanguageModel(selectedChatModel),
|
||||||
system: systemPrompt({ selectedChatModel, requestHints }),
|
system: systemPrompt({ selectedChatModel, requestHints }),
|
||||||
messages: await convertToModelMessages(uiMessages),
|
messages: modelMessages,
|
||||||
stopWhen: stepCountIs(5),
|
stopWhen: stepCountIs(5),
|
||||||
experimental_activeTools: isReasoningModel
|
experimental_activeTools: isReasoningModel
|
||||||
? []
|
? []
|
||||||
|
|
@ -184,9 +152,6 @@ export async function POST(request: Request) {
|
||||||
"updateDocument",
|
"updateDocument",
|
||||||
"requestSuggestions",
|
"requestSuggestions",
|
||||||
],
|
],
|
||||||
experimental_transform: isReasoningModel
|
|
||||||
? undefined
|
|
||||||
: smoothStream({ chunking: "word" }),
|
|
||||||
providerOptions: isReasoningModel
|
providerOptions: isReasoningModel
|
||||||
? {
|
? {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
|
|
@ -198,10 +163,7 @@ export async function POST(request: Request) {
|
||||||
getWeather,
|
getWeather,
|
||||||
createDocument: createDocument({ session, dataStream }),
|
createDocument: createDocument({ session, dataStream }),
|
||||||
updateDocument: updateDocument({ session, dataStream }),
|
updateDocument: updateDocument({ session, dataStream }),
|
||||||
requestSuggestions: requestSuggestions({
|
requestSuggestions: requestSuggestions({ session, dataStream }),
|
||||||
session,
|
|
||||||
dataStream,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
experimental_telemetry: {
|
experimental_telemetry: {
|
||||||
isEnabled: isProductionEnvironment,
|
isEnabled: isProductionEnvironment,
|
||||||
|
|
@ -209,28 +171,25 @@ export async function POST(request: Request) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
result.consumeStream();
|
dataStream.merge(result.toUIMessageStream({ sendReasoning: true }));
|
||||||
|
|
||||||
dataStream.merge(
|
if (titlePromise) {
|
||||||
result.toUIMessageStream({
|
const title = await titlePromise;
|
||||||
sendReasoning: true,
|
dataStream.write({ type: "data-chat-title", data: title });
|
||||||
})
|
updateChatTitleById({ chatId: id, title });
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
generateId: generateUUID,
|
generateId: generateUUID,
|
||||||
onFinish: async ({ messages: finishedMessages }) => {
|
onFinish: async ({ messages: finishedMessages }) => {
|
||||||
if (isToolApprovalFlow) {
|
if (isToolApprovalFlow) {
|
||||||
// For tool approval, update existing messages (tool state changed) and save new ones
|
|
||||||
for (const finishedMsg of finishedMessages) {
|
for (const finishedMsg of finishedMessages) {
|
||||||
const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id);
|
const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id);
|
||||||
if (existingMsg) {
|
if (existingMsg) {
|
||||||
// Update existing message with new parts (tool state changed)
|
|
||||||
await updateMessage({
|
await updateMessage({
|
||||||
id: finishedMsg.id,
|
id: finishedMsg.id,
|
||||||
parts: finishedMsg.parts,
|
parts: finishedMsg.parts,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Save new message
|
|
||||||
await saveMessages({
|
await saveMessages({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
|
@ -246,7 +205,6 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (finishedMessages.length > 0) {
|
} else if (finishedMessages.length > 0) {
|
||||||
// Normal flow - save all finished messages
|
|
||||||
await saveMessages({
|
await saveMessages({
|
||||||
messages: finishedMessages.map((currentMessage) => ({
|
messages: finishedMessages.map((currentMessage) => ({
|
||||||
id: currentMessage.id,
|
id: currentMessage.id,
|
||||||
|
|
@ -259,28 +217,30 @@ export async function POST(request: Request) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => "Oops, an error occurred!",
|
||||||
return "Oops, an error occurred!";
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const streamContext = getStreamContext();
|
return createUIMessageStreamResponse({
|
||||||
|
stream,
|
||||||
if (streamContext) {
|
async consumeSseStream({ stream: sseStream }) {
|
||||||
|
if (!process.env.REDIS_URL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resumableStream = await streamContext.resumableStream(
|
const streamContext = getStreamContext();
|
||||||
|
if (streamContext) {
|
||||||
|
const streamId = generateId();
|
||||||
|
await createStreamId({ streamId, chatId: id });
|
||||||
|
await streamContext.createNewResumableStream(
|
||||||
streamId,
|
streamId,
|
||||||
() => stream.pipeThrough(new JsonToSseTransformStream())
|
() => sseStream
|
||||||
);
|
);
|
||||||
if (resumableStream) {
|
|
||||||
return new Response(resumableStream);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
console.error("Failed to create resumable stream:", error);
|
// ignore redis errors
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
});
|
||||||
return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const vercelId = request.headers.get("x-vercel-id");
|
const vercelId = request.headers.get("x-vercel-id");
|
||||||
|
|
||||||
|
|
@ -288,7 +248,6 @@ export async function POST(request: Request) {
|
||||||
return error.toResponse();
|
return error.toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Vercel AI Gateway credit card error
|
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
error.message?.includes(
|
error.message?.includes(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
@import "katex/dist/katex.min.css";
|
@import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
/* include utility classes in streamdown */
|
/* include utility classes in streamdown */
|
||||||
@source '../node_modules/streamdown/dist/index.js';
|
@source "../node_modules/streamdown/dist/index.js";
|
||||||
|
|
||||||
/* custom variant for setting dark mode programmatically */
|
/* custom variant for setting dark mode programmatically */
|
||||||
@custom-variant dark (&:is(.dark, .dark *));
|
@custom-variant dark (&:is(.dark, .dark *));
|
||||||
|
|
|
||||||
47
biome.jsonc
47
biome.jsonc
|
|
@ -1,9 +1,23 @@
|
||||||
{
|
{
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
"extends": ["ultracite"],
|
"extends": [
|
||||||
|
"ultracite/biome/core",
|
||||||
|
"ultracite/biome/next",
|
||||||
|
"ultracite/biome/react"
|
||||||
|
],
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"includes": [
|
"includes": [
|
||||||
"**/*",
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.jsx",
|
||||||
|
"!node_modules",
|
||||||
|
"!.next",
|
||||||
|
"!ai-sdk",
|
||||||
"!components/ui",
|
"!components/ui",
|
||||||
"!lib/utils.ts",
|
"!lib/utils.ts",
|
||||||
"!hooks/use-mobile.ts"
|
"!hooks/use-mobile.ts"
|
||||||
|
|
@ -12,39 +26,34 @@
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
/* Needs more work to fix */
|
|
||||||
"noExplicitAny": "off",
|
"noExplicitAny": "off",
|
||||||
|
|
||||||
/* Allow for Tailwind @ rules */
|
|
||||||
"noUnknownAtRules": "off",
|
"noUnknownAtRules": "off",
|
||||||
|
|
||||||
/* Allowing console for debugging */
|
|
||||||
"noConsole": "off",
|
"noConsole": "off",
|
||||||
|
|
||||||
/* Needed for generateUUID() */
|
|
||||||
"noBitwiseOperators": "off"
|
"noBitwiseOperators": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
/* Allowing magic numbers */
|
|
||||||
"noMagicNumbers": "off",
|
"noMagicNumbers": "off",
|
||||||
|
"noNestedTernary": "off",
|
||||||
/* Needs more work to fix */
|
"useConsistentTypeDefinitions": "off"
|
||||||
"noNestedTernary": "off"
|
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
/* Too many false positives */
|
"noUnnecessaryConditions": "off",
|
||||||
"noUnnecessaryConditions": "off"
|
"useSortedClasses": "off"
|
||||||
},
|
},
|
||||||
"complexity": {
|
"complexity": {
|
||||||
/* Needs more work to fix */
|
|
||||||
"noExcessiveCognitiveComplexity": "off",
|
"noExcessiveCognitiveComplexity": "off",
|
||||||
|
|
||||||
/* This one has false positives. It's a bit... iffy 😉 */
|
|
||||||
"useSimplifiedLogicExpression": "off"
|
"useSimplifiedLogicExpression": "off"
|
||||||
},
|
},
|
||||||
"a11y": {
|
"a11y": {
|
||||||
/* Needs more work to fix */
|
|
||||||
"noSvgWithoutTitle": "off"
|
"noSvgWithoutTitle": "off"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"useUniqueElementIds": "off",
|
||||||
|
"useImageSize": "off"
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"noBarrelFile": "off",
|
||||||
|
"useTopLevelRegex": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ export const ChainOfThoughtStep = memo(
|
||||||
>
|
>
|
||||||
<div className="relative mt-0.5">
|
<div className="relative mt-0.5">
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
<div className="-mx-px absolute top-7 bottom-0 left-1/2 w-px bg-border" />
|
<div className="absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-2 overflow-hidden">
|
<div className="flex-1 space-y-2 overflow-hidden">
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
type ComponentProps,
|
|
||||||
createContext,
|
|
||||||
type HTMLAttributes,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
code: string;
|
|
||||||
language: BundledLanguage;
|
|
||||||
showLineNumbers?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CodeBlockContextType = {
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeBlockContext = createContext<CodeBlockContextType>({
|
|
||||||
code: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const lineNumberTransformer: ShikiTransformer = {
|
|
||||||
name: "line-numbers",
|
|
||||||
line(node, line) {
|
|
||||||
node.children.unshift({
|
|
||||||
type: "element",
|
|
||||||
tagName: "span",
|
|
||||||
properties: {
|
|
||||||
className: [
|
|
||||||
"inline-block",
|
|
||||||
"min-w-10",
|
|
||||||
"mr-4",
|
|
||||||
"text-right",
|
|
||||||
"select-none",
|
|
||||||
"text-muted-foreground",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
children: [{ type: "text", value: String(line) }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function highlightCode(
|
|
||||||
code: string,
|
|
||||||
language: BundledLanguage,
|
|
||||||
showLineNumbers = false
|
|
||||||
) {
|
|
||||||
const transformers: ShikiTransformer[] = showLineNumbers
|
|
||||||
? [lineNumberTransformer]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return await Promise.all([
|
|
||||||
codeToHtml(code, {
|
|
||||||
lang: language,
|
|
||||||
theme: "one-light",
|
|
||||||
transformers,
|
|
||||||
}),
|
|
||||||
codeToHtml(code, {
|
|
||||||
lang: language,
|
|
||||||
theme: "one-dark-pro",
|
|
||||||
transformers,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CodeBlock = ({
|
|
||||||
code,
|
|
||||||
language,
|
|
||||||
showLineNumbers = false,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: CodeBlockProps) => {
|
|
||||||
const [html, setHtml] = useState<string>("");
|
|
||||||
const [darkHtml, setDarkHtml] = useState<string>("");
|
|
||||||
const mounted = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
|
|
||||||
if (!mounted.current) {
|
|
||||||
setHtml(light);
|
|
||||||
setDarkHtml(dark);
|
|
||||||
mounted.current = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted.current = false;
|
|
||||||
};
|
|
||||||
}, [code, language, showLineNumbers]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeBlockContext.Provider value={{ code }}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="overflow-auto dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
|
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="hidden overflow-auto dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
|
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
|
||||||
dangerouslySetInnerHTML={{ __html: darkHtml }}
|
|
||||||
/>
|
|
||||||
{children && (
|
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-2">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CodeBlockContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
|
||||||
onCopy?: () => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CodeBlockCopyButton = ({
|
|
||||||
onCopy,
|
|
||||||
onError,
|
|
||||||
timeout = 2000,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: CodeBlockCopyButtonProps) => {
|
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
|
||||||
const { code } = useContext(CodeBlockContext);
|
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
|
||||||
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
|
||||||
onError?.(new Error("Clipboard API not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(code);
|
|
||||||
setIsCopied(true);
|
|
||||||
onCopy?.();
|
|
||||||
setTimeout(() => setIsCopied(false), timeout);
|
|
||||||
} catch (error) {
|
|
||||||
onError?.(error as Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <Icon size={14} />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -12,7 +12,6 @@ export const Image = ({
|
||||||
mediaType,
|
mediaType,
|
||||||
...props
|
...props
|
||||||
}: ImageProps) => (
|
}: ImageProps) => (
|
||||||
// biome-ignore lint/nursery/useImageSize: dynamic base64 content
|
|
||||||
// biome-ignore lint/performance/noImgElement: base64 data URLs require native img
|
// biome-ignore lint/performance/noImgElement: base64 data URLs require native img
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ export const ModelSelectorLogoGroup = ({
|
||||||
}: ModelSelectorLogoGroupProps) => (
|
}: ModelSelectorLogoGroupProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"-space-x-1 flex shrink-0 items-center [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
|
"flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,7 @@ export function PromptInputProvider({
|
||||||
(FileUIPart & { id: string })[]
|
(FileUIPart & { id: string })[]
|
||||||
>([]);
|
>([]);
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: noop initializer
|
const openRef = useRef<() => void>(() => undefined);
|
||||||
const openRef = useRef<() => void>(() => {});
|
|
||||||
|
|
||||||
const add = useCallback((files: File[] | FileList) => {
|
const add = useCallback((files: File[] | FileList) => {
|
||||||
const incoming = Array.from(files);
|
const incoming = Array.from(files);
|
||||||
|
|
@ -309,18 +308,18 @@ export function PromptInputAttachment({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative size-5 shrink-0">
|
<div className="relative size-5 shrink-0">
|
||||||
<div className="absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded bg-background transition-opacity group-hover:opacity-0">
|
<div className="flex overflow-hidden absolute inset-0 justify-center items-center rounded transition-opacity size-5 bg-background group-hover:opacity-0">
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
/* biome-ignore lint/performance/noImgElement: dynamic user uploads */
|
/* biome-ignore lint/performance/noImgElement: dynamic user uploads */
|
||||||
<img
|
<img
|
||||||
alt={filename || "attachment"}
|
alt={filename || "attachment"}
|
||||||
className="size-5 object-cover"
|
className="object-cover size-5"
|
||||||
height={20}
|
height={20}
|
||||||
src={data.url}
|
src={data.url}
|
||||||
width={20}
|
width={20}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-5 items-center justify-center text-muted-foreground">
|
<div className="flex justify-center items-center size-5 text-muted-foreground">
|
||||||
<PaperclipIcon className="size-3" />
|
<PaperclipIcon className="size-3" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -343,14 +342,14 @@ export function PromptInputAttachment({
|
||||||
<span className="flex-1 truncate">{attachmentLabel}</span>
|
<span className="flex-1 truncate">{attachmentLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<PromptInputHoverCardContent className="w-auto p-2">
|
<PromptInputHoverCardContent className="p-2 w-auto">
|
||||||
<div className="w-auto space-y-3">
|
<div className="space-y-3 w-auto">
|
||||||
{isImage && (
|
{isImage && (
|
||||||
<div className="flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border">
|
<div className="flex overflow-hidden justify-center items-center w-96 max-h-96 rounded-md border">
|
||||||
{/* biome-ignore lint/performance/noImgElement: dynamic user uploads */}
|
{/* biome-ignore lint/performance/noImgElement: dynamic user uploads */}
|
||||||
<img
|
<img
|
||||||
alt={filename || "attachment preview"}
|
alt={filename || "attachment preview"}
|
||||||
className="max-h-full max-w-full object-contain"
|
className="object-contain max-w-full max-h-full"
|
||||||
height={384}
|
height={384}
|
||||||
src={data.url}
|
src={data.url}
|
||||||
width={448}
|
width={448}
|
||||||
|
|
@ -359,11 +358,11 @@ export function PromptInputAttachment({
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="min-w-0 flex-1 space-y-1 px-0.5">
|
<div className="min-w-0 flex-1 space-y-1 px-0.5">
|
||||||
<h4 className="truncate font-semibold text-sm leading-none">
|
<h4 className="text-sm font-semibold leading-none truncate">
|
||||||
{filename || (isImage ? "Image" : "Attachment")}
|
{filename || (isImage ? "Image" : "Attachment")}
|
||||||
</h4>
|
</h4>
|
||||||
{data.mediaType && (
|
{data.mediaType && (
|
||||||
<p className="truncate font-mono text-muted-foreground text-xs">
|
<p className="font-mono text-xs truncate text-muted-foreground">
|
||||||
{data.mediaType}
|
{data.mediaType}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -395,7 +394,7 @@ export function PromptInputAttachments({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
|
className={cn("flex flex-wrap gap-2 items-center p-3 w-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{attachments.files.map((file) => (
|
{attachments.files.map((file) => (
|
||||||
|
|
@ -913,7 +912,7 @@ export const PromptInputTextarea = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroupTextarea
|
<InputGroupTextarea
|
||||||
className={cn("field-sizing-content max-h-48 min-h-16", className)}
|
className={cn("max-h-48 field-sizing-content min-h-16", className)}
|
||||||
name="message"
|
name="message"
|
||||||
onCompositionEnd={() => setIsComposing(false)}
|
onCompositionEnd={() => setIsComposing(false)}
|
||||||
onCompositionStart={() => setIsComposing(true)}
|
onCompositionStart={() => setIsComposing(true)}
|
||||||
|
|
@ -937,7 +936,7 @@ export const PromptInputHeader = ({
|
||||||
}: PromptInputHeaderProps) => (
|
}: PromptInputHeaderProps) => (
|
||||||
<InputGroupAddon
|
<InputGroupAddon
|
||||||
align="block-end"
|
align="block-end"
|
||||||
className={cn("order-first flex-wrap gap-1", className)}
|
className={cn("flex-wrap order-first gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -953,7 +952,7 @@ export const PromptInputFooter = ({
|
||||||
}: PromptInputFooterProps) => (
|
}: PromptInputFooterProps) => (
|
||||||
<InputGroupAddon
|
<InputGroupAddon
|
||||||
align="block-end"
|
align="block-end"
|
||||||
className={cn("justify-between gap-1", className)}
|
className={cn("gap-1 justify-between", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -964,7 +963,7 @@ export const PromptInputTools = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PromptInputToolsProps) => (
|
}: PromptInputToolsProps) => (
|
||||||
<div className={cn("flex items-center gap-1", className)} {...props} />
|
<div className={cn("flex gap-1 items-center", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
||||||
|
|
@ -1046,7 +1045,7 @@ export const PromptInputSubmit = ({
|
||||||
let Icon = <CornerDownLeftIcon className="size-4" />;
|
let Icon = <CornerDownLeftIcon className="size-4" />;
|
||||||
|
|
||||||
if (status === "submitted") {
|
if (status === "submitted") {
|
||||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
Icon = <Loader2Icon className="animate-spin size-4" />;
|
||||||
} else if (status === "streaming") {
|
} else if (status === "streaming") {
|
||||||
Icon = <SquareIcon className="size-4" />;
|
Icon = <SquareIcon className="size-4" />;
|
||||||
} else if (status === "error") {
|
} else if (status === "error") {
|
||||||
|
|
@ -1111,7 +1110,6 @@ interface SpeechRecognitionErrorEvent extends Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// biome-ignore lint/nursery/useConsistentTypeDefinitions: global augmentation requires interface
|
|
||||||
interface Window {
|
interface Window {
|
||||||
SpeechRecognition: {
|
SpeechRecognition: {
|
||||||
new (): SpeechRecognition;
|
new (): SpeechRecognition;
|
||||||
|
|
@ -1244,7 +1242,7 @@ export const PromptInputSelectTrigger = ({
|
||||||
}: PromptInputSelectTriggerProps) => (
|
}: PromptInputSelectTriggerProps) => (
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
"font-medium bg-transparent border-none shadow-none transition-colors text-muted-foreground",
|
||||||
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
@ -1332,7 +1330,7 @@ export const PromptInputTabLabel = ({
|
||||||
}: PromptInputTabLabelProps) => (
|
}: PromptInputTabLabelProps) => (
|
||||||
<h3
|
<h3
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-2 px-3 font-medium text-muted-foreground text-xs",
|
"px-3 mb-2 text-xs font-medium text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -1356,7 +1354,7 @@ export const PromptInputTabItem = ({
|
||||||
}: PromptInputTabItemProps) => (
|
}: PromptInputTabItemProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent",
|
"flex gap-2 items-center px-3 py-2 text-xs hover:bg-accent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ export const QueueList = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueListProps) => (
|
}: QueueListProps) => (
|
||||||
<ScrollArea className={cn("-mb-1 mt-2", className)} {...props}>
|
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
|
||||||
<div className="max-h-40 pr-4">
|
<div className="max-h-40 pr-4">
|
||||||
<ul>{children}</ul>
|
<ul>{children}</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -242,7 +242,7 @@ export const QueueSectionLabel = ({
|
||||||
...props
|
...props
|
||||||
}: QueueSectionLabelProps) => (
|
}: QueueSectionLabelProps) => (
|
||||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
<span className={cn("flex items-center gap-2", className)} {...props}>
|
||||||
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
||||||
{icon}
|
{icon}
|
||||||
<span>
|
<span>
|
||||||
{count} {label}
|
{count} {label}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CodeBlock } from "./code-block";
|
|
||||||
|
|
||||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
|
@ -111,9 +110,9 @@ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
Parameters
|
Parameters
|
||||||
</h4>
|
</h4>
|
||||||
<div className="rounded-md bg-muted/50">
|
<pre className="overflow-x-auto rounded-md bg-muted/50 p-3 font-mono text-xs">
|
||||||
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
{JSON.stringify(input, null, 2)}
|
||||||
</div>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -132,15 +131,21 @@ export const ToolOutput = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Output = <div>{output as ReactNode}</div>;
|
const renderOutput = () => {
|
||||||
|
|
||||||
if (typeof output === "object" && !isValidElement(output)) {
|
if (typeof output === "object" && !isValidElement(output)) {
|
||||||
Output = (
|
return (
|
||||||
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
|
<pre className="overflow-x-auto p-3 font-mono text-xs">
|
||||||
|
{JSON.stringify(output, null, 2)}
|
||||||
|
</pre>
|
||||||
);
|
);
|
||||||
} else if (typeof output === "string") {
|
|
||||||
Output = <CodeBlock code={output} language="json" />;
|
|
||||||
}
|
}
|
||||||
|
if (typeof output === "string") {
|
||||||
|
return (
|
||||||
|
<pre className="overflow-x-auto p-3 font-mono text-xs">{output}</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="p-3">{output as ReactNode}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2 p-4", className)} {...props}>
|
<div className={cn("space-y-2 p-4", className)} {...props}>
|
||||||
|
|
@ -155,8 +160,8 @@ export const ToolOutput = ({
|
||||||
: "bg-muted/50 text-foreground"
|
: "bg-muted/50 text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{errorText && <div>{errorText}</div>}
|
{errorText && <div className="p-3">{errorText}</div>}
|
||||||
{Output}
|
{!errorText && renderOutput()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,9 @@ export function Chat({
|
||||||
} = useChat<ChatMessage>({
|
} = useChat<ChatMessage>({
|
||||||
id,
|
id,
|
||||||
messages: initialMessages,
|
messages: initialMessages,
|
||||||
experimental_throttle: 100,
|
|
||||||
generateId: generateUUID,
|
generateId: generateUUID,
|
||||||
// Auto-continue after tool approval (only for APPROVED tools)
|
|
||||||
// Denied tools don't need server continuation - state is saved on next user message
|
|
||||||
sendAutomaticallyWhen: ({ messages: currentMessages }) => {
|
sendAutomaticallyWhen: ({ messages: currentMessages }) => {
|
||||||
const lastMessage = currentMessages.at(-1);
|
const lastMessage = currentMessages.at(-1);
|
||||||
// Only continue if a tool was APPROVED (not denied)
|
|
||||||
const shouldContinue =
|
const shouldContinue =
|
||||||
lastMessage?.parts?.some(
|
lastMessage?.parts?.some(
|
||||||
(part) =>
|
(part) =>
|
||||||
|
|
@ -111,10 +107,6 @@ export function Chat({
|
||||||
fetch: fetchWithErrorHandlers,
|
fetch: fetchWithErrorHandlers,
|
||||||
prepareSendMessagesRequest(request) {
|
prepareSendMessagesRequest(request) {
|
||||||
const lastMessage = request.messages.at(-1);
|
const lastMessage = request.messages.at(-1);
|
||||||
|
|
||||||
// Check if this is a tool approval continuation:
|
|
||||||
// - Last message is NOT a user message (meaning no new user input)
|
|
||||||
// - OR any message has tool parts that were responded to (approved or denied)
|
|
||||||
const isToolApprovalContinuation =
|
const isToolApprovalContinuation =
|
||||||
lastMessage?.role !== "user" ||
|
lastMessage?.role !== "user" ||
|
||||||
request.messages.some((msg) =>
|
request.messages.some((msg) =>
|
||||||
|
|
@ -129,7 +121,6 @@ export function Chat({
|
||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
// Send all messages for tool approval continuation, otherwise just the last user message
|
|
||||||
...(isToolApprovalContinuation
|
...(isToolApprovalContinuation
|
||||||
? { messages: request.messages }
|
? { messages: request.messages }
|
||||||
: { message: lastMessage }),
|
: { message: lastMessage }),
|
||||||
|
|
@ -148,7 +139,6 @@ export function Chat({
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error instanceof ChatSDKError) {
|
if (error instanceof ChatSDKError) {
|
||||||
// Check if it's a credit card error
|
|
||||||
if (
|
if (
|
||||||
error.message?.includes("AI Gateway requires a valid credit card")
|
error.message?.includes("AI Gateway requires a valid credit card")
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,6 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||||
{consoleOutput.contents.map((content, contentIndex) =>
|
{consoleOutput.contents.map((content, contentIndex) =>
|
||||||
content.type === "image" ? (
|
content.type === "image" ? (
|
||||||
<picture key={`${consoleOutput.id}-${contentIndex}`}>
|
<picture key={`${consoleOutput.id}-${contentIndex}`}>
|
||||||
{/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */}
|
|
||||||
<img
|
<img
|
||||||
alt="output"
|
alt="output"
|
||||||
className="w-full max-w-(--breakpoint-toast-mobile) rounded-md"
|
className="w-full max-w-(--breakpoint-toast-mobile) rounded-md"
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
|
|
||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import {
|
|
||||||
oneDark,
|
|
||||||
oneLight,
|
|
||||||
} from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type CodeBlockContextType = {
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeBlockContext = createContext<CodeBlockContextType>({
|
|
||||||
code: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
code: string;
|
|
||||||
language: string;
|
|
||||||
showLineNumbers?: boolean;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CodeBlock = ({
|
|
||||||
code,
|
|
||||||
language,
|
|
||||||
showLineNumbers = false,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: CodeBlockProps) => (
|
|
||||||
<CodeBlockContext.Provider value={{ code }}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<SyntaxHighlighter
|
|
||||||
className="overflow-hidden dark:hidden"
|
|
||||||
codeTagProps={{
|
|
||||||
className: "font-mono text-sm",
|
|
||||||
}}
|
|
||||||
customStyle={{
|
|
||||||
margin: 0,
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
background: "hsl(var(--background))",
|
|
||||||
color: "hsl(var(--foreground))",
|
|
||||||
overflowX: "auto",
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
|
||||||
language={language}
|
|
||||||
lineNumberStyle={{
|
|
||||||
color: "hsl(var(--muted-foreground))",
|
|
||||||
paddingRight: "1rem",
|
|
||||||
minWidth: "2.5rem",
|
|
||||||
}}
|
|
||||||
showLineNumbers={showLineNumbers}
|
|
||||||
style={oneLight}
|
|
||||||
>
|
|
||||||
{code}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
className="hidden overflow-hidden dark:block"
|
|
||||||
codeTagProps={{
|
|
||||||
className: "font-mono text-sm",
|
|
||||||
}}
|
|
||||||
customStyle={{
|
|
||||||
margin: 0,
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
background: "hsl(var(--background))",
|
|
||||||
color: "hsl(var(--foreground))",
|
|
||||||
overflowX: "auto",
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
|
||||||
language={language}
|
|
||||||
lineNumberStyle={{
|
|
||||||
color: "hsl(var(--muted-foreground))",
|
|
||||||
paddingRight: "1rem",
|
|
||||||
minWidth: "2.5rem",
|
|
||||||
}}
|
|
||||||
showLineNumbers={showLineNumbers}
|
|
||||||
style={oneDark}
|
|
||||||
>
|
|
||||||
{code}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
{children && (
|
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-2">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CodeBlockContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
|
||||||
onCopy?: () => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CodeBlockCopyButton = ({
|
|
||||||
onCopy,
|
|
||||||
onError,
|
|
||||||
timeout = 2000,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: CodeBlockCopyButtonProps) => {
|
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
|
||||||
const { code } = useContext(CodeBlockContext);
|
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
|
||||||
if (typeof window === "undefined" || !navigator.clipboard.writeText) {
|
|
||||||
onError?.(new Error("Clipboard API not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(code);
|
|
||||||
setIsCopied(true);
|
|
||||||
onCopy?.();
|
|
||||||
setTimeout(() => setIsCopied(false), timeout);
|
|
||||||
} catch (error) {
|
|
||||||
onError?.(error as Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <Icon size={14} />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -49,7 +49,7 @@ export const ConversationScrollButton = ({
|
||||||
!isAtBottom && (
|
!isAtBottom && (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"-translate-x-1/2 absolute bottom-4 left-1/2 z-10 rounded-full shadow-lg",
|
"absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full shadow-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={handleScrollToBottom}
|
onClick={handleScrollToBottom}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ export const Image = ({
|
||||||
mediaType,
|
mediaType,
|
||||||
...props
|
...props
|
||||||
}: ImageProps) => (
|
}: ImageProps) => (
|
||||||
// biome-ignore lint/nursery/useImageSize: "Generated image without explicit size"
|
// biome-ignore lint/performance/noImgElement: base64 data URLs require native img
|
||||||
// biome-ignore lint/performance/noImgElement: "Generated image without explicit size"
|
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
alt={props.alt}
|
alt={props.alt}
|
||||||
|
|
|
||||||
|
|
@ -50,22 +50,25 @@ export const PromptInputTextarea = ({
|
||||||
}: PromptInputTextareaProps) => {
|
}: PromptInputTextareaProps) => {
|
||||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
// Don't submit if IME composition is in progress
|
|
||||||
if (e.nativeEvent.isComposing) {
|
if (e.nativeEvent.isComposing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Allow newline
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit on Enter (without Shift)
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const form = e.currentTarget.form;
|
const form = e.currentTarget.form;
|
||||||
if (form) {
|
const submitButton = form?.querySelector(
|
||||||
form.requestSubmit();
|
'button[type="submit"]'
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
if (submitButton?.disabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form?.requestSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,16 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ResponseProps = ComponentProps<typeof Streamdown>;
|
type ResponseProps = ComponentProps<typeof Streamdown>;
|
||||||
|
|
||||||
export const Response = ({ className, ...props }: ResponseProps) => (
|
export function Response({ className, children, ...props }: ResponseProps) {
|
||||||
|
return (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</Streamdown>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
Response.displayName = "Response";
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import {
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CodeBlock } from "./code-block";
|
|
||||||
|
|
||||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
|
@ -111,9 +110,9 @@ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
Parameters
|
Parameters
|
||||||
</h4>
|
</h4>
|
||||||
<div className="rounded-md bg-muted/50">
|
<pre className="overflow-x-auto rounded-md bg-muted/50 p-3 font-mono text-xs">
|
||||||
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
{JSON.stringify(input, null, 2)}
|
||||||
</div>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ export function ImageEditor({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<picture>
|
<picture>
|
||||||
{/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */}
|
|
||||||
<img
|
<img
|
||||||
alt={title}
|
alt={title}
|
||||||
className={cn("h-fit w-full max-w-[800px]", {
|
className={cn("h-fit w-full max-w-[800px]", {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function PureMessageActions({
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{setMode && (
|
{setMode && (
|
||||||
<Action
|
<Action
|
||||||
className="-left-10 absolute top-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover/message:opacity-100"
|
className="absolute top-0 -left-10 opacity-0 transition-opacity focus-visible:opacity-100 group-hover/message:opacity-100"
|
||||||
data-testid="message-edit-button"
|
data-testid="message-edit-button"
|
||||||
onClick={() => setMode("edit")}
|
onClick={() => setMode("edit")}
|
||||||
tooltip="Edit"
|
tooltip="Edit"
|
||||||
|
|
|
||||||
|
|
@ -108,15 +108,19 @@ const PurePreviewMessage = ({
|
||||||
const { type } = part;
|
const { type } = part;
|
||||||
const key = `message-${message.id}-part-${index}`;
|
const key = `message-${message.id}-part-${index}`;
|
||||||
|
|
||||||
if (type === "reasoning" && part.text?.trim().length > 0) {
|
if (type === "reasoning") {
|
||||||
|
const hasContent = part.text?.trim().length > 0;
|
||||||
|
const isStreaming = "state" in part && part.state === "streaming";
|
||||||
|
if (hasContent || isStreaming) {
|
||||||
return (
|
return (
|
||||||
<MessageReasoning
|
<MessageReasoning
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isStreaming}
|
||||||
key={key}
|
key={key}
|
||||||
reasoning={part.text}
|
reasoning={part.text || ""}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
if (mode === "view") {
|
if (mode === "view") {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ function PureMessages({
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="Scroll to bottom"
|
aria-label="Scroll to bottom"
|
||||||
className={`-translate-x-1/2 absolute bottom-4 left-1/2 z-10 rounded-full border bg-background p-2 shadow-lg transition-all hover:bg-muted ${
|
className={`absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border bg-background p-2 shadow-lg transition-all hover:bg-muted ${
|
||||||
isAtBottom
|
isAtBottom
|
||||||
? "pointer-events-none scale-0 opacity-0"
|
? "pointer-events-none scale-0 opacity-0"
|
||||||
: "pointer-events-auto scale-100 opacity-100"
|
: "pointer-events-auto scale-100 opacity-100"
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ function PureMultimodalInput({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="-top-4 -left-4 pointer-events-none fixed size-0.5 opacity-0"
|
className="pointer-events-none fixed -top-4 -left-4 size-0.5 opacity-0"
|
||||||
multiple
|
multiple
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|
@ -320,6 +320,9 @@ function PureMultimodalInput({
|
||||||
className="rounded-xl border border-border bg-background p-3 shadow-xs transition-all duration-200 focus-within:border-border hover:border-muted-foreground/50"
|
className="rounded-xl border border-border bg-background p-3 shadow-xs transition-all duration-200 focus-within:border-border hover:border-muted-foreground/50"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!input.trim() && attachments.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (status !== "ready") {
|
if (status !== "ready") {
|
||||||
toast.error("Please wait for the model to finish its response!");
|
toast.error("Please wait for the model to finish its response!");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const Suggestion = ({
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ opacity: 1, y: -20 }}
|
animate={{ opacity: 1, y: -20 }}
|
||||||
className="-right-12 md:-right-16 absolute z-50 flex w-56 flex-col gap-3 rounded-2xl border bg-background p-3 font-sans text-sm shadow-xl"
|
className="absolute -right-12 z-50 flex w-56 flex-col gap-3 rounded-2xl border bg-background p-3 font-sans text-sm shadow-xl md:-right-16"
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
key={suggestion.id}
|
key={suggestion.id}
|
||||||
|
|
@ -61,7 +61,7 @@ export const Suggestion = ({
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn("cursor-pointer p-1 text-muted-foreground", {
|
className={cn("cursor-pointer p-1 text-muted-foreground", {
|
||||||
"-right-8 absolute": artifactKind === "text",
|
"absolute -right-8": artifactKind === "text",
|
||||||
"sticky top-0 right-4": artifactKind === "code",
|
"sticky top-0 right-4": artifactKind === "code",
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
||||||
|
|
@ -123,10 +123,17 @@ export const updateDocumentPrompt = (
|
||||||
${currentContent}`;
|
${currentContent}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const titlePrompt = `Generate a very short chat title (2-5 words max) based on the user's message.
|
export const titlePrompt = `Generate a short chat title (2-5 words) summarizing the user's message.
|
||||||
Rules:
|
|
||||||
- Maximum 30 characters
|
Output ONLY the title text. No prefixes, no formatting.
|
||||||
- No quotes, colons, hashtags, or markdown
|
|
||||||
- Just the topic/intent, not a full sentence
|
Examples:
|
||||||
- If the message is a greeting like "hi" or "hello", respond with just "New conversation"
|
- "what's the weather in nyc" → Weather in NYC
|
||||||
- Be concise: "Weather in NYC" not "User asking about the weather in New York City"`;
|
- "help me write an essay about space" → Space Essay Help
|
||||||
|
- "hi" → New Conversation
|
||||||
|
- "debug my python code" → Python Debugging
|
||||||
|
|
||||||
|
Bad outputs (never do this):
|
||||||
|
- "# Space Essay" (no hashtags)
|
||||||
|
- "Title: Weather" (no prefixes)
|
||||||
|
- ""NYC Weather"" (no quotes)`;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function getTitleModel() {
|
||||||
if (isTestEnvironment && myProvider) {
|
if (isTestEnvironment && myProvider) {
|
||||||
return myProvider.languageModel("title-model");
|
return myProvider.languageModel("title-model");
|
||||||
}
|
}
|
||||||
return gateway.languageModel("anthropic/claude-haiku-4.5");
|
return gateway.languageModel("google/gemini-2.5-flash-lite");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArtifactModel() {
|
export function getArtifactModel() {
|
||||||
|
|
|
||||||
1
lib/db/migrations/0008_flat_forgotten_one.sql
Normal file
1
lib/db/migrations/0008_flat_forgotten_one.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "Chat" DROP COLUMN IF EXISTS "lastContext";
|
||||||
506
lib/db/migrations/meta/0008_snapshot.json
Normal file
506
lib/db/migrations/meta/0008_snapshot.json
Normal file
|
|
@ -0,0 +1,506 @@
|
||||||
|
{
|
||||||
|
"id": "31934f42-f6af-42a3-9320-b2e86fb67e81",
|
||||||
|
"prevId": "097660a7-976a-4b3e-8ebb-79312e3ece6f",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.Chat": {
|
||||||
|
"name": "Chat",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'private'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Chat_userId_User_id_fk": {
|
||||||
|
"name": "Chat_userId_User_id_fk",
|
||||||
|
"tableFrom": "Chat",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": ["userId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Document": {
|
||||||
|
"name": "Document",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'text'"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Document_userId_User_id_fk": {
|
||||||
|
"name": "Document_userId_User_id_fk",
|
||||||
|
"tableFrom": "Document",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": ["userId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"Document_id_createdAt_pk": {
|
||||||
|
"name": "Document_id_createdAt_pk",
|
||||||
|
"columns": ["id", "createdAt"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Message_v2": {
|
||||||
|
"name": "Message_v2",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"parts": {
|
||||||
|
"name": "parts",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"name": "attachments",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Message_v2_chatId_Chat_id_fk": {
|
||||||
|
"name": "Message_v2_chatId_Chat_id_fk",
|
||||||
|
"tableFrom": "Message_v2",
|
||||||
|
"tableTo": "Chat",
|
||||||
|
"columnsFrom": ["chatId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Message": {
|
||||||
|
"name": "Message",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Message_chatId_Chat_id_fk": {
|
||||||
|
"name": "Message_chatId_Chat_id_fk",
|
||||||
|
"tableFrom": "Message",
|
||||||
|
"tableTo": "Chat",
|
||||||
|
"columnsFrom": ["chatId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Stream": {
|
||||||
|
"name": "Stream",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Stream_chatId_Chat_id_fk": {
|
||||||
|
"name": "Stream_chatId_Chat_id_fk",
|
||||||
|
"tableFrom": "Stream",
|
||||||
|
"tableTo": "Chat",
|
||||||
|
"columnsFrom": ["chatId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"Stream_id_pk": {
|
||||||
|
"name": "Stream_id_pk",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Suggestion": {
|
||||||
|
"name": "Suggestion",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"name": "documentId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"documentCreatedAt": {
|
||||||
|
"name": "documentCreatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"originalText": {
|
||||||
|
"name": "originalText",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"suggestedText": {
|
||||||
|
"name": "suggestedText",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"isResolved": {
|
||||||
|
"name": "isResolved",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Suggestion_userId_User_id_fk": {
|
||||||
|
"name": "Suggestion_userId_User_id_fk",
|
||||||
|
"tableFrom": "Suggestion",
|
||||||
|
"tableTo": "User",
|
||||||
|
"columnsFrom": ["userId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
|
||||||
|
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
|
||||||
|
"tableFrom": "Suggestion",
|
||||||
|
"tableTo": "Document",
|
||||||
|
"columnsFrom": ["documentId", "documentCreatedAt"],
|
||||||
|
"columnsTo": ["id", "createdAt"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"Suggestion_id_pk": {
|
||||||
|
"name": "Suggestion_id_pk",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.User": {
|
||||||
|
"name": "User",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Vote_v2": {
|
||||||
|
"name": "Vote_v2",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"name": "messageId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"isUpvoted": {
|
||||||
|
"name": "isUpvoted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Vote_v2_chatId_Chat_id_fk": {
|
||||||
|
"name": "Vote_v2_chatId_Chat_id_fk",
|
||||||
|
"tableFrom": "Vote_v2",
|
||||||
|
"tableTo": "Chat",
|
||||||
|
"columnsFrom": ["chatId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Vote_v2_messageId_Message_v2_id_fk": {
|
||||||
|
"name": "Vote_v2_messageId_Message_v2_id_fk",
|
||||||
|
"tableFrom": "Vote_v2",
|
||||||
|
"tableTo": "Message_v2",
|
||||||
|
"columnsFrom": ["messageId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"Vote_v2_chatId_messageId_pk": {
|
||||||
|
"name": "Vote_v2_chatId_messageId_pk",
|
||||||
|
"columns": ["chatId", "messageId"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.Vote": {
|
||||||
|
"name": "Vote",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"name": "messageId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"isUpvoted": {
|
||||||
|
"name": "isUpvoted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"Vote_chatId_Chat_id_fk": {
|
||||||
|
"name": "Vote_chatId_Chat_id_fk",
|
||||||
|
"tableFrom": "Vote",
|
||||||
|
"tableTo": "Chat",
|
||||||
|
"columnsFrom": ["chatId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"Vote_messageId_Message_id_fk": {
|
||||||
|
"name": "Vote_messageId_Message_id_fk",
|
||||||
|
"tableFrom": "Vote",
|
||||||
|
"tableTo": "Message",
|
||||||
|
"columnsFrom": ["messageId"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"Vote_chatId_messageId_pk": {
|
||||||
|
"name": "Vote_chatId_messageId_pk",
|
||||||
|
"columns": ["chatId", "messageId"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,13 @@
|
||||||
"when": 1757362773211,
|
"when": 1757362773211,
|
||||||
"tag": "0007_flowery_ben_parker",
|
"tag": "0007_flowery_ben_parker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768479010084,
|
||||||
|
"tag": "0008_flat_forgotten_one",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
package.json
22
package.json
|
|
@ -6,8 +6,8 @@
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"build": "tsx lib/db/migrate && next build",
|
"build": "tsx lib/db/migrate && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "npx ultracite@latest check",
|
"lint": "ultracite check",
|
||||||
"format": "npx ultracite@latest fix",
|
"format": "ultracite fix",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "npx tsx lib/db/migrate.ts",
|
"db:migrate": "npx tsx lib/db/migrate.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
"test": "export PLAYWRIGHT=True && pnpm exec playwright test"
|
"test": "export PLAYWRIGHT=True && pnpm exec playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/gateway": "2.0.0-beta.85",
|
"@ai-sdk/gateway": "^3.0.15",
|
||||||
"@ai-sdk/provider": "3.0.0-beta.27",
|
"@ai-sdk/provider": "^3.0.3",
|
||||||
"@ai-sdk/react": "3.0.0-beta.162",
|
"@ai-sdk/react": "3.0.39",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
"@vercel/functions": "^2.0.0",
|
"@vercel/functions": "^2.0.0",
|
||||||
"@vercel/otel": "^1.12.0",
|
"@vercel/otel": "^1.12.0",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
"ai": "6.0.0-beta.159",
|
"ai": "6.0.37",
|
||||||
"bcrypt-ts": "^5.0.2",
|
"bcrypt-ts": "^5.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|
@ -87,11 +87,11 @@
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"redis": "^5.0.0",
|
"redis": "^5.0.0",
|
||||||
"resumable-stream": "^2.0.0",
|
"resumable-stream": "^2.2.10",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shiki": "^3.14.0",
|
"shiki": "^3.21.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"streamdown": "^1.4.0",
|
"streamdown": "^2.0.1",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.2",
|
"@biomejs/biome": "2.3.11",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.13",
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
|
@ -116,7 +116,7 @@
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"ultracite": "5.3.9"
|
"ultracite": "^7.0.11"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.3"
|
"packageManager": "pnpm@9.12.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2353
pnpm-lock.yaml
generated
2353
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,12 +9,18 @@ test.describe("Model Selector", () => {
|
||||||
|
|
||||||
test("displays a model button", async ({ page }) => {
|
test("displays a model button", async ({ page }) => {
|
||||||
// Look for any button with model-related content
|
// Look for any button with model-related content
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await expect(modelButton).toBeVisible();
|
await expect(modelButton).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens model selector popover on click", async ({ page }) => {
|
test("opens model selector popover on click", async ({ page }) => {
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await modelButton.click();
|
await modelButton.click();
|
||||||
|
|
||||||
// Search input should be visible in the popover
|
// Search input should be visible in the popover
|
||||||
|
|
@ -22,7 +28,10 @@ test.describe("Model Selector", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can search for models", async ({ page }) => {
|
test("can search for models", async ({ page }) => {
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await modelButton.click();
|
await modelButton.click();
|
||||||
|
|
||||||
const searchInput = page.getByPlaceholder("Search models...");
|
const searchInput = page.getByPlaceholder("Search models...");
|
||||||
|
|
@ -33,7 +42,10 @@ test.describe("Model Selector", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can close model selector by clicking outside", async ({ page }) => {
|
test("can close model selector by clicking outside", async ({ page }) => {
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await modelButton.click();
|
await modelButton.click();
|
||||||
|
|
||||||
await expect(page.getByPlaceholder("Search models...")).toBeVisible();
|
await expect(page.getByPlaceholder("Search models...")).toBeVisible();
|
||||||
|
|
@ -45,7 +57,10 @@ test.describe("Model Selector", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows model provider groups", async ({ page }) => {
|
test("shows model provider groups", async ({ page }) => {
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await modelButton.click();
|
await modelButton.click();
|
||||||
|
|
||||||
// Should show provider group headers
|
// Should show provider group headers
|
||||||
|
|
@ -54,7 +69,10 @@ test.describe("Model Selector", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can select a different model", async ({ page }) => {
|
test("can select a different model", async ({ page }) => {
|
||||||
const modelButton = page.locator("button").filter({ hasText: MODEL_BUTTON_REGEX }).first();
|
const modelButton = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: MODEL_BUTTON_REGEX })
|
||||||
|
.first();
|
||||||
await modelButton.click();
|
await modelButton.click();
|
||||||
|
|
||||||
// Select a specific model
|
// Select a specific model
|
||||||
|
|
@ -64,6 +82,8 @@ test.describe("Model Selector", () => {
|
||||||
await expect(page.getByPlaceholder("Search models...")).not.toBeVisible();
|
await expect(page.getByPlaceholder("Search models...")).not.toBeVisible();
|
||||||
|
|
||||||
// Model button should now show the selected model
|
// Model button should now show the selected model
|
||||||
await expect(page.locator("button").filter({ hasText: "Claude Haiku" }).first()).toBeVisible();
|
await expect(
|
||||||
|
page.locator("button").filter({ hasText: "Claude Haiku" }).first()
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ export function getResponseChunksByPrompt(
|
||||||
{ type: "text-start", id: "t1" },
|
{ type: "text-start", id: "t1" },
|
||||||
{ type: "text-delta", id: "t1", delta: "Hello, world!" },
|
{ type: "text-delta", id: "t1", delta: "Hello, world!" },
|
||||||
{ type: "text-end", id: "t1" },
|
{ type: "text-end", id: "t1" },
|
||||||
{ type: "finish", finishReason: "stop", usage: mockUsage }
|
{
|
||||||
|
type: "finish",
|
||||||
|
finishReason: { unified: "stop", raw: "stop" },
|
||||||
|
usage: mockUsage,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return chunks;
|
return chunks;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue