"use client" import { useChat } from "@ai-sdk/react" import { DefaultChatTransport } from "ai" import { AlertTriangle, MessageSquarePlus, PanelRightClose, PanelRightOpen, Settings, } from "lucide-react" import Image from "next/image" import Link from "next/link" import type React from "react" import { useCallback, useEffect, useRef, useState } from "react" import { flushSync } from "react-dom" import { FaGithub } from "react-icons/fa" import { Toaster, toast } from "sonner" import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ChatInput } from "@/components/chat-input" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SettingsDialog } from "@/components/settings-dialog" import { useDiagram } from "@/contexts/diagram-context" import { getAIConfig } from "@/lib/ai-config" import { findCachedResponse } from "@/lib/cached-responses" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { useQuotaManager } from "@/lib/use-quota-manager" import { formatXML, wrapWithMxFile } from "@/lib/utils" import { ChatMessageDisplay } from "./chat-message-display" // localStorage keys for persistence const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages" const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots" const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id" export const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml" // Type for message parts (tool calls and their states) interface MessagePart { type: string state?: string toolName?: string [key: string]: unknown } interface ChatMessage { role: string parts?: MessagePart[] [key: string]: unknown } interface ChatPanelProps { isVisible: boolean onToggleVisibility: () => void drawioUi: "min" | "sketch" onToggleDrawioUi: () => void darkMode: boolean onToggleDarkMode: () => void isMobile?: boolean onCloseProtectionChange?: (enabled: boolean) => void } // Constants for tool states const TOOL_ERROR_STATE = "output-error" as const const DEBUG = process.env.NODE_ENV === "development" const MAX_AUTO_RETRY_COUNT = 1 /** * Check if auto-resubmit should happen based on tool errors. * Does NOT handle retry count or quota - those are handled by the caller. */ function hasToolErrors(messages: ChatMessage[]): boolean { const lastMessage = messages[messages.length - 1] if (!lastMessage || lastMessage.role !== "assistant") { return false } const toolParts = (lastMessage.parts as MessagePart[] | undefined)?.filter((part) => part.type?.startsWith("tool-"), ) || [] if (toolParts.length === 0) { return false } return toolParts.some((part) => part.state === TOOL_ERROR_STATE) } export default function ChatPanel({ isVisible, onToggleVisibility, drawioUi, onToggleDrawioUi, darkMode, onToggleDarkMode, isMobile = false, onCloseProtectionChange, }: ChatPanelProps) { const { loadDiagram: onDisplayChart, handleExport: onExport, handleExportWithoutHistory, resolverRef, chartXML, clearDiagram, isDrawioReady, } = useDiagram() const onFetchChart = (saveToHistory = true) => { return Promise.race([ new Promise((resolve) => { if (resolverRef && "current" in resolverRef) { resolverRef.current = resolve } if (saveToHistory) { onExport() } else { handleExportWithoutHistory() } }), new Promise((_, reject) => setTimeout( () => reject( new Error( "Chart export timed out after 10 seconds", ), ), 10000, ), ), ]) } // File processing using extracted hook const { files, pdfData, handleFileChange, setFiles } = useFileProcessor() const [showHistory, setShowHistory] = useState(false) const [showSettingsDialog, setShowSettingsDialog] = useState(false) const [, setAccessCodeRequired] = useState(false) const [input, setInput] = useState("") const [dailyRequestLimit, setDailyRequestLimit] = useState(0) const [dailyTokenLimit, setDailyTokenLimit] = useState(0) const [tpmLimit, setTpmLimit] = useState(0) const [showNewChatDialog, setShowNewChatDialog] = useState(false) // Check config on mount useEffect(() => { fetch("/api/config") .then((res) => res.json()) .then((data) => { setAccessCodeRequired(data.accessCodeRequired) setDailyRequestLimit(data.dailyRequestLimit || 0) setDailyTokenLimit(data.dailyTokenLimit || 0) setTpmLimit(data.tpmLimit || 0) }) .catch(() => setAccessCodeRequired(false)) }, []) // Quota management using extracted hook const quotaManager = useQuotaManager({ dailyRequestLimit, dailyTokenLimit, tpmLimit, }) // Generate a unique session ID for Langfuse tracing (restore from localStorage if available) const [sessionId, setSessionId] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem(STORAGE_SESSION_ID_KEY) if (saved) return saved } return `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` }) // Store XML snapshots for each user message (keyed by message index) const xmlSnapshotsRef = useRef>(new Map()) // Flag to track if we've restored from localStorage const hasRestoredRef = useRef(false) // Ref to track latest chartXML for use in callbacks (avoids stale closure) const chartXMLRef = useRef(chartXML) useEffect(() => { chartXMLRef.current = chartXML }, [chartXML]) // Ref to hold stop function for use in onToolCall (avoids stale closure) const stopRef = useRef<(() => void) | null>(null) // Ref to track consecutive auto-retry count (reset on user action) const autoRetryCountRef = useRef(0) // Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs const processedToolCallsRef = useRef>(new Set()) const { messages, sendMessage, addToolOutput, stop, status, error, setMessages, } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat", }), async onToolCall({ toolCall }) { if (DEBUG) { console.log( `[onToolCall] Tool: ${toolCall.toolName}, CallId: ${toolCall.toolCallId}`, ) } if (toolCall.toolName === "display_diagram") { const { xml } = toolCall.input as { xml: string } if (DEBUG) { console.log( `[display_diagram] Received XML length: ${xml.length}`, ) } // Wrap raw XML with full mxfile structure for draw.io const fullXml = wrapWithMxFile(xml) // loadDiagram validates and returns error if invalid const validationError = onDisplayChart(fullXml) if (validationError) { console.warn( "[display_diagram] Validation error:", validationError, ) // Return error to model - sendAutomaticallyWhen will trigger retry if (DEBUG) { console.log( "[display_diagram] Adding tool output with state: output-error", ) } addToolOutput({ tool: "display_diagram", toolCallId: toolCall.toolCallId, state: "output-error", errorText: `${validationError} Please fix the XML issues and call display_diagram again with corrected XML. Your failed XML: \`\`\`xml ${xml} \`\`\``, }) } else { // Success - diagram will be rendered by chat-message-display if (DEBUG) { console.log( "[display_diagram] Success! Adding tool output with state: output-available", ) } addToolOutput({ tool: "display_diagram", toolCallId: toolCall.toolCallId, output: "Successfully displayed the diagram.", }) if (DEBUG) { console.log( "[display_diagram] Tool output added. Diagram should be visible now.", ) } } } else if (toolCall.toolName === "edit_diagram") { const { edits } = toolCall.input as { edits: Array<{ search: string; replace: string }> } let currentXml = "" try { console.log("[edit_diagram] Starting...") // Use chartXML from ref directly - more reliable than export // especially on Vercel where DrawIO iframe may have latency issues // Using ref to avoid stale closure in callback const cachedXML = chartXMLRef.current if (cachedXML) { currentXml = cachedXML console.log( "[edit_diagram] Using cached chartXML, length:", currentXml.length, ) } else { // Fallback to export only if no cached XML console.log( "[edit_diagram] No cached XML, fetching from DrawIO...", ) currentXml = await onFetchChart(false) console.log( "[edit_diagram] Got XML from export, length:", currentXml.length, ) } const { replaceXMLParts } = await import("@/lib/utils") const editedXml = replaceXMLParts(currentXml, edits) // loadDiagram validates and returns error if invalid const validationError = onDisplayChart(editedXml) if (validationError) { console.warn( "[edit_diagram] Validation error:", validationError, ) addToolOutput({ tool: "edit_diagram", toolCallId: toolCall.toolCallId, state: "output-error", errorText: `Edit produced invalid XML: ${validationError} Current diagram XML: \`\`\`xml ${currentXml} \`\`\` Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid references).`, }) return } onExport() addToolOutput({ tool: "edit_diagram", toolCallId: toolCall.toolCallId, output: `Successfully applied ${edits.length} edit(s) to the diagram.`, }) console.log("[edit_diagram] Success") } catch (error) { console.error("[edit_diagram] Failed:", error) const errorMessage = error instanceof Error ? error.message : String(error) // Use addToolOutput with state: 'output-error' for proper error signaling addToolOutput({ tool: "edit_diagram", toolCallId: toolCall.toolCallId, state: "output-error", errorText: `Edit failed: ${errorMessage} Current diagram XML: \`\`\`xml ${currentXml || "No XML available"} \`\`\` Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`, }) } } }, onError: (error) => { // Silence access code error in console since it's handled by UI if (!error.message.includes("Invalid or missing access code")) { console.error("Chat error:", error) } // Translate technical errors into user-friendly messages // The server now handles detailed error messages, so we can display them directly. // But we still handle connection/network errors that happen before reaching the server. let friendlyMessage = error.message // Simple check for network errors if message is generic if (friendlyMessage === "Failed to fetch") { friendlyMessage = "Network error. Please check your connection." } // Translate image not supported error if (friendlyMessage.includes("image content block")) { friendlyMessage = "This model doesn't support image input." } // Add system message for error so it can be cleared setMessages((currentMessages) => { const errorMessage = { id: `error-${Date.now()}`, role: "system" as const, content: friendlyMessage, parts: [{ type: "text" as const, text: friendlyMessage }], } return [...currentMessages, errorMessage] }) if (error.message.includes("Invalid or missing access code")) { // Show settings button and open dialog to help user fix it setAccessCodeRequired(true) setShowSettingsDialog(true) } }, onFinish: ({ message }) => { // Track actual token usage from server metadata const metadata = message?.metadata as | Record | undefined if (metadata) { // Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true) const inputTokens = Number.isFinite(metadata.inputTokens) ? (metadata.inputTokens as number) : 0 const outputTokens = Number.isFinite(metadata.outputTokens) ? (metadata.outputTokens as number) : 0 const actualTokens = inputTokens + outputTokens if (actualTokens > 0) { quotaManager.incrementTokenCount(actualTokens) quotaManager.incrementTPMCount(actualTokens) } } }, sendAutomaticallyWhen: ({ messages }) => { const shouldRetry = hasToolErrors( messages as unknown as ChatMessage[], ) if (!shouldRetry) { // No error, reset retry count autoRetryCountRef.current = 0 if (DEBUG) { console.log("[sendAutomaticallyWhen] No errors, stopping") } return false } // Check retry count limit if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) { if (DEBUG) { console.log( `[sendAutomaticallyWhen] Max retry count (${MAX_AUTO_RETRY_COUNT}) reached, stopping`, ) } toast.error( `Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`, ) autoRetryCountRef.current = 0 return false } // Check quota limits before auto-retry const tokenLimitCheck = quotaManager.checkTokenLimit() if (!tokenLimitCheck.allowed) { if (DEBUG) { console.log( "[sendAutomaticallyWhen] Token limit exceeded, stopping", ) } quotaManager.showTokenLimitToast(tokenLimitCheck.used) autoRetryCountRef.current = 0 return false } const tpmCheck = quotaManager.checkTPMLimit() if (!tpmCheck.allowed) { if (DEBUG) { console.log( "[sendAutomaticallyWhen] TPM limit exceeded, stopping", ) } quotaManager.showTPMLimitToast() autoRetryCountRef.current = 0 return false } // Increment retry count and allow retry autoRetryCountRef.current++ if (DEBUG) { console.log( `[sendAutomaticallyWhen] Retrying (${autoRetryCountRef.current}/${MAX_AUTO_RETRY_COUNT})`, ) } return true }, }) // Update stopRef so onToolCall can access it stopRef.current = stop // Ref to track latest messages for unload persistence const messagesRef = useRef(messages) useEffect(() => { messagesRef.current = messages }, [messages]) const messagesEndRef = useRef(null) // Restore messages and XML snapshots from localStorage on mount useEffect(() => { if (hasRestoredRef.current) return hasRestoredRef.current = true try { // Restore messages const savedMessages = localStorage.getItem(STORAGE_MESSAGES_KEY) if (savedMessages) { const parsed = JSON.parse(savedMessages) if (Array.isArray(parsed) && parsed.length > 0) { setMessages(parsed) } } // Restore XML snapshots const savedSnapshots = localStorage.getItem( STORAGE_XML_SNAPSHOTS_KEY, ) if (savedSnapshots) { const parsed = JSON.parse(savedSnapshots) xmlSnapshotsRef.current = new Map(parsed) } } catch (error) { console.error("Failed to restore from localStorage:", error) } }, [setMessages]) // Restore diagram XML when DrawIO becomes ready const hasDiagramRestoredRef = useRef(false) const [canSaveDiagram, setCanSaveDiagram] = useState(false) useEffect(() => { // Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it) if (!isDrawioReady) { hasDiagramRestoredRef.current = false setCanSaveDiagram(false) return } if (hasDiagramRestoredRef.current) return hasDiagramRestoredRef.current = true try { const savedDiagramXml = localStorage.getItem( STORAGE_DIAGRAM_XML_KEY, ) console.log( "[ChatPanel] Restoring diagram, has saved XML:", !!savedDiagramXml, ) if (savedDiagramXml) { console.log( "[ChatPanel] Loading saved diagram XML, length:", savedDiagramXml.length, ) // Skip validation for trusted saved diagrams onDisplayChart(savedDiagramXml, true) chartXMLRef.current = savedDiagramXml } } catch (error) { console.error("Failed to restore diagram from localStorage:", error) } // Allow saving after restore is complete setTimeout(() => { console.log("[ChatPanel] Enabling diagram save") setCanSaveDiagram(true) }, 500) }, [isDrawioReady, onDisplayChart]) // Save messages to localStorage whenever they change useEffect(() => { if (!hasRestoredRef.current) return try { localStorage.setItem(STORAGE_MESSAGES_KEY, JSON.stringify(messages)) } catch (error) { console.error("Failed to save messages to localStorage:", error) } }, [messages]) // Save diagram XML to localStorage whenever it changes useEffect(() => { if (!canSaveDiagram) return if (chartXML && chartXML.length > 300) { localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML) } }, [chartXML, canSaveDiagram]) // Save XML snapshots to localStorage whenever they change const saveXmlSnapshots = useCallback(() => { try { const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries()) localStorage.setItem( STORAGE_XML_SNAPSHOTS_KEY, JSON.stringify(snapshotsArray), ) } catch (error) { console.error( "Failed to save XML snapshots to localStorage:", error, ) } }, []) // Save session ID to localStorage useEffect(() => { localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId) }, [sessionId]) useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) } }, [messages]) // Save state right before page unload (refresh/close) useEffect(() => { const handleBeforeUnload = () => { try { localStorage.setItem( STORAGE_MESSAGES_KEY, JSON.stringify(messagesRef.current), ) localStorage.setItem( STORAGE_XML_SNAPSHOTS_KEY, JSON.stringify( Array.from(xmlSnapshotsRef.current.entries()), ), ) const xml = chartXMLRef.current if (xml && xml.length > 300) { localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xml) } localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId) } catch (error) { console.error("Failed to persist state before unload:", error) } } window.addEventListener("beforeunload", handleBeforeUnload) return () => window.removeEventListener("beforeunload", handleBeforeUnload) }, [sessionId]) const onFormSubmit = async (e: React.FormEvent) => { e.preventDefault() const isProcessing = status === "streaming" || status === "submitted" if (input.trim() && !isProcessing) { // Check if input matches a cached example (only when no messages yet) if (messages.length === 0) { const cached = findCachedResponse( input.trim(), files.length > 0, ) if (cached) { // Add user message and fake assistant response to messages // The chat-message-display useEffect will handle displaying the diagram const toolCallId = `cached-${Date.now()}` // Build user message text including any file content const userText = await processFilesAndAppendContent( input, files, pdfData, ) setMessages([ { id: `user-${Date.now()}`, role: "user" as const, parts: [{ type: "text" as const, text: userText }], }, { id: `assistant-${Date.now()}`, role: "assistant" as const, parts: [ { type: "tool-display_diagram" as const, toolCallId, state: "output-available" as const, input: { xml: cached.xml }, output: "Successfully displayed the diagram.", }, ], }, ] as any) setInput("") setFiles([]) return } } try { let chartXml = await onFetchChart() chartXml = formatXML(chartXml) // Update ref directly to avoid race condition with React's async state update // This ensures edit_diagram has the correct XML before AI responds chartXMLRef.current = chartXml // Build user text by concatenating input with pre-extracted text // (Backend only reads first text part, so we must combine them) const parts: any[] = [] const userText = await processFilesAndAppendContent( input, files, pdfData, parts, ) // Add the combined text as the first part parts.unshift({ type: "text", text: userText }) // Get previous XML from the last snapshot (before this message) const snapshotKeys = Array.from( xmlSnapshotsRef.current.keys(), ).sort((a, b) => b - a) const previousXml = snapshotKeys.length > 0 ? xmlSnapshotsRef.current.get(snapshotKeys[0]) || "" : "" // Save XML snapshot for this message (will be at index = current messages.length) const messageIndex = messages.length xmlSnapshotsRef.current.set(messageIndex, chartXml) saveXmlSnapshots() // Check all quota limits if (!checkAllQuotaLimits()) return sendChatMessage(parts, chartXml, previousXml, sessionId) // Token count is tracked in onFinish with actual server usage setInput("") setFiles([]) } catch (error) { console.error("Error fetching chart data:", error) } } } const handleNewChat = useCallback(() => { setMessages([]) clearDiagram() handleFileChange([]) // Use handleFileChange to also clear pdfData const newSessionId = `session-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}` setSessionId(newSessionId) xmlSnapshotsRef.current.clear() // Clear localStorage with error handling try { localStorage.removeItem(STORAGE_MESSAGES_KEY) localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY) localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY) localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId) toast.success("Started a fresh chat") } catch (error) { console.error("Failed to clear localStorage:", error) toast.warning( "Chat cleared but browser storage could not be updated", ) } setShowNewChatDialog(false) }, [clearDiagram, handleFileChange, setMessages, setSessionId]) const handleInputChange = ( e: React.ChangeEvent, ) => { setInput(e.target.value) } // Helper functions for message actions (regenerate/edit) // Extract previous XML snapshot before a given message index const getPreviousXml = (beforeIndex: number): string => { const snapshotKeys = Array.from(xmlSnapshotsRef.current.keys()) .filter((k) => k < beforeIndex) .sort((a, b) => b - a) return snapshotKeys.length > 0 ? xmlSnapshotsRef.current.get(snapshotKeys[0]) || "" : "" } // Restore diagram from snapshot and update ref const restoreDiagramFromSnapshot = (savedXml: string) => { onDisplayChart(savedXml, true) // Skip validation for trusted snapshots chartXMLRef.current = savedXml } // Clean up snapshots after a given message index const cleanupSnapshotsAfter = (messageIndex: number) => { for (const key of xmlSnapshotsRef.current.keys()) { if (key > messageIndex) { xmlSnapshotsRef.current.delete(key) } } saveXmlSnapshots() } // Check all quota limits (daily requests, tokens, TPM) const checkAllQuotaLimits = (): boolean => { const limitCheck = quotaManager.checkDailyLimit() if (!limitCheck.allowed) { quotaManager.showQuotaLimitToast() return false } const tokenLimitCheck = quotaManager.checkTokenLimit() if (!tokenLimitCheck.allowed) { quotaManager.showTokenLimitToast(tokenLimitCheck.used) return false } const tpmCheck = quotaManager.checkTPMLimit() if (!tpmCheck.allowed) { quotaManager.showTPMLimitToast() return false } return true } // Send chat message with headers and increment quota const sendChatMessage = ( parts: any, xml: string, previousXml: string, sessionId: string, ) => { // Reset auto-retry count on user-initiated message autoRetryCountRef.current = 0 const config = getAIConfig() sendMessage( { parts }, { body: { xml, previousXml, sessionId }, headers: { "x-access-code": config.accessCode, ...(config.aiProvider && { "x-ai-provider": config.aiProvider, ...(config.aiBaseUrl && { "x-ai-base-url": config.aiBaseUrl, }), ...(config.aiApiKey && { "x-ai-api-key": config.aiApiKey, }), ...(config.aiModel && { "x-ai-model": config.aiModel }), }), }, }, ) quotaManager.incrementRequestCount() } // Process files and append content to user text (handles PDF, text, and optionally images) const processFilesAndAppendContent = async ( baseText: string, files: File[], pdfData: Map, imageParts?: any[], ): Promise => { let userText = baseText for (const file of files) { if (isPdfFile(file)) { const extracted = pdfData.get(file) if (extracted?.text) { userText += `\n\n[PDF: ${file.name}]\n${extracted.text}` } } else if (isTextFile(file)) { const extracted = pdfData.get(file) if (extracted?.text) { userText += `\n\n[File: ${file.name}]\n${extracted.text}` } } else if (imageParts) { // Handle as image (only if imageParts array provided) const reader = new FileReader() const dataUrl = await new Promise((resolve) => { reader.onload = () => resolve(reader.result as string) reader.readAsDataURL(file) }) imageParts.push({ type: "file", url: dataUrl, mediaType: file.type, }) } } return userText } const handleRegenerate = async (messageIndex: number) => { const isProcessing = status === "streaming" || status === "submitted" if (isProcessing) return // Find the user message before this assistant message let userMessageIndex = messageIndex - 1 while ( userMessageIndex >= 0 && messages[userMessageIndex].role !== "user" ) { userMessageIndex-- } if (userMessageIndex < 0) return const userMessage = messages[userMessageIndex] const userParts = userMessage.parts // Get the text from the user message const textPart = userParts?.find((p: any) => p.type === "text") if (!textPart) return // Get the saved XML snapshot for this user message const savedXml = xmlSnapshotsRef.current.get(userMessageIndex) if (!savedXml) { console.error( "No saved XML snapshot for message index:", userMessageIndex, ) return } // Get previous XML and restore diagram state const previousXml = getPreviousXml(userMessageIndex) restoreDiagramFromSnapshot(savedXml) // Clean up snapshots for messages after the user message (they will be removed) cleanupSnapshotsAfter(userMessageIndex) // Remove the user message AND assistant message onwards (sendMessage will re-add the user message) // Use flushSync to ensure state update is processed synchronously before sending const newMessages = messages.slice(0, userMessageIndex) flushSync(() => { setMessages(newMessages) }) // Check all quota limits if (!checkAllQuotaLimits()) return // Now send the message after state is guaranteed to be updated sendChatMessage(userParts, savedXml, previousXml, sessionId) // Token count is tracked in onFinish with actual server usage } const handleEditMessage = async (messageIndex: number, newText: string) => { const isProcessing = status === "streaming" || status === "submitted" if (isProcessing) return const message = messages[messageIndex] if (!message || message.role !== "user") return // Get the saved XML snapshot for this user message const savedXml = xmlSnapshotsRef.current.get(messageIndex) if (!savedXml) { console.error( "No saved XML snapshot for message index:", messageIndex, ) return } // Get previous XML and restore diagram state const previousXml = getPreviousXml(messageIndex) restoreDiagramFromSnapshot(savedXml) // Clean up snapshots for messages after the user message (they will be removed) cleanupSnapshotsAfter(messageIndex) // Create new parts with updated text const newParts = message.parts?.map((part: any) => { if (part.type === "text") { return { ...part, text: newText } } return part }) || [{ type: "text", text: newText }] // Remove the user message AND assistant message onwards (sendMessage will re-add the user message) // Use flushSync to ensure state update is processed synchronously before sending const newMessages = messages.slice(0, messageIndex) flushSync(() => { setMessages(newMessages) }) // Check all quota limits if (!checkAllQuotaLimits()) return // Now send the edited message after state is guaranteed to be updated sendChatMessage(newParts, savedXml, previousXml, sessionId) // Token count is tracked in onFinish with actual server usage } // Collapsed view (desktop only) if (!isVisible && !isMobile) { return (
AI Chat
) } // Full view return (
{/* Header */}
Next AI Drawio

Next AI Drawio

{!isMobile && ( About )} {!isMobile && ( )}
setShowNewChatDialog(true)} className="hover:bg-accent" >
setShowSettingsDialog(true)} className="hover:bg-accent" > {!isMobile && ( )}
{/* Messages */}
{/* Input */}
) }