diff --git a/package.json b/package.json index 42b00c54..b07c594e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "packageManager": "pnpm@10.14.0", "dependencies": { - "@ai-sdk/google": "^2.0.11", + "@ai-sdk/google": "^3.0.13", "@assistant-ui/react": "^0.12.1", "@assistant-ui/react-ai-sdk": "^1.3.1", "@assistant-ui/react-devtools": "^0.2.0", @@ -88,7 +88,7 @@ "@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-virtual": "^3.13.14", "@vercel/speed-insights": "^1.3.1", - "ai": "^5.0.116", + "ai": "^6.0.50", "assistant-cloud": "^0.1.13", "better-auth": "^1.4.10", "class-variance-authority": "^0.7.1", @@ -129,6 +129,7 @@ "sonner": "^2.0.7", "streamdown": "^2.1.0", "tailwind-merge": "^3.4.0", + "tw-shimmer": "^0.4.3", "typewriter-effect": "^2.22.0", "zod": "^4.3.4", "zustand": "^5.0.9" diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index f49dd48a..2fcb0278 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,5 +1,6 @@ import { google } from "@ai-sdk/google"; -import { streamText, convertToModelMessages, stepCountIs } from "ai"; +import { streamText, convertToModelMessages, stepCountIs, tool, zodSchema } from "ai"; +import type { UIMessage } from "ai"; import { logger } from "@/lib/utils/logger"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; @@ -175,7 +176,7 @@ export async function POST(req: Request) { const session = await auth.api.getSession({ headers: headersObj }); const userId = session?.user?.id || null; - const messages = body.messages || []; + const { messages = [] }: { messages?: UIMessage[] } = body; const system = body.system || ""; workspaceId = extractWorkspaceId(body); activeFolderId = body.activeFolderId; diff --git a/src/app/globals.css b/src/app/globals.css index b32e7bfc..1224d48f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "@daveyplate/better-auth-ui/css"; +@import "tw-shimmer"; /* Path to your installed `@blocknote/shadcn` package. */ @source "../node_modules/@blocknote/shadcn"; @@ -247,8 +248,6 @@ margin-top: -0.125rem !important; } - - .bn-block-content[data-content-type="checkListItem"] input[type="checkbox"] { margin-top: 0.25rem !important; flex-shrink: 0 !important; @@ -257,7 +256,6 @@ cursor: pointer !important; } - .bn-block-content[data-content-type="checkListItem"] .bn-inline-content { margin-top: 0 !important; flex: 1 !important; diff --git a/src/components/assistant-ui/AssistantDropzone.tsx b/src/components/assistant-ui/AssistantDropzone.tsx index bf5e7ee7..150f44d5 100644 --- a/src/components/assistant-ui/AssistantDropzone.tsx +++ b/src/components/assistant-ui/AssistantDropzone.tsx @@ -1,7 +1,7 @@ "use client"; import { useDropzone } from "react-dropzone"; -import { useAssistantApi } from "@assistant-ui/react"; +import { useAui } from "@assistant-ui/react"; import { useWorkspaceStore } from "@/lib/stores/workspace-store"; import { Upload } from "lucide-react"; import { useCallback, useState, useRef } from "react"; @@ -16,7 +16,7 @@ interface AssistantDropzoneProps { * Accepts all supported file types and adds them as attachments to the chat composer. */ export function AssistantDropzone({ children }: AssistantDropzoneProps) { - const api = useAssistantApi(); + const aui = useAui(); const currentWorkspaceId = useWorkspaceStore((state) => state.currentWorkspaceId); const [isDragging, setIsDragging] = useState(false); @@ -31,7 +31,7 @@ export function AssistantDropzone({ children }: AssistantDropzoneProps) { const onDrop = useCallback( async (acceptedFiles: File[]) => { - if (!currentWorkspaceId || !api) return; + if (!currentWorkspaceId || !aui) return; const MAX_FILES = 10; const MAX_FILE_SIZE_MB = 10; @@ -93,7 +93,7 @@ export function AssistantDropzone({ children }: AssistantDropzoneProps) { // Add each file to the composer const addPromises = validFiles.map(async (file) => { try { - await api.composer().addAttachment(file); + await aui.composer().addAttachment(file); } catch (error) { console.error("Failed to add attachment:", error); // Remove from processing set on error so it can be retried @@ -124,7 +124,7 @@ export function AssistantDropzone({ children }: AssistantDropzoneProps) { }, 200); } }, - [api, currentWorkspaceId] + [aui, currentWorkspaceId] ); // Clear processing state when drag ends (user drags away or cancels) @@ -139,7 +139,7 @@ export function AssistantDropzone({ children }: AssistantDropzoneProps) { onDrop, noClick: true, // Don't trigger on click, only drag and drop noKeyboard: true, // Don't trigger on keyboard - disabled: !currentWorkspaceId || !api, // Disable if no workspace is selected or api is not available + disabled: !currentWorkspaceId || !aui, // Disable if no workspace is selected or api is not available accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'], 'video/*': ['.mp4', '.webm', '.avi', '.mov', '.mkv'], diff --git a/src/components/assistant-ui/AssistantTextSelectionManager.tsx b/src/components/assistant-ui/AssistantTextSelectionManager.tsx index cf701b64..8ecb4d03 100644 --- a/src/components/assistant-ui/AssistantTextSelectionManager.tsx +++ b/src/components/assistant-ui/AssistantTextSelectionManager.tsx @@ -15,7 +15,7 @@ import SelectableText, { import { useUIStore, selectReplySelections } from "@/lib/stores/ui-store"; import { getHighlightColorById } from "@/lib/utils/highlight-colors"; import { useWorkspaceStore } from "@/lib/stores/workspace-store"; -import { useAssistantState } from "@assistant-ui/react"; +import { useAuiState } from "@assistant-ui/react"; import { useQueryClient } from "@tanstack/react-query"; import { Dialog, @@ -105,8 +105,8 @@ export default function AssistantTextSelectionManager({ // This component is always rendered within AssistantRuntimeProvider, so the hook is safe to use // Use threadListItem.id if available, otherwise fall back to mainThreadId // Using safe hooks to handle race condition during thread switching (GitHub issue #2722) - const threadListItemId = useAssistantState(({ threadListItem }) => (threadListItem as any)?.id); - const mainThreadId = useAssistantState(({ threads }) => (threads as any)?.mainThreadId); + const threadListItemId = useAuiState(({ threadListItem }) => (threadListItem as any)?.id); + const mainThreadId = useAuiState(({ threads }) => (threads as any)?.mainThreadId); const currentThreadId = threadListItemId || mainThreadId; diff --git a/src/components/assistant-ui/SafeAssistantRuntimeProvider.tsx b/src/components/assistant-ui/SafeAssistantRuntimeProvider.tsx deleted file mode 100644 index 468bcef4..00000000 --- a/src/components/assistant-ui/SafeAssistantRuntimeProvider.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { Component, ReactNode } from "react"; -import { AssistantRuntimeProvider } from "@assistant-ui/react"; -import { AssistantRuntime } from "@assistant-ui/react"; - -interface Props { - runtime: AssistantRuntime; - children: ReactNode; -} - -interface State { - hasError: boolean; - remountKey: number; -} - -export class SafeAssistantRuntimeProvider extends Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false, remountKey: 0 }; - } - - static getDerivedStateFromError(error: unknown) { - return { hasError: true }; - } - - componentDidCatch(error: unknown, errorInfo: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - - // Check if this is a known transient runtime error - const isTransientError = - errorMessage.includes("tapLookupResources") || - errorMessage.includes("Resource not found") || - errorMessage.includes("ThreadListItemRuntime is not available"); - - if (isTransientError) { - console.warn("[SafeAssistantRuntimeProvider] Caught transient runtime error, attempting graceful recovery:", error); - // Attempt to recover by resetting error state and incrementing remount key after a short delay - // This allows the tree to re-mount with a fresh key, hopefully after the race condition passes - setTimeout(() => { - if (this.state.hasError) { - this.setState({ - hasError: false, - remountKey: this.state.remountKey + 1 - }); - } - }, 100); - } else { - console.error("[SafeAssistantRuntimeProvider] Unhandled error:", error, errorInfo); - // For non-transient errors, still try to recover after a longer delay - setTimeout(() => { - if (this.state.hasError) { - this.setState({ - hasError: false, - remountKey: this.state.remountKey + 1 - }); - } - }, 500); - } - } - - componentDidUpdate(prevProps: Props) { - // If runtime changed, try to recover from error state - if (prevProps.runtime !== this.props.runtime && this.state.hasError) { - this.setState({ - hasError: false, - remountKey: this.state.remountKey + 1 - }); - } - } - - render() { - if (this.state.hasError) { - // During error state, render a minimal invisible placeholder - // This prevents gray screen while avoiding hook errors from children - // The setTimeout in componentDidCatch will trigger recovery - return ( -