Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/memory-context-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"think-app": patch
---

feat(app): add toggle to enable/disable memory context in chat

- Add "Memory on/off" toggle inside chat input container
- Skip automatic RAG retrieval when toggle is off
- Attached memories still work when toggle is off
- Toggle defaults to on (current behavior preserved)
65 changes: 37 additions & 28 deletions app/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ChatInputProps {
isLoading?: boolean;
placeholder?: string;
className?: string;
leftContent?: React.ReactNode;
}

export function ChatInput({
Expand All @@ -19,6 +20,7 @@ export function ChatInput({
isLoading,
placeholder = "Type your message...",
className,
leftContent,
}: ChatInputProps) {
const inputRef = useRef<HTMLTextAreaElement>(null);

Expand Down Expand Up @@ -53,7 +55,7 @@ export function ChatInput({
return (
<div
className={cn(
"relative flex items-end gap-2 p-2 rounded-2xl",
"relative rounded-2xl p-2",
// Glassmorphism
"bg-white/70 dark:bg-white/5 backdrop-blur-xl",
"border border-white/60 dark:border-white/10",
Expand All @@ -62,33 +64,40 @@ export function ChatInput({
className
)}
>
<textarea
ref={inputRef}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isLoading}
rows={1}
className={cn(
"flex-1 bg-transparent px-4 py-2 text-base min-h-[44px] max-h-[200px] resize-none",
"placeholder:text-muted-foreground/60",
"focus:outline-none",
"disabled:opacity-50"
)}
/>
<Button
size="icon"
className="h-10 w-10 rounded-full shrink-0"
onClick={onSubmit}
disabled={isLoading || !value.trim()}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
<div className="flex items-end gap-2">
<textarea
ref={inputRef}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isLoading}
rows={1}
className={cn(
"flex-1 bg-transparent px-4 py-2 text-base min-h-[44px] max-h-[200px] resize-none",
"placeholder:text-muted-foreground/60",
"focus:outline-none",
"disabled:opacity-50"
)}
/>
<Button
size="icon"
className="h-10 w-10 rounded-full shrink-0"
onClick={onSubmit}
disabled={isLoading || !value.trim()}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
{leftContent && (
<div className="px-2 pt-1">
{leftContent}
</div>
)}
</div>
);
}
21 changes: 19 additions & 2 deletions app/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { AttachedMemoryChips } from "@/components/AttachedMemoryChips";
import { useConversation } from "@/contexts/ConversationContext";
import { useConversations } from "@/hooks/useConversations";
import type { ChatMessage } from "@/types/chat";
import { FileText, Pin, Trash2 } from "lucide-react";
import { FileText, Pin, Trash2, Brain } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

export default function ChatPage() {
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [followupSuggestions, setFollowupSuggestions] = useState<string[]>([]);
const [useMemories, setUseMemories] = useState(true);
const isStartingNewChatRef = useRef(false);
const wantsNewChatRef = useRef(false);
const [searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -145,6 +146,7 @@ export default function ChatPage() {
message: userMessage.content,
conversation_id: effectiveConversationId,
attached_memory_ids: memoriesToSend.length > 0 ? memoriesToSend.map((m) => m.id) : undefined,
skip_memory_context: !useMemories,
}),
});

Expand Down Expand Up @@ -252,7 +254,7 @@ export default function ChatPage() {
// Clear pending conversation ref
pendingConversationRef.current = null;
}
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow, attachedMemories, clearAttachedMemories]);
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow, attachedMemories, clearAttachedMemories, useMemories]);

// Handler for manual chat input
const handleChat = useCallback(() => {
Expand Down Expand Up @@ -381,6 +383,21 @@ export default function ChatPage() {
? `Ask about ${attachedMemories[0].title}...`
: "Type your message..."
}
leftContent={
<button
onClick={() => setUseMemories(!useMemories)}
className={cn(
"flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium transition-colors",
useMemories
? "bg-muted/50 text-muted-foreground"
: "bg-destructive/10 text-destructive"
)}
title={useMemories ? "Click to disable memory context" : "Click to enable memory context"}
>
<Brain className="h-3 w-3" />
<span>{useMemories ? "Memory on" : "Memory off"}</span>
</button>
}
/>
</div>
<ContextUsageIndicator
Expand Down
14 changes: 7 additions & 7 deletions backend/app/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ async def chat_suggestions():


async def _retrieve_context(
message: str, history: list[dict], attached_memory_ids: list[int] | None = None
message: str, history: list[dict], attached_memory_ids: list[int] | None = None, skip_rag: bool = False
) -> tuple[str, list[dict]]:
"""Retrieve relevant context and sources using RAG.

If attached_memory_ids are provided, those memories are included as primary context.
RAG retrieval still runs to supplement with additional relevant memories.
RAG retrieval still runs to supplement with additional relevant memories (unless skip_rag=True).

Returns:
tuple[str, list[dict]]: (context string, list of source dicts)
Expand All @@ -83,8 +83,8 @@ async def _retrieve_context(
attached_context = "## User's Selected Memory:\n" + format_memories_as_context(attached_memories)
logger.info(f"Using {len(attached_memories)} attached memories as context")

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

try:
Expand Down Expand Up @@ -192,7 +192,7 @@ async def chat(request: ChatRequest):
]

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

try:
response = await ai_chat(request.message, context=context, history=history)
Expand Down Expand Up @@ -282,14 +282,14 @@ async def chat_stream(request: ChatRequest):
]

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

async def generate():
full_response = ""
usage_data = None

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

try:
async for token, usage in ai_chat_stream(request.message, context=context, history=history):
Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ChatRequest(BaseModel):
conversation_id: int | None = None
mode: str = "quick"
attached_memory_ids: list[int] | None = None
skip_memory_context: bool = False


# Conversation schemas
Expand Down