mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
Add real desktop memory search
This commit is contained in:
parent
5bd0682213
commit
93ec22555c
4 changed files with 135 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
57
apps/desktop/lib/search.ts
Normal file
57
apps/desktop/lib/search.ts
Normal 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
|
||||
}
|
||||
11
packages/lib/token-provider.ts
Normal file
11
packages/lib/token-provider.ts
Normal 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()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue