Add real desktop memory search

This commit is contained in:
Sreeram Sreedhar 2026-06-22 15:22:36 -07:00
parent 5bd0682213
commit 93ec22555c
4 changed files with 135 additions and 23 deletions

View file

@ -1,9 +1,11 @@
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { setTokenProvider } from "@lib/token-provider"
import { Toaster } from "@ui/components/sonner"
import { ThemeProvider } from "next-themes"
import { type ReactNode, useState } from "react"
import { type ReactNode, useEffect, useState } from "react"
import { getStoredToken } from "@/lib/auth"
export function Providers({ children }: { children: ReactNode }) {
// One QueryClient for the app's lifetime; lazy init avoids re-creating it on
@ -20,6 +22,10 @@ export function Providers({ children }: { children: ReactNode }) {
}),
)
useEffect(() => {
setTokenProvider(getStoredToken)
}, [])
// The app is dark-only, matching apps/web (forcedTheme="dark").
return (
<ThemeProvider

View file

@ -1,5 +1,6 @@
"use client"
import { useQuery } from "@tanstack/react-query"
import {
CommandDialog,
CommandEmpty,
@ -8,21 +9,9 @@ import {
CommandItem,
CommandList,
} from "@ui/components/command"
import { FileText, Hash, MessageSquare } from "lucide-react"
import { FileText, Loader2 } from "lucide-react"
import { useEffect, useState } from "react"
// Mock data for Phase 1. Phase 5 swaps this for /v3/search results and wires the
// OS-global hotkey via the Rust global-shortcut plugin. The point here is to
// mount the real command-palette UI (cmdk + shared theme) behind an in-app
// Cmd/Ctrl+K — the precursor to the spotlight window.
const MOCK_RESULTS = [
{ id: "1", title: "Q3 planning notes", kind: "doc" },
{ id: "2", title: "Tauri vs Electron — research", kind: "doc" },
{ id: "3", title: "engineering", kind: "space" },
{ id: "4", title: "Desktop app architecture", kind: "chat" },
] as const
const ICONS = { doc: FileText, space: Hash, chat: MessageSquare }
import { searchMemories } from "@/lib/search"
export function SearchCommand({
open,
@ -31,22 +20,71 @@ export function SearchCommand({
open: boolean
onOpenChange: (open: boolean) => void
}) {
const [query, setQuery] = useState("")
const trimmedQuery = query.trim()
const searchQuery = useQuery({
queryKey: ["desktop-search", trimmedQuery],
queryFn: () => searchMemories(trimmedQuery),
enabled: open && trimmedQuery.length > 0,
staleTime: 30 * 1000,
})
const results = searchQuery.data?.results ?? []
useEffect(() => {
if (!open) setQuery("")
}, [open])
return (
<CommandDialog open={open} onOpenChange={onOpenChange}>
<CommandInput placeholder="Search your memories…" />
<CommandInput
placeholder="Search your memories..."
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{trimmedQuery ? (
<CommandEmpty>
{searchQuery.isFetching ? "Searching..." : "No results found."}
</CommandEmpty>
) : (
<div className="px-4 py-6 text-center text-muted-foreground text-sm">
Type to search your memories.
</div>
)}
<CommandGroup heading="Memories">
{MOCK_RESULTS.map((result) => {
const Icon = ICONS[result.kind]
{searchQuery.isFetching ? (
<CommandItem disabled value="searching">
<Loader2 className="animate-spin" />
Searching...
</CommandItem>
) : null}
{searchQuery.isError ? (
<CommandItem disabled value="search-error">
<FileText />
Search failed. Check your token and API URL.
</CommandItem>
) : null}
{results.map((result) => {
const title = result.title ?? result.documentId
const preview =
result.summary ??
result.chunks.find((chunk) => chunk.isRelevant)?.content
return (
<CommandItem
key={result.id}
value={result.title}
key={result.documentId}
value={`${title} ${preview ?? ""}`}
onSelect={() => onOpenChange(false)}
className="items-start"
>
<Icon />
{result.title}
<FileText className="mt-0.5" />
<div className="min-w-0">
<div className="truncate font-medium">{title}</div>
{preview ? (
<div className="line-clamp-2 text-muted-foreground text-xs">
{preview}
</div>
) : null}
</div>
</CommandItem>
)
})}

View file

@ -0,0 +1,57 @@
"use client"
import { getAuthToken } from "@lib/token-provider"
const DEFAULT_API_URL = "https://api.supermemory.ai"
export type SearchResult = {
documentId: string
title: string | null
summary?: string | null
content?: string | null
type: string | null
score: number
chunks: Array<{
content: string
isRelevant: boolean
score: number
}>
}
type SearchResponse = {
results: SearchResult[]
total: number
timing: number
}
function apiUrl() {
return process.env.NEXT_PUBLIC_BACKEND_URL ?? DEFAULT_API_URL
}
export async function searchMemories(query: string) {
const token = await getAuthToken()
if (!token) {
throw new Error("No stored desktop token")
}
const response = await fetch(`${apiUrl().replace(/\/$/, "")}/v3/search`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"X-App-Source": "desktop",
},
body: JSON.stringify({
q: query,
limit: 10,
includeSummary: true,
}),
})
if (!response.ok) {
const body = await response.text()
throw new Error(`Search failed (${response.status}): ${body}`)
}
return (await response.json()) as SearchResponse
}

View file

@ -0,0 +1,11 @@
export type TokenProvider = () => Promise<string | null>
let tokenProvider: TokenProvider = async () => null
export function setTokenProvider(provider: TokenProvider) {
tokenProvider = provider
}
export function getAuthToken() {
return tokenProvider()
}