UI update with sidebar and settings tabs (#3288)
Co-authored-by: Nahiyan Khan <nahiyan@squareup.com> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Lily Delalande <119957291+lily-de@users.noreply.github.com> Co-authored-by: Spence <spencrmartin@gmail.com> Co-authored-by: spencrmartin <spencermartin@squareup.com> Co-authored-by: Judson Stephenson <Jud@users.noreply.github.com> Co-authored-by: Max Novich <mnovich@squareup.com> Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com> Co-authored-by: caroline-a-mckenzie <cmckenzie@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { createRecipe, Recipe } from '../recipe';
|
||||
import { Message, createUserMessage } from '../types/message';
|
||||
import { updateSystemPromptWithParameters } from '../utils/providerUtils';
|
||||
import { useChatContext } from '../contexts/ChatContext';
|
||||
|
||||
interface LocationState {
|
||||
recipeConfig?: Recipe;
|
||||
disableAnimation?: boolean;
|
||||
reset?: boolean;
|
||||
}
|
||||
|
||||
export const useRecipeManager = (messages: Message[], locationState?: LocationState) => {
|
||||
const [isGeneratingRecipe, setIsGeneratingRecipe] = useState(false);
|
||||
const [isParameterModalOpen, setIsParameterModalOpen] = useState(false);
|
||||
const [recipeParameters, setRecipeParameters] = useState<Record<string, string> | null>(null);
|
||||
const [readyForAutoUserPrompt, setReadyForAutoUserPrompt] = useState(false);
|
||||
const [recipeError, setRecipeError] = useState<string | null>(null);
|
||||
|
||||
// Get chat context to access persisted recipe
|
||||
const chatContext = useChatContext();
|
||||
|
||||
// Use a ref to capture the current messages for the event handler
|
||||
const messagesRef = useRef(messages);
|
||||
const isCreatingRecipeRef = useRef(false);
|
||||
|
||||
// Update the ref when messages change
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages;
|
||||
}, [messages]);
|
||||
|
||||
// Get recipeConfig from multiple sources with priority:
|
||||
// 1. Chat context (persisted recipe)
|
||||
// 2. Navigation state (from recipes view)
|
||||
// 3. App config (from deeplinks)
|
||||
const recipeConfig = useMemo(() => {
|
||||
// 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) {
|
||||
return chatContext.chat.recipeConfig; // This could be null or a Recipe
|
||||
}
|
||||
|
||||
// Then check if recipe config is passed via navigation state
|
||||
if (locationState?.recipeConfig) {
|
||||
return locationState.recipeConfig as Recipe;
|
||||
}
|
||||
|
||||
// Fallback to app config (from deeplinks)
|
||||
const appRecipeConfig = window.appConfig.get('recipeConfig') as Recipe | null;
|
||||
if (appRecipeConfig) {
|
||||
return appRecipeConfig;
|
||||
}
|
||||
return null;
|
||||
}, [chatContext, locationState]);
|
||||
|
||||
// Effect to persist recipe config to chat context when it changes
|
||||
useEffect(() => {
|
||||
if (!chatContext?.setRecipeConfig) return;
|
||||
|
||||
// If we have a recipe from navigation state, persist it
|
||||
if (locationState?.recipeConfig && !chatContext.chat.recipeConfig) {
|
||||
chatContext.setRecipeConfig(locationState.recipeConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a recipe from app config (deeplink), persist it
|
||||
// But only if the chat context doesn't explicitly have null (which indicates it was cleared)
|
||||
const appRecipeConfig = window.appConfig.get('recipeConfig') as Recipe | null;
|
||||
if (appRecipeConfig && chatContext.chat.recipeConfig === undefined) {
|
||||
// Only set if recipeConfig is undefined, not if it's explicitly null
|
||||
chatContext.setRecipeConfig(appRecipeConfig);
|
||||
}
|
||||
}, [chatContext, locationState]);
|
||||
|
||||
// Show parameter modal if recipe has parameters and they haven't been set yet
|
||||
useEffect(() => {
|
||||
if (recipeConfig?.parameters && recipeConfig.parameters.length > 0) {
|
||||
// If we have parameters and they haven't been set yet, open the modal.
|
||||
if (!recipeParameters) {
|
||||
setIsParameterModalOpen(true);
|
||||
}
|
||||
}
|
||||
}, [recipeConfig, recipeParameters]);
|
||||
|
||||
// Set ready for auto user prompt after component initialization
|
||||
useEffect(() => {
|
||||
setReadyForAutoUserPrompt(true);
|
||||
}, []);
|
||||
|
||||
// Substitute parameters in prompt
|
||||
const substituteParameters = (prompt: string, params: Record<string, string>): string => {
|
||||
let substitutedPrompt = prompt;
|
||||
|
||||
for (const key in params) {
|
||||
// Escape special characters in the key (parameter) and match optional whitespace
|
||||
const regex = new RegExp(`{{\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*}}`, 'g');
|
||||
substitutedPrompt = substitutedPrompt.replace(regex, params[key]);
|
||||
}
|
||||
return substitutedPrompt;
|
||||
};
|
||||
|
||||
// Pre-fill input with recipe prompt instead of auto-sending it
|
||||
const initialPrompt = useMemo(() => {
|
||||
if (!recipeConfig?.prompt) return '';
|
||||
|
||||
const hasRequiredParams = recipeConfig.parameters && recipeConfig.parameters.length > 0;
|
||||
|
||||
// If params are required and have been collected, substitute them into the prompt.
|
||||
if (hasRequiredParams && recipeParameters) {
|
||||
return substituteParameters(recipeConfig.prompt, recipeParameters);
|
||||
}
|
||||
|
||||
// If there are no parameters, return the original prompt.
|
||||
if (!hasRequiredParams) {
|
||||
return recipeConfig.prompt;
|
||||
}
|
||||
|
||||
// Otherwise, we are waiting for parameters, so the input should be empty.
|
||||
return '';
|
||||
}, [recipeConfig, recipeParameters]);
|
||||
|
||||
// Handle parameter submission
|
||||
const handleParameterSubmit = async (inputValues: Record<string, string>) => {
|
||||
setRecipeParameters(inputValues);
|
||||
setIsParameterModalOpen(false);
|
||||
|
||||
// Update the system prompt with parameter-substituted instructions
|
||||
try {
|
||||
await updateSystemPromptWithParameters(inputValues);
|
||||
} catch (error) {
|
||||
console.error('Failed to update system prompt with parameters:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-execution handler for scheduled recipes
|
||||
const handleAutoExecution = (append: (message: Message) => void, isLoading: boolean) => {
|
||||
const hasRequiredParams = recipeConfig?.parameters && recipeConfig.parameters.length > 0;
|
||||
|
||||
if (
|
||||
recipeConfig?.isScheduledExecution &&
|
||||
recipeConfig?.prompt &&
|
||||
(!hasRequiredParams || recipeParameters) &&
|
||||
messages.length === 0 &&
|
||||
!isLoading &&
|
||||
readyForAutoUserPrompt
|
||||
) {
|
||||
// Substitute parameters if they exist
|
||||
const finalPrompt = recipeParameters
|
||||
? substituteParameters(recipeConfig.prompt, recipeParameters)
|
||||
: recipeConfig.prompt;
|
||||
|
||||
console.log('Auto-sending substituted prompt for scheduled execution:', finalPrompt);
|
||||
|
||||
const userMessage = createUserMessage(finalPrompt);
|
||||
append(userMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for make-agent-from-chat event
|
||||
useEffect(() => {
|
||||
const handleMakeAgent = async () => {
|
||||
// Prevent duplicate calls using global flag
|
||||
if (window.isCreatingRecipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate calls using local ref
|
||||
if (isCreatingRecipeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.electron.logInfo('Making recipe from chat...');
|
||||
|
||||
// Set both local and global flags
|
||||
isCreatingRecipeRef.current = true;
|
||||
window.isCreatingRecipe = true;
|
||||
setIsGeneratingRecipe(true);
|
||||
|
||||
try {
|
||||
// Create recipe directly from chat messages using the ref to get current messages
|
||||
const createRecipeRequest = {
|
||||
messages: messagesRef.current,
|
||||
title: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const response = await createRecipe(createRecipeRequest);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`Failed to create recipe: ${response.error}`);
|
||||
}
|
||||
|
||||
window.electron.logInfo('Created recipe successfully');
|
||||
|
||||
// Verify the recipe data
|
||||
if (!response.recipe) {
|
||||
throw new Error('No recipe data received');
|
||||
}
|
||||
|
||||
// Create a new window for the recipe editor
|
||||
const recipeConfig = {
|
||||
id: response.recipe.title || 'untitled',
|
||||
title: response.recipe.title || 'Untitled Recipe',
|
||||
description: response.recipe.description || '',
|
||||
instructions: response.recipe.instructions || '',
|
||||
activities: response.recipe.activities || [],
|
||||
prompt: response.recipe.prompt || '',
|
||||
};
|
||||
|
||||
// Set a flag to prevent the current window from reacting to recipe config changes
|
||||
// This prevents navigation conflicts when creating new windows
|
||||
window.sessionStorage.setItem('ignoreRecipeConfigChanges', 'true');
|
||||
|
||||
// Create the new window
|
||||
window.electron.createChatWindow(
|
||||
undefined, // query
|
||||
undefined, // dir
|
||||
undefined, // version
|
||||
undefined, // resumeSessionId
|
||||
recipeConfig, // recipe config
|
||||
'recipeEditor' // view type
|
||||
);
|
||||
|
||||
window.electron.logInfo('Opening recipe editor window');
|
||||
|
||||
// Clear the ignore flag after a short delay
|
||||
setTimeout(() => {
|
||||
window.sessionStorage.removeItem('ignoreRecipeConfigChanges');
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
window.electron.logInfo('Failed to create recipe:');
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
window.electron.logInfo(errorMessage);
|
||||
|
||||
// Set the error state to show in modal
|
||||
setRecipeError(errorMessage);
|
||||
} finally {
|
||||
isCreatingRecipeRef.current = false;
|
||||
window.isCreatingRecipe = false;
|
||||
setIsGeneratingRecipe(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('make-agent-from-chat', handleMakeAgent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('make-agent-from-chat', handleMakeAgent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
recipeConfig,
|
||||
initialPrompt,
|
||||
isGeneratingRecipe,
|
||||
isParameterModalOpen,
|
||||
setIsParameterModalOpen,
|
||||
recipeParameters,
|
||||
handleParameterSubmit,
|
||||
handleAutoExecution,
|
||||
recipeError,
|
||||
setRecipeError,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user