mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-09 03:18:04 +00:00
new chat (incomplete)
This commit is contained in:
parent
f4917ef8f6
commit
91b566b616
8 changed files with 288 additions and 191 deletions
|
|
@ -17,8 +17,9 @@
|
|||
"src/content.tsx"
|
||||
],
|
||||
"matches": [
|
||||
"http://localhost:3000/",
|
||||
"https://anycontext.dhr.wtf/",
|
||||
"http://localhost:3000/*",
|
||||
"https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*",
|
||||
"https://anycontext.dhr.wtf/*",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"activeTab",
|
||||
"storage",
|
||||
"http://localhost:3000/*",
|
||||
"https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*",
|
||||
"https://anycontext.dhr.wtf/*"
|
||||
],
|
||||
"background": {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { db } from '@/server/db';
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
contentToSpace,
|
||||
sessions,
|
||||
|
|
@ -6,28 +6,28 @@ import {
|
|||
StoredContent,
|
||||
storedContent,
|
||||
users,
|
||||
} from '@/server/db/schema';
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Sidebar from '@/components/Sidebar/index';
|
||||
import Main from '@/components/Main';
|
||||
import MessagePoster from './MessagePoster';
|
||||
import { transformContent } from '../../types/memory';
|
||||
import { MemoryProvider } from '@/contexts/MemoryContext';
|
||||
import Content from './content';
|
||||
} from "@/server/db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar/index";
|
||||
import Main from "@/components/Main";
|
||||
import MessagePoster from "./MessagePoster";
|
||||
import { transformContent } from "../../types/memory";
|
||||
import { MemoryProvider } from "@/contexts/MemoryContext";
|
||||
import Content from "./content";
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const runtime = "edge";
|
||||
|
||||
export default async function Home() {
|
||||
const token =
|
||||
cookies().get('next-auth.session-token')?.value ??
|
||||
cookies().get('__Secure-authjs.session-token')?.value ??
|
||||
cookies().get('authjs.session-token')?.value ??
|
||||
headers().get('Authorization')?.replace('Bearer ', '');
|
||||
cookies().get("next-auth.session-token")?.value ??
|
||||
cookies().get("__Secure-authjs.session-token")?.value ??
|
||||
cookies().get("authjs.session-token")?.value ??
|
||||
headers().get("Authorization")?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
return redirect('/api/auth/signin');
|
||||
return redirect("/api/auth/signin");
|
||||
}
|
||||
|
||||
const session = await db
|
||||
|
|
@ -36,7 +36,7 @@ export default async function Home() {
|
|||
.where(eq(sessions.sessionToken, token!));
|
||||
|
||||
if (!session || session.length === 0) {
|
||||
return redirect('/api/auth/signin');
|
||||
return redirect("/api/auth/signin");
|
||||
}
|
||||
|
||||
const [userData] = await db
|
||||
|
|
@ -46,7 +46,7 @@ export default async function Home() {
|
|||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
return redirect('/api/auth/signin');
|
||||
return redirect("/api/auth/signin");
|
||||
}
|
||||
|
||||
// Fetch all content for the user
|
||||
|
|
@ -61,17 +61,17 @@ export default async function Home() {
|
|||
|
||||
collectedSpaces.push({
|
||||
id: 2,
|
||||
title: 'Test',
|
||||
title: "Test",
|
||||
content: [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Test',
|
||||
title: 'Vscode',
|
||||
description: 'Test',
|
||||
url: 'https://vscode-remake.vercel.app/',
|
||||
content: "Test",
|
||||
title: "Vscode",
|
||||
description: "Test",
|
||||
url: "https://vscode-remake.vercel.app/",
|
||||
savedAt: new Date(),
|
||||
baseUrl: 'https://vscode-remake.vercel.app/',
|
||||
image: 'https://vscode-remake.vercel.app/favicon.svg',
|
||||
baseUrl: "https://vscode-remake.vercel.app/",
|
||||
image: "https://vscode-remake.vercel.app/favicon.svg",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -79,7 +79,7 @@ export default async function Home() {
|
|||
return (
|
||||
<MemoryProvider spaces={collectedSpaces}>
|
||||
<Content />
|
||||
<MessagePoster jwt={token} />
|
||||
{/* <MessagePoster jwt={token} /> */}
|
||||
</MemoryProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { User } from 'next-auth';
|
||||
import { User2 } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import React from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { User } from "next-auth";
|
||||
import { User2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
function ChatMessage({
|
||||
message,
|
||||
user,
|
||||
export function ChatAnswer({
|
||||
children: message,
|
||||
sources,
|
||||
}: {
|
||||
message: string;
|
||||
user: User | 'ai';
|
||||
children: string;
|
||||
sources?: string[];
|
||||
}) {
|
||||
return <div className="mt-5 w-full text-lg">{message}</div>;
|
||||
}
|
||||
|
||||
export function ChatQuestion({ children }: { children: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
className={`font-bold ${!(user === 'ai') && 'text-xl '} flex flex-col md:flex-row items-center gap-4`}
|
||||
>
|
||||
<Avatar>
|
||||
{user === 'ai' ? (
|
||||
<Image
|
||||
src="/logo.png"
|
||||
width={48}
|
||||
height={48}
|
||||
alt="AI"
|
||||
className="rounded-md w-12 h-12"
|
||||
/>
|
||||
) : user?.image ? (
|
||||
<>
|
||||
<AvatarImage
|
||||
className="h-6 w-6 rounded-lg"
|
||||
src={user?.image}
|
||||
alt="user pfp"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{user?.name?.split(' ').map((n) => n[0])}{' '}
|
||||
</AvatarFallback>
|
||||
</>
|
||||
) : (
|
||||
<User2 strokeWidth={1.3} className="h-6 w-6" />
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="ml-4">{message}</div>
|
||||
</div>
|
||||
<div className="w-full h-0.5 bg-gray-700 my-2 md:my-0 md:mx-4 mt-8"></div>
|
||||
<div
|
||||
className={`text-rgray-12 w-full text-left ${children.length > 200 ? "text-xl" : "text-2xl"} font-light`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ChatMessage };
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { Textarea2 } from "./ui/textarea";
|
|||
import { ArrowRight, ArrowUp } from "lucide-react";
|
||||
import { MemoryDrawer } from "./MemoryDrawer";
|
||||
import useViewport from "@/hooks/useViewport";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn, countLines } from "@/lib/utils";
|
||||
import { ChatHistory } from "../../types/memory";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { ChatAnswer, ChatQuestion } from "./ChatMessage";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ function supportsDVH() {
|
|||
|
||||
export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
||||
const [hide, setHide] = useState(false);
|
||||
const [layout, setLayout] = useState<"chat" | "initial">("initial");
|
||||
const [value, setValue] = useState("");
|
||||
const { width } = useViewport();
|
||||
const [searchResults, setSearchResults] = useState<string[]>([]);
|
||||
|
|
@ -57,7 +58,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
|
||||
const [toBeParsed, setToBeParsed] = useState("");
|
||||
|
||||
const textArea = useRef<HTMLTextAreaElement>(null);
|
||||
const textArea = useRef<HTMLDivElement>(null);
|
||||
const main = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -162,8 +163,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
}
|
||||
}, [toBeParsed]);
|
||||
|
||||
const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const getSearchResults = async () => {
|
||||
setIsAiLoading(true);
|
||||
|
||||
console.log(value);
|
||||
|
|
@ -185,7 +185,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
};
|
||||
|
||||
setSearchResults((prev) =>
|
||||
Array.from(new Set([...prev, ...sourcesInJson.ids])),
|
||||
Array.from(new Set([...prev, ...(sourcesInJson.ids ?? [])])),
|
||||
);
|
||||
|
||||
// TODO: PASS THE `SPACE` TO THE API
|
||||
|
|
@ -223,74 +223,110 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
}
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
setLayout("chat");
|
||||
await getSearchResults();
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.main
|
||||
data-sidebar-open={sidebarOpen}
|
||||
ref={main}
|
||||
className={cn(
|
||||
"sidebar flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]",
|
||||
hide ? "" : "main-hidden",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
{chatHistory.map((chat, index) => (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
message={chat.parts.map((part) => part.text).join("")}
|
||||
user={chat.role === "model" ? "ai" : session?.user!}
|
||||
/>
|
||||
))}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h1>Related memories</h1>
|
||||
<div className="grid gap-6">
|
||||
{searchResults.map((value, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="space-y-2">{value}</CardContent>
|
||||
</Card>
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
{layout === "chat" ? (
|
||||
<Chat key="chat" sidebarOpen={sidebarOpen} />
|
||||
) : (
|
||||
<main
|
||||
data-sidebar-open={sidebarOpen}
|
||||
ref={main}
|
||||
className={cn(
|
||||
"sidebar flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]",
|
||||
hide ? "" : "main-hidden",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
{chatHistory.map((chat, index) => (
|
||||
// <ChatMessage
|
||||
// key={index}
|
||||
// message={chat.parts.map((part) => part.text).join("")}
|
||||
// user={chat.role === "model" ? "ai" : session?.user!}
|
||||
// />
|
||||
))}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h1>Related memories</h1>
|
||||
<div className="grid gap-6">
|
||||
{searchResults.map((value, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="space-y-2">{value}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0">
|
||||
Ask your Second brain
|
||||
</h1>
|
||||
<form
|
||||
className="overflow-none mt-auto h-max min-h-[3em] w-full resize-y flex-row items-start justify-center py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2"
|
||||
onSubmit={async (e) => await getSearchResults(e)}
|
||||
>
|
||||
<Textarea2
|
||||
ref={textArea}
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
}}
|
||||
>
|
||||
<div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2">
|
||||
<FilterCombobox className="hidden md:flex" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0"
|
||||
<h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0">
|
||||
Ask your Second brain
|
||||
</h1>
|
||||
|
||||
<Textarea2
|
||||
ref={textArea}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
}}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.2,
|
||||
}}
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
onKeyDown: (e) => {
|
||||
console.log(e.key, e.ctrlKey, e.metaKey);
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
onSend();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
</form>
|
||||
{width <= 768 && <MemoryDrawer hide={hide} />}
|
||||
</motion.main>
|
||||
<div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2">
|
||||
<FilterCombobox
|
||||
onClose={() => {
|
||||
textArea.current?.querySelector("textarea")?.focus();
|
||||
}}
|
||||
className="hidden md:flex"
|
||||
/>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0"
|
||||
>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
{width <= 768 && <MemoryDrawer hide={hide} />}
|
||||
</main>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Chat({ sidebarOpen }: { sidebarOpen: boolean }) {
|
||||
const textArea = useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const value = e.target.value;
|
||||
setValue(value);
|
||||
const lines = countLines(e.target);
|
||||
e.target.rows = Math.min(5, lines);
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
data-sidebar-open={sidebarOpen}
|
||||
|
|
@ -298,52 +334,58 @@ export function Chat({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
"sidebar relative flex w-full flex-col items-end gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]",
|
||||
)}
|
||||
>
|
||||
<div className="absolute bottom-5 left-5 w-full ">
|
||||
{/* <Textarea2
|
||||
// ref={textArea}
|
||||
className="fixed bottom-5 mt-auto h-min max-h-[30em] min-h-[3em] w-[50%] resize-y flex-row items-start justify-center overflow-auto border-red-500 py-5 md:mt-0 md:h-[20vh] md:resize-none md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2"
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full border md:resize-none text-lg py-0 px-2 w-full md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
}}
|
||||
>
|
||||
<div className="text-rgray-11/70 ml-auto flex h-full w-min items-center justify-center pl-0 md:p-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:mt-0"
|
||||
>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2> */}
|
||||
|
||||
<Textarea2
|
||||
// ref={textArea}
|
||||
className="fixed bottom-5 mt-auto h-[3em] max-h-[3em] w-[50%] resize-y flex-row items-start justify-center overflow-auto border-red-500 py-5 md:mt-0 md:h-[20vh] md:resize-none md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2"
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"overflow-auto md:h-full h-[2em] max-h-[2em] p-0 border md:resize-none text-lg py-0 px-2 w-full md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
}}
|
||||
>
|
||||
<div className="text-rgray-11/70 ml-auto flex h-full w-min items-center justify-center pl-0 md:p-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:mt-0"
|
||||
>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
<div className="min-h-[100%] w-full px-5 pt-10">
|
||||
<ChatQuestion>who is dhravya</ChatQuestion>
|
||||
<ChatAnswer>
|
||||
Dhravya Shah is an 18-year-old full-stack developer based in Arizona,
|
||||
USA. He is a passionate developer who focuses on creating products
|
||||
that people love. Dhravya has a background in entrepreneurship, having
|
||||
been a 2x acquired founder and a participant in various hackathons. He
|
||||
is also involved in open-source contributions, content creation to
|
||||
inspire others in coding, and has a growing community of developers.
|
||||
Dhravya's work spans from creating AI-powered note-taking apps to
|
||||
personalized music companions and educational tools. Additionally, he
|
||||
is a guitarist, student, and active in sharing his experiences as a
|
||||
developer and entrepreneur
|
||||
</ChatAnswer>
|
||||
</div>
|
||||
<div
|
||||
data-sidebar-open={sidebarOpen}
|
||||
className="absolute flex w-full items-center justify-center"
|
||||
>
|
||||
<div className="animate-from-top fixed bottom-10 mt-auto flex w-[50%] flex-col items-start justify-center gap-2">
|
||||
<FilterCombobox
|
||||
onClose={() => {
|
||||
textArea.current?.querySelector("textarea")?.focus();
|
||||
}}
|
||||
side="top"
|
||||
align="start"
|
||||
className="bg-white/5"
|
||||
/>
|
||||
<Textarea2
|
||||
ref={textArea}
|
||||
className="h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:items-center md:justify-center"
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full",
|
||||
value,
|
||||
rows: 1,
|
||||
autoFocus: true,
|
||||
onChange: onValueChange,
|
||||
}}
|
||||
>
|
||||
<div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,19 @@ import { SpaceIcon } from "@/assets/Memories";
|
|||
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
||||
import { useMemory } from "@/contexts/MemoryContext";
|
||||
|
||||
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
side?: "top" | "bottom";
|
||||
align?: "end" | "start" | "center";
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function FilterCombobox({ className, ...props }: Props) {
|
||||
export function FilterCombobox({
|
||||
className,
|
||||
side = "bottom",
|
||||
align = "center",
|
||||
onClose,
|
||||
...props
|
||||
}: Props) {
|
||||
const { spaces, addSpace } = useMemory();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
|
@ -38,6 +48,12 @@ export function FilterCombobox({ className, ...props }: Props) {
|
|||
: 0,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
onClose?.();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
<LayoutGroup>
|
||||
|
|
@ -62,7 +78,12 @@ export function FilterCombobox({ className, ...props }: Props) {
|
|||
</div>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<PopoverContent
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
align={align}
|
||||
side={side}
|
||||
className="w-[200px] p-0"
|
||||
>
|
||||
<Command
|
||||
filter={(val, search) =>
|
||||
spaces
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HTMLMotionProps, motion } from "framer-motion";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
|
@ -21,16 +22,18 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export interface Textarea2Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface Textarea2Props extends HTMLMotionProps<"div"> {
|
||||
textAreaProps?: TextareaProps;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
}
|
||||
|
||||
const Textarea2 = React.forwardRef<HTMLTextAreaElement, Textarea2Props>(
|
||||
const Textarea2 = React.forwardRef<HTMLDivElement, Textarea2Props>(
|
||||
({ className, children, textAreaProps: _textAreaProps, ...props }, ref) => {
|
||||
const { className: textAreaClassName, ...textAreaProps } =
|
||||
_textAreaProps || {};
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-rgray-6 text-rgray-11 has-[textarea:focus-visible]:ring-rgray-7 flex h-auto min-h-[80px] w-full flex-col items-start justify-center rounded-md border bg-transparent px-3 py-2 text-sm transition has-[textarea:focus-visible]:ring-2",
|
||||
className,
|
||||
|
|
@ -42,11 +45,10 @@ const Textarea2 = React.forwardRef<HTMLTextAreaElement, Textarea2Props>(
|
|||
"text-rgray-11 h-full w-full resize-none bg-transparent placeholder:text-white/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
textAreaClassName,
|
||||
)}
|
||||
ref={ref}
|
||||
{...textAreaProps}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,3 +25,49 @@ export function generateId() {
|
|||
export function svgId(prefix: string, id: string) {
|
||||
return `${prefix}-${id}`;
|
||||
}
|
||||
|
||||
export function countLines(textarea: HTMLTextAreaElement): number {
|
||||
let _buffer: HTMLTextAreaElement | null = null;
|
||||
|
||||
if (_buffer == null) {
|
||||
_buffer = document.createElement("textarea");
|
||||
_buffer.style.border = "none";
|
||||
_buffer.style.height = "0";
|
||||
_buffer.style.overflow = "hidden";
|
||||
_buffer.style.padding = "0";
|
||||
_buffer.style.position = "absolute";
|
||||
_buffer.style.left = "0";
|
||||
_buffer.style.top = "0";
|
||||
_buffer.style.zIndex = "-1";
|
||||
document.body.appendChild(_buffer);
|
||||
}
|
||||
|
||||
const cs = window.getComputedStyle(textarea);
|
||||
const pl = parseInt(cs.paddingLeft as string);
|
||||
const pr = parseInt(cs.paddingRight as string);
|
||||
let lh = parseInt(cs.lineHeight as string);
|
||||
|
||||
// [cs.lineHeight] may return 'normal', which means line height = font size.
|
||||
if (isNaN(lh)) lh = parseInt(cs.fontSize as string);
|
||||
|
||||
// Copy content width.
|
||||
if (_buffer) {
|
||||
_buffer.style.width = textarea.clientWidth - pl - pr + "px";
|
||||
|
||||
// Copy text properties.
|
||||
_buffer.style.font = cs.font as string;
|
||||
_buffer.style.letterSpacing = cs.letterSpacing as string;
|
||||
_buffer.style.whiteSpace = cs.whiteSpace as string;
|
||||
_buffer.style.wordBreak = cs.wordBreak as string;
|
||||
_buffer.style.wordSpacing = cs.wordSpacing as string;
|
||||
_buffer.style.wordWrap = cs.wordWrap as string;
|
||||
|
||||
// Copy value.
|
||||
_buffer.value = textarea.value;
|
||||
|
||||
const result = Math.floor(_buffer.scrollHeight / lh);
|
||||
return result > 0 ? result : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,22 @@ const config: Config = {
|
|||
opacity: "1",
|
||||
},
|
||||
},
|
||||
"from-top": {
|
||||
"0%": {
|
||||
transform: "translateY(-50px)",
|
||||
opacity: "0",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateY(0)",
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"scale-in": "scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
"scale-out": "scale-out 0.2s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
"fade-in": "fade-in 0.2s 0.5s forwards cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
"from-top": "from-top 0.2s ease-in-out",
|
||||
},
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue