import React, { useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { SearchView } from './conversation/SearchView'; import LoadingGoose from './LoadingGoose'; import PopularChatTopics from './PopularChatTopics'; import ProgressiveMessageList from './ProgressiveMessageList'; import { View, ViewOptions } from '../utils/navigationUtils'; import { ContextManagerProvider } from './context_management/ContextManager'; import { MainPanelLayout } from './Layout/MainPanelLayout'; import ChatInput from './ChatInput'; import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area'; import { useFileDrop } from '../hooks/useFileDrop'; import { Message, Session } from '../api'; import { ChatState } from '../types/chatState'; import { ChatType } from '../types/chat'; import { useIsMobile } from '../hooks/use-mobile'; import { useSidebar } from './ui/sidebar'; import { cn } from '../utils'; import { useChatStream } from '../hooks/useChatStream'; import { loadSession } from '../utils/sessionCache'; interface BaseChatProps { chat: ChatType; setChat: (chat: ChatType) => void; setView: (view: View, viewOptions?: ViewOptions) => void; setIsGoosehintsModalOpen?: (isOpen: boolean) => void; onMessageStreamFinish?: () => void; onMessageSubmit?: (message: string) => void; renderHeader?: () => React.ReactNode; renderBeforeMessages?: () => React.ReactNode; renderAfterMessages?: () => React.ReactNode; customChatInputProps?: Record; customMainLayoutProps?: Record; contentClassName?: string; disableSearch?: boolean; showPopularTopics?: boolean; suppressEmptyState?: boolean; autoSubmit?: boolean; resumeSessionId?: string; // Optional session ID to resume on mount } function BaseChatContent({ chat, setChat, setView, setIsGoosehintsModalOpen, renderHeader, renderBeforeMessages, renderAfterMessages, customChatInputProps = {}, customMainLayoutProps = {}, disableSearch = false, resumeSessionId, }: BaseChatProps) { const location = useLocation(); const scrollRef = useRef(null); const disableAnimation = location.state?.disableAnimation || false; // const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false); // const [currentRecipeTitle, setCurrentRecipeTitle] = React.useState(null); // const { isCompacting, handleManualCompaction } = useContextManager(); const isMobile = useIsMobile(); const { state: sidebarState } = useSidebar(); const contentClassName = cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11'); // Use shared file drop const { droppedFiles, setDroppedFiles, handleDrop, handleDragOver } = useFileDrop(); // Use shared cost tracking // const { sessionCosts } = useCostTracking({ // sessionInputTokens, // sessionOutputTokens, // localInputTokens, // localOutputTokens, // session: sessionMetadata, // }); // Session loading state const [sessionLoadError, setSessionLoadError] = useState(null); const hasLoadedSessionRef = useRef(false); const [messages, setMessages] = useState(chat.messages || []); // Load session on mount if resumeSessionId is provided useEffect(() => { const needsLoad = resumeSessionId && !hasLoadedSessionRef.current; if (needsLoad) { hasLoadedSessionRef.current = true; setSessionLoadError(null); // Set chat to empty session to indicate loading state // todo: set to null instead and handle that in other places const emptyChat: ChatType = { sessionId: resumeSessionId, name: 'Loading...', messageHistoryIndex: 0, messages: [], recipe: null, recipeParameters: null, }; setChat(emptyChat); loadSession(resumeSessionId) .then((session: Session) => { const conversation = session.conversation || []; const loadedChat: ChatType = { sessionId: session.id, name: session.name || 'Untitled Chat', messageHistoryIndex: 0, messages: conversation, recipe: null, recipeParameters: null, }; setChat(loadedChat); }) .catch((error: Error) => { const errorMessage = error.message || 'Failed to load session'; setSessionLoadError(errorMessage); }); } }, [resumeSessionId, setChat]); // Update messages when chat changes (e.g., when resuming a session) useEffect(() => { if (chat.messages) { setMessages(chat.messages); } }, [chat.messages, chat.sessionId]); const { chatState, handleSubmit, stopStreaming } = useChatStream({ sessionId: chat.sessionId || '', messages, setMessages, onStreamFinish: () => {}, }); const handleFormSubmit = (e: React.FormEvent) => { const customEvent = e as unknown as CustomEvent; const textValue = customEvent.detail?.value || ''; // if (recipe && textValue.trim()) { // setHasStartedUsingRecipe(true); // } // // if (onMessageSubmit && textValue.trim()) { // onMessageSubmit(textValue); // } handleSubmit(textValue); }; // TODO(Douwe): send this to the chatbox instead, possibly autosubmit? or backend const append = (_txt: string) => {}; useEffect(() => { window.electron.logInfo( 'Initial messages when resuming session: ' + JSON.stringify(messages, null, 2) ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Track if this is the initial render for session resuming const initialRenderRef = useRef(true); const recipe = chat?.recipe; // Auto-scroll when messages are loaded (for session resuming) const handleRenderingComplete = React.useCallback(() => { // Only force scroll on the very first render if (initialRenderRef.current && messages.length > 0) { initialRenderRef.current = false; if (scrollRef.current?.scrollToBottom) { scrollRef.current.scrollToBottom(); } } else if (scrollRef.current?.isFollowing) { if (scrollRef.current?.scrollToBottom) { scrollRef.current.scrollToBottom(); } } }, [messages.length]); //const toolCount = useToolCount(chat.sessionId); // Wrapper for append that tracks recipe usage // const appendWithTracking = (text: string | Message) => { // // Mark that user has started using the recipe when they use append // if (recipe) { // setHasStartedUsingRecipe(true); // } // append(text); // }; // Listen for global scroll-to-bottom requests (e.g., from MCP UI prompt actions) useEffect(() => { const handleGlobalScrollRequest = () => { // Add a small delay to ensure content has been rendered setTimeout(() => { if (scrollRef.current?.scrollToBottom) { scrollRef.current.scrollToBottom(); } }, 200); }; window.addEventListener('scroll-chat-to-bottom', handleGlobalScrollRequest); return () => window.removeEventListener('scroll-chat-to-bottom', handleGlobalScrollRequest); }, []); const renderProgressiveMessageList = (chat: ChatType) => ( <> { // const updatedMessages = [...messages, newMessage]; // setMessages(updatedMessages); // }} isUserMessage={(m: Message) => m.role === 'user'} isStreamingMessage={chatState !== ChatState.Idle} // onMessageUpdate={onMessageUpdate} onRenderingComplete={handleRenderingComplete} /> ); const showPopularTopics = messages.length === 0; // TODO(Douwe): get this from the backend const isCompacting = false; const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : ''; return (

