mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
added progressive loading to speed up resuming large chat session loading and added search view to session history
This commit is contained in:
@@ -752,7 +752,7 @@ export default function App() {
|
||||
}
|
||||
initAttemptedRef.current = true;
|
||||
|
||||
console.log(`Initializing app with settings v2`);
|
||||
console.log(`Initializing app`);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
|
||||
@@ -42,22 +42,20 @@
|
||||
* while remaining flexible enough to support different UI contexts (Hub vs Pair).
|
||||
*/
|
||||
|
||||
import React, { useEffect, useContext, createContext, useRef } from 'react';
|
||||
import React, { useEffect, useContext, createContext, useRef, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import UserMessage from './UserMessage';
|
||||
import { SearchView } from './conversation/SearchView';
|
||||
import { AgentHeader } from './AgentHeader';
|
||||
import LayingEggLoader from './LayingEggLoader';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import Splash from './Splash';
|
||||
import PopularChatTopics from './PopularChatTopics';
|
||||
import ProgressiveMessageList from './ProgressiveMessageList';
|
||||
import { SessionSummaryModal } from './context_management/SessionSummaryModal';
|
||||
import {
|
||||
ChatContextManagerProvider,
|
||||
useChatContextManager,
|
||||
} from './context_management/ChatContextManager';
|
||||
import { ContextHandler } from './context_management/ContextHandler';
|
||||
import { type View, ViewOptions } from '../App';
|
||||
import { MainPanelLayout } from './Layout/MainPanelLayout';
|
||||
import ChatInput from './ChatInput';
|
||||
@@ -137,8 +135,6 @@ function BaseChatContent({
|
||||
resetMessagesWithSummary,
|
||||
closeSummaryModal,
|
||||
updateSummary,
|
||||
hasContextHandlerContent,
|
||||
getContextHandlerType,
|
||||
} = useChatContextManager();
|
||||
|
||||
// Use shared chat engine
|
||||
@@ -298,75 +294,14 @@ function BaseChatContent({
|
||||
}
|
||||
append(text);
|
||||
};
|
||||
const renderMessages = () => {
|
||||
return filteredMessages.map((message, index) => {
|
||||
const isUser = isUserMessage(message);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'}`}
|
||||
data-testid="message-container"
|
||||
>
|
||||
{isUser ? (
|
||||
<>
|
||||
{hasContextHandlerContent(message) ? (
|
||||
<ContextHandler
|
||||
messages={messages}
|
||||
messageId={message.id ?? message.created.toString()}
|
||||
chatId={chat.id}
|
||||
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
contextType={getContextHandlerType(message)}
|
||||
onSummaryComplete={() => {
|
||||
// Auto-scroll to bottom when summary is complete
|
||||
setTimeout(() => {
|
||||
if (scrollRef.current?.scrollToBottom) {
|
||||
scrollRef.current.scrollToBottom();
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UserMessage message={message} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasContextHandlerContent(message) ? (
|
||||
<ContextHandler
|
||||
messages={messages}
|
||||
messageId={message.id ?? message.created.toString()}
|
||||
chatId={chat.id}
|
||||
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
contextType={getContextHandlerType(message)}
|
||||
onSummaryComplete={() => {
|
||||
// Auto-scroll to bottom when summary is complete
|
||||
setTimeout(() => {
|
||||
if (scrollRef.current?.scrollToBottom) {
|
||||
scrollRef.current.scrollToBottom();
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<GooseMessage
|
||||
messageHistoryIndex={chat?.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
append={append}
|
||||
appendMessage={(newMessage) => {
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
setMessages(updatedMessages);
|
||||
}}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
// Callback to handle scroll to bottom from ProgressiveMessageList
|
||||
const handleScrollToBottom = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollRef.current?.scrollToBottom) {
|
||||
scrollRef.current.scrollToBottom();
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
@@ -443,10 +378,34 @@ function BaseChatContent({
|
||||
<>
|
||||
{disableSearch ? (
|
||||
// Render messages without SearchView wrapper when search is disabled
|
||||
renderMessages()
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={chat}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
append={append}
|
||||
appendMessage={(newMessage) => {
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
setMessages(updatedMessages);
|
||||
}}
|
||||
isUserMessage={isUserMessage}
|
||||
onScrollToBottom={handleScrollToBottom}
|
||||
/>
|
||||
) : (
|
||||
// Render messages with SearchView wrapper when search is enabled
|
||||
<SearchView>{renderMessages()}</SearchView>
|
||||
<SearchView>
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={chat}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
append={append}
|
||||
appendMessage={(newMessage) => {
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
setMessages(updatedMessages);
|
||||
}}
|
||||
isUserMessage={isUserMessage}
|
||||
onScrollToBottom={handleScrollToBottom}
|
||||
/>
|
||||
</SearchView>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* ProgressiveMessageList Component
|
||||
*
|
||||
* A performance-optimized message list that renders messages progressively
|
||||
* to prevent UI blocking when loading long chat sessions. This component
|
||||
* renders messages in batches with a loading indicator, maintaining full
|
||||
* compatibility with the search functionality.
|
||||
*
|
||||
* Key Features:
|
||||
* - Progressive rendering in configurable batches
|
||||
* - Loading indicator during batch processing
|
||||
* - Maintains search functionality compatibility
|
||||
* - Smooth user experience with responsive UI
|
||||
* - Configurable batch size and delay
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Message } from '../types/message';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import UserMessage from './UserMessage';
|
||||
import { ContextHandler } from './context_management/ContextHandler';
|
||||
import { useChatContextManager } from './context_management/ChatContextManager';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
|
||||
interface ProgressiveMessageListProps {
|
||||
messages: Message[];
|
||||
chat?: { id: string; messageHistoryIndex: number }; // Make optional for session history
|
||||
toolCallNotifications?: Map<string, NotificationEvent[]>; // Make optional
|
||||
append?: (value: string) => void; // Make optional
|
||||
appendMessage?: (message: Message) => void; // Make optional
|
||||
isUserMessage: (message: Message) => boolean;
|
||||
onScrollToBottom?: () => void;
|
||||
batchSize?: number;
|
||||
batchDelay?: number;
|
||||
showLoadingThreshold?: number; // Only show loading if more than X messages
|
||||
// Custom render function for messages
|
||||
renderMessage?: (message: Message, index: number) => React.ReactNode | null;
|
||||
}
|
||||
|
||||
export default function ProgressiveMessageList({
|
||||
messages,
|
||||
chat,
|
||||
toolCallNotifications = new Map(),
|
||||
append = () => {},
|
||||
appendMessage = () => {},
|
||||
isUserMessage,
|
||||
onScrollToBottom,
|
||||
batchSize = 15, // Render 15 messages per batch (reduced for better UX)
|
||||
batchDelay = 30, // 30ms delay between batches (faster)
|
||||
showLoadingThreshold = 30, // Only show progressive loading for 30+ messages (lower threshold)
|
||||
renderMessage, // Custom render function
|
||||
}: ProgressiveMessageListProps) {
|
||||
const [renderedCount, setRenderedCount] = useState(() => {
|
||||
// Initialize with either all messages (if small) or first batch (if large)
|
||||
return messages.length <= showLoadingThreshold
|
||||
? messages.length
|
||||
: Math.min(batchSize, messages.length);
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(() => messages.length > showLoadingThreshold);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
// Try to use context manager, but don't require it for session history
|
||||
let hasContextHandlerContent: ((message: Message) => boolean) | undefined;
|
||||
let getContextHandlerType:
|
||||
| ((message: Message) => 'contextLengthExceeded' | 'summarizationRequested')
|
||||
| undefined;
|
||||
|
||||
try {
|
||||
const contextManager = useChatContextManager();
|
||||
hasContextHandlerContent = contextManager.hasContextHandlerContent;
|
||||
getContextHandlerType = contextManager.getContextHandlerType;
|
||||
} catch (error) {
|
||||
// Context manager not available (e.g., in session history view)
|
||||
// This is fine, we'll just skip context handler functionality
|
||||
hasContextHandlerContent = undefined;
|
||||
getContextHandlerType = undefined;
|
||||
}
|
||||
|
||||
// Simple progressive loading - start immediately when component mounts if needed
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
`ProgressiveMessageList: ${messages.length} messages, threshold: ${showLoadingThreshold}, renderedCount: ${renderedCount}`
|
||||
);
|
||||
|
||||
if (messages.length <= showLoadingThreshold) {
|
||||
// Small list - render all immediately
|
||||
console.log('ProgressiveMessageList: Small list, rendering all immediately');
|
||||
setRenderedCount(messages.length);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('ProgressiveMessageList: Large list, starting progressive loading');
|
||||
|
||||
// Large list - start progressive loading
|
||||
const loadNextBatch = () => {
|
||||
setRenderedCount((current) => {
|
||||
const nextCount = Math.min(current + batchSize, messages.length);
|
||||
console.log(
|
||||
`ProgressiveMessageList: Loading batch, now showing ${nextCount}/${messages.length}`
|
||||
);
|
||||
|
||||
if (nextCount >= messages.length) {
|
||||
console.log('ProgressiveMessageList: All messages loaded');
|
||||
setIsLoading(false);
|
||||
// Trigger scroll to bottom
|
||||
window.setTimeout(() => {
|
||||
onScrollToBottom?.();
|
||||
}, 100);
|
||||
} else {
|
||||
// Schedule next batch
|
||||
timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay);
|
||||
}
|
||||
|
||||
return nextCount;
|
||||
});
|
||||
};
|
||||
|
||||
// Start loading after a short delay
|
||||
timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay);
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [
|
||||
messages.length,
|
||||
batchSize,
|
||||
batchDelay,
|
||||
showLoadingThreshold,
|
||||
onScrollToBottom,
|
||||
renderedCount,
|
||||
]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Force complete rendering when search is active
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isMac = window.electron.platform === 'darwin';
|
||||
const isSearchShortcut = (isMac ? e.metaKey : e.ctrlKey) && e.key === 'f';
|
||||
|
||||
if (isSearchShortcut && isLoading) {
|
||||
// Immediately render all messages when search is triggered
|
||||
setRenderedCount(messages.length);
|
||||
setIsLoading(false);
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isLoading, messages.length]);
|
||||
|
||||
// Render messages up to the current rendered count
|
||||
const renderMessages = useCallback(() => {
|
||||
const messagesToRender = messages.slice(0, renderedCount);
|
||||
|
||||
const renderedMessages = messagesToRender
|
||||
.map((message, index) => {
|
||||
// Use custom render function if provided
|
||||
if (renderMessage) {
|
||||
return renderMessage(message, index);
|
||||
}
|
||||
|
||||
// Default rendering logic (for BaseChat)
|
||||
if (!chat) {
|
||||
console.warn(
|
||||
'ProgressiveMessageList: chat prop is required when not using custom renderMessage'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const isUser = isUserMessage(message);
|
||||
|
||||
const result = (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'}`}
|
||||
data-testid="message-container"
|
||||
>
|
||||
{isUser ? (
|
||||
<>
|
||||
{hasContextHandlerContent && hasContextHandlerContent(message) ? (
|
||||
<ContextHandler
|
||||
messages={messages}
|
||||
messageId={message.id ?? message.created.toString()}
|
||||
chatId={chat.id}
|
||||
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
contextType={getContextHandlerType!(message)}
|
||||
onSummaryComplete={() => {
|
||||
window.setTimeout(() => onScrollToBottom?.(), 100);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UserMessage message={message} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasContextHandlerContent && hasContextHandlerContent(message) ? (
|
||||
<ContextHandler
|
||||
messages={messages}
|
||||
messageId={message.id ?? message.created.toString()}
|
||||
chatId={chat.id}
|
||||
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
contextType={getContextHandlerType!(message)}
|
||||
onSummaryComplete={() => {
|
||||
window.setTimeout(() => onScrollToBottom?.(), 100);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<GooseMessage
|
||||
messageHistoryIndex={chat.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
append={append}
|
||||
appendMessage={appendMessage}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return result;
|
||||
})
|
||||
.filter(Boolean); // Filter out null values
|
||||
|
||||
return renderedMessages;
|
||||
}, [
|
||||
messages,
|
||||
renderedCount,
|
||||
renderMessage,
|
||||
isUserMessage,
|
||||
hasContextHandlerContent,
|
||||
getContextHandlerType,
|
||||
chat,
|
||||
append,
|
||||
appendMessage,
|
||||
toolCallNotifications,
|
||||
onScrollToBottom,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderMessages()}
|
||||
|
||||
{/* Loading indicator when progressively rendering */}
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<LoadingGoose message={`Loading messages... (${renderedCount}/${messages.length})`} />
|
||||
<div className="text-xs text-text-muted mt-2">
|
||||
Press Cmd/Ctrl+F to load all messages immediately for search
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -28,10 +28,47 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../ui/dialog';
|
||||
import UserMessage from '../UserMessage';
|
||||
import GooseMessage from '../GooseMessage';
|
||||
import ProgressiveMessageList from '../ProgressiveMessageList';
|
||||
import { SearchView } from '../conversation/SearchView';
|
||||
import { ChatContextManagerProvider } from '../context_management/ChatContextManager';
|
||||
import { Message } from '../../types/message';
|
||||
|
||||
// Helper function to determine if a message is a user message (same as useChatEngine)
|
||||
const isUserMessage = (message: Message): boolean => {
|
||||
if (message.role === 'assistant') {
|
||||
return false;
|
||||
}
|
||||
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Filter messages for display (same logic as useChatEngine)
|
||||
const filterMessagesForDisplay = (messages: Message[]): Message[] => {
|
||||
return messages.filter((message) => {
|
||||
// Only filter out when display is explicitly false
|
||||
if (message.display === false) return false;
|
||||
|
||||
// Keep all assistant messages and user messages that aren't just tool responses
|
||||
if (message.role === 'assistant') return true;
|
||||
|
||||
// For user messages, check if they're only tool responses
|
||||
if (message.role === 'user') {
|
||||
const hasOnlyToolResponses = message.content.every((c) => c.type === 'toolResponse');
|
||||
const hasTextContent = message.content.some((c) => c.type === 'text');
|
||||
const hasToolConfirmation = message.content.every(
|
||||
(c) => c.type === 'toolConfirmationRequest'
|
||||
);
|
||||
|
||||
// Keep the message if it has text content or tool confirmation or is not just tool responses
|
||||
return hasTextContent || !hasOnlyToolResponses || hasToolConfirmation;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
interface SessionHistoryViewProps {
|
||||
session: SessionDetails;
|
||||
isLoading: boolean;
|
||||
@@ -64,13 +101,16 @@ const SessionHeader: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
// New component for displaying session messages with chat styling
|
||||
// Session messages component that uses the same rendering as BaseChat
|
||||
const SessionMessages: React.FC<{
|
||||
messages: Message[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
onRetry: () => void;
|
||||
}> = ({ messages, isLoading, error, onRetry }) => {
|
||||
// Filter messages for display (same as BaseChat)
|
||||
const filteredMessages = filterMessagesForDisplay(messages);
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="pb-24 pt-8">
|
||||
@@ -90,35 +130,30 @@ const SessionMessages: React.FC<{
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : messages?.length > 0 ? (
|
||||
<div className="max-w-4xl mx-auto w-full">
|
||||
{messages
|
||||
.map((message) => {
|
||||
// Skip pure tool response messages for cleaner display
|
||||
const isOnlyToolResponse =
|
||||
message.content.length > 0 &&
|
||||
message.content.every((c) => c.type === 'toolResponse');
|
||||
|
||||
if (isOnlyToolResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return message.role === 'user' ? (
|
||||
<UserMessage key={message.id} message={message} />
|
||||
) : (
|
||||
<GooseMessage
|
||||
key={message.id}
|
||||
messageHistoryIndex={messages.length}
|
||||
message={message}
|
||||
messages={messages}
|
||||
append={() => {}}
|
||||
appendMessage={() => {}}
|
||||
toolCallNotifications={new Map()}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</div>
|
||||
) : filteredMessages?.length > 0 ? (
|
||||
<ChatContextManagerProvider>
|
||||
<div className="max-w-4xl mx-auto w-full">
|
||||
<SearchView>
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={{
|
||||
id: 'session-preview',
|
||||
messageHistoryIndex: filteredMessages.length,
|
||||
}}
|
||||
toolCallNotifications={new Map()}
|
||||
append={() => {}} // Read-only for session history
|
||||
appendMessage={(newMessage) => {
|
||||
// Read-only - do nothing
|
||||
console.log('appendMessage called in read-only session history:', newMessage);
|
||||
}}
|
||||
isUserMessage={isUserMessage} // Use the same function as BaseChat
|
||||
batchSize={15} // Same as BaseChat default
|
||||
batchDelay={30} // Same as BaseChat default
|
||||
showLoadingThreshold={30} // Same as BaseChat default
|
||||
/>
|
||||
</SearchView>
|
||||
</div>
|
||||
</ChatContextManagerProvider>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<MessageSquareText className="w-12 h-12 mb-4" />
|
||||
|
||||
@@ -27,33 +27,19 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
|
||||
// First check if we have a persisted recipe in chat context
|
||||
// We need to distinguish between null (explicitly cleared) and undefined (not set)
|
||||
if (chatContext?.chat.recipeConfig !== undefined) {
|
||||
console.log('useRecipeManager: Using recipe from chat context:', {
|
||||
recipeConfig: chatContext.chat.recipeConfig,
|
||||
title: chatContext.chat.recipeConfig?.title,
|
||||
});
|
||||
return chatContext.chat.recipeConfig; // This could be null or a Recipe
|
||||
}
|
||||
|
||||
// Then check if recipe config is passed via navigation state
|
||||
if (locationState?.recipeConfig) {
|
||||
console.log('useRecipeManager: Using recipe from location state:', {
|
||||
recipeConfig: locationState.recipeConfig,
|
||||
title: locationState.recipeConfig.title,
|
||||
});
|
||||
return locationState.recipeConfig as Recipe;
|
||||
}
|
||||
|
||||
// Fallback to app config (from deeplinks)
|
||||
const appRecipeConfig = window.appConfig.get('recipeConfig') as Recipe | null;
|
||||
if (appRecipeConfig) {
|
||||
console.log('useRecipeManager: Using recipe from app config:', {
|
||||
recipeConfig: appRecipeConfig,
|
||||
title: appRecipeConfig.title,
|
||||
});
|
||||
return appRecipeConfig;
|
||||
}
|
||||
|
||||
console.log('useRecipeManager: No recipe config found');
|
||||
return null;
|
||||
}, [chatContext, locationState]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user