diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e386d5..9b52f23 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,23 @@ "editor.codeActionsOnSave": { "source.fixAll.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" } } diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts index 588ff6a..d104b65 100644 --- a/app/(auth)/api/auth/[...nextauth]/route.ts +++ b/app/(auth)/api/auth/[...nextauth]/route.ts @@ -1,2 +1 @@ -// biome-ignore lint/performance/noBarrelFile: "Required" export { GET, POST } from "@/app/(auth)/auth"; diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index dbebb1d..ffd72be 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -16,7 +16,6 @@ declare module "next-auth" { } & DefaultSession["user"]; } - // biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required" interface User { id?: string; email?: string | null; diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 19f73c1..7846b80 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -22,13 +22,15 @@ export async function generateTitleFromUserMessage({ }: { message: UIMessage; }) { - const { text: title } = await generateText({ + const { text } = await generateText({ model: getTitleModel(), system: titlePrompt, prompt: getTextFromMessage(message), }); - - return title; + return text + .replace(/^[#*"\s]+/, "") + .replace(/["]+$/, "") + .trim(); } export async function deleteTrailingMessages({ id }: { id: string }) { diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts index 48352e9..3713ec5 100644 --- a/app/(chat)/api/chat/[id]/stream/route.ts +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -1,113 +1,3 @@ -import { createUIMessageStream, JsonToSseTransformStream } from "ai"; -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 }); - } - - 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({ - // 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({ - 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 }); +export function GET() { + return new Response(null, { status: 204 }); } diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index e79bf4b..3645819 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2,16 +2,13 @@ import { geolocation } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, - JsonToSseTransformStream, - smoothStream, + createUIMessageStreamResponse, + generateId, stepCountIs, streamText, } from "ai"; import { after } from "next/server"; -import { - createResumableStreamContext, - type ResumableStreamContext, -} from "resumable-stream"; +import { createResumableStreamContext } from "resumable-stream"; import { auth, type UserType } from "@/app/(auth)/auth"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; @@ -41,28 +38,16 @@ import { type PostRequestBody, postRequestBodySchema } from "./schema"; export const maxDuration = 60; -let globalStreamContext: ResumableStreamContext | null = null; - -export function getStreamContext() { - if (!globalStreamContext) { - try { - globalStreamContext = createResumableStreamContext({ - waitUntil: after, - }); - } catch (error: any) { - if (error.message.includes("REDIS_URL")) { - console.log( - " > Resumable streams are disabled due to missing REDIS_URL" - ); - } else { - console.error(error); - } - } +function getStreamContext() { + try { + return createResumableStreamContext({ waitUntil: after }); + } catch (_) { + return null; } - - return globalStreamContext; } +export { getStreamContext }; + export async function POST(request: Request) { let requestBody: PostRequestBody; @@ -94,7 +79,6 @@ export async function POST(request: Request) { return new ChatSDKError("rate_limit:chat").toResponse(); } - // Check if this is a tool approval flow (all messages sent) const isToolApprovalFlow = Boolean(messages); const chat = await getChatById({ id }); @@ -105,24 +89,19 @@ export async function POST(request: Request) { if (chat.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } - // Only fetch messages if chat already exists and not tool approval if (!isToolApprovalFlow) { messagesFromDb = await getMessagesByChatId({ id }); } } else if (message?.role === "user") { - // Save chat immediately with placeholder title await saveChat({ id, userId: session.user.id, title: "New chat", visibility: selectedVisibilityType, }); - - // Start title generation in parallel (don't await) titlePromise = generateTitleFromUserMessage({ message }); } - // Use all messages for tool approval, otherwise DB messages + new message const uiMessages = isToolApprovalFlow ? (messages as ChatMessage[]) : [...convertToUIMessages(messagesFromDb), message as ChatMessage]; @@ -136,7 +115,6 @@ export async function POST(request: Request) { country, }; - // Only save user messages to the database (not tool approval responses) if (message?.role === "user") { await saveMessages({ messages: [ @@ -152,29 +130,19 @@ export async function POST(request: Request) { }); } - const streamId = generateUUID(); - await createStreamId({ streamId, chatId: id }); + const isReasoningModel = + selectedChatModel.includes("reasoning") || + selectedChatModel.includes("thinking"); + + const modelMessages = await convertToModelMessages(uiMessages); 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 = - selectedChatModel.includes("reasoning") || - selectedChatModel.includes("thinking"); - const result = streamText({ model: getLanguageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), - messages: await convertToModelMessages(uiMessages), + messages: modelMessages, stopWhen: stepCountIs(5), experimental_activeTools: isReasoningModel ? [] @@ -184,9 +152,6 @@ export async function POST(request: Request) { "updateDocument", "requestSuggestions", ], - experimental_transform: isReasoningModel - ? undefined - : smoothStream({ chunking: "word" }), providerOptions: isReasoningModel ? { anthropic: { @@ -198,10 +163,7 @@ export async function POST(request: Request) { getWeather, createDocument: createDocument({ session, dataStream }), updateDocument: updateDocument({ session, dataStream }), - requestSuggestions: requestSuggestions({ - session, - dataStream, - }), + requestSuggestions: requestSuggestions({ session, dataStream }), }, experimental_telemetry: { isEnabled: isProductionEnvironment, @@ -209,28 +171,25 @@ export async function POST(request: Request) { }, }); - result.consumeStream(); + dataStream.merge(result.toUIMessageStream({ sendReasoning: true })); - dataStream.merge( - result.toUIMessageStream({ - sendReasoning: true, - }) - ); + if (titlePromise) { + const title = await titlePromise; + dataStream.write({ type: "data-chat-title", data: title }); + updateChatTitleById({ chatId: id, title }); + } }, generateId: generateUUID, onFinish: async ({ messages: finishedMessages }) => { if (isToolApprovalFlow) { - // For tool approval, update existing messages (tool state changed) and save new ones for (const finishedMsg of finishedMessages) { const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id); if (existingMsg) { - // Update existing message with new parts (tool state changed) await updateMessage({ id: finishedMsg.id, parts: finishedMsg.parts, }); } else { - // Save new message await saveMessages({ messages: [ { @@ -246,7 +205,6 @@ export async function POST(request: Request) { } } } else if (finishedMessages.length > 0) { - // Normal flow - save all finished messages await saveMessages({ messages: finishedMessages.map((currentMessage) => ({ id: currentMessage.id, @@ -259,28 +217,30 @@ export async function POST(request: Request) { }); } }, - onError: () => { - return "Oops, an error occurred!"; - }, + onError: () => "Oops, an error occurred!", }); - const streamContext = getStreamContext(); - - if (streamContext) { - try { - const resumableStream = await streamContext.resumableStream( - streamId, - () => stream.pipeThrough(new JsonToSseTransformStream()) - ); - if (resumableStream) { - return new Response(resumableStream); + return createUIMessageStreamResponse({ + stream, + async consumeSseStream({ stream: sseStream }) { + if (!process.env.REDIS_URL) { + return; } - } catch (error) { - console.error("Failed to create resumable stream:", error); - } - } - - return new Response(stream.pipeThrough(new JsonToSseTransformStream())); + try { + const streamContext = getStreamContext(); + if (streamContext) { + const streamId = generateId(); + await createStreamId({ streamId, chatId: id }); + await streamContext.createNewResumableStream( + streamId, + () => sseStream + ); + } + } catch (_) { + // ignore redis errors + } + }, + }); } catch (error) { const vercelId = request.headers.get("x-vercel-id"); @@ -288,7 +248,6 @@ export async function POST(request: Request) { return error.toResponse(); } - // Check for Vercel AI Gateway credit card error if ( error instanceof Error && error.message?.includes( diff --git a/app/globals.css b/app/globals.css index 814c76b..de6fc04 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,7 +2,7 @@ @import "katex/dist/katex.min.css"; /* 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 dark (&:is(.dark, .dark *)); diff --git a/biome.jsonc b/biome.jsonc index ba27d8f..dbaebba 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,9 +1,23 @@ { "$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": { "includes": [ - "**/*", + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "!node_modules", + "!.next", + "!ai-sdk", "!components/ui", "!lib/utils.ts", "!hooks/use-mobile.ts" @@ -12,39 +26,34 @@ "linter": { "rules": { "suspicious": { - /* Needs more work to fix */ "noExplicitAny": "off", - - /* Allow for Tailwind @ rules */ "noUnknownAtRules": "off", - - /* Allowing console for debugging */ "noConsole": "off", - - /* Needed for generateUUID() */ "noBitwiseOperators": "off" }, "style": { - /* Allowing magic numbers */ "noMagicNumbers": "off", - - /* Needs more work to fix */ - "noNestedTernary": "off" + "noNestedTernary": "off", + "useConsistentTypeDefinitions": "off" }, "nursery": { - /* Too many false positives */ - "noUnnecessaryConditions": "off" + "noUnnecessaryConditions": "off", + "useSortedClasses": "off" }, "complexity": { - /* Needs more work to fix */ "noExcessiveCognitiveComplexity": "off", - - /* This one has false positives. It's a bit... iffy 😉 */ "useSimplifiedLogicExpression": "off" }, "a11y": { - /* Needs more work to fix */ "noSvgWithoutTitle": "off" + }, + "correctness": { + "useUniqueElementIds": "off", + "useImageSize": "off" + }, + "performance": { + "noBarrelFile": "off", + "useTopLevelRegex": "off" } } } diff --git a/components/ai-elements/chain-of-thought.tsx b/components/ai-elements/chain-of-thought.tsx index afa892e..11f2323 100644 --- a/components/ai-elements/chain-of-thought.tsx +++ b/components/ai-elements/chain-of-thought.tsx @@ -143,7 +143,7 @@ export const ChainOfThoughtStep = memo( >
-
+
{label}
diff --git a/components/ai-elements/code-block.tsx b/components/ai-elements/code-block.tsx deleted file mode 100644 index d538c59..0000000 --- a/components/ai-elements/code-block.tsx +++ /dev/null @@ -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 & { - code: string; - language: BundledLanguage; - showLineNumbers?: boolean; -}; - -type CodeBlockContextType = { - code: string; -}; - -const CodeBlockContext = createContext({ - 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(""); - const [darkHtml, setDarkHtml] = useState(""); - 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 ( - -
-
-
-
- {children && ( -
- {children} -
- )} -
-
- - ); -}; - -export type CodeBlockCopyButtonProps = ComponentProps & { - 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 ( - - ); -}; diff --git a/components/ai-elements/image.tsx b/components/ai-elements/image.tsx index 37ae0dd..c104080 100644 --- a/components/ai-elements/image.tsx +++ b/components/ai-elements/image.tsx @@ -12,7 +12,6 @@ export const Image = ({ mediaType, ...props }: ImageProps) => ( - // biome-ignore lint/nursery/useImageSize: dynamic base64 content // biome-ignore lint/performance/noImgElement: base64 data URLs require native img (
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 )} {...props} diff --git a/components/ai-elements/prompt-input.tsx b/components/ai-elements/prompt-input.tsx index fdf874d..599972d 100644 --- a/components/ai-elements/prompt-input.tsx +++ b/components/ai-elements/prompt-input.tsx @@ -154,8 +154,7 @@ export function PromptInputProvider({ (FileUIPart & { id: string })[] >([]); const fileInputRef = useRef(null); - // biome-ignore lint/suspicious/noEmptyBlockStatements: noop initializer - const openRef = useRef<() => void>(() => {}); + const openRef = useRef<() => void>(() => undefined); const add = useCallback((files: File[] | FileList) => { const incoming = Array.from(files); @@ -309,18 +308,18 @@ export function PromptInputAttachment({ {...props} >
-
+
{isImage ? ( /* biome-ignore lint/performance/noImgElement: dynamic user uploads */ {filename ) : ( -
+
)} @@ -343,14 +342,14 @@ export function PromptInputAttachment({ {attachmentLabel}
- -
+ +
{isImage && ( -
+
{/* biome-ignore lint/performance/noImgElement: dynamic user uploads */} {filename
-

+

{filename || (isImage ? "Image" : "Attachment")}

{data.mediaType && ( -

+

{data.mediaType}

)} @@ -395,7 +394,7 @@ export function PromptInputAttachments({ return (
{attachments.files.map((file) => ( @@ -913,7 +912,7 @@ export const PromptInputTextarea = ({ return ( setIsComposing(false)} onCompositionStart={() => setIsComposing(true)} @@ -937,7 +936,7 @@ export const PromptInputHeader = ({ }: PromptInputHeaderProps) => ( ); @@ -953,7 +952,7 @@ export const PromptInputFooter = ({ }: PromptInputFooterProps) => ( ); @@ -964,7 +963,7 @@ export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => ( -
+
); export type PromptInputButtonProps = ComponentProps; @@ -1046,7 +1045,7 @@ export const PromptInputSubmit = ({ let Icon = ; if (status === "submitted") { - Icon = ; + Icon = ; } else if (status === "streaming") { Icon = ; } else if (status === "error") { @@ -1111,7 +1110,6 @@ interface SpeechRecognitionErrorEvent extends Event { } declare global { - // biome-ignore lint/nursery/useConsistentTypeDefinitions: global augmentation requires interface interface Window { SpeechRecognition: { new (): SpeechRecognition; @@ -1244,7 +1242,7 @@ export const PromptInputSelectTrigger = ({ }: PromptInputSelectTriggerProps) => ( (

(
( - +
    {children}
@@ -242,7 +242,7 @@ export const QueueSectionLabel = ({ ...props }: QueueSectionLabelProps) => ( - + {icon} {count} {label} diff --git a/components/ai-elements/tool.tsx b/components/ai-elements/tool.tsx index 3df5d10..cd60f18 100644 --- a/components/ai-elements/tool.tsx +++ b/components/ai-elements/tool.tsx @@ -18,7 +18,6 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; -import { CodeBlock } from "./code-block"; export type ToolProps = ComponentProps; @@ -111,9 +110,9 @@ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (

Parameters

-
- -
+
+      {JSON.stringify(input, null, 2)}
+    
); @@ -132,15 +131,21 @@ export const ToolOutput = ({ return null; } - let Output =
{output as ReactNode}
; - - if (typeof output === "object" && !isValidElement(output)) { - Output = ( - - ); - } else if (typeof output === "string") { - Output = ; - } + const renderOutput = () => { + if (typeof output === "object" && !isValidElement(output)) { + return ( +
+          {JSON.stringify(output, null, 2)}
+        
+ ); + } + if (typeof output === "string") { + return ( +
{output}
+ ); + } + return
{output as ReactNode}
; + }; return (
@@ -155,8 +160,8 @@ export const ToolOutput = ({ : "bg-muted/50 text-foreground" )} > - {errorText &&
{errorText}
} - {Output} + {errorText &&
{errorText}
} + {!errorText && renderOutput()}

); diff --git a/components/chat.tsx b/components/chat.tsx index 03bc28c..13902a4 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -89,13 +89,9 @@ export function Chat({ } = useChat({ id, messages: initialMessages, - experimental_throttle: 100, 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 }) => { const lastMessage = currentMessages.at(-1); - // Only continue if a tool was APPROVED (not denied) const shouldContinue = lastMessage?.parts?.some( (part) => @@ -111,10 +107,6 @@ export function Chat({ fetch: fetchWithErrorHandlers, prepareSendMessagesRequest(request) { 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 = lastMessage?.role !== "user" || request.messages.some((msg) => @@ -129,7 +121,6 @@ export function Chat({ return { body: { id: request.id, - // Send all messages for tool approval continuation, otherwise just the last user message ...(isToolApprovalContinuation ? { messages: request.messages } : { message: lastMessage }), @@ -148,7 +139,6 @@ export function Chat({ }, onError: (error) => { if (error instanceof ChatSDKError) { - // Check if it's a credit card error if ( error.message?.includes("AI Gateway requires a valid credit card") ) { diff --git a/components/console.tsx b/components/console.tsx index a999578..ddaaf68 100644 --- a/components/console.tsx +++ b/components/console.tsx @@ -165,7 +165,6 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) { {consoleOutput.contents.map((content, contentIndex) => content.type === "image" ? ( - {/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */} output({ - code: "", -}); - -export type CodeBlockProps = HTMLAttributes & { - code: string; - language: string; - showLineNumbers?: boolean; - children?: ReactNode; -}; - -export const CodeBlock = ({ - code, - language, - showLineNumbers = false, - className, - children, - ...props -}: CodeBlockProps) => ( - -
-
- - {code} - - - {code} - - {children && ( -
- {children} -
- )} -
-
-
-); - -export type CodeBlockCopyButtonProps = ComponentProps & { - 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 ( - - ); -}; diff --git a/components/elements/conversation.tsx b/components/elements/conversation.tsx index 3721013..9d44ec8 100644 --- a/components/elements/conversation.tsx +++ b/components/elements/conversation.tsx @@ -49,7 +49,7 @@ export const ConversationScrollButton = ({ !isAtBottom && (
); diff --git a/components/image-editor.tsx b/components/image-editor.tsx index 2ee56b9..32525fc 100644 --- a/components/image-editor.tsx +++ b/components/image-editor.tsx @@ -34,7 +34,6 @@ export function ImageEditor({
) : ( - {/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */} {title} {setMode && ( setMode("edit")} tooltip="Edit" diff --git a/components/message.tsx b/components/message.tsx index 66dde7f..5ffe67e 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -108,14 +108,18 @@ const PurePreviewMessage = ({ const { type } = part; const key = `message-${message.id}-part-${index}`; - if (type === "reasoning" && part.text?.trim().length > 0) { - return ( - - ); + if (type === "reasoning") { + const hasContent = part.text?.trim().length > 0; + const isStreaming = "state" in part && part.state === "streaming"; + if (hasContent || isStreaming) { + return ( + + ); + } } if (type === "text") { diff --git a/components/messages.tsx b/components/messages.tsx index 9ab3787..bcf1025 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -91,7 +91,7 @@ function PureMessages({