aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authoryxshv <[email protected]>2024-04-13 18:11:14 +0530
committeryxshv <[email protected]>2024-04-13 18:11:14 +0530
commit5b340071245cfe9906fd3534ec5176de1e3fd3bd (patch)
treebf4860f810f58adcfec4cc0ad3fdcc3da62bda95 /apps
parentsearch results (diff)
downloadsupermemory-5b340071245cfe9906fd3534ec5176de1e3fd3bd.tar.xz
supermemory-5b340071245cfe9906fd3534ec5176de1e3fd3bd.zip
spaces dialog
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/actions/db.ts19
-rw-r--r--apps/web/src/components/Sidebar/AddMemoryDialog.tsx66
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx159
-rw-r--r--apps/web/src/components/Sidebar/MemoriesBar.tsx4
-rw-r--r--apps/web/src/components/ui/command.tsx9
-rw-r--r--apps/web/src/contexts/MemoryContext.tsx14
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,