mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-10 20:30:41 +00:00
new modals
This commit is contained in:
parent
3ae0c13de9
commit
fa39265142
13 changed files with 590 additions and 122 deletions
|
|
@ -4,14 +4,12 @@ import { db } from "@/server/db";
|
|||
import {
|
||||
contentToSpace,
|
||||
sessions,
|
||||
StoredContent,
|
||||
storedContent,
|
||||
StoredSpace,
|
||||
users,
|
||||
space,
|
||||
} from "@/server/db/schema";
|
||||
import { SearchResult } from "@/contexts/MemoryContext";
|
||||
import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm";
|
||||
import { like, eq, and, sql, exists, asc, notExists, inArray, notInArray } from "drizzle-orm";
|
||||
import { union } from "drizzle-orm/sqlite-core";
|
||||
import { env } from "@/env";
|
||||
|
||||
|
|
@ -82,6 +80,22 @@ export async function searchMemoriesAndSpaces(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getMemoriesFromUrl(urls: string[]) {
|
||||
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return urls.length > 0 ? await db.select()
|
||||
.from(storedContent)
|
||||
.where(and(
|
||||
inArray(storedContent.url, urls),
|
||||
eq(storedContent.user, user.id)
|
||||
)).all() : []
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const token =
|
||||
cookies().get("next-auth.session-token")?.value ??
|
||||
|
|
@ -167,6 +181,38 @@ export async function addSpace(name: string, memories: number[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function fetchContent(id: number) {
|
||||
|
||||
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fetchedMemory = await db.select()
|
||||
.from(storedContent)
|
||||
.where(and(
|
||||
eq(storedContent.id, id),
|
||||
eq(storedContent.user, user.id)
|
||||
));
|
||||
|
||||
const memory = fetchedMemory.length > 0 ? fetchedMemory[0] : null
|
||||
|
||||
const spaces = memory ? await db.select()
|
||||
.from(contentToSpace)
|
||||
.where(
|
||||
eq(contentToSpace.contentId, memory.id)
|
||||
) : []
|
||||
|
||||
|
||||
return {
|
||||
memory,
|
||||
spaces: spaces.map(s => s.spaceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function fetchContentForSpace(
|
||||
spaceId: number,
|
||||
range?: {
|
||||
|
|
@ -174,6 +220,13 @@ export async function fetchContentForSpace(
|
|||
limit: number;
|
||||
},
|
||||
) {
|
||||
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = db
|
||||
.select()
|
||||
.from(storedContent)
|
||||
|
|
@ -184,9 +237,19 @@ export async function fetchContentForSpace(
|
|||
.from(contentToSpace)
|
||||
.where(
|
||||
and(
|
||||
eq(contentToSpace.spaceId, spaceId),
|
||||
eq(contentToSpace.contentId, storedContent.id),
|
||||
),
|
||||
and(
|
||||
eq(contentToSpace.spaceId, spaceId),
|
||||
eq(contentToSpace.contentId, storedContent.id),
|
||||
),
|
||||
exists(
|
||||
db.select()
|
||||
.from(space)
|
||||
.where(and(
|
||||
eq(space.user, user.id),
|
||||
eq(space.id, contentToSpace.spaceId)
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -207,25 +270,30 @@ export async function fetchFreeMemories(range?: {
|
|||
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.savedAt));
|
||||
try {
|
||||
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.savedAt));
|
||||
|
||||
return range
|
||||
? await query.limit(range.limit).offset(range.offset)
|
||||
: await query.all();
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
return range
|
||||
? await query.limit(range.limit).offset(range.offset)
|
||||
: await query.all();
|
||||
}
|
||||
|
||||
export async function addMemory(
|
||||
|
|
@ -238,7 +306,7 @@ export async function addMemory(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!content.content || content.content == "") {
|
||||
if (!content.content || content.content.trim() === "") {
|
||||
const resp = await fetch(
|
||||
`https://cf-ai-backend.dhravya.workers.dev/getPageContent?url=${content.url}`,
|
||||
{
|
||||
|
|
@ -259,30 +327,7 @@ export async function addMemory(
|
|||
return null;
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
|
||||
console.log({ ...content, user: user.email });
|
||||
|
||||
// Add to vectorDB
|
||||
const res = (await Promise.race([
|
||||
fetch("https://cf-ai-backend.dhravya.workers.dev/add", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pageContent: content.content,
|
||||
title: content.title,
|
||||
url: content.url,
|
||||
user: user.email,
|
||||
}),
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Request timed out")), 40000),
|
||||
),
|
||||
])) as Response;
|
||||
|
||||
const [addedMemory] = await db
|
||||
let [addedMemory] = await db
|
||||
.insert(storedContent)
|
||||
.values({
|
||||
user: user.id,
|
||||
|
|
@ -303,12 +348,142 @@ export async function addMemory(
|
|||
.returning()
|
||||
: [];
|
||||
|
||||
if (content.type === 'note') {
|
||||
addedMemory = (await db.update(storedContent)
|
||||
.set({
|
||||
url: addedMemory.url + addedMemory.id
|
||||
})
|
||||
.where(eq(storedContent.id, addedMemory.id))
|
||||
.returning())[0]
|
||||
}
|
||||
|
||||
|
||||
// Add to vectorDB
|
||||
const res = (await Promise.race([
|
||||
fetch("https://cf-ai-backend.dhravya.workers.dev/add", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pageContent: addedMemory.content,
|
||||
title: addedMemory.title,
|
||||
url: addedMemory.url,
|
||||
user: user.email,
|
||||
}),
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Request timed out")), 40000),
|
||||
),
|
||||
])) as Response;
|
||||
|
||||
return {
|
||||
memory: addedMemory,
|
||||
addedToSpaces,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export async function updateMemory(
|
||||
id: number,
|
||||
{ title, content, spaces }: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
spaces?: number[]
|
||||
}
|
||||
) {
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("updating")
|
||||
|
||||
const [prev] = await db.select()
|
||||
.from(storedContent)
|
||||
.where(and(
|
||||
eq(storedContent.user, user.id),
|
||||
eq(storedContent.id, id)
|
||||
));
|
||||
|
||||
if (!prev) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newContent = {
|
||||
...(title ? { title }: {}),
|
||||
...(content ? { content }: {}),
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...newContent,
|
||||
...prev
|
||||
}
|
||||
|
||||
// Add to vectorDB
|
||||
const res = (await Promise.race([
|
||||
fetch("https://cf-ai-backend.dhravya.workers.dev/edit?uniqueUrl="+updated.url , {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pageContent: updated.content,
|
||||
title: updated.title,
|
||||
url: updated.url,
|
||||
user: user.email,
|
||||
}),
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Request timed out")), 40000),
|
||||
),
|
||||
])) as Response;
|
||||
|
||||
const [updatedMemory] = await db
|
||||
.update(storedContent)
|
||||
.set(newContent)
|
||||
.where(
|
||||
eq(storedContent.id, id)
|
||||
)
|
||||
.returning();
|
||||
|
||||
console.log(updatedMemory, newContent)
|
||||
|
||||
const removedFromSpaces = spaces ?
|
||||
spaces.length > 0 ?
|
||||
await db.delete(contentToSpace)
|
||||
.where(and(
|
||||
notInArray(contentToSpace.spaceId, spaces),
|
||||
eq(contentToSpace.contentId, id)
|
||||
)).returning()
|
||||
: await db.delete(contentToSpace)
|
||||
.where(
|
||||
eq(contentToSpace.contentId, id)
|
||||
)
|
||||
: [];
|
||||
|
||||
const addedToSpaces =
|
||||
(spaces && spaces.length > 0)
|
||||
? await db
|
||||
.insert(contentToSpace)
|
||||
.values(
|
||||
spaces.map((s) => ({
|
||||
contentId: id,
|
||||
spaceId: s,
|
||||
})),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning()
|
||||
: [];
|
||||
|
||||
return {
|
||||
memory: updatedMemory,
|
||||
addedToSpaces,
|
||||
removedFromSpaces
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteSpace(id: number) {
|
||||
const user = await getUser();
|
||||
|
||||
|
|
@ -340,5 +515,20 @@ export async function deleteMemory(id: number) {
|
|||
.where(and(eq(storedContent.user, user.id), eq(storedContent.id, id)))
|
||||
.returning();
|
||||
|
||||
if (deleted) {
|
||||
|
||||
const res = (await Promise.race([
|
||||
fetch(`https://cf-ai-backend.dhravya.workers.dev/delete?websiteUrl=${deleted.url}&user=${user.email}` , {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
}
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Request timed out")), 40000),
|
||||
),
|
||||
])) as Response;
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,18 +57,22 @@ body {
|
|||
padding-bottom: 15dvh;
|
||||
}
|
||||
|
||||
.chat-answer pre {
|
||||
@apply bg-rgray-3 rounded-md border border-rgray-5 p-3 text-sm my-5;
|
||||
.chat-answer code {
|
||||
@apply bg-rgray-3 rounded-md border border-rgray-5 p-1 text-sm text-rgray-11;
|
||||
}
|
||||
|
||||
.novel-editor pre {
|
||||
@apply bg-rgray-3 rounded-md border border-rgray-5 p-4 text-sm text-rgray-11;
|
||||
@apply bg-rgray-3 rounded-md border border-rgray-5 p-4 my-5 text-sm text-rgray-11;
|
||||
}
|
||||
|
||||
.chat-answer h1 {
|
||||
@apply text-rgray-11 my-5 text-xl font-medium;
|
||||
}
|
||||
|
||||
.chat-answer a {
|
||||
@apply underline underline-offset-1 opacity-90 hover:opacity-100;
|
||||
}
|
||||
|
||||
.chat-answer img {
|
||||
@apply rounded-md font-medium my-5;
|
||||
}
|
||||
|
|
@ -122,4 +126,4 @@ body {
|
|||
|
||||
.novel-editor .drag-handle {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ export default async function Home() {
|
|||
// Fetch only first 3 content of each spaces
|
||||
let contents: ChachedSpaceContent[] = [];
|
||||
|
||||
//console.log(await db.select().from(storedContent).)
|
||||
|
||||
await Promise.all([
|
||||
collectedSpaces.forEach(async (space) => {
|
||||
console.log("fetching ");
|
||||
|
|
@ -82,7 +80,7 @@ export default async function Home() {
|
|||
}),
|
||||
]);
|
||||
|
||||
console.log(contents);
|
||||
console.log('contents', contents);
|
||||
|
||||
// freeMemories
|
||||
const freeMemories = await fetchFreeMemories(userData.id);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowUpRight, Globe } from "lucide-react";
|
||||
import { ArrowUpRight, Globe, Text } from "lucide-react";
|
||||
import { convertRemToPixels } from "@/lib/utils";
|
||||
import { SpaceIcon } from "@/assets/Memories";
|
||||
import Markdown from "react-markdown";
|
||||
import { ChatHistory } from "../../types/memory";
|
||||
|
||||
export function ChatAnswer({
|
||||
children: message,
|
||||
|
|
@ -11,7 +12,7 @@ export function ChatAnswer({
|
|||
loading = false,
|
||||
}: {
|
||||
children: string;
|
||||
sources?: string[];
|
||||
sources?: ChatHistory['answer']['sources'];
|
||||
loading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -29,15 +30,22 @@ export function ChatAnswer({
|
|||
<SpaceIcon className="h-6 w-6 -translate-y-[2px]" />
|
||||
Related Memories
|
||||
</h1>
|
||||
<div className="animate-fade-in -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]">
|
||||
{sources?.map((source) => (
|
||||
<div className="animate-fade-in gap-1 -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]">
|
||||
{sources?.map((source) => source.isNote ? (
|
||||
<button
|
||||
className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"
|
||||
>
|
||||
<Text className="w-4 h-4" />
|
||||
{source.source}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"
|
||||
key={source}
|
||||
href={source}
|
||||
key={source.source}
|
||||
href={source.source}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
{cleanUrl(source)}
|
||||
{cleanUrl(source.source)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import { useMemory } from "@/contexts/MemoryContext";
|
||||
|
||||
import Image from "next/image";
|
||||
import { getMemoriesFromUrl } from "@/actions/db";
|
||||
|
||||
function supportsDVH() {
|
||||
try {
|
||||
|
|
@ -185,11 +186,25 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
},
|
||||
);
|
||||
|
||||
const sourcesInJson = (await sourcesResponse.json()) as {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
console.log(sourcesInJson);
|
||||
const sourcesInJson = getIdsFromSource(((await sourcesResponse.json()) as {
|
||||
ids: string[]
|
||||
}).ids) ?? [];
|
||||
|
||||
|
||||
const notesInSources = sourcesInJson.filter(
|
||||
(urls) => urls.startsWith("https://notes.supermemory.dhr.wtf/")
|
||||
)
|
||||
const nonNotes = sourcesInJson.filter(
|
||||
i => !notesInSources.includes(i)
|
||||
)
|
||||
|
||||
const fetchedTitles = await getMemoriesFromUrl(notesInSources);
|
||||
|
||||
const sources = [
|
||||
...nonNotes.map(n => ({ isNote: false, source: n ?? "<unnamed>" })),
|
||||
...fetchedTitles.map(n => ({ isNote: true, source: n.title ?? "<unnamed>" }))
|
||||
]
|
||||
|
||||
setIsAiLoading(false);
|
||||
setChatHistory((prev) => {
|
||||
|
|
@ -200,7 +215,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
...lastMessage,
|
||||
answer: {
|
||||
parts: lastMessage.answer.parts,
|
||||
sources: getIdsFromSource(sourcesInJson.ids) ?? [],
|
||||
sources
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -165,13 +165,12 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
onClick={() => {
|
||||
if (check()) {
|
||||
setLoading(true);
|
||||
const randomId = Math.floor(Math.random() * 1000000);
|
||||
addMemory(
|
||||
{
|
||||
content,
|
||||
title: name,
|
||||
type: "note",
|
||||
url: `https://notes.supermemory.dhr.wtf/${randomId}`,
|
||||
url: `https://notes.supermemory.dhr.wtf/`,
|
||||
image: "",
|
||||
savedAt: new Date(),
|
||||
},
|
||||
|
|
|
|||
35
apps/web/src/components/Sidebar/DeleteConfirmation.tsx
Normal file
35
apps/web/src/components/Sidebar/DeleteConfirmation.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Dialog, DialogContent, DialogTrigger, DialogTitle, DialogDescription, DialogClose, DialogFooter } from "../ui/dialog";
|
||||
|
||||
export default function DeleteConfirmation({ onDelete, trigger = true, children }: { trigger?: boolean, onDelete?: () => void; children: React.ReactNode }) {
|
||||
return (
|
||||
<Dialog>
|
||||
{trigger ? (
|
||||
<DialogTrigger asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
<DialogContent>
|
||||
<DialogTitle className="text-xl">Are you sure?</DialogTitle>
|
||||
<DialogDescription className="text-md">
|
||||
You will not be able to recover this it.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose
|
||||
type={undefined}
|
||||
onClick={onDelete}
|
||||
className="ml-auto flex items-center justify-center rounded-md text-red-400 bg-red-100/10 px-3 py-2 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30"
|
||||
>
|
||||
Delete
|
||||
</DialogClose>
|
||||
<DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2">
|
||||
Cancel
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
152
apps/web/src/components/Sidebar/EditNoteDialog.tsx
Normal file
152
apps/web/src/components/Sidebar/EditNoteDialog.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
|
||||
import { Editor } from "novel";
|
||||
import {
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FilterSpaces } from "./FilterCombobox";
|
||||
import { useMemory } from "@/contexts/MemoryContext";
|
||||
import { Loader, Plus, Trash, X } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { StoredContent } from "@/server/db/schema";
|
||||
import { fetchContent } from "@/actions/db";
|
||||
import { isArraysEqual } from "@/lib/utils";
|
||||
import DeleteConfirmation from "./DeleteConfirmation";
|
||||
|
||||
|
||||
export function NoteEdit({ memory, closeDialog }: { memory: StoredContent, closeDialog: () => any }) {
|
||||
const { updateMemory, deleteMemory } = useMemory();
|
||||
|
||||
const [initialSpaces, setInitialSpaces] = useState<number[]>([])
|
||||
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [name, setName] = useState(memory.title ?? "");
|
||||
const [content, setContent] = useState(memory.content);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
function check(): boolean {
|
||||
const data = {
|
||||
name: name.trim(),
|
||||
content,
|
||||
};
|
||||
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.dataset["error"] = "true";
|
||||
setTimeout(() => {
|
||||
inputRef.current!.placeholder = "Title of the note";
|
||||
inputRef.current!.dataset["error"] = "false";
|
||||
}, 500);
|
||||
inputRef.current.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchContent(memory.id).then((data) => {
|
||||
if (data?.spaces) {
|
||||
setInitialSpaces(data.spaces)
|
||||
setSelectedSpacesId(data.spaces)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-error="false"
|
||||
className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400"
|
||||
placeholder="Title of the note"
|
||||
value={name}
|
||||
disabled={loading}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Editor
|
||||
disableLocalStorage
|
||||
defaultValue={memory.content}
|
||||
onUpdate={(editor) => {
|
||||
if (!editor) return;
|
||||
setContent(editor.storage.markdown.getMarkdown());
|
||||
}}
|
||||
extensions={[Markdown]}
|
||||
className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<FilterSpaces
|
||||
selectedSpaces={selectedSpacesId}
|
||||
setSelectedSpaces={setSelectedSpacesId}
|
||||
className="hover:bg-rgray-5 mr-auto bg-white/5"
|
||||
name={"Spaces"}
|
||||
/>
|
||||
<DeleteConfirmation onDelete={() => {
|
||||
deleteMemory(memory.id)
|
||||
}}>
|
||||
<button
|
||||
type={undefined}
|
||||
disabled={loading}
|
||||
className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10 hover:bg-red-100 dark:hover:bg-red-100/10 hover:text-red-400 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
<Trash className="w-5 h-5" />
|
||||
</button>
|
||||
</DeleteConfirmation>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (check()) {
|
||||
setLoading(true);
|
||||
console.log(
|
||||
|
||||
{
|
||||
title: name === memory.title ? undefined : name,
|
||||
content: content === memory.content ? undefined : content,
|
||||
spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId,
|
||||
},
|
||||
)
|
||||
updateMemory(
|
||||
memory.id,
|
||||
{
|
||||
title: name === memory.title ? undefined : name,
|
||||
content: content === memory.content ? undefined : content,
|
||||
spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId,
|
||||
},
|
||||
).then(closeDialog);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ x: "-50%", y: "-100%" }}
|
||||
animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
|
||||
>
|
||||
<Loader className="text-rgray-11 h-5 w-5 animate-spin" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ y: "0%" }}
|
||||
animate={loading && { opacity: 0, y: "30%" }}
|
||||
>
|
||||
Save
|
||||
</motion.div>
|
||||
</button>
|
||||
<DialogClose
|
||||
type={undefined}
|
||||
disabled={loading}
|
||||
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 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
Cancel
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +90,7 @@ export function FilterSpaces({
|
|||
align={align}
|
||||
side={side}
|
||||
className="w-[200px] p-0"
|
||||
onCloseAutoFocus={e => e.preventDefault()}
|
||||
>
|
||||
<Command
|
||||
filter={(val, search) =>
|
||||
|
|
@ -128,7 +129,7 @@ export function FilterSpaces({
|
|||
className="text-rgray-11"
|
||||
>
|
||||
<SpaceIcon className="mr-2 h-4 w-4" />
|
||||
{space.name}
|
||||
{space.name.length > 10 ? space.name.slice(0, 10) + "..." : space.name}
|
||||
{selectedSpaces.includes(space.id)}
|
||||
<Check
|
||||
data-state-on={selectedSpaces.includes(space.id)}
|
||||
|
|
@ -267,7 +268,7 @@ export function FilterMemories({
|
|||
}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
{m.title}
|
||||
{(m.title && m.title?.length > 14) ? m.title?.slice(0, 14) + "..." : m.title}
|
||||
<Check
|
||||
data-state-on={
|
||||
selected.find((i) => i.id === m.id) !== undefined
|
||||
|
|
|
|||
|
|
@ -37,15 +37,15 @@ import {
|
|||
DialogFooter,
|
||||
DialogClose,
|
||||
} from "../ui/dialog";
|
||||
import { Label } from "../ui/label";
|
||||
import useViewport from "@/hooks/useViewport";
|
||||
import useTouchHold from "@/hooks/useTouchHold";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog";
|
||||
import { ExpandedSpace } from "./ExpandedSpace";
|
||||
import { StoredContent, StoredSpace } from "@/server/db/schema";
|
||||
import Image from "next/image";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { NoteEdit } from "./EditNoteDialog";
|
||||
import DeleteConfirmation from "./DeleteConfirmation";
|
||||
|
||||
export function MemoriesBar() {
|
||||
const [parent, enableAnimations] = useAutoAnimate();
|
||||
|
|
@ -194,39 +194,58 @@ const SpaceExitVariant: Variant = {
|
|||
},
|
||||
};
|
||||
|
||||
export function MemoryItem({ id, title, image, type }: StoredContent) {
|
||||
export function MemoryItem(props: StoredContent) {
|
||||
|
||||
const { id, title, image, type } = props
|
||||
|
||||
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">
|
||||
{name}
|
||||
</button>
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
<div className="flex h-24 w-24 items-center justify-center">
|
||||
{type === "page" ? (
|
||||
<img
|
||||
className="h-16 w-16"
|
||||
id={id.toString()}
|
||||
src={image!}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src =
|
||||
"/icons/white_without_bg.png";
|
||||
}}
|
||||
/>
|
||||
) : type === "note" ? (
|
||||
<div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md">
|
||||
<Text className="h-10 w-10" />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Dialog open={type === "note" ? isDialogOpen : false} onOpenChange={setIsDialogOpen}>
|
||||
<div onClick={() => setIsDialogOpen(true)} className="cursor-pointer 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">
|
||||
{
|
||||
type === "note" ?
|
||||
(
|
||||
<DialogTrigger asChild>
|
||||
<button data-space-text className="focus-visible:outline-none">
|
||||
{name}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<button data-space-text className="focus-visible:outline-none">
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<div className="flex h-24 w-24 items-center justify-center">
|
||||
{type === "page" ? (
|
||||
<img
|
||||
className="h-16 w-16"
|
||||
id={id.toString()}
|
||||
src={image!}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src =
|
||||
"/icons/white_without_bg.png";
|
||||
}}
|
||||
/>
|
||||
) : type === "note" ? (
|
||||
<Text className="h-16 w-16" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogContent className="w-max max-w-[auto]">
|
||||
<NoteEdit closeDialog={() => setIsDialogOpen(false)} memory={props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -254,6 +273,9 @@ export function SpaceItem({
|
|||
}, [cachedMemories]);
|
||||
|
||||
const _name = name.length > 10 ? name.slice(0, 10) + "..." : name;
|
||||
|
||||
console.log(spaceMemories)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={itemRef}
|
||||
|
|
@ -396,7 +418,7 @@ export function SpaceMoreButton({
|
|||
setIsOpen?: (open: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DeleteConfirmation onDelete={onDelete} trigger={false}>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
|
|
@ -426,25 +448,7 @@ export function SpaceMoreButton({
|
|||
</DialogTrigger>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent>
|
||||
<DialogTitle className="text-xl">Are you sure?</DialogTitle>
|
||||
<DialogDescription className="text-md">
|
||||
You will not be able to recover this space
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose
|
||||
type={undefined}
|
||||
onClick={onDelete}
|
||||
className="ml-auto flex items-center justify-center rounded-md bg-red-500/40 px-3 py-2 transition hover:bg-red-500/60 focus-visible:bg-red-500/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
|
||||
>
|
||||
Delete
|
||||
</DialogClose>
|
||||
<DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2">
|
||||
Cancel
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DeleteConfirmation>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
deleteSpace,
|
||||
deleteMemory,
|
||||
fetchFreeMemories,
|
||||
updateMemory,
|
||||
} from "@/actions/db";
|
||||
import { User } from "next-auth";
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ export const MemoryContext = React.createContext<{
|
|||
search: typeof searchMemoriesAndSpaces;
|
||||
deleteSpace: typeof deleteSpace;
|
||||
deleteMemory: typeof deleteMemory;
|
||||
updateMemory: typeof updateMemory;
|
||||
}>({
|
||||
spaces: [],
|
||||
freeMemories: [],
|
||||
|
|
@ -42,6 +44,7 @@ export const MemoryContext = React.createContext<{
|
|||
search: async () => [],
|
||||
deleteMemory: (() => {}) as unknown as typeof deleteMemory,
|
||||
deleteSpace: (() => {}) as unknown as typeof deleteSpace,
|
||||
updateMemory: (() => {}) as unknown as typeof updateMemory,
|
||||
});
|
||||
|
||||
export const MemoryProvider: React.FC<
|
||||
|
|
@ -98,7 +101,7 @@ export const MemoryProvider: React.FC<
|
|||
await fetchContentForSpace(addedSpace.id, {
|
||||
offset: 0,
|
||||
limit: 3,
|
||||
})
|
||||
}) ?? []
|
||||
).map((m) => ({ ...m, space: addedSpace.id }));
|
||||
|
||||
setCachedMemories((prev) => [...prev, ...cachedMemories]);
|
||||
|
|
@ -132,6 +135,40 @@ export const MemoryProvider: React.FC<
|
|||
};
|
||||
};
|
||||
|
||||
const _updateMemory: typeof updateMemory = async (id, _data) => {
|
||||
const data = await updateMemory(id, _data);
|
||||
|
||||
console.log(data)
|
||||
|
||||
if (data) {
|
||||
if (!_data.spaces) {
|
||||
console.log("non spaces", freeMemories.map(i => i.id === data.memory.id ? data.memory : i ))
|
||||
setCachedMemories(prev => prev.map(i => i.id === data.memory.id ? { ...data.memory, space: i.space } : i ))
|
||||
setFreeMemories(prev => prev.map(i => i.id === data.memory.id ? data.memory : i ))
|
||||
return data
|
||||
}
|
||||
setCachedMemories(prev => prev.filter(i => i.id !== data.memory.id))
|
||||
setFreeMemories(prev => prev.filter(i => i.id !== data.memory.id))
|
||||
if (_data.spaces.length > 0) {
|
||||
console.log('has space')
|
||||
setCachedMemories(
|
||||
prev => [
|
||||
...prev,
|
||||
..._data.spaces!.map(s => ({
|
||||
...data.memory,
|
||||
space: s
|
||||
}))
|
||||
]
|
||||
)
|
||||
} else {
|
||||
console.log('does nto have space')
|
||||
setFreeMemories(prev => [...prev, data.memory])
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
return (
|
||||
<MemoryContext.Provider
|
||||
value={{
|
||||
|
|
@ -143,6 +180,7 @@ export const MemoryProvider: React.FC<
|
|||
cachedMemories,
|
||||
deleteMemory: _deleteMemory,
|
||||
addMemory: _addMemory,
|
||||
updateMemory: _updateMemory
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -87,3 +87,27 @@ export function countLines(textarea: HTMLTextAreaElement): number {
|
|||
export function convertRemToPixels(rem: number) {
|
||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
}
|
||||
|
||||
export function isArraysEqual(a: any[], b: any[]) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
let isEqual = true;
|
||||
|
||||
a.forEach(i => {
|
||||
if (!isEqual) return
|
||||
isEqual = b.includes(i)
|
||||
})
|
||||
|
||||
if (!isEqual)
|
||||
return isEqual
|
||||
|
||||
b.forEach(i => {
|
||||
if (!isEqual) return
|
||||
isEqual = a.includes(i)
|
||||
})
|
||||
|
||||
return isEqual
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,6 @@ export type ChatHistory = {
|
|||
question: string;
|
||||
answer: {
|
||||
parts: { text: string }[];
|
||||
sources: string[];
|
||||
sources: { isNote: boolean; source: string }[];
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue