aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/components
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-02-14 12:43:55 -0800
committerDhravya Shah <[email protected]>2025-02-14 12:43:55 -0800
commit186efa4244846bf761c7cf4f9cc5d1087b8f95df (patch)
tree16cca1d69a22ccee586ea0eb64703817ac2330f9 /apps/web/app/components
parentdocs: remove getting started title (diff)
downloadsupermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.tar.xz
supermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.zip
delete spaces
Diffstat (limited to 'apps/web/app/components')
-rw-r--r--apps/web/app/components/Landing.tsx430
-rw-r--r--apps/web/app/components/Landing/Feature.tsx157
-rw-r--r--apps/web/app/components/Landing/Footer.tsx142
-rw-r--r--apps/web/app/components/Landing/Hero.tsx290
-rw-r--r--apps/web/app/components/Landing/Note.tsx73
-rw-r--r--apps/web/app/components/Landing/Private.tsx62
-rw-r--r--apps/web/app/components/Landing/index.tsx17
-rw-r--r--apps/web/app/components/Landing/plus-grid.tsx88
-rw-r--r--apps/web/app/components/editor/use-chat.tsx492
-rw-r--r--apps/web/app/components/editor/use-create-editor.tsx285
-rw-r--r--apps/web/app/components/editor/writing-playground.tsx242
-rw-r--r--apps/web/app/components/icons/IntegrationIcons.tsx46
-rw-r--r--apps/web/app/components/memories/SharedCard.tsx2
13 files changed, 1475 insertions, 851 deletions
diff --git a/apps/web/app/components/Landing.tsx b/apps/web/app/components/Landing.tsx
deleted file mode 100644
index 3ed8fa7c..00000000
--- a/apps/web/app/components/Landing.tsx
+++ /dev/null
@@ -1,430 +0,0 @@
-import { useEffect, useRef } from "react";
-
-import { Logo } from "./icons/Logo";
-
-import { motion, useMotionTemplate, useScroll, useSpring, useTransform } from "framer-motion";
-
-// Interfaces
-interface Memory {
- x: number;
- y: number;
- size: number;
- type: "bookmark" | "note" | "tweet" | "doc";
- color: string;
- orbitRadius: number;
- orbitSpeed: number;
- orbitOffset: number;
- opacity: number;
-}
-
-// Components
-const ProductHuntBadge = () => (
- <a
- href="https://www.producthunt.com/posts/supermemory"
- target="_blank"
- rel="noopener noreferrer"
- className="inline-block hover:opacity-90 transition-opacity"
- >
- <img
- src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=472686&theme=neutral&period=daily"
- alt="Supermemory - #1 Product of the Day on Product Hunt"
- className="h-[54px] w-[250px]"
- height="54"
- width="250"
- />
- </a>
-);
-
-const SupermemoryBackground = () => {
- const canvasRef = useRef<HTMLCanvasElement>(null);
- const memoriesRef = useRef<Memory[]>([]);
- const rafRef = useRef<number>();
- const centerRef = useRef({ x: 0, y: 0 });
- const circleRadiusRef = useRef(150);
- const targetRadiusRef = useRef(0);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- const resize = () => {
- canvas.width = window.innerWidth;
- canvas.height = window.innerHeight;
- centerRef.current = {
- x: canvas.width / 2,
- y: canvas.height / 2,
- };
- targetRadiusRef.current =
- Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2)) / 1.5;
- };
-
- const createMemories = () => {
- const memories: Memory[] = [];
- const types: ("bookmark" | "note" | "tweet" | "doc")[] = ["bookmark", "note", "tweet", "doc"];
- const colors = {
- bookmark: "#3B82F6",
- note: "#10B981",
- tweet: "#60A5FA",
- doc: "#818CF8",
- };
-
- const orbits = [250, 350, 450];
- orbits.forEach((orbitRadius, orbitIndex) => {
- const memoriesInOrbit = 8 + orbitIndex * 4;
- for (let i = 0; i < memoriesInOrbit; i++) {
- const type = types[i % 4];
- const angle = (Math.PI * 2 * i) / memoriesInOrbit;
- memories.push({
- x: centerRef.current.x + Math.cos(angle) * orbitRadius,
- y: centerRef.current.y + Math.sin(angle) * orbitRadius,
- size: 3 + Math.random() * 2,
- type,
- color: colors[type],
- orbitRadius,
- orbitSpeed: (0.1 + Math.random() * 0.05) * (1 - orbitIndex * 0.2),
- orbitOffset: angle,
- opacity: 0.15 + Math.random() * 0.15,
- });
- }
- });
-
- return memories;
- };
-
- const drawMemory = (ctx: CanvasRenderingContext2D, memory: Memory, time: number) => {
- const angle = memory.orbitOffset + time * memory.orbitSpeed;
- memory.x = centerRef.current.x + Math.cos(angle) * memory.orbitRadius;
- memory.y = centerRef.current.y + Math.sin(angle) * memory.orbitRadius;
-
- const dx = memory.x - centerRef.current.x;
- const dy = memory.y - centerRef.current.y;
- const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
-
- if (distanceFromCenter <= circleRadiusRef.current) {
- // Draw connections
- memoriesRef.current.forEach((otherMemory) => {
- if (memory === otherMemory) return;
- const connectionDx = memory.x - otherMemory.x;
- const connectionDy = memory.y - otherMemory.y;
- const connectionDistance = Math.sqrt(
- connectionDx * connectionDx + connectionDy * connectionDy,
- );
-
- const otherDx = otherMemory.x - centerRef.current.x;
- const otherDy = otherMemory.y - centerRef.current.y;
- const otherDistanceFromCenter = Math.sqrt(otherDx * otherDx + otherDy * otherDy);
-
- if (connectionDistance < 80 && otherDistanceFromCenter <= circleRadiusRef.current) {
- const opacity = (1 - connectionDistance / 80) * 0.04;
- ctx.beginPath();
- ctx.moveTo(memory.x, memory.y);
- ctx.lineTo(otherMemory.x, otherMemory.y);
- ctx.strokeStyle = `rgba(59, 130, 246, ${opacity})`;
- ctx.lineWidth = 0.5;
- ctx.stroke();
- }
- });
-
- // Draw node
- const gradient = ctx.createRadialGradient(
- memory.x,
- memory.y,
- 0,
- memory.x,
- memory.y,
- memory.size * 2,
- );
- gradient.addColorStop(0, memory.color.replace(")", `,${memory.opacity})`));
- gradient.addColorStop(1, memory.color.replace(")", ",0)"));
-
- ctx.beginPath();
- ctx.arc(memory.x, memory.y, memory.size, 0, Math.PI * 2);
- ctx.fillStyle = gradient;
- ctx.fill();
- }
- };
-
- memoriesRef.current = createMemories();
-
- const animate = () => {
- if (!ctx || !canvas) return;
-
- ctx.fillStyle = "rgba(17, 24, 39, 1)";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
-
- const time = Date.now() * 0.001;
-
- // Grow circle with easing
- const radiusDiff = targetRadiusRef.current - circleRadiusRef.current;
- if (Math.abs(radiusDiff) > 1) {
- circleRadiusRef.current += radiusDiff * 0.02;
- }
-
- // Create clipping region
- ctx.save();
- ctx.beginPath();
- ctx.arc(centerRef.current.x, centerRef.current.y, circleRadiusRef.current, 0, Math.PI * 2);
- ctx.clip();
-
- // Draw orbit paths
- [250, 350, 450].forEach((radius) => {
- ctx.beginPath();
- ctx.arc(centerRef.current.x, centerRef.current.y, radius, 0, Math.PI * 2);
- ctx.strokeStyle = "rgba(255, 255, 255, 0.02)";
- ctx.stroke();
- });
-
- memoriesRef.current.forEach((memory) => drawMemory(ctx, memory, time));
-
- ctx.restore();
- rafRef.current = requestAnimationFrame(animate);
- };
-
- resize();
- window.addEventListener("resize", resize);
- animate();
-
- return () => {
- if (rafRef.current) cancelAnimationFrame(rafRef.current);
- window.removeEventListener("resize", resize);
- };
- }, []);
-
- return (
- <canvas
- ref={canvasRef}
- className="fixed inset-0 w-full h-full"
- // style={{ background: "rgb(17, 24, 39)" }}
- />
- );
-};
-
-export default function Landing() {
- const { scrollYProgress } = useScroll();
- const scrollProgress = useSpring(scrollYProgress);
- const boxOpacity = useTransform(scrollYProgress, [0.3, 0.6], [0, 1]);
- const boxScale = useTransform(scrollYProgress, [0.3, 0.6], [0.8, 1]);
-
- return (
- <div className="flex flex-col relative font-geistSans overflow-hidden items-center justify-between min-h-screen ">
- <SupermemoryBackground />
-
- <div className="relative w-full flex items-center min-h-[90vh] p-4 lg:p-8">
- <motion.div
- initial={{ y: -20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.2 }}
- className="absolute top-0 left-0 right-0 p-4 lg:p-8 flex justify-between items-center z-20"
- >
- <div className="inline-flex gap-2 items-center">
- <Logo />
- <span className="text-lg lg:text-xl font-medium bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400">
- supermemory.ai
- </span>
- </div>
- <div className="flex items-center gap-6">
- <a
- href="https://twitter.com/supermemoryai"
- target="_blank"
- rel="noopener noreferrer"
- className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors hidden sm:flex items-center gap-2"
- >
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
- <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path>
- </svg>
- <span>Follow us</span>
- </a>
- <motion.button
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="px-4 py-2 rounded-full bg-blue-600 text-white font-medium text-sm hover:bg-blue-700 transition-colors"
- >
- Get Started
- </motion.button>
- </div>
- </motion.div>
-
- <div className="absolute inset-0 overflow-hidden">
- <div className="absolute inset-0 opacity-[0.02] w-full bg-[linear-gradient(to_right,#3b82f6_1px,transparent_1px),linear-gradient(to_bottom,#3b82f6_1px,transparent_1px)] bg-[size:4rem_4rem]" />
- </div>
-
- <div className="relative mx-auto max-w-5xl text-center z-10 mt-20">
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8 }}
- >
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.2 }}
- className="mb-8"
- >
- <ProductHuntBadge />
- </motion.div>
-
- <h1 className="text-4xl lg:text-7xl font-bold mb-8 leading-tight tracking-tight">
- <span className="bg-clip-text text-transparent bg-gradient-to-r from-gray-900 via-blue-900 to-blue-700 dark:from-white dark:via-blue-200 dark:to-blue-400">
- Your second brain for all
- <br />
- your saved content
- </span>
- </h1>
- <motion.p
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.6 }}
- className="text-xl lg:text-2xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto mb-6 leading-relaxed"
- >
- Save anything from anywhere. Supermemory connects your bookmarks, notes, and research
- into a powerful, searchable knowledge base.
- </motion.p>
- {/* <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.7 }}
- className="flex flex-col gap-4 items-center mb-12"
- >
- <div className="flex items-center gap-2 text-lg text-gray-600 dark:text-gray-400">
- <svg
- className="w-5 h-5 text-blue-500"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>Chrome extension for one-click saving</span>
- </div>
- <div className="flex items-center gap-2 text-lg text-gray-600 dark:text-gray-400">
- <svg
- className="w-5 h-5 text-blue-500"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>AI-powered search across all your content</span>
- </div>
- <div className="flex items-center gap-2 text-lg text-gray-600 dark:text-gray-400">
- <svg
- className="w-5 h-5 text-blue-500"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>Integrates with Notion, Twitter, and more</span>
- </div>
- </motion.div> */}
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.8 }}
- className="flex gap-4 justify-center"
- >
- <motion.button
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="px-8 py-4 rounded-full bg-blue-600 text-white font-medium text-lg hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20 flex items-center gap-2"
- >
- Try it free
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M17 8l4 4m0 0l-4 4m4-4H3"
- />
- </svg>
- </motion.button>
- <motion.a
- href="https://github.com/dhravya/supermemory"
- target="_blank"
- rel="noopener noreferrer"
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="px-8 py-4 rounded-full border border-gray-200 dark:border-gray-800 text-gray-600 dark:text-gray-400 font-medium text-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors flex items-center gap-2"
- >
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
- <path
- fillRule="evenodd"
- d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
- clipRule="evenodd"
- />
- </svg>
- Star on GitHub
- </motion.a>
- </motion.div>
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 1 }}
- className="mt-16 flex justify-center gap-8 items-center opacity-60"
- >
- <img
- src="/medium-logo.png"
- alt="Medium"
- className="h-8 hover:opacity-100 transition-opacity"
- />
- <img
- src="/notion-logo.png"
- alt="Notion"
- className="h-8 hover:opacity-100 transition-opacity"
- />
- <img
- src="/reddit-logo.png"
- alt="Reddit"
- className="h-8 hover:opacity-100 transition-opacity"
- />
- <img
- src="/twitter-logo.png"
- alt="Twitter"
- className="h-8 hover:opacity-100 transition-opacity"
- />
- </motion.div>
- </motion.div>
- </div>
- </div>
-
- <div className="relative w-full min-h-screen bg-gradient-to-b from-white to-blue-50 dark:from-gray-900 dark:to-blue-950 flex items-center justify-center">
- <motion.div
- style={{
- opacity: boxOpacity,
- scale: boxScale,
- }}
- className="relative w-[600px] h-[400px] rounded-2xl bg-gradient-to-br from-blue-400/10 to-blue-600/10 backdrop-blur-lg border border-blue-200/20 dark:border-blue-700/20 p-8"
- >
- <div className="absolute inset-0 bg-grid-pattern opacity-5" />
- <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
- All your knowledge in one place
- </h2>
- <p className="text-gray-600 dark:text-gray-400">
- Supermemory intelligently organizes and connects your saved content, making it easy to
- find and use when you need it.
- </p>
- </motion.div>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/Feature.tsx b/apps/web/app/components/Landing/Feature.tsx
new file mode 100644
index 00000000..e46fa41e
--- /dev/null
+++ b/apps/web/app/components/Landing/Feature.tsx
@@ -0,0 +1,157 @@
+"use client";
+export default function Feature2() {
+ return (
+ <div className="flex flex-col gap-2 justify-center items-center">
+ <svg
+ className="w-[40%] h-[40%] mx-auto"
+ viewBox="0 0 604 283"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <defs>
+ <linearGradient id="pulseGradient1" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
+ <animate
+ attributeName="y1"
+ from="0%"
+ to="100%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0s"
+ ></animate>
+ <animate
+ attributeName="y2"
+ from="100%"
+ to="200%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0s"
+ ></animate>
+ </linearGradient>
+ <linearGradient id="pulseGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
+ <animate
+ attributeName="y1"
+ from="0%"
+ to="100%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.25s"
+ ></animate>
+ <animate
+ attributeName="y2"
+ from="100%"
+ to="200%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.25s"
+ ></animate>
+ </linearGradient>
+ <linearGradient id="pulseGradient3" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
+ <animate
+ attributeName="y1"
+ from="0%"
+ to="100%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.5s"
+ ></animate>
+ <animate
+ attributeName="y2"
+ from="100%"
+ to="200%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.5s"
+ ></animate>
+ </linearGradient>
+ <linearGradient id="pulseGradient4" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
+ <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
+ <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
+ <animate
+ attributeName="y1"
+ from="0%"
+ to="100%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.75s"
+ ></animate>
+ <animate
+ attributeName="y2"
+ from="100%"
+ to="200%"
+ dur="2s"
+ repeatCount="indefinite"
+ begin="0.75s"
+ ></animate>
+ </linearGradient>
+ <linearGradient id="blueBase" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(48, 165, 255, 0)"></stop>
+ <stop offset="100%" stop-color="#30A5FF"></stop>
+ </linearGradient>
+ <linearGradient id="yellowBase" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255, 186, 23, 0)"></stop>
+ <stop offset="100%" stop-color="#FFBA17"></stop>
+ </linearGradient>
+ <linearGradient id="redBase" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(255, 134, 111, 0)"></stop>
+ <stop offset="100%" stop-color="#FF866F"></stop>
+ </linearGradient>
+ <linearGradient id="purpleBase" x1="0%" y1="0%" x2="0%" y2="100%">
+ <stop offset="0%" stop-color="rgba(151, 0, 244, 0)"></stop>
+ <stop offset="100%" stop-color="#9700F4"></stop>
+ </linearGradient>
+ </defs>
+ <path d="M3 0C3 157 280 90 280 282" stroke="url(#blueBase)" stroke-width="3"></path>
+ <path d="M200 0C200 157 294 90 294 282" stroke="url(#yellowBase)" stroke-width="3"></path>
+ <path d="M400 0C400 157 307 90 307 282" stroke="url(#redBase)" stroke-width="3"></path>
+ <path d="M601 0C601 157 320 90 320 282" stroke="url(#purpleBase)" stroke-width="3"></path>
+ <path d="M3 0C3 157 280 90 280 282" stroke="url(#pulseGradient1)" stroke-width="3"></path>
+ <path
+ d="M200 0C200 157 294 90 294 282"
+ stroke="url(#pulseGradient2)"
+ stroke-width="3"
+ ></path>
+ <path
+ d="M400 0C400 157 307 90 307 282"
+ stroke="url(#pulseGradient3)"
+ stroke-width="3"
+ ></path>
+ <path
+ d="M601 0C601 157 320 90 320 282"
+ stroke="url(#pulseGradient4)"
+ stroke-width="3"
+ ></path>
+ </svg>
+ <div className="w-full mx-auto text-center">
+ <h2 className="text-3xl md:text-4xl mb-1">Meet Supermemory.</h2>
+ <h3 className="text-3xl md:text-4xl mb-8">Your second brain for knowledge.</h3>
+ <p className="text-gray-600 max-w-3xl mx-auto">
+ Save the things you like, and over time, build the knowledge base of your dreams.
+ <br />
+ Go down rabbit holes, make connections, search what's important to you.
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/app/components/Landing/Footer.tsx b/apps/web/app/components/Landing/Footer.tsx
new file mode 100644
index 00000000..3f0b3f3e
--- /dev/null
+++ b/apps/web/app/components/Landing/Footer.tsx
@@ -0,0 +1,142 @@
+import { PlusGrid, PlusGridItem, PlusGridRow } from "./plus-grid";
+
+function SitemapHeading({ children }: { children: React.ReactNode }) {
+ return <h3 className="text-sm/6 font-medium text-gray-950/50 dark:text-gray-200">{children}</h3>;
+}
+
+function SitemapLinks({ children }: { children: React.ReactNode }) {
+ return <ul className="mt-6 space-y-4 text-sm/6">{children}</ul>;
+}
+
+function SitemapLink(props: React.ComponentPropsWithoutRef<"a">) {
+ return (
+ <li>
+ <a
+ {...props}
+ className="font-medium text-gray-950 data-[hover]:text-gray-950/75 dark:text-gray-400"
+ />
+ </li>
+ );
+}
+
+function Sitemap() {
+ return (
+ <>
+ <div>
+ <SitemapHeading>Product</SitemapHeading>
+ <SitemapLinks>
+ <SitemapLink href="https://docs.supermemory.ai">Documentation</SitemapLink>
+ <SitemapLink href="https://supermemory.ai/extension">Chrome Extension</SitemapLink>
+ <SitemapLink href="/shortcut">iOS Shortcut</SitemapLink>
+ </SitemapLinks>
+ </div>
+ <div>
+ <SitemapHeading>Community</SitemapHeading>
+ <SitemapLinks>
+ <SitemapLink href="https://discord.gg/b3BgKWpbtR">Discord</SitemapLink>
+ <SitemapLink href="https://github.com/supermemoryai/supermemory/issues">
+ Report Issue
+ </SitemapLink>
+ <SitemapLink href="mailto:[email protected]">Get Help</SitemapLink>
+ </SitemapLinks>
+ </div>
+ <div>
+ <SitemapHeading>Legal</SitemapHeading>
+ <SitemapLinks>
+ <SitemapLink href="https://supermemory.ai/tos">Terms of Service</SitemapLink>
+ <SitemapLink href="https://supermemory.ai/privacy">Privacy Policy</SitemapLink>
+ </SitemapLinks>
+ </div>
+ </>
+ );
+}
+
+function SocialIconX(props: React.ComponentPropsWithoutRef<"svg">) {
+ return (
+ <svg viewBox="0 0 16 16" fill="currentColor" {...props}>
+ <path d="M12.6 0h2.454l-5.36 6.778L16 16h-4.937l-3.867-5.594L2.771 16H.316l5.733-7.25L0 0h5.063l3.495 5.114L12.6 0zm-.86 14.376h1.36L4.323 1.539H2.865l8.875 12.837z" />
+ </svg>
+ );
+}
+
+function SocialIconGitHub(props: React.ComponentPropsWithoutRef<"svg">) {
+ return (
+ <svg viewBox="0 0 16 16" fill="currentColor" {...props}>
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
+ </svg>
+ );
+}
+
+function SocialLinks() {
+ return (
+ <>
+ <a
+ href="https://github.com/supermemoryai"
+ target="_blank"
+ aria-label="Visit us on GitHub"
+ className="text-gray-950 data-[hover]:text-gray-950/75"
+ >
+ <SocialIconGitHub className="size-4" />
+ </a>
+ <a
+ href="https://x.com/supermemoryai"
+ target="_blank"
+ aria-label="Visit us on X"
+ className="text-gray-950 data-[hover]:text-gray-950/75"
+ >
+ <SocialIconX className="size-4" />
+ </a>
+ </>
+ );
+}
+
+function Copyright() {
+ return (
+ <div className="text-sm/6 text-gray-950 dark:text-gray-100">
+ &copy; {new Date().getFullYear()} Supermemory, Inc.
+ </div>
+ );
+}
+
+export default function Footer() {
+ return (
+ <footer className="mt-16 font-dm">
+ <div className="absolute inset-2 rounded-4xl" />
+ <div className="px-6 lg:px-8">
+ <div className="mx-auto max-w-2xl lg:max-w-7xl">
+ <PlusGrid className="pb-16">
+ <PlusGridRow>
+ <div className="grid grid-cols-2 gap-y-10 pb-6 lg:grid-cols-6 lg:gap-8">
+ <div className="col-span-2 flex">
+ <PlusGridItem className="pt-6 lg:pb-6">
+ <h1 className="text-2xl font-semibold tracking-tighter dark:text-gray-300">
+ Supermemory
+ </h1>
+ <p className="text-gray-500">
+ Supermemory is a free, open-source AI knowlege platform.
+ </p>
+ </PlusGridItem>
+ </div>
+ <div className="col-span-2 grid grid-cols-2 gap-x-8 gap-y-12 lg:col-span-4 lg:grid-cols-subgrid lg:pt-6">
+ <Sitemap />
+ </div>
+ </div>
+ </PlusGridRow>
+ <PlusGridRow className="flex justify-between">
+ <div>
+ <PlusGridItem className="py-3">
+ <Copyright />
+ </PlusGridItem>
+ </div>
+ <div className="flex">
+ <PlusGridItem className="flex items-center gap-8 py-3">
+ <SocialLinks />
+ </PlusGridItem>
+ </div>
+ </PlusGridRow>
+ </PlusGrid>
+ </div>
+ </div>
+ </footer>
+ );
+}
diff --git a/apps/web/app/components/Landing/Hero.tsx b/apps/web/app/components/Landing/Hero.tsx
new file mode 100644
index 00000000..06d82dfb
--- /dev/null
+++ b/apps/web/app/components/Landing/Hero.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import { DiscordIcon, GithubIcon } from "../icons/IntegrationIcons";
+import { Logo } from "../icons/Logo";
+
+interface PlusPatternBackgroundProps {
+ plusSize?: number;
+ plusColor?: string;
+ backgroundColor?: string;
+ className?: string;
+ style?: React.CSSProperties;
+ fade?: boolean;
+ [key: string]: any;
+}
+
+export const BackgroundPlus: React.FC<PlusPatternBackgroundProps> = ({
+ plusColor = "#CCE5FF",
+ backgroundColor = "transparent",
+ plusSize = 60,
+ className,
+ fade = true,
+ style,
+ ...props
+}) => {
+ const encodedPlusColor = encodeURIComponent(plusColor);
+
+ const maskStyle: React.CSSProperties = fade
+ ? {
+ maskImage: "radial-gradient(circle, white 10%, transparent 90%)",
+ WebkitMaskImage: "radial-gradient(circle, white 10%, transparent 90%)",
+ }
+ : {};
+
+ const backgroundStyle: React.CSSProperties = {
+ backgroundColor,
+ backgroundImage: `url("data:image/svg+xml,%3Csvg width='${plusSize}' height='${plusSize}' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='${encodedPlusColor}' fill-opacity='0.5'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
+ ...maskStyle,
+ ...style,
+ };
+
+ return (
+ <div
+ className={`absolute inset-0 h-full w-full opacity-50 ${className}`}
+ style={backgroundStyle}
+ {...props}
+ ></div>
+ );
+};
+
+export default function Hero() {
+ return (
+ <div className="relative z-[10] min-h-screen overflow-hidden">
+ <div className="fixed bottom-0 left-0 right-0 flex justify-center z-[45] pointer-events-none">
+ <div
+ className="h-48 w-[95%] overflow-x-hidden bg-[#3B82F6] bg-opacity-100 md:bg-opacity-70 blur-[400px]"
+ style={{ transform: "rotate(-30deg)" }}
+ />
+ </div>
+ <BackgroundPlus />
+
+ {/* Header */}
+ <header className="fixed top-0 left-0 right-0 bg-white/80 backdrop-blur-md z-50 border-b border-gray-100/20">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="flex justify-between items-center h-16 md:h-20">
+ {/* Left section */}
+ <div className="flex items-center space-x-8">
+ <div className="inline-flex gap-2 items-center">
+ <Logo />
+ <span className="text-lg lg:text-xl font-medium bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400">
+ supermemory.ai
+ </span>
+ </div>
+ <nav className="hidden lg:flex items-center space-x-8">
+ {/* Products dropdown commented out for now
+ <Popover className="relative">
+ ...
+ </Popover>
+ */}
+
+ <a href="https://docs.supermemory.ai" className="text-gray-600 hover:text-gray-900 transition-colors">
+ Docs
+ </a>
+ </nav>
+ </div>
+
+ {/* Right section */}
+ <div className="flex items-center space-x-6">
+ <div className="hidden sm:flex items-center space-x-6">
+ <a href="#" className="text-gray-600 hover:text-gray-900 transition-colors">
+ <GithubIcon className="h-6 w-6" />
+ </a>
+ <a href="#" className="text-gray-600 hover:text-gray-900 transition-colors">
+ <DiscordIcon className="h-6 w-6" />
+ </a>
+ </div>
+ <div className="flex items-center space-x-4">
+ <a
+ href="/signin"
+ className="[box-shadow:0_-20px_80px_-20px_#CCE5FF_inset] bg-[#1E3A8A] text-white px-5 py-2.5 rounded-lg hover:bg-opacity-90 transition-all duration-200 hover:translate-y-[-1px]"
+ >
+ Get started
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </header>
+
+ {/* Hero Section */}
+ <main className="pt-32 md:pt-40 relative z-[20] pb-24">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="grid lg:grid-cols-2 gap-8 items-start">
+ {/* Hero Content */}
+ <div className="text-left max-w-xl mx-auto lg:mx-0">
+ {/* Announcement Banner */}
+ <div className="flex mb-10">
+ <div className="inline-flex items-center space-x-3 bg-white/90 rounded-full px-5 py-2.5 shadow-sm hover:shadow-md transition-all duration-200">
+ <span className="bg-[#3B82F6] text-white text-xs px-2.5 py-1 rounded-full font-medium">
+ NEW
+ </span>
+ <span className="text-gray-600">Top OSS Repository in 2024</span>
+ <a
+ href="https://runacap.com/ross-index/q3-2024/"
+ className="text-[#1E3A8A] font-medium hover:text-[#3B82F6] transition-colors"
+ >
+ Read more →
+ </a>
+ </div>
+ </div>
+ <h1 className="text-5xl md:text-6xl font-bold text-gray-900 tracking-tight leading-[1.1]">
+ AI for all your knowledge.
+ </h1>
+ <p className="text-xl text-gray-600 mt-6 mb-8 leading-relaxed">
+ Supermemory helps you collect, organize, and recall all your knowledge.
+ {/* list of notable features */}
+ <ul className="list-none space-y-3 mt-6">
+ <li className="flex items-center space-x-3">
+ <svg
+ className="h-5 w-5 text-[#3B82F6]"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M5 13l4 4L19 7"
+ />
+ </svg>
+ <span>Connect with your existing tools and bookmarks</span>
+ </li>
+ <li className="flex items-center space-x-3">
+ <svg
+ className="h-5 w-5 text-[#3B82F6]"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M5 13l4 4L19 7"
+ />
+ </svg>
+ <span>Chat and find with AI & actually use your knowledge</span>
+ </li>
+ <li className="flex items-center space-x-3">
+ <svg
+ className="h-5 w-5 text-[#3B82F6]"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M5 13l4 4L19 7"
+ />
+ </svg>
+ <span>Share your knowledge with your friends and colleagues</span>
+ </li>
+ </ul>
+ </p>
+ <div className="flex flex-col space-y-8">
+ <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-4 sm:space-y-0 sm:space-x-6">
+ <a
+ href="/signin"
+ className="w-full sm:w-auto [box-shadow:0_-20px_80px_-20px_#CCE5FF_inset] bg-gradient-to-tr from-[#1E3A8A] to-[#3B82F6] text-white px-8 py-4 rounded-xl hover:shadow-lg hover:translate-y-[-2px] transition-all duration-200 text-center font-medium"
+ >
+ Get started for free
+ </a>
+ <div className="flex items-center space-x-6 text-sm text-gray-600">
+ <a
+ href="https://git.new/memory"
+ className="flex items-center hover:text-[#1E3A8A] transition-colors group"
+ >
+ <GithubIcon className="h-5 w-5 mr-2 group-hover:scale-110 transition-transform" />
+ GitHub
+ </a>
+ <a
+ href="https://docs.supermemory.ai"
+ className="flex items-center hover:text-[#1E3A8A] transition-colors group"
+ >
+ <svg
+ className="h-5 w-5 mr-2 group-hover:scale-110 transition-transform"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
+ />
+ </svg>
+ Documentation
+ </a>
+ </div>
+ </div>
+
+ <div className="flex items-center space-x-4">
+ <img
+ src="/product-of-the-day.png"
+ className="w-44 hover:opacity-90 transition-opacity"
+ alt="Product of the Day on Product Hunt"
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Video Section */}
+ <div className="w-full mt-24">
+ <div
+ style={{ position: "relative", paddingTop: "56.25%" }}
+ className="rounded-2xl overflow-hidden shadow-2xl hover:shadow-3xl transition-shadow duration-300"
+ >
+ <iframe
+ src="https://customer-5xczlbkyq4f9ejha.cloudflarestream.com/111c4828c3587348bc703e67bfca9682/iframe?muted=true&poster=https%3A%2F%2Fcustomer-5xczlbkyq4f9ejha.cloudflarestream.com%2F111c4828c3587348bc703e67bfca9682%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600"
+ loading="lazy"
+ style={{
+ border: "none",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ height: "100%",
+ width: "100%",
+ }}
+ allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
+ allowFullScreen={true}
+ ></iframe>
+ </div>
+ </div>
+ </div>
+
+ {/* Integration Tags */}
+ <div className="mt-32">
+ <div className="text-gray-900 font-medium mb-8 text-center text-lg">
+ Integrate with your favorite tools
+ </div>
+ <div className="flex flex-wrap justify-center gap-4">
+ {[
+ "Notion",
+ "Twitter",
+ "Obsidian",
+ "Reddit",
+ "LinkedIn",
+ "Chrome Extension",
+ "iOS App",
+ "Slack",
+ // "Google Drive",
+ // "Microsoft Teams"
+ ].map((tool) => (
+ <div
+ key={tool}
+ className="bg-white/90 rounded-full px-5 py-2.5 shadow-sm hover:shadow-md hover:bg-white hover:translate-y-[-1px] transition-all duration-200 cursor-pointer"
+ >
+ {tool}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </main>
+ </div>
+ );
+}
diff --git a/apps/web/app/components/Landing/Note.tsx b/apps/web/app/components/Landing/Note.tsx
new file mode 100644
index 00000000..8fe2d4e1
--- /dev/null
+++ b/apps/web/app/components/Landing/Note.tsx
@@ -0,0 +1,73 @@
+export default function Note() {
+ return (
+ <div className="bg-gradient-to-b from-white to-gray-50 py-24">
+ <div className="px-6 lg:px-8">
+ <div className="mx-auto max-w-2xl lg:max-w-7xl">
+ <div className="max-w-4xl mx-auto">
+ <div className="flex flex-col items-center mb-12">
+ <div className="bg-gray-300 w-40 h-1 rounded-full mb-2" />
+ <div className="text-sm text-gray-500">Today</div>
+ </div>
+
+ <div className="flex justify-center">
+ <div className="relative max-w-2xl w-full">
+ {/* Profile Image */}
+ <div className="absolute -top-12 left-4">
+ <img
+ src="https://pbs.twimg.com/profile_images/1813041528278843392/u50EIuLZ_400x400.jpg"
+ alt="Dhravya Shah"
+ className="w-16 h-16 rounded-full border-4 border-white shadow-lg"
+ />
+ </div>
+
+ {/* Message Bubble */}
+ <div className="bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-3xl px-8 py-6 shadow-lg">
+ <p className="text-lg leading-relaxed space-y-4">
+ <span className="block">👋 Hey there! I'm Dhravya</span>
+
+ <span className="block">
+ I'm a college student who built Supermemory as a weekend project. What started
+ as a simple idea has grown into something I'm really proud of, thanks to
+ amazing support from the open-source community! 🚀
+ </span>
+
+ <span className="block">
+ When you see "we" on the website - that's actually just me! 😅 I maintain and
+ build everything myself, supported by wonderful donors and grants that help
+ keep this project free and open source.
+ </span>
+
+ <span className="block">
+ In this AI-driven world, I believe in augmenting human knowledge rather than
+ replacing it. My goal is simple: build something that genuinely helps people
+ learn and grow. 💡
+ </span>
+
+ <span className="block">
+ If you'd like to follow my journey, you can find me on{" "}
+ <a href="https://x.com/dhravyashah" className="underline hover:text-blue-100">
+ Twitter
+ </a>{" "}
+ and{" "}
+ <a href="https://git.new/memory" className="underline hover:text-blue-100">
+ GitHub
+ </a>
+ . And if you believe in what we're building, consider{" "}
+ <a
+ href="https://github.com/sponsors/dhravya"
+ className="underline hover:text-blue-100"
+ >
+ supporting Supermemory's development
+ </a>{" "}
+ ❤️
+ </span>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/app/components/Landing/Private.tsx b/apps/web/app/components/Landing/Private.tsx
new file mode 100644
index 00000000..7e944887
--- /dev/null
+++ b/apps/web/app/components/Landing/Private.tsx
@@ -0,0 +1,62 @@
+import { Eye, Lock, ShieldCheck } from "lucide-react";
+
+export default function Private() {
+ const privacyFeatures = [
+ { icon: ShieldCheck, text: "End-to-end encryption" },
+ { icon: Lock, text: "Self-hosted option available" },
+ { icon: Eye, text: "Zero knowledge architecture" },
+ ];
+
+ return (
+ <div className="min-h-full my-7 flex items-center justify-center p-4">
+ <div className="max-w-[1000px] px-2 md:px-10 w-full bg-[#F5F7FF] rounded-3xl p-16 shadow-[0_2px_40px_rgba(0,0,0,0.05)]">
+ <div className="space-y-6 text-center">
+ <h1 className="text-[40px] leading-[1.2] font-medium tracking-[-0.02em] text-[#111111]">
+ Your knowledge stays
+ <br />
+ private and secure
+ </h1>
+
+ <p className="text-[#666666] text-lg leading-relaxed max-w-[600px] mx-auto">
+ We take privacy seriously. Your data is fully encrypted, never shared with third
+ parties. Even on the hosted version, we securely store your data in our own servers.
+ </p>
+
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 pt-8 max-w-[700px] mx-auto">
+ {privacyFeatures.map((feature, index) => (
+ <div key={index} className="flex flex-col items-center gap-3">
+ <div className="w-12 h-12 rounded-xl bg-white shadow-[0_2px_8px_rgba(0,0,0,0.05)] flex items-center justify-center">
+ <feature.icon className="w-6 h-6 text-[#3B82F6]" />
+ </div>
+ <span className="text-sm text-gray-700 font-medium">{feature.text}</span>
+ </div>
+ ))}
+ </div>
+
+ <div className="pt-10 flex flex-wrap justify-center gap-4">
+ <a
+ href="https://docs.supermemory.ai/self-hosting"
+ className="inline-flex items-center gap-2 bg-[#1E3A8A] text-white py-3 px-6 rounded-lg hover:bg-opacity-90 transition-all duration-200 hover:translate-y-[-1px]"
+ >
+ Self-host Supermemory
+ </a>
+
+ <a
+ href="https://docs.supermemory.ai/essentials/architecture"
+ className="inline-flex items-center gap-2 bg-white text-gray-700 py-3 px-6 rounded-lg hover:bg-gray-50 transition-all duration-200 hover:translate-y-[-1px]"
+ >
+ Our architecture
+ </a>
+
+ <a
+ href="https://git.new/memory"
+ className="inline-flex items-center gap-2 bg-white text-gray-700 py-3 px-6 rounded-lg hover:bg-gray-50 transition-all duration-200 hover:translate-y-[-1px]"
+ >
+ Check out the code
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/app/components/Landing/index.tsx b/apps/web/app/components/Landing/index.tsx
new file mode 100644
index 00000000..0a432ec2
--- /dev/null
+++ b/apps/web/app/components/Landing/index.tsx
@@ -0,0 +1,17 @@
+import Feature2 from "./Feature";
+import Footer from "./Footer";
+import Hero from "./Hero";
+import Note from "./Note";
+import Private from "./Private";
+
+export default function Landing() {
+ return (
+ <div className="overflow-hidden">
+ <Hero />
+ <Feature2 />
+ <Private />
+ <Note />
+ <Footer />
+ </div>
+ );
+}
diff --git a/apps/web/app/components/Landing/plus-grid.tsx b/apps/web/app/components/Landing/plus-grid.tsx
new file mode 100644
index 00000000..752ccefc
--- /dev/null
+++ b/apps/web/app/components/Landing/plus-grid.tsx
@@ -0,0 +1,88 @@
+import { clsx } from "clsx";
+
+export function PlusGrid({
+ className = "",
+ children,
+}: {
+ className?: string;
+ children: React.ReactNode;
+}) {
+ return <div className={className}>{children}</div>;
+}
+
+export function PlusGridRow({
+ className = "",
+ children,
+}: {
+ className?: string;
+ children: React.ReactNode;
+}) {
+ return (
+ <div
+ className={clsx(
+ className,
+ "group/row relative isolate pt-[calc(theme(spacing.2)+1px)] last:pb-[calc(theme(spacing.2)+1px)]",
+ )}
+ >
+ <div
+ aria-hidden="true"
+ className="absolute inset-y-0 left-1/2 -z-10 w-screen -translate-x-1/2"
+ >
+ <div className="absolute inset-x-0 top-0 border-t border-black/10 dark:border-white/10"></div>
+ <div className="absolute inset-x-0 top-2 border-t border-black/10 dark:border-white/10"></div>
+ <div className="absolute inset-x-0 bottom-0 hidden border-b border-black/10 group-last/row:block dark:border-white/10"></div>
+ <div className="b absolute inset-x-0 bottom-2 hidden border-b border-black/10 group-last/row:block dark:border-white/10"></div>
+ </div>
+ {children}
+ </div>
+ );
+}
+
+export function PlusGridItem({
+ className = "",
+ children,
+}: {
+ className?: string;
+ children: React.ReactNode;
+}) {
+ return (
+ <div className={clsx(className, "group/item relative")}>
+ <PlusGridIcon placement="top left" className="hidden group-first/item:block" />
+ <PlusGridIcon placement="top right" />
+ <PlusGridIcon
+ placement="bottom left"
+ className="hidden group-last/row:group-first/item:block"
+ />
+ <PlusGridIcon placement="bottom right" className="hidden group-last/row:block" />
+ {children}
+ </div>
+ );
+}
+
+export function PlusGridIcon({
+ className = "",
+ placement,
+}: {
+ className?: string;
+ placement: `${"top" | "bottom"} ${"right" | "left"}`;
+}) {
+ let [yAxis, xAxis] = placement.split(" ");
+
+ let yClass = yAxis === "top" ? "-top-2" : "-bottom-2";
+ let xClass = xAxis === "left" ? "-left-2" : "-right-2";
+
+ return (
+ <svg
+ viewBox="0 0 15 15"
+ aria-hidden="true"
+ className={clsx(
+ className,
+ "absolute size-[15px] fill-black/5 dark:fill-white/10",
+ yClass,
+ xClass,
+ )}
+ >
+ <path d="M8 0H7V7H0V8H7V15H8V8H15V7H8V0Z" />
+ </svg>
+ );
+}
diff --git a/apps/web/app/components/editor/use-chat.tsx b/apps/web/app/components/editor/use-chat.tsx
index 8c60f88d..11b6ee6e 100644
--- a/apps/web/app/components/editor/use-chat.tsx
+++ b/apps/web/app/components/editor/use-chat.tsx
@@ -1,314 +1,270 @@
-'use client';
+"use client";
-import { type ReactNode, createContext, useContext, useState } from 'react';
+import { type ReactNode, createContext, useContext, useState } from "react";
-import { faker } from '@faker-js/faker';
-import { cn } from '@udecode/cn';
-import { CopilotPlugin } from '@udecode/plate-ai/react';
-import { useEditorPlugin } from '@udecode/plate-common/react';
-import { useChat as useBaseChat } from 'ai/react';
+import { faker } from "@faker-js/faker";
+import { cn } from "@udecode/cn";
+import { CopilotPlugin } from "@udecode/plate-ai/react";
+import { useEditorPlugin } from "@udecode/plate-common/react";
+import { useChat as useBaseChat } from "ai/react";
+import { ArrowUpRight, Check, ChevronsUpDown, Eye, EyeOff, Settings } from "lucide-react";
+import { Button } from "~/components/plate-ui/button";
import {
- ArrowUpRight,
- Check,
- ChevronsUpDown,
- Eye,
- EyeOff,
- Settings,
-} from 'lucide-react';
-
-import { Button } from '~/components/plate-ui/button';
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from '~/components/plate-ui/command';
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "~/components/plate-ui/command";
import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '~/components/plate-ui/dialog';
-import { Input } from '~/components/plate-ui/input';
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from '~/components/plate-ui/popover';
-
-export const useChat = () => {
- return useBaseChat({
- id: 'editor',
- api: '/api/ai/command',
- body: {
- apiKey: useOpenAI().apiKey,
- model: useOpenAI().model.value,
- },
- fetch: async (input, init) => {
- const res = await fetch(input, init);
-
- if (!res.ok) {
- // Mock the API response. Remove it when you implement the route /api/ai/command
- await new Promise((resolve) => setTimeout(resolve, 400));
-
- const stream = fakeStreamText();
-
- return new Response(stream, {
- headers: {
- Connection: 'keep-alive',
- 'Content-Type': 'text/plain',
- },
- });
- }
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "~/components/plate-ui/dialog";
+import { Input } from "~/components/plate-ui/input";
+import { Popover, PopoverContent, PopoverTrigger } from "~/components/plate-ui/popover";
- return res;
- },
- });
+export const useChat = (props?: Parameters<typeof useBaseChat>[0]) => {
+ return useBaseChat({
+ id: "editor",
+ api: "/api/ai/command",
+ ...props,
+ });
};
// Used for testing. Remove it after implementing useChat api.
const fakeStreamText = ({
- chunkCount = 10,
- streamProtocol = 'data',
+ chunkCount = 10,
+ streamProtocol = "data",
}: {
- chunkCount?: number;
- streamProtocol?: 'data' | 'text';
+ chunkCount?: number;
+ streamProtocol?: "data" | "text";
} = {}) => {
- const chunks = Array.from({ length: chunkCount }, () => ({
- delay: faker.number.int({ max: 150, min: 50 }),
- texts: faker.lorem.words({ max: 3, min: 1 }) + ' ',
- }));
- const encoder = new TextEncoder();
+ const chunks = Array.from({ length: chunkCount }, () => ({
+ delay: faker.number.int({ max: 150, min: 50 }),
+ texts: faker.lorem.words({ max: 3, min: 1 }) + " ",
+ }));
+ const encoder = new TextEncoder();
- return new ReadableStream({
- async start(controller) {
- for (const chunk of chunks) {
- await new Promise((resolve) => setTimeout(resolve, chunk.delay));
+ return new ReadableStream({
+ async start(controller) {
+ for (const chunk of chunks) {
+ await new Promise((resolve) => setTimeout(resolve, chunk.delay));
- if (streamProtocol === 'text') {
- controller.enqueue(encoder.encode(chunk.texts));
- } else {
- controller.enqueue(
- encoder.encode(`0:${JSON.stringify(chunk.texts)}\n`)
- );
- }
- }
+ if (streamProtocol === "text") {
+ controller.enqueue(encoder.encode(chunk.texts));
+ } else {
+ controller.enqueue(encoder.encode(`0:${JSON.stringify(chunk.texts)}\n`));
+ }
+ }
- if (streamProtocol === 'data') {
- controller.enqueue(
- `d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":${chunks.length}}}\n`
- );
- }
+ if (streamProtocol === "data") {
+ controller.enqueue(
+ `d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":${chunks.length}}}\n`,
+ );
+ }
- controller.close();
- },
- });
+ controller.close();
+ },
+ });
};
interface Model {
- label: string;
- value: string;
+ label: string;
+ value: string;
}
interface OpenAIContextType {
- apiKey: string;
- model: Model;
- setApiKey: (key: string) => void;
- setModel: (model: Model) => void;
+ apiKey: string;
+ model: Model;
+ setApiKey: (key: string) => void;
+ setModel: (model: Model) => void;
}
export const models: Model[] = [
- { label: 'gpt-4o-mini', value: 'gpt-4o-mini' },
- { label: 'gpt-4o', value: 'gpt-4o' },
- { label: 'gpt-4-turbo', value: 'gpt-4-turbo' },
- { label: 'gpt-4', value: 'gpt-4' },
- { label: 'gpt-3.5-turbo', value: 'gpt-3.5-turbo' },
- { label: 'gpt-3.5-turbo-instruct', value: 'gpt-3.5-turbo-instruct' },
+ { label: "gpt-4o-mini", value: "gpt-4o-mini" },
+ { label: "gpt-4o", value: "gpt-4o" },
+ { label: "gpt-4-turbo", value: "gpt-4-turbo" },
+ { label: "gpt-4", value: "gpt-4" },
+ { label: "gpt-3.5-turbo", value: "gpt-3.5-turbo" },
+ { label: "gpt-3.5-turbo-instruct", value: "gpt-3.5-turbo-instruct" },
];
const OpenAIContext = createContext<OpenAIContextType | undefined>(undefined);
export function OpenAIProvider({ children }: { children: ReactNode }) {
- const [apiKey, setApiKey] = useState('');
- const [model, setModel] = useState<Model>(models[0]);
+ const [apiKey, setApiKey] = useState("");
+ const [model, setModel] = useState<Model>(models[0]);
- return (
- <OpenAIContext.Provider value={{ apiKey, model, setApiKey, setModel }}>
- {children}
- </OpenAIContext.Provider>
- );
+ return (
+ <OpenAIContext.Provider value={{ apiKey, model, setApiKey, setModel }}>
+ {children}
+ </OpenAIContext.Provider>
+ );
}
export function useOpenAI() {
- const context = useContext(OpenAIContext);
+ const context = useContext(OpenAIContext);
- return (
- context ??
- ({
- apiKey: '',
- model: models[0],
- setApiKey: () => {},
- setModel: () => {},
- } as OpenAIContextType)
- );
+ return (
+ context ??
+ ({
+ apiKey: "",
+ model: models[0],
+ setApiKey: () => {},
+ setModel: () => {},
+ } as OpenAIContextType)
+ );
}
export function SettingsDialog() {
- const { apiKey, model, setApiKey, setModel } = useOpenAI();
- const [tempKey, setTempKey] = useState(apiKey);
- const [showKey, setShowKey] = useState(false);
- const [open, setOpen] = useState(false);
- const [openModel, setOpenModel] = useState(false);
+ const { apiKey, model, setApiKey, setModel } = useOpenAI();
+ const [tempKey, setTempKey] = useState(apiKey);
+ const [showKey, setShowKey] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [openModel, setOpenModel] = useState(false);
- const { getOptions, setOption } = useEditorPlugin(CopilotPlugin);
+ const { getOptions, setOption } = useEditorPlugin(CopilotPlugin);
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- setApiKey(tempKey);
- setOpen(false);
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setApiKey(tempKey);
+ setOpen(false);
- const completeOptions = getOptions().completeOptions ?? {};
+ const completeOptions = getOptions().completeOptions ?? {};
- setOption('completeOptions', {
- ...completeOptions,
- body: {
- ...completeOptions.body,
- apiKey: tempKey,
- model: model.value,
- },
- });
- };
+ setOption("completeOptions", {
+ ...completeOptions,
+ body: {
+ ...completeOptions.body,
+ apiKey: tempKey,
+ model: model.value,
+ },
+ });
+ };
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>
- <Button
- size="icon"
- variant="default"
- className={cn(
- 'group fixed bottom-4 right-4 z-50 size-10 overflow-hidden',
- 'rounded-full shadow-md hover:shadow-lg',
- 'transition-all duration-300 ease-in-out hover:w-[106px]'
- )}
- data-block-hide
- >
- <div className="flex size-full items-center justify-start gap-2">
- <Settings className="ml-1.5 size-4" />
- <span
- className={cn(
- 'whitespace-nowrap opacity-0 transition-all duration-300 ease-in-out',
- 'group-hover:translate-x-0 group-hover:opacity-100',
- '-translate-x-2'
- )}
- >
- Settings
- </span>
- </div>
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader className="space-y-4">
- <DialogTitle>AI Settings</DialogTitle>
- <DialogDescription>
- Enter your{' '}
- <a
- className="inline-flex items-center font-medium text-primary hover:underline"
- href="https://platform.openai.com/api-keys"
- rel="noreferrer"
- target="_blank"
- >
- OpenAI API key
- <ArrowUpRight className="size-[14px]" />
- </a>{' '}
- to use AI features.
- </DialogDescription>
- </DialogHeader>
- <form className="space-y-4" onSubmit={handleSubmit}>
- <div className="relative">
- <Input
- className="pr-10"
- value={tempKey}
- onChange={(e) => setTempKey(e.target.value)}
- placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- data-1p-ignore
- type={showKey ? 'text' : 'password'}
- />
- <Button
- size="icon"
- variant="ghost"
- className="absolute right-0 top-0 h-full"
- onClick={() => setShowKey(!showKey)}
- type="button"
- >
- {showKey ? (
- <EyeOff className="size-4" />
- ) : (
- <Eye className="size-4" />
- )}
- <span className="sr-only">
- {showKey ? 'Hide' : 'Show'} API key
- </span>
- </Button>
- </div>
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button
+ size="icon"
+ variant="default"
+ className={cn(
+ "group fixed bottom-4 right-4 z-50 size-10 overflow-hidden",
+ "rounded-full shadow-md hover:shadow-lg",
+ "transition-all duration-300 ease-in-out hover:w-[106px]",
+ )}
+ data-block-hide
+ >
+ <div className="flex size-full items-center justify-start gap-2">
+ <Settings className="ml-1.5 size-4" />
+ <span
+ className={cn(
+ "whitespace-nowrap opacity-0 transition-all duration-300 ease-in-out",
+ "group-hover:translate-x-0 group-hover:opacity-100",
+ "-translate-x-2",
+ )}
+ >
+ Settings
+ </span>
+ </div>
+ </Button>
+ </DialogTrigger>
+ <DialogContent>
+ <DialogHeader className="space-y-4">
+ <DialogTitle>AI Settings</DialogTitle>
+ <DialogDescription>
+ Enter your{" "}
+ <a
+ className="inline-flex items-center font-medium text-primary hover:underline"
+ href="https://platform.openai.com/api-keys"
+ rel="noreferrer"
+ target="_blank"
+ >
+ OpenAI API key
+ <ArrowUpRight className="size-[14px]" />
+ </a>{" "}
+ to use AI features.
+ </DialogDescription>
+ </DialogHeader>
+ <form className="space-y-4" onSubmit={handleSubmit}>
+ <div className="relative">
+ <Input
+ className="pr-10"
+ value={tempKey}
+ onChange={(e) => setTempKey(e.target.value)}
+ placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ data-1p-ignore
+ type={showKey ? "text" : "password"}
+ />
+ <Button
+ size="icon"
+ variant="ghost"
+ className="absolute right-0 top-0 h-full"
+ onClick={() => setShowKey(!showKey)}
+ type="button"
+ >
+ {showKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
+ <span className="sr-only">{showKey ? "Hide" : "Show"} API key</span>
+ </Button>
+ </div>
- <Popover open={openModel} onOpenChange={setOpenModel}>
- <PopoverTrigger asChild>
- <Button
- size="lg"
- variant="outline"
- className="w-full justify-between"
- aria-expanded={openModel}
- role="combobox"
- >
- <code>{model.label}</code>
- <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0">
- <Command>
- <CommandInput placeholder="Search model..." />
- <CommandEmpty>No model found.</CommandEmpty>
+ <Popover open={openModel} onOpenChange={setOpenModel}>
+ <PopoverTrigger asChild>
+ <Button
+ size="lg"
+ variant="outline"
+ className="w-full justify-between"
+ aria-expanded={openModel}
+ role="combobox"
+ >
+ <code>{model.label}</code>
+ <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-full p-0">
+ <Command>
+ <CommandInput placeholder="Search model..." />
+ <CommandEmpty>No model found.</CommandEmpty>
- <CommandList>
- <CommandGroup>
- {models.map((m) => (
- <CommandItem
- key={m.value}
- value={m.value}
- onSelect={() => {
- setModel(m);
- setOpenModel(false);
- }}
- >
- <Check
- className={cn(
- 'mr-2 size-4',
- model.value === m.value
- ? 'opacity-100'
- : 'opacity-0'
- )}
- />
- <code>{m.label}</code>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
+ <CommandList>
+ <CommandGroup>
+ {models.map((m) => (
+ <CommandItem
+ key={m.value}
+ value={m.value}
+ onSelect={() => {
+ setModel(m);
+ setOpenModel(false);
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 size-4",
+ model.value === m.value ? "opacity-100" : "opacity-0",
+ )}
+ />
+ <code>{m.label}</code>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
- <Button size="lg" className="w-full" type="submit">
- Save
- </Button>
- </form>
- <p className="mt-4 text-sm text-muted-foreground">
- Not stored anywhere. Used only for current session requests.
- </p>
- </DialogContent>
- </Dialog>
- );
+ <Button size="lg" className="w-full" type="submit">
+ Save
+ </Button>
+ </form>
+ <p className="mt-4 text-sm text-muted-foreground">
+ Not stored anywhere. Used only for current session requests.
+ </p>
+ </DialogContent>
+ </Dialog>
+ );
}
diff --git a/apps/web/app/components/editor/use-create-editor.tsx b/apps/web/app/components/editor/use-create-editor.tsx
index 598af916..bae45a74 100644
--- a/apps/web/app/components/editor/use-create-editor.tsx
+++ b/apps/web/app/components/editor/use-create-editor.tsx
@@ -1,156 +1,137 @@
-import { withProps } from '@udecode/cn';
-import { AIPlugin } from '@udecode/plate-ai/react';
+import { withProps } from "@udecode/cn";
+import { AIPlugin } from "@udecode/plate-ai/react";
import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- SubscriptPlugin,
- SuperscriptPlugin,
- UnderlinePlugin,
-} from '@udecode/plate-basic-marks/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
+ BoldPlugin,
+ CodePlugin,
+ ItalicPlugin,
+ StrikethroughPlugin,
+ SubscriptPlugin,
+ SuperscriptPlugin,
+ UnderlinePlugin,
+} from "@udecode/plate-basic-marks/react";
+import { BlockquotePlugin } from "@udecode/plate-block-quote/react";
+import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from "@udecode/plate-code-block/react";
+import { CommentsPlugin } from "@udecode/plate-comments/react";
+import { Value } from "@udecode/plate-common";
+import { ParagraphPlugin, PlateLeaf, usePlateEditor } from "@udecode/plate-common/react";
+import { DatePlugin } from "@udecode/plate-date/react";
+import { ExcalidrawPlugin } from "@udecode/plate-excalidraw/react";
+import { HEADING_KEYS } from "@udecode/plate-heading";
+import { TocPlugin } from "@udecode/plate-heading/react";
+import { HighlightPlugin } from "@udecode/plate-highlight/react";
+import { HorizontalRulePlugin } from "@udecode/plate-horizontal-rule/react";
+import { KbdPlugin } from "@udecode/plate-kbd/react";
+import { ColumnItemPlugin, ColumnPlugin } from "@udecode/plate-layout/react";
+import { LinkPlugin } from "@udecode/plate-link/react";
+import { MarkdownPlugin } from "@udecode/plate-markdown";
+import { ImagePlugin, MediaEmbedPlugin } from "@udecode/plate-media/react";
+import { MentionInputPlugin, MentionPlugin } from "@udecode/plate-mention/react";
+import { SlashInputPlugin } from "@udecode/plate-slash-command/react";
import {
- CodeBlockPlugin,
- CodeLinePlugin,
- CodeSyntaxPlugin,
-} from '@udecode/plate-code-block/react';
-import { CommentsPlugin } from '@udecode/plate-comments/react';
-import {
- ParagraphPlugin,
- PlateLeaf,
- usePlateEditor,
-} from '@udecode/plate-common/react';
-import { DatePlugin } from '@udecode/plate-date/react';
-
-import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { TocPlugin } from '@udecode/plate-heading/react';
-import { HighlightPlugin } from '@udecode/plate-highlight/react';
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { KbdPlugin } from '@udecode/plate-kbd/react';
-import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
-import { LinkPlugin } from '@udecode/plate-link/react';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
-import {
- MentionInputPlugin,
- MentionPlugin,
-} from '@udecode/plate-mention/react';
-import { SlashInputPlugin } from '@udecode/plate-slash-command/react';
-import {
- TableCellHeaderPlugin,
- TableCellPlugin,
- TablePlugin,
- TableRowPlugin,
-} from '@udecode/plate-table/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-
-import { copilotPlugins } from '~/components/editor/plugins/copilot-plugins';
-import { editorPlugins } from '~/components/editor/plugins/editor-plugins';
-import { FixedToolbarPlugin } from '~/components/editor/plugins/fixed-toolbar-plugin';
-import { FloatingToolbarPlugin } from '~/components/editor/plugins/floating-toolbar-plugin';
-import { AILeaf } from '~/components/plate-ui/ai-leaf';
-import { BlockquoteElement } from '~/components/plate-ui/blockquote-element';
-import { CodeBlockElement } from '~/components/plate-ui/code-block-element';
-import { CodeLeaf } from '~/components/plate-ui/code-leaf';
-import { CodeLineElement } from '~/components/plate-ui/code-line-element';
-import { CodeSyntaxLeaf } from '~/components/plate-ui/code-syntax-leaf';
-import { ColumnElement } from '~/components/plate-ui/column-element';
-import { ColumnGroupElement } from '~/components/plate-ui/column-group-element';
-import { CommentLeaf } from '~/components/plate-ui/comment-leaf';
-import { DateElement } from '~/components/plate-ui/date-element';
-import { ExcalidrawElement } from '~/components/plate-ui/excalidraw-element';
-import { HeadingElement } from '~/components/plate-ui/heading-element';
-import { HighlightLeaf } from '~/components/plate-ui/highlight-leaf';
-import { HrElement } from '~/components/plate-ui/hr-element';
-import { ImageElement } from '~/components/plate-ui/image-element';
-import { KbdLeaf } from '~/components/plate-ui/kbd-leaf';
-import { LinkElement } from '~/components/plate-ui/link-element';
-import { MediaEmbedElement } from '~/components/plate-ui/media-embed-element';
-import { MentionElement } from '~/components/plate-ui/mention-element';
-import { MentionInputElement } from '~/components/plate-ui/mention-input-element';
-import { ParagraphElement } from '~/components/plate-ui/paragraph-element';
-import { withPlaceholders } from '~/components/plate-ui/placeholder';
-import { SlashInputElement } from '~/components/plate-ui/slash-input-element';
-import {
- TableCellElement,
- TableCellHeaderElement,
-} from '~/components/plate-ui/table-cell-element';
-import { TableElement } from '~/components/plate-ui/table-element';
-import { TableRowElement } from '~/components/plate-ui/table-row-element';
-import { TocElement } from '~/components/plate-ui/toc-element';
-import { ToggleElement } from '~/components/plate-ui/toggle-element';
-import { withDraggables } from '~/components/plate-ui/with-draggables';
+ TableCellHeaderPlugin,
+ TableCellPlugin,
+ TablePlugin,
+ TableRowPlugin,
+} from "@udecode/plate-table/react";
+import { TogglePlugin } from "@udecode/plate-toggle/react";
+import { copilotPlugins } from "~/components/editor/plugins/copilot-plugins";
+import { editorPlugins } from "~/components/editor/plugins/editor-plugins";
+import { FixedToolbarPlugin } from "~/components/editor/plugins/fixed-toolbar-plugin";
+import { FloatingToolbarPlugin } from "~/components/editor/plugins/floating-toolbar-plugin";
+import { AILeaf } from "~/components/plate-ui/ai-leaf";
+import { BlockquoteElement } from "~/components/plate-ui/blockquote-element";
+import { CodeBlockElement } from "~/components/plate-ui/code-block-element";
+import { CodeLeaf } from "~/components/plate-ui/code-leaf";
+import { CodeLineElement } from "~/components/plate-ui/code-line-element";
+import { CodeSyntaxLeaf } from "~/components/plate-ui/code-syntax-leaf";
+import { ColumnElement } from "~/components/plate-ui/column-element";
+import { ColumnGroupElement } from "~/components/plate-ui/column-group-element";
+import { CommentLeaf } from "~/components/plate-ui/comment-leaf";
+import { DateElement } from "~/components/plate-ui/date-element";
+import { ExcalidrawElement } from "~/components/plate-ui/excalidraw-element";
+import { HeadingElement } from "~/components/plate-ui/heading-element";
+import { HighlightLeaf } from "~/components/plate-ui/highlight-leaf";
+import { HrElement } from "~/components/plate-ui/hr-element";
+import { ImageElement } from "~/components/plate-ui/image-element";
+import { KbdLeaf } from "~/components/plate-ui/kbd-leaf";
+import { LinkElement } from "~/components/plate-ui/link-element";
+import { MediaEmbedElement } from "~/components/plate-ui/media-embed-element";
+import { MentionElement } from "~/components/plate-ui/mention-element";
+import { MentionInputElement } from "~/components/plate-ui/mention-input-element";
+import { ParagraphElement } from "~/components/plate-ui/paragraph-element";
+import { withPlaceholders } from "~/components/plate-ui/placeholder";
+import { SlashInputElement } from "~/components/plate-ui/slash-input-element";
+import { TableCellElement, TableCellHeaderElement } from "~/components/plate-ui/table-cell-element";
+import { TableElement } from "~/components/plate-ui/table-element";
+import { TableRowElement } from "~/components/plate-ui/table-row-element";
+import { TocElement } from "~/components/plate-ui/toc-element";
+import { ToggleElement } from "~/components/plate-ui/toggle-element";
+import { withDraggables } from "~/components/plate-ui/with-draggables";
-export const useCreateEditor = () => {
- return usePlateEditor({
- override: {
- components: withDraggables(
- withPlaceholders({
- [AIPlugin.key]: AILeaf,
- [BlockquotePlugin.key]: BlockquoteElement,
- [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
- [CodeBlockPlugin.key]: CodeBlockElement,
- [CodeLinePlugin.key]: CodeLineElement,
- [CodePlugin.key]: CodeLeaf,
- [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
- [ColumnItemPlugin.key]: ColumnElement,
- [ColumnPlugin.key]: ColumnGroupElement,
- [CommentsPlugin.key]: CommentLeaf,
- [DatePlugin.key]: DateElement,
- // [EmojiInputPlugin.key]: EmojiInputElement,
- [ExcalidrawPlugin.key]: ExcalidrawElement,
- [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
- [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
- [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
- [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),
- [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),
- [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),
- [HighlightPlugin.key]: HighlightLeaf,
- [HorizontalRulePlugin.key]: HrElement,
- [ImagePlugin.key]: ImageElement,
- [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
- [KbdPlugin.key]: KbdLeaf,
- [LinkPlugin.key]: LinkElement,
- [MediaEmbedPlugin.key]: MediaEmbedElement,
- [MentionInputPlugin.key]: MentionInputElement,
- [MentionPlugin.key]: MentionElement,
- [ParagraphPlugin.key]: ParagraphElement,
- [SlashInputPlugin.key]: SlashInputElement,
- [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
- [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),
- [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),
- [TableCellHeaderPlugin.key]: TableCellHeaderElement,
- [TableCellPlugin.key]: TableCellElement,
- [TablePlugin.key]: TableElement,
- [TableRowPlugin.key]: TableRowElement,
- [TocPlugin.key]: TocElement,
- [TogglePlugin.key]: ToggleElement,
- [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
- })
- ),
- },
- plugins: [
- ...copilotPlugins,
- ...editorPlugins,
- FixedToolbarPlugin,
- FloatingToolbarPlugin,
- ],
- value: [
- {
- children: [{ text: 'Playground' }],
- type: 'h1',
- },
- {
- children: [
- { text: 'A rich-text editor with AI capabilities. Try the ' },
- { bold: true, text: 'AI commands' },
- { text: ' or use ' },
- { kbd: true, text: 'Cmd+J' },
- { text: ' to open the AI menu.' },
- ],
- type: ParagraphPlugin.key,
- },
- ],
- });
+export const useCreateEditor = ({ initialValue }: { initialValue?: Value }) => {
+ return usePlateEditor({
+ override: {
+ components: withDraggables(
+ withPlaceholders({
+ [AIPlugin.key]: AILeaf,
+ [BlockquotePlugin.key]: BlockquoteElement,
+ [BoldPlugin.key]: withProps(PlateLeaf, { as: "strong" }),
+ [CodeBlockPlugin.key]: CodeBlockElement,
+ [CodeLinePlugin.key]: CodeLineElement,
+ [CodePlugin.key]: CodeLeaf,
+ [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
+ [ColumnItemPlugin.key]: ColumnElement,
+ [ColumnPlugin.key]: ColumnGroupElement,
+ [CommentsPlugin.key]: CommentLeaf,
+ [DatePlugin.key]: DateElement,
+ // [EmojiInputPlugin.key]: EmojiInputElement,
+ [ExcalidrawPlugin.key]: ExcalidrawElement,
+ [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: "h1" }),
+ [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: "h2" }),
+ [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: "h3" }),
+ [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: "h4" }),
+ [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: "h5" }),
+ [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: "h6" }),
+ [HighlightPlugin.key]: HighlightLeaf,
+ [HorizontalRulePlugin.key]: HrElement,
+ [ImagePlugin.key]: ImageElement,
+ [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
+ [KbdPlugin.key]: KbdLeaf,
+ [LinkPlugin.key]: LinkElement,
+ [MediaEmbedPlugin.key]: MediaEmbedElement,
+ [MentionInputPlugin.key]: MentionInputElement,
+ [MentionPlugin.key]: MentionElement,
+ [ParagraphPlugin.key]: ParagraphElement,
+ [SlashInputPlugin.key]: SlashInputElement,
+ [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
+ [SubscriptPlugin.key]: withProps(PlateLeaf, { as: "sub" }),
+ [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: "sup" }),
+ [TableCellHeaderPlugin.key]: TableCellHeaderElement,
+ [TableCellPlugin.key]: TableCellElement,
+ [TablePlugin.key]: TableElement,
+ [TableRowPlugin.key]: TableRowElement,
+ [TocPlugin.key]: TocElement,
+ [TogglePlugin.key]: ToggleElement,
+ [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
+ }),
+ ),
+ },
+ plugins: [
+ ...copilotPlugins,
+ ...editorPlugins,
+ FixedToolbarPlugin,
+ FloatingToolbarPlugin,
+ MarkdownPlugin,
+ ],
+ value: initialValue ?? [
+ {
+ children: [{ text: "Supermemory docs" }],
+ type: "h1",
+ },
+ {
+ children: [{ text: "Auto-updating AI-powered docs" }],
+ type: ParagraphPlugin.key,
+ },
+ ],
+ });
};
diff --git a/apps/web/app/components/editor/writing-playground.tsx b/apps/web/app/components/editor/writing-playground.tsx
new file mode 100644
index 00000000..b303ffc8
--- /dev/null
+++ b/apps/web/app/components/editor/writing-playground.tsx
@@ -0,0 +1,242 @@
+import { lazy, memo, useEffect, useRef, useState } from "react";
+import { DndProvider } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
+
+import { useChat } from "@ai-sdk/react";
+import { Message } from "ai";
+import { OpenAIProvider } from "~/components/editor/use-chat";
+import { useCreateEditor } from "~/components/editor/use-create-editor";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
+import { useLiveTranscript } from "~/lib/hooks/use-live-transcript";
+import { Theme, useTheme } from "~/lib/theme-provider";
+
+const PlateEditorImport = lazy(() =>
+ import("@udecode/plate-common/react").then((mod) => ({ default: mod.Plate })),
+);
+
+const EditorContainerImport = lazy(() =>
+ import("~/components/plate-ui/editor").then((mod) => ({ default: mod.EditorContainer })),
+);
+
+const EditorImport = lazy(() =>
+ import("~/components/plate-ui/editor").then((mod) => ({ default: mod.Editor })),
+);
+
+const Plate = memo(PlateEditorImport);
+const EditorContainer = memo(EditorContainerImport);
+const Editor = memo(EditorImport);
+
+export function WritingPlayground() {
+ if (typeof window === "undefined") {
+ return <div>Loading...</div>;
+ }
+ const localValue = localStorage.getItem("editorContent");
+ const editor = useCreateEditor({ initialValue: localValue ? JSON.parse(localValue) : undefined });
+
+ const [theme, setTheme] = useTheme();
+
+ useEffect(() => {
+ setTheme(Theme.LIGHT);
+ }, [theme, setTheme]);
+
+ const { toggleMicrophone, caption, status, isListening, isLoading } = useLiveTranscript();
+
+ const { messages, input, handleInputChange, handleSubmit } = useChat({
+ id: "editor",
+ api: "/api/ai/command",
+ initialMessages: [
+ {
+ id: "1",
+ content: "Hi! I am here to help you quickly find what you're looking for.",
+ role: "assistant",
+ },
+ {
+ id: "2",
+ content: "Just drop a question when you need me, ok?",
+ role: "assistant",
+ },
+ ],
+ keepLastMessageOnError: true,
+ // @ts-expect-error
+ experimental_prepareRequestBody: (request) => {
+ // messages with the documentation content
+ // @ts-expect-error
+ const markdown = editor.api.markdown.serialize();
+ console.log(JSON.stringify(editor.children));
+ console.log(markdown);
+ return {
+ messages: [
+ ...request.messages,
+ {
+ id: "3",
+ content: `Here is the documentation for the company: ${markdown}`,
+ role: "user",
+ } satisfies Message,
+ ],
+ };
+ },
+ });
+
+ const [lastProcessedLength, setLastProcessedLength] = useState(0);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const updateTimeoutRef = useRef<NodeJS.Timeout>();
+
+ useEffect(() => {
+ if (!isListening) {
+ return;
+ }
+ // Clear existing timeout
+ if (updateTimeoutRef.current) {
+ clearTimeout(updateTimeoutRef.current);
+ }
+
+ const currentSentences = caption.split(".").length;
+
+ // Only process if we have new sentences and aren't currently processing
+ if (currentSentences > lastProcessedLength && !isProcessing) {
+ // Debounce the update for 2 seconds
+ updateTimeoutRef.current = setTimeout(async () => {
+ setIsProcessing(true);
+ try {
+ const result = await fetch("/api/ai/update", {
+ method: "POST",
+ body: JSON.stringify({ caption, document: editor.children }),
+ });
+ const data = (await result.json()) as {
+ action: "edit" | "delete" | "append" | "ignore";
+ blockId?: string;
+ content?: string;
+ reason: string;
+ };
+ if (data.action === "ignore") {
+ return;
+ }
+ if (data.action === "edit") {
+ // Make a copy of the editor children
+ const newChildren = [...editor.children];
+ // Find and update the block in the copy
+ const blockIndex = newChildren.findIndex((block) => block.id === data.blockId);
+ if (blockIndex !== -1) {
+ newChildren[blockIndex] = {
+ ...newChildren[blockIndex],
+ children: [{ text: data.content ?? "" }],
+ };
+ }
+ editor.tf.setValue(newChildren);
+ } else if (data.action === "delete") {
+ // Make a copy of the editor children and filter out the block
+ const newChildren = editor.children.filter((block) => block.id !== data.blockId);
+ editor.tf.setValue(newChildren);
+ } else if (data.action === "append") {
+ editor.tf.setValue([
+ ...editor.children,
+ {
+ type: "paragraph",
+ children: [{ text: data.content ?? "" }],
+ },
+ ]);
+ }
+ setLastProcessedLength(currentSentences);
+ } catch (error) {
+ console.error("Error updating editor:", error);
+ } finally {
+ setIsProcessing(false);
+ }
+ }, 2000);
+ }
+
+ return () => {
+ if (updateTimeoutRef.current) {
+ clearTimeout(updateTimeoutRef.current);
+ }
+ };
+ }, [caption, editor, lastProcessedLength, isProcessing]);
+
+ return (
+ <div className="grid gap-4 grid-cols-2">
+ <Tabs defaultValue="docs">
+ <TabsList className="absolute top-12 left-12 z-50">
+ <TabsTrigger value="docs">Docs</TabsTrigger>
+ <TabsTrigger value="transcript">Transcript</TabsTrigger>
+
+ <div className="flex items-center gap-2">
+ <button
+ onClick={toggleMicrophone}
+ className={`flex items-center justify-center w-10 h-10 rounded-full transition-colors ${
+ isListening
+ ? "bg-red-500 hover:bg-red-600"
+ : "bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-800 dark:hover:bg-zinc-700"
+ }`}
+ title={isListening ? "Stop recording" : "Start recording"}
+ >
+ <div className={`w-3 h-3 rounded-full ${isListening ? "bg-white" : "bg-red-500"}`} />
+ </button>
+ <span className="text-sm text-zinc-600 dark:text-zinc-400">{status}</span>
+ </div>
+ </TabsList>
+ <div
+ className="h-screen col-span-1 dark:caret-white relative overflow-auto"
+ data-registry="plate"
+ >
+ <TabsContent value="docs">
+ <OpenAIProvider>
+ <DndProvider backend={HTML5Backend}>
+ <Plate
+ onChange={({ value }) => {
+ // For performance, debounce your saving logic
+ localStorage.setItem("editorContent", JSON.stringify(value));
+ }}
+ editor={editor}
+ >
+ <EditorContainer className="w-full border">
+ <Editor variant="default" />
+ </EditorContainer>
+ </Plate>
+ </DndProvider>
+ </OpenAIProvider>
+ </TabsContent>
+ <TabsContent value="transcript">
+ <div className="h-screen p-16 pt-24">{caption}</div>
+ </TabsContent>
+ </div>
+ </Tabs>
+ <div className="h-screen col-span-1 border flex flex-col">
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
+ {messages.map((message) => (
+ <div
+ key={message.id}
+ className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
+ >
+ <div
+ className={`max-w-[80%] rounded-xl p-3 ${
+ message.role === "user"
+ ? "bg-zinc-900 text-white"
+ : "bg-zinc-100 dark:bg-zinc-800"
+ }`}
+ >
+ {message.content}
+ </div>
+ </div>
+ ))}
+ </div>
+ <div className="border-t border-zinc-200 dark:border-zinc-800 p-4">
+ <form onSubmit={handleSubmit} className="flex gap-2">
+ <input
+ name="prompt"
+ value={input}
+ onChange={handleInputChange}
+ placeholder="Type a message..."
+ className="flex-1 rounded-xl border border-zinc-200 dark:border-zinc-800 p-2 focus:outline-none focus:ring-1 bg-zinc-100 focus:ring-zinc-400 dark:bg-zinc-900 dark:text-white"
+ />
+ <button
+ type="submit"
+ className="rounded-xl bg-zinc-900 px-4 py-2 text-white hover:bg-zinc-800 focus:outline-none focus:ring-1 focus:ring-zinc-400 transition-colors"
+ >
+ Send
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/apps/web/app/components/icons/IntegrationIcons.tsx b/apps/web/app/components/icons/IntegrationIcons.tsx
index d818678f..1a8998b5 100644
--- a/apps/web/app/components/icons/IntegrationIcons.tsx
+++ b/apps/web/app/components/icons/IntegrationIcons.tsx
@@ -22,3 +22,49 @@ export const GoogleCalendarIcon = (props: SVGProps<SVGSVGElement>) => (
<path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
</svg>
);
+
+export const TwitterIcon = (props: SVGProps<SVGSVGElement>) => (
+ <svg
+ viewBox="0 0 256 209"
+ width="1em"
+ height="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ preserveAspectRatio="xMidYMid"
+ {...props}
+ >
+ <path
+ d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45"
+ fill="#55acee"
+ />
+ </svg>
+);
+
+export const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
+ <svg
+ viewBox="0 0 256 250"
+ width="1em"
+ height="1em"
+ fill="#fff"
+ xmlns="http://www.w3.org/2000/svg"
+ preserveAspectRatio="xMidYMid"
+ {...props}
+ >
+ <path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
+ </svg>
+);
+
+export const DiscordIcon = (props: SVGProps<SVGSVGElement>) => (
+ <svg
+ viewBox="0 0 256 199"
+ width="1em"
+ height="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ preserveAspectRatio="xMidYMid"
+ {...props}
+ >
+ <path
+ d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
+ fill="#5865F2"
+ />
+ </svg>
+);
diff --git a/apps/web/app/components/memories/SharedCard.tsx b/apps/web/app/components/memories/SharedCard.tsx
index 7bd2cc6a..8bf00361 100644
--- a/apps/web/app/components/memories/SharedCard.tsx
+++ b/apps/web/app/components/memories/SharedCard.tsx
@@ -226,7 +226,7 @@ const renderContent = {
<div className="flex flex-col gap-3 mt-4">
<div className="flex items-center gap-2">
- {data.isSuccessfullyProcessed ? (
+ {data.permissions?.isPublic ? (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"