diff options
| author | Dhravya Shah <[email protected]> | 2025-02-14 12:43:55 -0800 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-02-14 12:43:55 -0800 |
| commit | 186efa4244846bf761c7cf4f9cc5d1087b8f95df (patch) | |
| tree | 16cca1d69a22ccee586ea0eb64703817ac2330f9 /apps/web/app/components | |
| parent | docs: remove getting started title (diff) | |
| download | supermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.tar.xz supermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.zip | |
delete spaces
Diffstat (limited to 'apps/web/app/components')
| -rw-r--r-- | apps/web/app/components/Landing.tsx | 430 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/Feature.tsx | 157 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/Footer.tsx | 142 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/Hero.tsx | 290 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/Note.tsx | 73 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/Private.tsx | 62 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/index.tsx | 17 | ||||
| -rw-r--r-- | apps/web/app/components/Landing/plus-grid.tsx | 88 | ||||
| -rw-r--r-- | apps/web/app/components/editor/use-chat.tsx | 492 | ||||
| -rw-r--r-- | apps/web/app/components/editor/use-create-editor.tsx | 285 | ||||
| -rw-r--r-- | apps/web/app/components/editor/writing-playground.tsx | 242 | ||||
| -rw-r--r-- | apps/web/app/components/icons/IntegrationIcons.tsx | 46 | ||||
| -rw-r--r-- | apps/web/app/components/memories/SharedCard.tsx | 2 |
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"> + © {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" |