Skip to content

Commit c40f37b

Browse files
authored
feat(app): add toggle to enable/disable memory context in chat (#102)
1 parent 16103f3 commit c40f37b

5 files changed

Lines changed: 74 additions & 37 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"think-app": patch
3+
---
4+
5+
feat(app): add toggle to enable/disable memory context in chat
6+
7+
- Add "Memory on/off" toggle inside chat input container
8+
- Skip automatic RAG retrieval when toggle is off
9+
- Attached memories still work when toggle is off
10+
- Toggle defaults to on (current behavior preserved)

app/src/components/ChatInput.tsx

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ChatInputProps {
1010
isLoading?: boolean;
1111
placeholder?: string;
1212
className?: string;
13+
leftContent?: React.ReactNode;
1314
}
1415

1516
export function ChatInput({
@@ -19,6 +20,7 @@ export function ChatInput({
1920
isLoading,
2021
placeholder = "Type your message...",
2122
className,
23+
leftContent,
2224
}: ChatInputProps) {
2325
const inputRef = useRef<HTMLTextAreaElement>(null);
2426

@@ -53,7 +55,7 @@ export function ChatInput({
5355
return (
5456
<div
5557
className={cn(
56-
"relative flex items-end gap-2 p-2 rounded-2xl",
58+
"relative rounded-2xl p-2",
5759
// Glassmorphism
5860
"bg-white/70 dark:bg-white/5 backdrop-blur-xl",
5961
"border border-white/60 dark:border-white/10",
@@ -62,33 +64,40 @@ export function ChatInput({
6264
className
6365
)}
6466
>
65-
<textarea
66-
ref={inputRef}
67-
value={value}
68-
onChange={(e) => onChange(e.target.value)}
69-
onKeyDown={handleKeyDown}
70-
placeholder={placeholder}
71-
disabled={isLoading}
72-
rows={1}
73-
className={cn(
74-
"flex-1 bg-transparent px-4 py-2 text-base min-h-[44px] max-h-[200px] resize-none",
75-
"placeholder:text-muted-foreground/60",
76-
"focus:outline-none",
77-
"disabled:opacity-50"
78-
)}
79-
/>
80-
<Button
81-
size="icon"
82-
className="h-10 w-10 rounded-full shrink-0"
83-
onClick={onSubmit}
84-
disabled={isLoading || !value.trim()}
85-
>
86-
{isLoading ? (
87-
<Loader2 className="h-4 w-4 animate-spin" />
88-
) : (
89-
<Send className="h-4 w-4" />
90-
)}
91-
</Button>
67+
<div className="flex items-end gap-2">
68+
<textarea
69+
ref={inputRef}
70+
value={value}
71+
onChange={(e) => onChange(e.target.value)}
72+
onKeyDown={handleKeyDown}
73+
placeholder={placeholder}
74+
disabled={isLoading}
75+
rows={1}
76+
className={cn(
77+
"flex-1 bg-transparent px-4 py-2 text-base min-h-[44px] max-h-[200px] resize-none",
78+
"placeholder:text-muted-foreground/60",
79+
"focus:outline-none",
80+
"disabled:opacity-50"
81+
)}
82+
/>
83+
<Button
84+
size="icon"
85+
className="h-10 w-10 rounded-full shrink-0"
86+
onClick={onSubmit}
87+
disabled={isLoading || !value.trim()}
88+
>
89+
{isLoading ? (
90+
<Loader2 className="h-4 w-4 animate-spin" />
91+
) : (
92+
<Send className="h-4 w-4" />
93+
)}
94+
</Button>
95+
</div>
96+
{leftContent && (
97+
<div className="px-2 pt-1">
98+
{leftContent}
99+
</div>
100+
)}
92101
</div>
93102
);
94103
}

app/src/pages/ChatPage.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { AttachedMemoryChips } from "@/components/AttachedMemoryChips";
1010
import { useConversation } from "@/contexts/ConversationContext";
1111
import { useConversations } from "@/hooks/useConversations";
1212
import type { ChatMessage } from "@/types/chat";
13-
import { FileText, Pin, Trash2 } from "lucide-react";
13+
import { FileText, Pin, Trash2, Brain } from "lucide-react";
1414
import { Button } from "@/components/ui/button";
1515
import { cn } from "@/lib/utils";
1616

1717
export default function ChatPage() {
1818
const [message, setMessage] = useState("");
1919
const [isLoading, setIsLoading] = useState(false);
2020
const [followupSuggestions, setFollowupSuggestions] = useState<string[]>([]);
21+
const [useMemories, setUseMemories] = useState(true);
2122
const isStartingNewChatRef = useRef(false);
2223
const wantsNewChatRef = useRef(false);
2324
const [searchParams, setSearchParams] = useSearchParams();
@@ -145,6 +146,7 @@ export default function ChatPage() {
145146
message: userMessage.content,
146147
conversation_id: effectiveConversationId,
147148
attached_memory_ids: memoriesToSend.length > 0 ? memoriesToSend.map((m) => m.id) : undefined,
149+
skip_memory_context: !useMemories,
148150
}),
149151
});
150152

@@ -252,7 +254,7 @@ export default function ChatPage() {
252254
// Clear pending conversation ref
253255
pendingConversationRef.current = null;
254256
}
255-
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow, attachedMemories, clearAttachedMemories]);
257+
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow, attachedMemories, clearAttachedMemories, useMemories]);
256258

257259
// Handler for manual chat input
258260
const handleChat = useCallback(() => {
@@ -381,6 +383,21 @@ export default function ChatPage() {
381383
? `Ask about ${attachedMemories[0].title}...`
382384
: "Type your message..."
383385
}
386+
leftContent={
387+
<button
388+
onClick={() => setUseMemories(!useMemories)}
389+
className={cn(
390+
"flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium transition-colors",
391+
useMemories
392+
? "bg-muted/50 text-muted-foreground"
393+
: "bg-destructive/10 text-destructive"
394+
)}
395+
title={useMemories ? "Click to disable memory context" : "Click to enable memory context"}
396+
>
397+
<Brain className="h-3 w-3" />
398+
<span>{useMemories ? "Memory on" : "Memory off"}</span>
399+
</button>
400+
}
384401
/>
385402
</div>
386403
<ContextUsageIndicator

backend/app/routes/chat.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ async def chat_suggestions():
5252

5353

5454
async def _retrieve_context(
55-
message: str, history: list[dict], attached_memory_ids: list[int] | None = None
55+
message: str, history: list[dict], attached_memory_ids: list[int] | None = None, skip_rag: bool = False
5656
) -> tuple[str, list[dict]]:
5757
"""Retrieve relevant context and sources using RAG.
5858
5959
If attached_memory_ids are provided, those memories are included as primary context.
60-
RAG retrieval still runs to supplement with additional relevant memories.
60+
RAG retrieval still runs to supplement with additional relevant memories (unless skip_rag=True).
6161
6262
Returns:
6363
tuple[str, list[dict]]: (context string, list of source dicts)
@@ -83,8 +83,8 @@ async def _retrieve_context(
8383
attached_context = "## User's Selected Memory:\n" + format_memories_as_context(attached_memories)
8484
logger.info(f"Using {len(attached_memories)} attached memories as context")
8585

86-
# Skip RAG for very short messages (< 10 chars)
87-
if len(message.strip()) < 10:
86+
# Skip RAG for very short messages (< 10 chars) or when explicitly disabled
87+
if len(message.strip()) < 10 or skip_rag:
8888
return attached_context, sources
8989

9090
try:
@@ -192,7 +192,7 @@ async def chat(request: ChatRequest):
192192
]
193193

194194
# --- RAG: Retrieve relevant memories ---
195-
context, sources = await _retrieve_context(request.message, history, request.attached_memory_ids)
195+
context, sources = await _retrieve_context(request.message, history, request.attached_memory_ids, skip_rag=request.skip_memory_context)
196196

197197
try:
198198
response = await ai_chat(request.message, context=context, history=history)
@@ -282,14 +282,14 @@ async def chat_stream(request: ChatRequest):
282282
]
283283

284284
# RAG: Retrieve relevant memories
285-
context, sources = await _retrieve_context(request.message, history, request.attached_memory_ids)
285+
context, sources = await _retrieve_context(request.message, history, request.attached_memory_ids, skip_rag=request.skip_memory_context)
286286

287287
async def generate():
288288
full_response = ""
289289
usage_data = None
290290

291291
# Send metadata first (conversation_id, sources)
292-
yield f"data: {json.dumps({'type': 'meta', 'conversation_id': conversation_id, 'sources': sources, 'searched': True})}\n\n"
292+
yield f"data: {json.dumps({'type': 'meta', 'conversation_id': conversation_id, 'sources': sources, 'searched': not request.skip_memory_context})}\n\n"
293293

294294
try:
295295
async for token, usage in ai_chat_stream(request.message, context=context, history=history):

backend/app/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ChatRequest(BaseModel):
4242
conversation_id: int | None = None
4343
mode: str = "quick"
4444
attached_memory_ids: list[int] | None = None
45+
skip_memory_context: bool = False
4546

4647

4748
# Conversation schemas

0 commit comments

Comments
 (0)