Warning: BaseChat2!

{/* Custom header */} {renderHeader && renderHeader()} {/* Chat container with sticky recipe header */}
{/*/!* Recipe agent header - sticky at top of chat container *!/*/} {/*{recipe?.title && (*/} {/*
*/} {/* {*/} {/* console.log('Change profile clicked');*/} {/* }}*/} {/* showBorder={true}*/} {/* />*/} {/*
*/} {/*)}*/} {/* Custom content before messages */} {renderBeforeMessages && renderBeforeMessages()} {/*/!* Recipe Activities - always show when recipe is active and accepted *!/*/} {/*{recipe && recipeAccepted && !suppressEmptyState && (*/} {/*
*/} {/* appendWithTracking(text)}*/} {/* activities={Array.isArray(recipe.activities) ? recipe.activities : null}*/} {/* title={recipe.title}*/} {/* parameterValues={recipeParameters || {}}*/} {/* />*/} {/*
*/} {/*)}*/} {sessionLoadError && (

Failed to Load Session

{sessionLoadError}

)} {/* Messages or Popular Topics */} { messages.length > 0 || recipe ? ( <> {disableSearch ? ( renderProgressiveMessageList(chat) ) : ( // Render messages with SearchView wrapper when search is enabled {renderProgressiveMessageList(chat)} )} {/*{error && (*/} {/* <>*/} {/*
*/} {/*
*/} {/* {error.message || 'Honk! Goose experienced an error while responding'}*/} {/*
*/} {/* /!* Action buttons for all errors including token limit errors *!/*/} {/*
*/} {/* {*/} {/* clearError();*/} {/* await handleManualCompaction(*/} {/* messages,*/} {/* setMessages,*/} {/* append,*/} {/* chat.sessionId*/} {/* );*/} {/* }}*/} {/* >*/} {/* Summarize Conversation*/} {/*
*/} {/* {*/} {/* // Find the last user message*/} {/* const lastUserMessage = messages.reduceRight(*/} {/* (found, m) => found || (m.role === 'user' ? m : null),*/} {/* null as Message | null*/} {/* );*/} {/* if (lastUserMessage) {*/} {/* await append(lastUserMessage);*/} {/* }*/} {/* }}*/} {/* >*/} {/* Retry Last Message*/} {/*
*/} {/*
*/} {/*
*/} {/* */} {/*)}*/}
) : !recipe && showPopularTopics ? ( /* Show PopularChatTopics when no messages, no recipe, and showPopularTopics is true (Pair view) */ append(text)} /> ) : null /* Show nothing when messages.length === 0 && suppressEmptyState === true */ } {/* Custom content after messages */} {renderAfterMessages && renderAfterMessages()} {/* Fixed loading indicator at bottom left of chat container */} {(messages.length === 0 || isCompacting) && !sessionLoadError && (
)}
setDroppedFiles([])} // Clear dropped files after processing messages={messages} setMessages={(_m) => {}} disableAnimation={disableAnimation} //sessionCosts={sessionCosts} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} recipe={recipe} //recipeAccepted={recipeAccepted} initialPrompt={initialPrompt} //toolCount={toolCount || 0} toolCount={0} //autoSubmit={autoSubmit} autoSubmit={false} //append={append} {...customChatInputProps} />
{/*/!* Recipe Warning Modal *!/*/} {/**/} {/*/!* Recipe Parameter Modal *!/*/} {/*{isParameterModalOpen && filteredParameters.length > 0 && (*/} {/* setIsParameterModalOpen(false)}*/} {/* />*/} {/*)}*/} {/*/!* Create Recipe from Session Modal *!/*/} {/* setIsCreateRecipeModalOpen(false)}*/} {/* sessionId={chat.sessionId}*/} {/* onRecipeCreated={handleRecipeCreated}*/} {/*/>*/} ); } export default function BaseChat(props: BaseChatProps) { return ( ); }