From b6a9b1ea7b6eadb4650b109d6eecb67f6d8fbff9 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sat, 16 May 2026 23:55:34 -0700 Subject: [PATCH] Update chat empty state and Auto space mode (#954) --- apps/mcp/src/client.ts | 2 +- apps/mcp/src/server.ts | 4 +- apps/web/app/(app)/page.tsx | 10 +- .../chat/chat-graph-context-rail.tsx | 8 +- .../components/chat/home-chat-composer.tsx | 42 ++--- apps/web/components/chat/index.tsx | 178 +++++++++++------- apps/web/components/select-spaces-modal.tsx | 80 +++++++- apps/web/components/space-selector.tsx | 29 ++- apps/web/lib/chat-auto-space.ts | 1 + 9 files changed, 250 insertions(+), 104 deletions(-) create mode 100644 apps/web/lib/chat-auto-space.ts diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index ace65912..8fdb6748 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -329,7 +329,7 @@ export class SupermemoryClient { async getDocuments( containerTags?: string[], page = 1, - limit = 200, + limit = 10, ): Promise { try { const response = await fetch(`${this.apiUrl}/v3/documents/documents`, { diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index 682e75c1..510c1481 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -318,7 +318,7 @@ export class SupermemoryMCP extends McpAgent { ? [effectiveContainerTag] : undefined - const result = await client.getDocuments(containerTags, 1, 200) + const result = await client.getDocuments(containerTags, 1, 10) const memoryCount = result.documents.reduce( (sum, d) => sum + d.memoryEntries.length, @@ -366,7 +366,7 @@ export class SupermemoryMCP extends McpAgent { inputSchema: z.object({ containerTag: z.string().optional(), page: z.number().optional().default(1), - limit: z.number().optional().default(200), + limit: z.number().optional().default(10), }), _meta: { ui: { diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index da76a70b..f5bb9aeb 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -159,6 +159,9 @@ export default function NewPage() { const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") const [queuedChatSeed, setQueuedChatSeed] = useState(null) const [queuedChatModel, setQueuedChatModel] = useState(null) + const [queuedChatProject, setQueuedChatProject] = useState( + null, + ) const [queuedHighlightContent, setQueuedHighlightContent] = useState< string | null >(null) @@ -473,6 +476,7 @@ export default function NewPage() { setQueuedHighlightContent(highlightContent) setQueuedChatSeed(userReply) setQueuedChatModel(null) + setQueuedChatProject(null) setQueuedMessageSource("highlight") void setViewMode("chat") }, @@ -480,10 +484,11 @@ export default function NewPage() { ) const handleHomeChatStart = useCallback( - (message: string, model: ModelId) => { + (message: string, model: ModelId, projectId: string) => { setQueuedHighlightContent(null) setQueuedChatSeed(message) setQueuedChatModel(model) + setQueuedChatProject(projectId) setQueuedMessageSource("home") void setViewMode("chat") }, @@ -493,6 +498,7 @@ export default function NewPage() { const consumeQueuedChat = useCallback(() => { setQueuedChatSeed(null) setQueuedChatModel(null) + setQueuedChatProject(null) setQueuedHighlightContent(null) setQueuedMessageSource("highlight") }, []) @@ -608,7 +614,7 @@ export default function NewPage() { onConsumeQueuedMessage={consumeQueuedChat} queuedMessageSource={queuedMessageSource} initialSelectedModel={queuedChatModel} - emptyStateSuggestions={highlightsData?.questions} + initialChatProject={queuedChatProject} /> ) : viewMode === "integrations" ? ( diff --git a/apps/web/components/chat/chat-graph-context-rail.tsx b/apps/web/components/chat/chat-graph-context-rail.tsx index 5e712cf7..cb83141b 100644 --- a/apps/web/components/chat/chat-graph-context-rail.tsx +++ b/apps/web/components/chat/chat-graph-context-rail.tsx @@ -11,12 +11,18 @@ import { dmSansClassName } from "@/lib/fonts" export function ChatGraphContextRail({ messages, + containerTags, className, }: { messages: UIMessage[] + containerTags?: string[] | null className?: string }) { const { effectiveContainerTags } = useProject() + const graphContainerTags = + containerTags === undefined + ? effectiveContainerTags + : (containerTags ?? undefined) const highlightIds = useMemo( () => extractHighlightDocumentIdsFromMessages(messages), [messages], @@ -45,7 +51,7 @@ export function ChatGraphContextRail({
0} diff --git a/apps/web/components/chat/home-chat-composer.tsx b/apps/web/components/chat/home-chat-composer.tsx index 00179658..98d75eed 100644 --- a/apps/web/components/chat/home-chat-composer.tsx +++ b/apps/web/components/chat/home-chat-composer.tsx @@ -1,41 +1,33 @@ "use client" -import { useCallback, useMemo, useState } from "react" +import { useCallback, useState } from "react" import ChatInput from "./input" import ChatModelSelector from "./model-selector" -import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" import { useProject } from "@/stores" -import { useContainerTags } from "@/hooks/use-container-tags" -import { dmSansClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import type { ModelId } from "@/lib/models" +import { SpaceSelector } from "@/components/space-selector" export function HomeChatComposer({ onStartChat, className, }: { - onStartChat: (message: string, model: ModelId) => void + onStartChat: (message: string, model: ModelId, projectId: string) => void className?: string }) { const [input, setInput] = useState("") const [selectedModel, setSelectedModel] = useState("gemini-2.5-pro") const { selectedProject } = useProject() - const { allProjects } = useContainerTags() - const chatSpaceLabel = useMemo( - () => - getChatSpaceDisplayLabel({ - selectedProject, - allProjects, - }), - [selectedProject, allProjects], - ) + const [chatSpaceProjects, setChatSpaceProjects] = useState([ + selectedProject, + ]) const send = useCallback(() => { const t = input.trim() if (!t) return - onStartChat(t, selectedModel) + onStartChat(t, selectedModel, chatSpaceProjects[0] ?? selectedProject) setInput("") - }, [input, onStartChat, selectedModel]) + }, [chatSpaceProjects, input, onStartChat, selectedModel, selectedProject]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -62,17 +54,13 @@ export function HomeChatComposer({ onModelChange={setSelectedModel} minimal /> -
- - {chatSpaceLabel} - -
+ } /> diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index cc0ea763..adfeabf2 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -22,7 +22,6 @@ import { ChevronDownIcon, HistoryIcon, Plus, - SearchIcon, SquarePenIcon, Trash2, XIcon, @@ -33,11 +32,11 @@ import { dmSansClassName } from "@/lib/fonts" import ChatInput from "./input" import ChatModelSelector from "./model-selector" import { getNovaChatErrorCopy } from "@/lib/chat-stream-error" -import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo" import { useProject } from "@/stores" import { useContainerTags } from "@/hooks/use-container-tags" import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" import { modelNames, type ModelId } from "@/lib/models" +import { SpaceSelector } from "@/components/space-selector" import { SuperLoader } from "../superloader" import { UserMessage } from "./message/user-message" import { AgentMessage } from "./message/agent-message" @@ -49,50 +48,78 @@ import { analytics } from "@/lib/analytics" import { generateId } from "@lib/generate-id" import { useViewMode } from "@/lib/view-mode-context" import { threadParam } from "@/lib/search-params" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" -const DEFAULT_SUGGESTIONS = [ - "Show me all content related to Supermemory.", - "Summarize the key ideas from My Gita.", - "Which memories connect design and AI?", - "What are the main themes across my memories?", +const DEFAULT_CHAT_PROMPTS = [ + "What do you know about me?", + "What have I been working on lately?", + "What themes keep showing up in my memories?", ] +const chatEmptyCardClass = cn( + "flex min-h-[76px] flex-col justify-between rounded-lg border border-[#2B3038] bg-[#14161A]/95 p-3 text-left md:min-h-[88px]", + "shadow-[0_18px_50px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.04)]", + "transition-colors hover:border-[#3374FF]/55 hover:bg-[#1A1F26] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/70", +) + function ChatEmptyStatePlaceholder({ onSuggestionClick, - suggestions = DEFAULT_SUGGESTIONS, + suggestions = DEFAULT_CHAT_PROMPTS, }: { onSuggestionClick: (suggestion: string) => void suggestions?: string[] }) { + const promptCards = suggestions.slice(0, 3) + return (
-
- - -
-
-

Ask me anything about your memories…

-
+
+
+ +

- {suggestions.map((suggestion) => ( - + ))}

@@ -150,6 +177,7 @@ export function ChatSidebar({ onConsumeQueuedMessage, queuedMessageSource = "highlight", initialSelectedModel = null, + initialChatProject = null, emptyStateSuggestions, layout = "sidebar", }: { @@ -160,6 +188,7 @@ export function ChatSidebar({ onConsumeQueuedMessage?: () => void queuedMessageSource?: "highlight" | "home" initialSelectedModel?: ModelId | null + initialChatProject?: string | null emptyStateSuggestions?: string[] layout?: "sidebar" | "page" }) { @@ -196,16 +225,22 @@ export function ChatSidebar({ const pendingHighlightMessageRef = useRef(null) const targetHighlightChatIdRef = useRef(null) const { selectedProject } = useProject() + const [chatSpaceProjects, setChatSpaceProjects] = useState([ + initialChatProject ?? selectedProject, + ]) + const chatProject = chatSpaceProjects[0] ?? selectedProject const { allProjects } = useContainerTags() - const selectedProjectRef = useRef(selectedProject) - selectedProjectRef.current = selectedProject + const selectedProjectRef = useRef(chatProject) + selectedProjectRef.current = chatProject const chatSpaceLabel = useMemo( () => - getChatSpaceDisplayLabel({ - selectedProject, - allProjects, - }), - [selectedProject, allProjects], + chatProject === AUTO_CHAT_SPACE_ID + ? "Auto" + : getChatSpaceDisplayLabel({ + selectedProject: chatProject, + allProjects, + }), + [chatProject, allProjects], ) const { viewMode } = useViewMode() const { user: _user } = useAuth() @@ -232,6 +267,12 @@ export function ChatSidebar({ metadata: { chatId: chatIdRef.current, projectId: selectedProjectRef.current, + spaceMode: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID + ? "auto" + : "manual", + enableSpaceDiscovery: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID, model: selectedModelRef.current, }, }, @@ -326,6 +367,25 @@ export function ChatSidebar({ scrollToBottom() } + const handleSuggestedQuestion = useCallback( + (suggestion: string) => { + if (status === "submitted" || status === "streaming") return + if (!threadId) setThreadId(fallbackChatId) + analytics.chatSuggestedQuestionClicked() + analytics.chatMessageSent({ source: "suggested" }) + sendMessage({ text: suggestion }) + scrollToBottom() + }, + [ + fallbackChatId, + sendMessage, + setThreadId, + status, + threadId, + scrollToBottom, + ], + ) + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() @@ -394,7 +454,7 @@ export function ChatSidebar({ setIsLoadingThreads(true) try { const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads?projectId=${selectedProject}`, + `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads?projectId=${chatProject}`, { credentials: "include" }, ) if (response.ok) { @@ -406,7 +466,7 @@ export function ChatSidebar({ } finally { setIsLoadingThreads(false) } - }, [selectedProject]) + }, [chatProject]) useEffect(() => { if (!isHistoryOpen) return @@ -908,20 +968,13 @@ export function ChatSidebar({ selectedModel={selectedModel} onModelChange={handleModelChange} /> -
- - {chatSpaceLabel} - -
+ )}
@@ -948,11 +1001,7 @@ export function ChatSidebar({ )} {messages.length === 0 && ( { - analytics.chatSuggestedQuestionClicked() - analytics.chatMessageSent({ source: "suggested" }) - sendMessage({ text: suggestion }) - }} + onSuggestionClick={handleSuggestedQuestion} suggestions={emptyStateSuggestions} /> )} @@ -1105,17 +1154,13 @@ export function ChatSidebar({ onModelChange={handleModelChange} minimal /> -
- - {chatSpaceLabel} - -
+ ) : undefined } @@ -1165,7 +1210,12 @@ export function ChatSidebar({ {chatHistorySheet} {isPageDesktop ? (
- +
{pageDesktopToolbarRow}
diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 4162dd57..55fdaf9f 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -19,6 +19,7 @@ import { Loader, Pencil, Check, + Sparkles, } from "lucide-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" @@ -43,6 +44,7 @@ import { } from "@/lib/plugin-catalog" import { InstallSteps, PillButton } from "./integrations/install-steps" import { useProjectMutations } from "@/hooks/use-project-mutations" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" interface SelectSpacesModalProps { isOpen: boolean @@ -52,6 +54,7 @@ interface SelectSpacesModalProps { projects: ContainerTagListType[] recents?: string[] showNewSpace?: boolean + includeAuto?: boolean onNewSpace?: () => void enableDelete?: boolean onDeleteRequest?: (project: { @@ -90,6 +93,7 @@ export function SelectSpacesModal({ projects, recents, showNewSpace = false, + includeAuto = false, onNewSpace, enableDelete = false, onDeleteRequest, @@ -198,6 +202,7 @@ export function SelectSpacesModal({ const defaultCategory = useMemo(() => { if (!currentSelection) return "all" + if (currentSelection === AUTO_CHAT_SPACE_ID) return "all" const plugin = detectPluginSpace(currentSelection) if (plugin) return `plugin:${plugin.pluginId}` return "my" @@ -380,6 +385,15 @@ export function SelectSpacesModal({ [onApply], ) + const handleSelectAuto = useCallback(() => { + setEditingProject(null) + setIsBulkDeleteMode(false) + setBulkDeleteTags(new Set()) + setLastBulkDeleteTag(null) + onApply([AUTO_CHAT_SPACE_ID]) + setSearchQuery("") + }, [onApply]) + const handleBulkModeToggle = useCallback(() => { setEditingProject(null) setBulkDeleteTags(new Set()) @@ -478,6 +492,19 @@ export function SelectSpacesModal({ [filteredProjects, recentSet], ) + const showAutoRow = useMemo(() => { + if (!includeAuto) return false + if (isBulkDeleteMode) return false + if (activeCategory !== "all" && activeCategory !== "my") return false + const query = searchQuery.trim().toLowerCase() + if (!query) return true + return ( + "auto".includes(query) || + "let nova choose the right spaces".includes(query) || + "discover spaces".includes(query) + ) + }, [includeAuto, isBulkDeleteMode, activeCategory, searchQuery]) + const visibleBulkDeleteTags = useMemo( () => [...recentProjects, ...mainList] @@ -742,6 +769,48 @@ export function SelectSpacesModal({ ], ) + const renderAutoRow = useCallback(() => { + const isSelected = currentSelection === AUTO_CHAT_SPACE_ID + return ( +
+
+ {isSelected &&
} +
+ +
+ ) + }, [currentSelection, handleSelectAuto]) + return (
- {filteredProjects.length === 0 ? ( + {filteredProjects.length === 0 && !showAutoRow ? (

No spaces found

) : (
+ {showAutoRow && ( + <> +
+ Mode +
+ {renderAutoRow()} +
+ + )} {recentProjects.length > 0 && ( <>
diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index 3b0daa71..689219b1 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -7,8 +7,9 @@ import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { DEFAULT_PROJECT_ID } from "@lib/constants" -import { XIcon, Loader2, Trash2 } from "lucide-react" +import { ChevronDownIcon, Sparkles, XIcon, Loader2, Trash2 } from "lucide-react" import type { ContainerTagListType } from "@lib/types" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import { AddSpaceModal } from "./add-space-modal" import { SelectSpacesModal } from "./select-spaces-modal" import { useProjectMutations } from "@/hooks/use-project-mutations" @@ -46,6 +47,7 @@ export interface SpaceSelectorProps { showNewSpace?: boolean enableDelete?: boolean compact?: boolean + includeAuto?: boolean } const triggerVariants = { @@ -104,6 +106,7 @@ export function SpaceSelector({ showNewSpace = true, enableDelete = false, compact = false, + includeAuto = false, }: SpaceSelectorProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false) @@ -158,7 +161,7 @@ export function SpaceSelector({ return data?.pagination?.totalItems ?? 0 }, staleTime: 30 * 1000, - enabled: !!activeTag, + enabled: !!activeTag && activeTag !== AUTO_CHAT_SPACE_ID, }) const pluginTags = useMemo( @@ -176,10 +179,14 @@ export function SpaceSelector({ name: string emoji: string | null plugin: ReturnType + isAuto: boolean }>(() => { const containerTag = selectedProjects[0] ?? "" + if (includeAuto && containerTag === AUTO_CHAT_SPACE_ID) { + return { name: "Auto", emoji: null, plugin: null, isAuto: true } + } if (!containerTag || containerTag === DEFAULT_PROJECT_ID) { - return { name: "My Space", emoji: "📁", plugin: null } + return { name: "My Space", emoji: "📁", plugin: null, isAuto: false } } const found = allProjects.find( (p: ContainerTagListType) => p.containerTag === containerTag, @@ -195,8 +202,9 @@ export function SpaceSelector({ : spaceSelectorDisplayName(found, containerTag), emoji: found?.emoji || "📁", plugin, + isAuto: false, } - }, [allProjects, selectedProjects, pluginMetaMap]) + }, [allProjects, selectedProjects, pluginMetaMap, includeAuto]) const pushRecent = useCallback((tag: string) => { setRecents((prev) => { @@ -212,7 +220,7 @@ export function SpaceSelector({ const selectedTag = next[0] setShowSelectSpacesModal(false) onValueChange(next) - if (selectedTag) { + if (selectedTag && selectedTag !== AUTO_CHAT_SPACE_ID) { queueMicrotask(() => { analytics.spaceSwitched({ space_id: selectedTag }) pushRecent(selectedTag) @@ -356,7 +364,9 @@ export function SpaceSelector({ triggerClassName, )} > - {displayInfo.plugin ? ( + {displayInfo.isAuto ? ( + + ) : displayInfo.plugin ? ( displayInfo.plugin.iconSrc ? ( )} + {!compact && ( + + )} {compact && ( {isLoading ? "Loading" : displayInfo.name} @@ -433,6 +449,7 @@ export function SpaceSelector({ projects={allProjects} recents={recents} showNewSpace={showNewSpace} + includeAuto={includeAuto} onNewSpace={handleNewSpace} enableDelete={enableDelete} onDeleteRequest={handleDeleteRequest} diff --git a/apps/web/lib/chat-auto-space.ts b/apps/web/lib/chat-auto-space.ts new file mode 100644 index 00000000..8f766248 --- /dev/null +++ b/apps/web/lib/chat-auto-space.ts @@ -0,0 +1 @@ +export const AUTO_CHAT_SPACE_ID = "__supermemory_auto_space__"