mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
Update chat empty state and Auto space mode (#954)
This commit is contained in:
parent
d0f6a495a6
commit
b6a9b1ea7b
9 changed files with 250 additions and 104 deletions
|
|
@ -329,7 +329,7 @@ export class SupermemoryClient {
|
|||
async getDocuments(
|
||||
containerTags?: string[],
|
||||
page = 1,
|
||||
limit = 200,
|
||||
limit = 10,
|
||||
): Promise<DocumentsApiResponse> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
? [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<Env, unknown, Props> {
|
|||
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: {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ export default function NewPage() {
|
|||
const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
|
||||
const [queuedChatSeed, setQueuedChatSeed] = useState<string | null>(null)
|
||||
const [queuedChatModel, setQueuedChatModel] = useState<ModelId | null>(null)
|
||||
const [queuedChatProject, setQueuedChatProject] = useState<string | null>(
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
) : viewMode === "integrations" ? (
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-24 bg-gradient-to-r from-transparent to-[#05080D]" />
|
||||
<div className="relative z-[2] min-h-0 flex-1 pt-10">
|
||||
<MemoryGraph
|
||||
containerTags={effectiveContainerTags}
|
||||
containerTags={graphContainerTags}
|
||||
variant="consumer"
|
||||
highlightDocumentIds={highlightIds}
|
||||
highlightsVisible={highlightIds.length > 0}
|
||||
|
|
|
|||
|
|
@ -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<ModelId>("gemini-2.5-pro")
|
||||
const { selectedProject } = useProject()
|
||||
const { allProjects } = useContainerTags()
|
||||
const chatSpaceLabel = useMemo(
|
||||
() =>
|
||||
getChatSpaceDisplayLabel({
|
||||
selectedProject,
|
||||
allProjects,
|
||||
}),
|
||||
[selectedProject, allProjects],
|
||||
)
|
||||
const [chatSpaceProjects, setChatSpaceProjects] = useState<string[]>([
|
||||
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
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex max-w-[min(160px,35vw)] min-w-0 shrink items-center rounded-full bg-fg-primary/5 px-3 py-1.5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
title={chatSpaceLabel}
|
||||
>
|
||||
<span className="truncate text-sm text-fg-primary">
|
||||
{chatSpaceLabel}
|
||||
</span>
|
||||
</div>
|
||||
<SpaceSelector
|
||||
selectedProjects={chatSpaceProjects}
|
||||
onValueChange={setChatSpaceProjects}
|
||||
variant="insideOut"
|
||||
includeAuto
|
||||
triggerClassName="h-auto min-h-0 max-w-[min(160px,35vw)] rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5 shadow-none hover:bg-[#05080D]"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
id="chat-empty-state"
|
||||
className="flex flex-col items-center justify-center h-full"
|
||||
className="relative flex min-h-full items-center justify-center overflow-hidden px-0 py-6 md:px-3"
|
||||
>
|
||||
<div className="relative size-32">
|
||||
<GradientLogo className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-16" />
|
||||
<LogoBgGradient className="size-full" />
|
||||
</div>
|
||||
<div className="gap-3 flex flex-col items-center justify-center">
|
||||
<p>Ask me anything about your memories…</p>
|
||||
<div
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-[-1rem] inset-y-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.28)_1px,transparent_1px)] bg-size-[32px_32px] opacity-80 mask-[radial-gradient(ellipse_at_center,black_52%,transparent_100%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-[-1rem] bottom-0 h-2/3 bg-[radial-gradient(ellipse_at_bottom,rgba(20,65,255,0.42),transparent_68%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative z-10 flex w-full max-w-xl flex-col items-center text-center">
|
||||
<NovaOrb size={52} className="mb-3 blur-[1.5px]!" />
|
||||
<h2
|
||||
className={cn(
|
||||
"mb-1 max-w-[420px] text-[24px] font-medium leading-[1.12] tracking-normal text-white md:text-[30px]",
|
||||
dmSansClassName(),
|
||||
"flex flex-col gap-2 justify-center items-center",
|
||||
)}
|
||||
>
|
||||
{suggestions.map((suggestion) => (
|
||||
<Button
|
||||
Nova knows you.
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
"mb-4 max-w-[420px] text-[14px] leading-5 text-[#8B8B8B] md:text-[15px]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<span className="text-[#FAFAFA]">
|
||||
Your personal memories are all here.
|
||||
</span>{" "}
|
||||
Chat with supermemory and ask about...
|
||||
</p>
|
||||
<div className="mb-3 grid w-full grid-cols-1 gap-2.5 sm:grid-cols-3">
|
||||
{promptCards.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
variant="default"
|
||||
className="rounded-full text-base gap-1 h-10! border-[#2261CA33] bg-[#041127] border w-fit max-w-[400px] py-[4px] pl-[8px] pr-[12px] hover:bg-[#0A1A3A] hover:[&_span]:text-white hover:[&_svg]:text-white transition-colors cursor-pointer"
|
||||
type="button"
|
||||
onClick={() => onSuggestionClick(suggestion)}
|
||||
className={chatEmptyCardClass}
|
||||
>
|
||||
<SearchIcon className="size-4 text-[#267BF1] shrink-0" />
|
||||
<span className="text-[#267BF1] text-[12px] truncate">
|
||||
<span className="flex size-5 items-center justify-center rounded-full border border-[#3374FF]/35 bg-[#071B3A] text-[11px] font-medium text-[#4BA0FA]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="mt-2 line-clamp-3 text-[13px] font-medium leading-[18px] text-white md:text-[14px] md:leading-5">
|
||||
{suggestion}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<UIMessage[] | null>(null)
|
||||
const targetHighlightChatIdRef = useRef<string | null>(null)
|
||||
const { selectedProject } = useProject()
|
||||
const [chatSpaceProjects, setChatSpaceProjects] = useState<string[]>([
|
||||
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}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-10 max-w-[min(192px,42vw)] shrink min-w-0 items-center rounded-full border border-[#73737333] bg-[#0D121A] px-3",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}}
|
||||
title={chatSpaceLabel}
|
||||
>
|
||||
<span className="truncate text-sm text-white">
|
||||
{chatSpaceLabel}
|
||||
</span>
|
||||
</div>
|
||||
<SpaceSelector
|
||||
selectedProjects={chatSpaceProjects}
|
||||
onValueChange={setChatSpaceProjects}
|
||||
variant="insideOut"
|
||||
includeAuto
|
||||
triggerClassName="h-10 min-h-10 max-w-[min(192px,42vw)] border border-[#73737333] bg-[#0D121A] shadow-[1.5px_1.5px_4.5px_0_rgba(0,0,0,0.70)_inset]"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -948,11 +1001,7 @@ export function ChatSidebar({
|
|||
)}
|
||||
{messages.length === 0 && (
|
||||
<ChatEmptyStatePlaceholder
|
||||
onSuggestionClick={(suggestion) => {
|
||||
analytics.chatSuggestedQuestionClicked()
|
||||
analytics.chatMessageSent({ source: "suggested" })
|
||||
sendMessage({ text: suggestion })
|
||||
}}
|
||||
onSuggestionClick={handleSuggestedQuestion}
|
||||
suggestions={emptyStateSuggestions}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1105,17 +1154,13 @@ export function ChatSidebar({
|
|||
onModelChange={handleModelChange}
|
||||
minimal
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex max-w-[min(160px,35vw)] shrink min-w-0 items-center rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
title={chatSpaceLabel}
|
||||
>
|
||||
<span className="truncate text-sm text-[#FAFAFA]">
|
||||
{chatSpaceLabel}
|
||||
</span>
|
||||
</div>
|
||||
<SpaceSelector
|
||||
selectedProjects={chatSpaceProjects}
|
||||
onValueChange={setChatSpaceProjects}
|
||||
variant="insideOut"
|
||||
includeAuto
|
||||
triggerClassName="h-auto min-h-0 max-w-[min(160px,35vw)] rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5 shadow-none hover:bg-[#05080D]"
|
||||
/>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
|
|
@ -1165,7 +1210,12 @@ export function ChatSidebar({
|
|||
{chatHistorySheet}
|
||||
{isPageDesktop ? (
|
||||
<div className="flex h-full min-h-0 w-full flex-1 flex-row">
|
||||
<ChatGraphContextRail messages={messages} />
|
||||
<ChatGraphContextRail
|
||||
messages={messages}
|
||||
containerTags={
|
||||
chatProject === AUTO_CHAT_SPACE_ID ? null : [chatProject]
|
||||
}
|
||||
/>
|
||||
<div className="flex h-full min-h-0 w-full min-w-0 max-w-[720px] shrink-0 basis-[min(720px,50vw)] flex-col">
|
||||
{pageDesktopToolbarRow}
|
||||
<div className="relative mx-auto flex h-full min-h-0 w-full min-w-0 max-w-[720px] flex-1 flex-col">
|
||||
|
|
|
|||
|
|
@ -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<CategoryId>(() => {
|
||||
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 (
|
||||
<div
|
||||
key={AUTO_CHAT_SPACE_ID}
|
||||
className={cn(
|
||||
"group flex min-w-0 max-w-full items-center gap-3 w-full px-3 py-2.5 rounded-[12px] transition-colors",
|
||||
isSelected
|
||||
? "bg-[#14161A] shadow-inside-out"
|
||||
: "hover:bg-[#14161A]/50",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-4 h-4 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
|
||||
isSelected ? "border-[#4BA0FA]" : "border-[#737373]",
|
||||
)}
|
||||
>
|
||||
{isSelected && <div className="w-2 h-2 rounded-full bg-[#4BA0FA]" />}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAuto}
|
||||
className="flex min-w-0 flex-1 items-center gap-3 text-left cursor-pointer focus:outline-none focus:ring-0"
|
||||
>
|
||||
<span
|
||||
className="shrink-0 flex h-5 w-5 items-center justify-center rounded-[6px] bg-[#071B3A] text-[#4BA0FA]"
|
||||
aria-hidden
|
||||
>
|
||||
<Sparkles className="size-3.5" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[#fafafa] text-sm font-medium">
|
||||
Auto
|
||||
<span className="ml-1.5 text-[12px] text-[#737373]">
|
||||
· Nova chooses spaces
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}, [currentSelection, handleSelectAuto])
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
|
|
@ -946,12 +1015,21 @@ export function SelectSpacesModal({
|
|||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin pr-1 sm:max-h-[360px]">
|
||||
{filteredProjects.length === 0 ? (
|
||||
{filteredProjects.length === 0 && !showAutoRow ? (
|
||||
<p className="text-center text-[#737373] text-sm py-8">
|
||||
No spaces found
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{showAutoRow && (
|
||||
<>
|
||||
<div className="px-3 pt-1 pb-0.5 text-[10px] uppercase tracking-[0.08em] text-[#737373]">
|
||||
Mode
|
||||
</div>
|
||||
{renderAutoRow()}
|
||||
<div className="my-1.5 h-px bg-[rgba(82,89,102,0.18)]" />
|
||||
</>
|
||||
)}
|
||||
{recentProjects.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 px-3 pt-1 pb-0.5 text-[10px] uppercase tracking-[0.08em] text-[#737373]">
|
||||
|
|
|
|||
|
|
@ -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<typeof detectPluginSpace>
|
||||
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 ? (
|
||||
<Sparkles className="size-3.5 shrink-0 text-[#4BA0FA]" />
|
||||
) : displayInfo.plugin ? (
|
||||
displayInfo.plugin.iconSrc ? (
|
||||
<Image
|
||||
src={displayInfo.plugin.iconSrc}
|
||||
|
|
@ -404,6 +414,12 @@ export function SpaceSelector({
|
|||
· {formatCount(spaceCountData)}
|
||||
</span>
|
||||
)}
|
||||
{!compact && (
|
||||
<ChevronDownIcon
|
||||
className="size-3.5 shrink-0 text-[#737373]"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{compact && (
|
||||
<span className="sr-only">
|
||||
{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}
|
||||
|
|
|
|||
1
apps/web/lib/chat-auto-space.ts
Normal file
1
apps/web/lib/chat-auto-space.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const AUTO_CHAT_SPACE_ID = "__supermemory_auto_space__"
|
||||
Loading…
Add table
Add a link
Reference in a new issue