diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 7daffb6dd9..da2e6839b1 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -49,6 +49,8 @@ interface BaseChatProps { enhancingPrompt?: boolean; promptEnhanced?: boolean; input?: string; + projectMemory?: string; + setProjectMemory?: (value: string) => void; model?: string; setModel?: (model: string) => void; provider?: ProviderInfo; @@ -97,6 +99,8 @@ export const BaseChat = React.forwardRef( setProvider, providerList, input = '', + projectMemory, + setProjectMemory, enhancingPrompt, handleInputChange, @@ -442,6 +446,8 @@ export const BaseChat = React.forwardRef( setImageDataList={setImageDataList} textareaRef={textareaRef} input={input} + projectMemory={projectMemory} + setProjectMemory={setProjectMemory} handleInputChange={handleInputChange} handlePaste={handlePaste} TEXTAREA_MIN_HEIGHT={TEXTAREA_MIN_HEIGHT} diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c4706e1764..387f743fa4 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -6,6 +6,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks'; import { description, useChatHistory } from '~/lib/persistence'; +import { chatId } from '~/lib/persistence/useChatHistory'; +import { getProjectMemory, setProjectMemory as persistProjectMemory } from '~/lib/persistence/projectMemory'; import { chatStore } from '~/lib/stores/chat'; import { workbenchStore } from '~/lib/stores/workbench'; import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants'; @@ -101,6 +103,9 @@ export const ChatImpl = memo( ); const supabaseAlert = useStore(workbenchStore.supabaseAlert); const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); + const currentChatId = useStore(chatId); + const [projectMemory, setProjectMemory] = useState(''); + const skipNextProjectMemorySave = useRef(false); const [llmErrorAlert, setLlmErrorAlert] = useState(undefined); const [model, setModel] = useState(() => { const savedModel = Cookies.get('selectedModel'); @@ -117,6 +122,33 @@ export const ChatImpl = memo( const [selectedElement, setSelectedElement] = useState(null); const mcpSettings = useMCPStore((state) => state.settings); + useEffect(() => { + if (!currentChatId) { + setProjectMemory(''); + skipNextProjectMemorySave.current = false; + + return; + } + + skipNextProjectMemorySave.current = true; + + const settings = getProjectMemory(currentChatId); + setProjectMemory(settings.memory); + }, [currentChatId]); + + useEffect(() => { + if (!currentChatId) { + return; + } + + if (skipNextProjectMemorySave.current) { + skipNextProjectMemorySave.current = false; + return; + } + + persistProjectMemory(currentChatId, { memory: projectMemory }); + }, [currentChatId, projectMemory]); + const { messages, isLoading, @@ -140,6 +172,7 @@ export const ChatImpl = memo( contextOptimization: contextOptimizationEnabled, chatMode, designScheme, + projectMemory, supabase: { isConnected: supabaseConn.isConnected, hasSelectedProject: !!selectedProject, @@ -344,7 +377,7 @@ export const ChatImpl = memo( ]; // Add image parts if any - images.forEach((imageData) => { + images.filter(Boolean).forEach((imageData) => { // Extract correct MIME type from the data URL const mimeType = imageData.split(';')[0].split(':')[1] || 'image/jpeg'; @@ -643,6 +676,8 @@ export const ChatImpl = memo( apiKeys, ); }} + projectMemory={projectMemory} + setProjectMemory={setProjectMemory} uploadedFiles={uploadedFiles} setUploadedFiles={setUploadedFiles} imageDataList={imageDataList} diff --git a/app/components/chat/ChatBox.tsx b/app/components/chat/ChatBox.tsx index 4cd9a149a2..b4f2df1577 100644 --- a/app/components/chat/ChatBox.tsx +++ b/app/components/chat/ChatBox.tsx @@ -19,6 +19,7 @@ import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog'; import type { DesignScheme } from '~/types/design-scheme'; import type { ElementInfo } from '~/components/workbench/Inspector'; import { McpTools } from './MCPTools'; +import { ProjectMemoryDialog } from './ProjectMemoryDialog'; interface ChatBoxProps { isModelSettingsCollapsed: boolean; @@ -55,6 +56,8 @@ interface ChatBoxProps { handleStop?: (() => void) | undefined; enhancingPrompt?: boolean | undefined; enhancePrompt?: (() => void) | undefined; + projectMemory?: string; + setProjectMemory?: ((value: string) => void) | undefined; chatMode?: 'discuss' | 'build'; setChatMode?: (mode: 'discuss' | 'build') => void; designScheme?: DesignScheme; @@ -262,6 +265,13 @@ export const ChatBox: React.FC = (props) => {
+ { + props.setProjectMemory?.(value); + toast.success(value ? 'Project memory saved' : 'Project memory cleared'); + }} + /> props.handleFileUpload()}>
diff --git a/app/components/chat/ProjectMemoryDialog.tsx b/app/components/chat/ProjectMemoryDialog.tsx new file mode 100644 index 0000000000..17fa0d6723 --- /dev/null +++ b/app/components/chat/ProjectMemoryDialog.tsx @@ -0,0 +1,103 @@ +import { useEffect, useMemo, useState } from 'react'; +import { IconButton } from '~/components/ui/IconButton'; +import { Dialog, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; +import { classNames } from '~/utils/classNames'; + +const MAX_MEMORY_CHARS = 2000; + +interface ProjectMemoryDialogProps { + memory?: string; + onSave: (value: string) => void; +} + +export function ProjectMemoryDialog({ memory = '', onSave }: ProjectMemoryDialogProps) { + const [isOpen, setIsOpen] = useState(false); + const [draft, setDraft] = useState(memory); + + useEffect(() => { + if (isOpen) { + setDraft(memory); + } + }, [isOpen, memory]); + + const trimmedDraft = useMemo(() => draft.trim(), [draft]); + const remainingChars = MAX_MEMORY_CHARS - draft.length; + + const handleSave = () => { + onSave(trimmedDraft.slice(0, MAX_MEMORY_CHARS)); + setIsOpen(false); + }; + + const handleClear = () => { + setDraft(''); + onSave(''); + setIsOpen(false); + }; + + return ( + <> + setIsOpen(true)} + > +
+ {memory ? Memory : } + + + +
+
+ Project Memory + + Notes saved here are injected into every prompt for this chat. Use this for tech stack decisions, + conventions, and constraints you want the model to remember. + +
+
+