diff --git a/client/package.json b/client/package.json index f3aa2262..d2162c51 100644 --- a/client/package.json +++ b/client/package.json @@ -112,6 +112,7 @@ "tailwind-merge": "^1.9.1", "tailwindcss-animate": "^1.0.5", "tailwindcss-radix": "^2.8.0", + "uuid": "^13.0.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/client/src/components/Audio/TTS.tsx b/client/src/components/Audio/TTS.tsx index 0d935170..f0a521bb 100644 --- a/client/src/components/Audio/TTS.tsx +++ b/client/src/components/Audio/TTS.tsx @@ -47,11 +47,12 @@ export function BrowserTTS({ } }, [audioRef, isSpeaking, playbackRate, messageId]); - logger.log( - 'MessageAudio: audioRef.current?.src, audioRef.current', - audioRef.current?.src, - audioRef.current, - ); + // Silenced: Audio reference logging (too verbose) + // logger.log( + // 'MessageAudio: audioRef.current?.src, audioRef.current', + // audioRef.current?.src, + // audioRef.current, + // ); const handleClick = () => { if (audioRef.current) { @@ -91,7 +92,8 @@ export function BrowserTTS({ }} src={audioRef.current?.src} onError={(error) => { - logger.error('Error fetching audio:', error); + // Silenced: Audio fetch errors are expected when TTS is not configured + // logger.error('Error fetching audio:', error); }} id={`audio-${messageId}`} autoPlay @@ -140,11 +142,12 @@ export function ExternalTTS({ } }, [audioRef, isSpeaking, playbackRate, messageId]); - logger.log( - 'MessageAudio: audioRef.current?.src, audioRef.current', - audioRef.current?.src, - audioRef.current, - ); + // Silenced: Audio reference logging (too verbose) + // logger.log( + // 'MessageAudio: audioRef.current?.src, audioRef.current', + // audioRef.current?.src, + // audioRef.current, + // ); return ( <> diff --git a/client/src/hooks/Letta/useLettaMessageHandler.ts b/client/src/hooks/Letta/useLettaMessageHandler.ts index 10d20271..16585048 100644 --- a/client/src/hooks/Letta/useLettaMessageHandler.ts +++ b/client/src/hooks/Letta/useLettaMessageHandler.ts @@ -486,35 +486,57 @@ export default function useLettaMessageHandler({ if (isNewConversation) { logger.log('letta', 'Saving new conversation to backend...'); - // Create conversation object to save + // Generate a proper UUID for the backend (backend requires UUID format) + const { v4: uuidv4 } = await import('uuid'); + const realUUID = uuidv4(); + logger.log('letta', 'Generated UUID for conversation:', realUUID); + + // Create conversation object to save with real UUID const conversationToSave = { ...conversation, - conversationId, // Use the Letta-generated ID + conversationId: realUUID, // Use real UUID instead of Letta ID title: conversation?.title || conversation?.agentOptions?.lettaAgentName || 'New Chat', + // Ensure agentOptions has model field for validation + agentOptions: { + ...conversation?.agentOptions, + model: conversation?.model || conversation?.agentOptions?.model, + }, }; - // Save conversation to backend - this generates a real UUID + // Save conversation to backend const savedConversation = await dataService.updateConversation({ - conversationId, // Old ID (letta-xxx or 'new') + conversationId: realUUID, ...conversationToSave, }); logger.log('letta', 'Conversation saved with ID:', savedConversation.conversationId); - finalConversationId = savedConversation.conversationId!; + finalConversationId = realUUID; - // Update query cache with saved conversation + // Update query cache with saved conversation AND messages queryClient.setQueryData( [QueryKeys.conversation, finalConversationId], savedConversation, ); + // IMPORTANT: Also update messages cache with the new conversationId + // This prevents messages from disappearing during navigation + queryClient.setQueryData( + [QueryKeys.messages, finalConversationId], + [userMessage, finalAssistantMessage], + ); + // Navigate to the new conversation URL with the real UUID if (finalConversationId !== conversationId) { - logger.log('letta', 'Navigating to saved conversation:', finalConversationId); + console.log('🟣 [PERSISTENCE DEBUG] Navigating to saved conversation:', { + from: conversationId, + to: finalConversationId, + }); navigate(`/c/${finalConversationId}`, { replace: true }); + } else { + console.log('⚠️ [PERSISTENCE DEBUG] ConversationId unchanged - not navigating'); } } else { - logger.log('letta', 'Updating existing conversation...'); + console.log('🟡 [PERSISTENCE DEBUG] EXISTING conversation, updating...'); finalConversationId = conversation.conversationId!; // For existing conversations, just update the title if needed await dataService.updateConversation({ @@ -524,25 +546,39 @@ export default function useLettaMessageHandler({ } // Save messages to backend (both user and assistant messages) - logger.log('letta', 'Saving messages to backend...'); + console.log('🔵 [PERSISTENCE DEBUG] Saving messages to backend...'); // Save user message const userMessageToSave = { ...userMessage, conversationId: finalConversationId, }; - await dataService.saveMessage(userMessageToSave, finalConversationId); + console.log('🔵 [PERSISTENCE DEBUG] Saving user message:', userMessageToSave); + const savedUserMsg = await dataService.saveMessage(userMessageToSave, finalConversationId); + console.log('🟢 [PERSISTENCE DEBUG] User message saved:', savedUserMsg); // Save assistant message const assistantMessageToSave = { ...finalAssistantMessage, conversationId: finalConversationId, }; - await dataService.saveMessage(assistantMessageToSave, finalConversationId); + console.log('🔵 [PERSISTENCE DEBUG] Saving assistant message:', assistantMessageToSave); + const savedAssistantMsg = await dataService.saveMessage(assistantMessageToSave, finalConversationId); + console.log('🟢 [PERSISTENCE DEBUG] Assistant message saved:', savedAssistantMsg); + + console.log('✅ [PERSISTENCE DEBUG] All saves completed successfully!'); - logger.log('letta', 'Messages saved to backend successfully'); + // Update messages in UI with the correct conversationId + if (isNewConversation) { + const updatedUserMessage = { ...userMessage, conversationId: finalConversationId }; + const updatedAssistantMessage = { ...finalAssistantMessage, conversationId: finalConversationId }; + setMessages([...currentMessages, updatedUserMessage, updatedAssistantMessage]); + logger.log('letta', 'Updated messages with new conversationId:', finalConversationId); + } } catch (saveError) { + console.error('❌ [PERSISTENCE DEBUG] Save failed:', saveError); + console.error('❌ [PERSISTENCE DEBUG] Error stack:', (saveError as Error).stack); logger.error('letta', 'Failed to save to backend:', saveError); // Don't fail the whole operation if save fails - messages are still in memory } diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index 240b5583..f26c8378 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -11,7 +11,8 @@ import { ToolCallsMapProvider } from '~/Providers'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; import temporaryStore from '~/store/temporary'; -import { useRecoilCallback } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { lettaAgentMapAtom } from '~/store/letta'; import store from '~/store'; export default function ChatRoute() { @@ -32,6 +33,7 @@ export default function ChatRoute() { useIdChangeEffect(conversationId); const { hasSetConversation, conversation } = store.useCreateConversationAtom(index); const { newConversation } = useNewConvo(); + const setLettaAgentMap = useSetRecoilState(lettaAgentMapAtom); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated, @@ -54,6 +56,31 @@ export default function ChatRoute() { } }, [conversationId, isTemporaryChat, setIsTemporary]); + // Restore Letta agent mapping when conversation is loaded from backend + useEffect(() => { + if (!conversation || !conversation.conversationId) { + return; + } + + // Check if this is a Letta conversation by looking at model or agentOptions + const lettaAgentId = conversation.agentOptions?.lettaAgentId || + (conversation.model?.startsWith('letta:') + ? conversation.model.replace('letta:', '') + : null); + + if (lettaAgentId && conversation.conversationId) { + logger.log('letta', 'Restoring Letta agent mapping for loaded conversation:', { + conversationId: conversation.conversationId, + lettaAgentId, + }); + + setLettaAgentMap((prev) => ({ + ...prev, + [conversation.conversationId!]: lettaAgentId, + })); + } + }, [conversation?.conversationId, conversation?.model, conversation?.agentOptions, setLettaAgentMap]); + /** This effect is mainly for the first conversation state change on first load of the page. * Adjusting this may have unintended consequences on the conversation state. */ diff --git a/package-lock.json b/package-lock.json index 1e8ccb1f..61064299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "LibreChat", + "name": "Letta Chat", "version": "v0.8.1-rc2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "LibreChat", + "name": "Letta Chat", "version": "v0.8.1-rc2", "license": "ISC", "workspaces": [ @@ -535,6 +535,7 @@ "tailwind-merge": "^1.9.1", "tailwindcss-animate": "^1.0.5", "tailwindcss-radix": "^2.8.0", + "uuid": "^13.0.0", "zod": "^3.22.4" }, "devDependencies": { @@ -2048,6 +2049,19 @@ "browserslist": ">= 4.21.0" } }, + "client/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "client/node_modules/vite-plugin-pwa": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz", diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index ecf0a925..5ea32e40 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -579,7 +579,7 @@ export const eAgentOptionsSchema = z.nativeEnum(EAgent); export const tAgentOptionsSchema = z.object({ agent: z.string().default(EAgent.functions), skipCompletion: z.boolean().default(agentOptionSettings.skipCompletion.default), - model: z.string(), + model: z.string().optional(), // Made optional for Letta agents temperature: z.number().default(agentOptionSettings.temperature.default), });