diff options
| author | yxshv <[email protected]> | 2024-04-13 18:11:14 +0530 |
|---|---|---|
| committer | yxshv <[email protected]> | 2024-04-13 18:11:14 +0530 |
| commit | 5b340071245cfe9906fd3534ec5176de1e3fd3bd (patch) | |
| tree | bf4860f810f58adcfec4cc0ad3fdcc3da62bda95 /apps | |
| parent | search results (diff) | |
| download | supermemory-5b340071245cfe9906fd3534ec5176de1e3fd3bd.tar.xz supermemory-5b340071245cfe9906fd3534ec5176de1e3fd3bd.zip | |
spaces dialog
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/src/actions/db.ts | 19 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 66 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 159 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/ui/command.tsx | 9 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 14 |
6 files changed, 166 insertions, 105 deletions
diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index db301e01..b12ed13b 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -15,7 +15,7 @@ import { like, eq, and, sql } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core" // @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces(query: string): Promise<SearchResult[]> { +export async function searchMemoriesAndSpaces(query: string, opts?: { filter?: { memories?: boolean, spaces?: boolean }, range?: { offset: number, limit: number } }): Promise<SearchResult[]> { const user = await getUser() @@ -31,7 +31,7 @@ export async function searchMemoriesAndSpaces(query: string): Promise<SearchResu }).from(storedContent).where(and( eq(storedContent.user, user.id), like(storedContent.title, `%${query}%`) - )).all() + )); const searchSpacesQuery = db.select({ type: sql<string>`'space'`, @@ -42,9 +42,20 @@ export async function searchMemoriesAndSpaces(query: string): Promise<SearchResu eq(space.user, user.id), like(space.name, `%${query}%`) ) - ).all() + ); + + let queries = []; - const data = await Promise.all([searchSpacesQuery, searchMemoriesQuery]) + [undefined, true].includes(opts?.filter?.memories) && queries.push(searchMemoriesQuery); + [undefined, true].includes(opts?.filter?.spaces) && queries.push(searchSpacesQuery); + + if (opts?.range) { + queries = queries.map(q => q.offset(opts.range!.offset).limit(opts.range!.limit)) + } else { + queries = queries.map(q => q.all()) + } + + const data = await Promise.all(queries) return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[] } catch { diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 886507ff..4f8ef734 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -10,8 +10,11 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; +import { FilterMemories, FilterSpaces } from "./FilterCombobox"; import { useMemory } from "@/contexts/MemoryContext"; +import { Command, Plus, X } from "lucide-react"; +import { StoredContent } from "@/server/db/schema"; +import { cleanUrl } from "@/lib/utils"; export function AddMemoryPage() { const { addMemory } = useMemory(); @@ -153,29 +156,28 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { } export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); const inputRef = useRef<HTMLInputElement>(null); const [name, setName] = useState(""); - const [content, setContent] = useState(""); const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState<StoredContent[]>([]); + + function check(): boolean { const data = { name: name.trim(), - content, }; - console.log(name); if (!data.name || data.name.length < 1) { if (!inputRef.current) { alert("Please enter a name for the note"); return false; } inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the note"; + inputRef.current.placeholder = "Please enter a title for the space"; inputRef.current.dataset["error"] = "true"; setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.placeholder = "Enter the name of the space"; inputRef.current!.dataset["error"] = "false"; }, 500); inputRef.current.focus(); @@ -191,19 +193,48 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { </DialogHeader> <Label className="mt-5 block">Name</Label> <Input + ref={inputRef} placeholder="Enter the name of the space" type="url" data-modal-autofocus - className="bg-rgray-4 mt-2 w-full" + value={name} + onChange={e => setName(e.target.value)} + className="bg-rgray-4 mt-2 w-full focus-visible:data-[error=true]:ring-red-500/10 data-[error=true]:placeholder:text-red-400 placeholder:transition placeholder:duration-500" /> - <Label className="mt-5 block">Memories</Label> + {selected.length > 0 && ( + <> + <Label className="mt-5 block">Add Memories</Label> + <div className="flex min-h-5 py-2 flex-col justify-center items-center"> + {selected.map(i => ( + <MemorySelectedItem + key={i.id} + onRemove={() => setSelected(prev => prev.filter(p => p.id !== i.id))} + {...i} + /> + ))} + </div> + </> + )} <DialogFooter> - <DialogClose + <FilterMemories + selected={selected} + setSelected={setSelected} + className="mr-auto bg-white/5 hover:bg-rgray-4 focus-visible:bg-rgray-4" + > + <Plus className="w-5 h-5" /> + Memory + </FilterMemories> + <button type={undefined} + onClick={() => { + if (check()) { + + } + }} className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" > Add - </DialogClose> + </button> <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> Cancel </DialogClose> @@ -211,3 +242,16 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { </div> ); } + +export function MemorySelectedItem({ id, title, url, image, onRemove }: StoredContent & { onRemove: () => void; }) { + return ( + <div className="flex justify-start gap-2 p-1 px-2 w-full items-center text-sm rounded-md hover:bg-rgray-4 focus-within-bg-rgray-4 [&:hover>[data-icon]]:block [&:hover>img]:hidden"> + <img src={image ?? "/icons/logo_without_bg.png"} className="h-5 w-5" /> + <button onClick={onRemove} data-icon className="w-5 h-5 p-0 m-0 hidden focus-visible:outline-none"> + <X className="w-5 h-5 scale-90" /> + </button> + <span>{title}</span> + <span className="ml-auto block opacity-50">{cleanUrl(url)}</span> + </div> + ) +} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index bd432215..30463672 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -20,9 +20,11 @@ import { } from "@/components/ui/popover"; import { SpaceIcon } from "@/assets/Memories"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { useMemory } from "@/contexts/MemoryContext"; +import { SearchResult, useMemory } from "@/contexts/MemoryContext"; +import { useDebounce } from "@/hooks/useDebounce"; +import { StoredContent } from "@/server/db/schema"; -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { +export interface FilterSpacesProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { side?: "top" | "bottom"; align?: "end" | "start" | "center"; onClose?: () => void; @@ -42,7 +44,7 @@ export function FilterSpaces({ setSelectedSpaces, name, ...props -}: Props) { +}: FilterSpacesProps) { const { spaces } = useMemory(); const [open, setOpen] = React.useState(false); @@ -150,26 +152,58 @@ export function FilterSpaces({ ); } +export type FilterMemoriesProps = { + side?: "top" | "bottom"; + align?: "end" | "start" | "center"; + onClose?: () => void; + selected: StoredContent[]; + setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; +} & React.ButtonHTMLAttributes<HTMLButtonElement> + export function FilterMemories({ className, side = "bottom", align = "center", onClose, - selectedSpaces, - setSelectedSpaces, - name, + selected, + setSelected, ...props -}: Props) { - const { spaces } = useMemory(); +}: FilterMemoriesProps) { + + const { search } = useMemory(); + const [open, setOpen] = React.useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + const query = useDebounce(searchQuery, 500) - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); + const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); + const [isSearching, setIsSearching] = React.useState(false) + + const results = React.useMemo(() => { + console.log("use memo") + return searchResults.map(r => r.memory) + }, [searchResults]) + + console.log('memoized', results) + + React.useEffect(() => { + const q = query.trim() + if (q.length > 0) { + setIsSearching(true); + (async () => { + const results = await search(q, { + filter: { + memories: true, + spaces: false + } + }) + setSearchResults(results) + setIsSearching(false) + })(); + } else { + setSearchResults([]) + } + }, [query]) React.useEffect(() => { if (!open) { @@ -177,6 +211,7 @@ export function FilterMemories({ } }, [open]); + console.log(searchResults); return ( <AnimatePresence mode="popLayout"> <LayoutGroup> @@ -191,15 +226,7 @@ export function FilterMemories({ )} {...props} > - <SpaceIcon className="mr-1 h-5 w-5" /> - {name} - <ChevronsUpDown className="h-4 w-4" /> - <div - data-state-on={selectedSpaces.length > 0} - className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" - > - {selectedSpaces.length} - </div> + {props.children} </button> </PopoverTrigger> <PopoverContent @@ -209,56 +236,42 @@ export function FilterMemories({ className="w-[200px] p-0" > <Command - filter={(val, search) => - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } + shouldFilter={false} > - <CommandInput placeholder="Filter spaces..." /> - <CommandList asChild> - <motion.div layoutScroll> - <CommandEmpty>Nothing found</CommandEmpty> - <CommandGroup> - {sortedSpaces.map((space) => ( - <CommandItem - key={space.id} - value={space.id.toString()} - onSelect={(val) => { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1, transition: { delay: 0.05 } }} - transition={{ duration: 0.15 }} - layout - layoutId={`space-combobox-${space.id}`} - className="text-rgray-11" - > - <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name} - {selectedSpaces.includes(space.id)} - <Check - data-state-on={selectedSpaces.includes(space.id)} - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </motion.div> - </CommandItem> - ))} - </CommandGroup> - </motion.div> - </CommandList> + <CommandInput isSearching={isSearching} value={searchQuery} onValueChange={setSearchQuery} placeholder="Filter memories..." /> + <CommandList> + <CommandGroup> + <CommandEmpty className="text-rgray-11 text-sm text-center py-5">{isSearching ? "Searching..." : query.trim().length > 0 ? "Nothing Found" : "Search something"}</CommandEmpty> + {results.map((m) => ( + <CommandItem + key={m.id} + value={m.id.toString()} + onSelect={(val) => { + setSelected((prev) => + prev.find(p => p.id === parseInt(val)) + ? prev.filter((v) => v.id !== parseInt(val)) + : [...prev, m], + ); + }} + asChild + > + <div + className="text-rgray-11" + > + <img src={m.image ?? "/icons/logo_without_bg.png"} className="mr-2 h-4 w-4" /> + {m.title} + <Check + data-state-on={selected.find(i => i.id === m.id) !== undefined} + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> + </div> + </CommandItem> + ))} + + </CommandGroup> + </CommandList> </Command> </PopoverContent> </Popover> diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 213667c8..1c9e7143 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -60,7 +60,7 @@ export function MemoriesBar() { const [expandedSpace, setExpandedSpace] = useState<number | null>(null); const [searchQuery, setSearcyQuery] = useState(""); const [searchLoading, setSearchLoading] = useState(false) - const query = useDebounce(searchQuery, 1000) + const query = useDebounce(searchQuery, 500) const [searchResults, setSearchResults] = useState<SearchResult[]>([]) @@ -148,7 +148,7 @@ export function MemoriesBar() { ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" > - {searchQuery.trim().length > 0 ? ( + {query.trim().length > 0 ? ( <> {searchResults.map(({ type, space, memory }, i) => ( <> diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index 74b7f2e8..f3534b55 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -3,10 +3,11 @@ import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; -import { Search } from "lucide-react"; +import { Loader, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { isSea } from "node:sea"; const Command = React.forwardRef< React.ElementRef<typeof CommandPrimitive>, @@ -39,13 +40,13 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandInput = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { isSearching?: boolean } +>(({ className, isSearching = false ,...props }, ref) => ( <div className="border-rgray-6 flex items-center border-b px-3" cmdk-input-wrapper="" > - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + {isSearching ? <Loader className="mr-2 h-4 w-4 shrink-9 opacity-50 animate-spin" /> : <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />} <CommandPrimitive.Input ref={ref} className={cn( diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index b805d41e..f2f09e80 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -22,7 +22,7 @@ export const MemoryContext = React.createContext<{ spaces?: number[], ) => Promise<void>; cachedMemories: ChachedSpaceContent[]; - search: (query: string) => Promise<SearchResult[]>; + search: typeof searchMemoriesAndSpaces; }>({ spaces: [], freeMemories: [], @@ -57,15 +57,7 @@ export const MemoryProvider: React.FC< const deleteSpace = async (id: number) => { setSpaces((prev) => prev.filter((s) => s.id !== id)); } - - const search = async (query: string) => { - if (!user.id) { - throw new Error('user id is not define') - } - const data = await searchMemoriesAndSpaces(query) - return data as SearchResult[] - } - + // const fetchMemories = useCallback(async (query: string) => { // const response = await fetch(`/api/memories?${query}`); // }, []); @@ -80,7 +72,7 @@ export const MemoryProvider: React.FC< return ( <MemoryContext.Provider value={{ - search, + search: searchMemoriesAndSpaces, spaces, addSpace, deleteSpace, |