"use client"; import { useChat } from "@ai-sdk/react"; import { ClientSideChatTransport } from "@/app/(core)/util/client-side-chat-transport"; import { Message, MessageAvatar, MessageContent, } from "@/components/ai-elements/message"; import { Confirmation, ConfirmationTitle, ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected, ConfirmationActions, ConfirmationAction, } from "@/components/ai-elements/confirmation"; import { PromptInput, PromptInputButton, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar, PromptInputTools, } from "@/components/ai-elements/prompt-input"; import { Context, ContextContent, ContextContentHeader, ContextTrigger, } from "@/components/ai-elements/context"; import { Conversation, ConversationContent, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Response } from "@/components/ai-elements/response"; import { Loader } from "@/components/ai-elements/loader"; import { Button } from "@/components/ui/button"; import { PlusIcon, MicIcon, GlobeIcon, RefreshCcw, Copy, X, CheckIcon, XIcon, } from "lucide-react"; import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput, } from "@/components/ai-elements/tool"; import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { ModeToggle } from "@/components/ui/mode-toggle"; import { doesBrowserSupportBrowserAI } from "@browser-ai/core"; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithApprovalResponses, UIMessage, } from "ai"; import { toast } from "sonner"; import { BrowserAIUIMessage } from "@browser-ai/core"; import Image from "next/image"; import { Progress } from "@/components/ui/progress"; import { AudioFileDisplay } from "@/components/audio-file-display"; import { Kbd, KbdKey } from "@/components/ui/kbd"; import { ModelSelector } from "@/components/model-selector"; import { SiGithub } from "@icons-pack/react-simple-icons"; import Link from "next/link"; import { BrowserSupportInstructions } from "@/components/browser-support-instructions"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; const doesBrowserSupportModel = doesBrowserSupportBrowserAI(); export default function Chat() { const [browserSupportsModel, setBrowserSupportsModel] = useState< boolean | null >(null); const [isClient, setIsClient] = useState(false); const [input, setInput] = useState(""); const [files, setFiles] = useState(undefined); const fileInputRef = useRef(null); const [quotaOverflow, setQuotaOverflow] = useState(false); const [contextUsage, setContextUsage] = useState( undefined, ); const [contextWindow, setContextWindow] = useState( undefined, ); const clientTransport = useMemo( () => doesBrowserSupportModel ? new ClientSideChatTransport({ onContextOverflow: () => setQuotaOverflow(true), }) : null, [], ); const transport = useMemo( () => clientTransport ?? new DefaultChatTransport({ api: "/api/chat", }), [clientTransport], ); // Check browser support only on client side useEffect(() => { setIsClient(true); setBrowserSupportsModel(doesBrowserSupportBrowserAI()); }, []); const { error, status, sendMessage, messages, regenerate, stop, addToolApprovalResponse, } = useChat({ transport, sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, onError(error) { toast.error(error.message); }, onData: (dataPart) => { // Handle transient notifications // we can also access the date-modelDownloadProgress here if (dataPart.type === "data-notification") { if (dataPart.data.level === "error") { toast.error(dataPart.data.message); } else if (dataPart.data.level === "warning") { toast.warning(dataPart.data.message); } else { toast.info(dataPart.data.message); } } }, experimental_throttle: 75, }); const syncInputContext = useCallback(() => { if (!clientTransport) return; setContextUsage(clientTransport.getContextUsage()); setContextWindow(clientTransport.getContextWindow()); }, [clientTransport]); useEffect(() => { syncInputContext(); }, [messages, status, syncInputContext]); useEffect(() => { if ( !clientTransport || (status !== "submitted" && status !== "streaming") ) { return; } const intervalId = window.setInterval(syncInputContext, 250); return () => window.clearInterval(intervalId); }, [clientTransport, status, syncInputContext]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (quotaOverflow) return; if ((input.trim() || files) && status === "ready") { sendMessage({ text: input, files, }); setInput(""); setFiles(undefined); if (fileInputRef.current) { fileInputRef.current.value = ""; } } }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { setFiles(e.target.files); } }; const removeFile = (indexToRemove: number) => { if (files) { const dt = new DataTransfer(); Array.from(files).forEach((file, index) => { if (index !== indexToRemove) { dt.items.add(file); } }); setFiles(dt.files); if (fileInputRef.current) { fileInputRef.current.files = dt.files; } } }; const copyMessageToClipboard = (message: any) => { const textContent = message.parts .filter((part: any) => part.type === "text") .map((part: any) => part.text) .join("\n"); navigator.clipboard.writeText(textContent); }; // Show loading state until client-side check completes if (!isClient) { return (
); } return (
{quotaOverflow && ( Model quota exceeded Start a new chat by refreshing the page. )} {messages.length === 0 && ( <> {browserSupportsModel ? (

@browser-ai/core demo

Using your browser's AI model

Your browser supports browser AI models

) : ( )} )} {messages.map((m, index) => ( {/* Render parts in chronological order */} {m.parts.map((part, partIndex) => { // Handle download progress parts if (part.type === "data-modelDownloadProgress") { // Only show if message is not empty (hiding completed/cleared progress) if (!part.data.message) return null; // Don't show the entire div when actively streaming if (status === "ready") return null; return (
{part.data.message}
{part.data.status === "downloading" && part.data.progress !== undefined && ( )}
); } // Handle file parts if (part.type === "file") { if (part.mediaType?.startsWith("image/")) { return (
{part.filename
); } if (part.mediaType?.startsWith("audio/")) { return ( ); } // TODO: Handle other file types return null; } // Handle tool parts if (part.type.startsWith("tool-")) { // Type guard to ensure part is a ToolUIPart if (!("state" in part)) return null; // Handle tool states that need confirmation UI const needsConfirmation = part.state === "approval-requested" || part.state === "approval-responded" || part.state === "output-denied"; if (needsConfirmation && "approval" in part) { const toolName = part.type.replace("tool-", ""); return ( {"input" in part && part.input !== undefined && ( )} Allow {toolName} to execute with these parameters? Accepted Rejected addToolApprovalResponse({ id: part.approval!.id, approved: false, reason: "User denied tool execution", }) } variant="outline" > Reject addToolApprovalResponse({ id: part.approval!.id, approved: true, }) } variant="default" > Accept ); } // Map state values to the expected type const toolState = part.state === "streaming" || part.state === "done" ? "output-available" : part.state || "input-streaming"; // Format output as ReactNode const formatOutput = (output: unknown): React.ReactNode => { if (output === undefined || output === null) return undefined; if (typeof output === "string") return output; return (
                          {JSON.stringify(output, null, 2)}
                        
); }; return ( {"input" in part && part.input !== undefined && ( )} {("output" in part || "errorText" in part) && ( )} ); } // Handle text parts if (part.type === "text") { return {part.text}; } return null; })} {/* Loading state when tool approval was sent and we're waiting for response */} {(m.role === "assistant" || m.role === "system") && index === messages.length - 1 && status === "submitted" && m.parts.some( (part) => part.type.startsWith("tool-") && "state" in part && part.state === "approval-responded", ) && (
Thinking...
)} {/* Action buttons for assistant messages */} {(m.role === "assistant" || m.role === "system") && index === messages.length - 1 && status === "ready" && (
)}
))} {/* Loading state - only show as separate message if not after tool approval */} {status === "submitted" && !messages.some( (m, index) => index === messages.length - 1 && (m.role === "assistant" || m.role === "system") && m.parts.some( (part) => part.type.startsWith("tool-") && "state" in part && part.state === "approval-responded", ), ) && (
Thinking...
)} {/* Error state */} {error && (
An error occurred.
)}
setInput(e.target.value)} placeholder="What would you like to know?" minHeight={48} maxHeight={164} className="bg-accent dark:bg-card" disabled={quotaOverflow} /> fileInputRef.current?.click()}> Search
{contextWindow !== undefined && contextWindow > 0 && ( )} Ctrl Enter
{/* File preview area - moved inside the form */} {files && files.length > 0 && (
{Array.from(files).map((file, index) => (
{file.type.startsWith("image/") ? (
{file.name}
) : file.type.startsWith("audio/") ? (
{file.name}
) : (
{file.name}
)}
))}
)}
); }