mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
space add
This commit is contained in:
parent
5b34007124
commit
0d4e91068f
4 changed files with 144 additions and 26 deletions
|
|
@ -8,10 +8,10 @@ import {
|
|||
storedContent,
|
||||
StoredSpace,
|
||||
users,
|
||||
space
|
||||
space,
|
||||
} from "@/server/db/schema";
|
||||
import { SearchResult } from "@/contexts/MemoryContext";
|
||||
import { like, eq, and, sql } from "drizzle-orm";
|
||||
import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm";
|
||||
import { union } from "drizzle-orm/sqlite-core"
|
||||
|
||||
// @todo: (future) pagination not yet needed
|
||||
|
|
@ -137,3 +137,81 @@ export async function addMemory(
|
|||
);
|
||||
return _content;
|
||||
}
|
||||
|
||||
export async function addSpace(name: string, memories: number[]) {
|
||||
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [addedSpace] = await db
|
||||
.insert(space)
|
||||
.values({
|
||||
name: name,
|
||||
user: user.id
|
||||
}).returning();
|
||||
|
||||
const addedMemories = memories.length > 0 ? await db.insert(contentToSpace)
|
||||
.values(memories.map(m => ({
|
||||
contentId: m,
|
||||
spaceId: addedSpace.id
|
||||
}))).returning() : []
|
||||
|
||||
return {
|
||||
space: addedSpace,
|
||||
addedMemories
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function fetchContentForSpace(
|
||||
spaceId: number,
|
||||
range?: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
},
|
||||
) {
|
||||
|
||||
const query = db
|
||||
.select()
|
||||
.from(storedContent)
|
||||
.where(
|
||||
exists(
|
||||
db.select().from(contentToSpace).where(and(eq(contentToSpace.spaceId, spaceId), eq(contentToSpace.contentId, storedContent.id))),
|
||||
),
|
||||
).orderBy(asc(storedContent.title))
|
||||
|
||||
return range ? await query.limit(range.limit).offset(range.offset) : await query.all()
|
||||
}
|
||||
|
||||
export async function fetchFreeMemories(
|
||||
range?: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
) {
|
||||
|
||||
const user = await getUser()
|
||||
|
||||
if (!user) {
|
||||
return []
|
||||
}
|
||||
|
||||
const query = db
|
||||
.select()
|
||||
.from(storedContent)
|
||||
.where(
|
||||
and(
|
||||
notExists(
|
||||
db.select().from(contentToSpace).where(eq(contentToSpace.contentId, storedContent.id)),
|
||||
),
|
||||
eq(storedContent.user, user.id),
|
||||
)
|
||||
|
||||
).orderBy(asc(storedContent.title))
|
||||
|
||||
return range ? await query.limit(range.limit).offset(range.offset) : await query.all()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import { Markdown } from "tiptap-markdown";
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { FilterMemories, FilterSpaces } from "./FilterCombobox";
|
||||
import { useMemory } from "@/contexts/MemoryContext";
|
||||
import { Command, Plus, X } from "lucide-react";
|
||||
import { Loader, Plus, X } from "lucide-react";
|
||||
import { StoredContent } from "@/server/db/schema";
|
||||
import { cleanUrl } from "@/lib/utils";
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export function AddMemoryPage() {
|
||||
const { addMemory } = useMemory();
|
||||
|
|
@ -157,8 +158,11 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
|
||||
export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
||||
|
||||
const { addSpace } = useMemory()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [selected, setSelected] = useState<StoredContent[]>([]);
|
||||
|
|
@ -198,6 +202,7 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
type="url"
|
||||
data-modal-autofocus
|
||||
value={name}
|
||||
disabled={loading}
|
||||
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"
|
||||
/>
|
||||
|
|
@ -219,7 +224,8 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
<FilterMemories
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
className="mr-auto bg-white/5 hover:bg-rgray-4 focus-visible:bg-rgray-4"
|
||||
disabled={loading}
|
||||
className="mr-auto bg-white/5 hover:bg-rgray-4 focus-visible:bg-rgray-4 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Memory
|
||||
|
|
@ -228,14 +234,28 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
type={undefined}
|
||||
onClick={() => {
|
||||
if (check()) {
|
||||
|
||||
setLoading(true)
|
||||
addSpace(name, selected.map(s => s.id)).then(() => closeDialog())
|
||||
}
|
||||
}}
|
||||
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"
|
||||
disabled={loading}
|
||||
className="relative disabled:opacity-70 disabled:cursor-not-allowed 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
|
||||
<motion.div
|
||||
initial={{ x: '-50%', y: '-100%' }}
|
||||
animate={loading && { y: '-50%', x: '-50%', opacity: 1 }}
|
||||
className="opacity-0 absolute top-1/2 left-1/2 translate-y-[-100%] -translate-x-1/2"
|
||||
>
|
||||
<Loader className="w-5 h-5 animate-spin text-rgray-11" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ y: '0%' }}
|
||||
animate={loading && { opacity: 0, y: '30%' }}
|
||||
>
|
||||
Add
|
||||
</motion.div>
|
||||
</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">
|
||||
<DialogClose disabled={loading} className="disabled:opacity-70 disabled:cursor-not-allowed 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>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -196,13 +196,16 @@ export function MemoryItem({
|
|||
title,
|
||||
image
|
||||
}: StoredContent) {
|
||||
|
||||
const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : '<no title>';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"
|
||||
>
|
||||
<button data-space-text className="focus-visible:outline-none">
|
||||
{title}
|
||||
{name}
|
||||
</button>
|
||||
|
||||
<div className="w-24 h-24 flex justify-center items-center">
|
||||
|
|
@ -240,6 +243,8 @@ export function SpaceItem({
|
|||
return cachedMemories.filter(m => m.space === id)
|
||||
}, [cachedMemories])
|
||||
|
||||
const _name = name.length > 10 ? name.slice(0, 10) + "..." : name
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={itemRef}
|
||||
|
|
@ -248,7 +253,7 @@ export function SpaceItem({
|
|||
className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"
|
||||
>
|
||||
<button data-space-text className="focus-visible:outline-none">
|
||||
{name}
|
||||
{_name}
|
||||
</button>
|
||||
<SpaceMoreButton
|
||||
isOpen={moreDropdownOpen}
|
||||
|
|
@ -341,19 +346,23 @@ export function SpaceItem({
|
|||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
) : spaceMemories.length === 1 ? (
|
||||
) : spaceMemories.length > 1 ? (
|
||||
<MemoryWithImages2
|
||||
className="h-24 w-24"
|
||||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
) : spaceMemories.length === 1 ? (
|
||||
<MemoryWithImage
|
||||
className="h-24 w-24"
|
||||
id={id.toString()}
|
||||
image={spaceMemories[0].image!}
|
||||
/>
|
||||
) : (
|
||||
<MemoryWithImages2
|
||||
className="h-24 w-24"
|
||||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-rgray-4 opacity-30 rounded-full w-24 h-24 scale-50 shadow-">
|
||||
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
"use client";
|
||||
import React, { useCallback } from "react";
|
||||
import { CollectedSpaces } from "../../types/memory";
|
||||
import { ChachedSpaceContent, StoredContent, storedContent, StoredSpace } from "@/server/db/schema";
|
||||
import { addMemory, searchMemoriesAndSpaces } from "@/actions/db";
|
||||
import { addMemory, searchMemoriesAndSpaces, addSpace, fetchContentForSpace } from "@/actions/db";
|
||||
import { User } from "next-auth";
|
||||
|
||||
export type SearchResult = {
|
||||
|
|
@ -16,7 +15,7 @@ export const MemoryContext = React.createContext<{
|
|||
spaces: StoredSpace[];
|
||||
deleteSpace: (id: number) => Promise<void>;
|
||||
freeMemories: StoredContent[];
|
||||
addSpace: (space: StoredSpace) => Promise<void>;
|
||||
addSpace: typeof addSpace;
|
||||
addMemory: (
|
||||
memory: typeof storedContent.$inferInsert,
|
||||
spaces?: number[],
|
||||
|
|
@ -27,7 +26,7 @@ export const MemoryContext = React.createContext<{
|
|||
spaces: [],
|
||||
freeMemories: [],
|
||||
addMemory: async () => {},
|
||||
addSpace: async () => {},
|
||||
addSpace: (async () => {}) as unknown as (typeof addSpace),
|
||||
deleteSpace: async () => {},
|
||||
cachedMemories: [],
|
||||
search: async () => []
|
||||
|
|
@ -49,10 +48,6 @@ export const MemoryProvider: React.FC<
|
|||
const [cachedMemories, setCachedMemories] = React.useState<ChachedSpaceContent[]>(
|
||||
initialCachedMemories
|
||||
);
|
||||
|
||||
const addSpace = async (space: StoredSpace) => {
|
||||
setSpaces((prev) => [...prev, space]);
|
||||
}
|
||||
|
||||
const deleteSpace = async (id: number) => {
|
||||
setSpaces((prev) => prev.filter((s) => s.id !== id));
|
||||
|
|
@ -68,13 +63,29 @@ export const MemoryProvider: React.FC<
|
|||
) => {
|
||||
const content = await addMemory(memory, spaces);
|
||||
}
|
||||
|
||||
const _addSpace: typeof addSpace = async (...params) => {
|
||||
const { space: addedSpace, addedMemories } = (await addSpace(...params))!;
|
||||
|
||||
setSpaces(prev => [...prev, addedSpace])
|
||||
const cachedMemories = (await fetchContentForSpace(addedSpace.id, {
|
||||
offset: 0,
|
||||
limit: 3
|
||||
})).map(m => ({ ...m, space: addedSpace.id }))
|
||||
|
||||
setCachedMemories(prev => [...prev, ...cachedMemories])
|
||||
|
||||
return {
|
||||
space: addedSpace, addedMemories
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MemoryContext.Provider
|
||||
value={{
|
||||
search: searchMemoriesAndSpaces,
|
||||
spaces,
|
||||
addSpace,
|
||||
addSpace: _addSpace,
|
||||
deleteSpace,
|
||||
freeMemories,
|
||||
cachedMemories,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue