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 | |
| parent | docs: remove getting started title (diff) | |
| download | supermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.tar.xz supermemory-186efa4244846bf761c7cf4f9cc5d1087b8f95df.zip | |
delete spaces
29 files changed, 1817 insertions, 938 deletions
diff --git a/apps/backend/src/index.tsx b/apps/backend/src/index.tsx index 55aa58c7..1d62b543 100644 --- a/apps/backend/src/index.tsx +++ b/apps/backend/src/index.tsx @@ -128,28 +128,31 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>() .all("/api/*", async (c) => { // Get the full URL and path const url = new URL(c.req.url); - const path = url.pathname.replace("/api", "/v1"); + const path = url.pathname; + const newPath = path.replace("/api", "/v1"); // Preserve query parameters and build target URL - const redirectUrl = path + url.search; - - // Forward the request with same method, headers and body - const response = await fetch(redirectUrl, { - method: c.req.method, - headers: c.req.raw.headers, - body: - c.req.method !== "GET" && c.req.method !== "HEAD" - ? await c.req.blob() - : undefined, - }); + const redirectUrl = "https://api.supermemory.ai" + newPath + url.search; - return response; + // Use c.redirect() for a proper redirect + return c.redirect(redirectUrl); }) .route("/v1/user", user) .route("/v1/spaces", spacesRoute) .route("/v1", actions) .route("/v1/integrations", integrations) .route("/v1/memories", memories) + .get("/v1/session", (c) => { + const user = c.get("user"); + + if (!user) { + return c.json({ error: "Unauthorized" }, 401); + } + + return c.json({ + user, + }); + }) .post( "/waitlist", zValidator( diff --git a/apps/backend/src/routes/actions.ts b/apps/backend/src/routes/actions.ts index bcddec43..c0801ada 100644 --- a/apps/backend/src/routes/actions.ts +++ b/apps/backend/src/routes/actions.ts @@ -88,7 +88,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() apiKey: c.env.BRAINTRUST_API_KEY, }); - const googleClient = wrapAISDKModel(openai(c.env).chat("gpt-4o")); + const googleClient = wrapAISDKModel(openai(c.env).chat("gpt-4o-mini-2024-07-18")); // Get last user message and generate embedding in parallel with thread creation let lastUserMessage = coreMessages.findLast((i) => i.role === "user"); @@ -170,8 +170,17 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() try { const data = new StreamData(); + // De-duplicate chunks by URL to avoid showing duplicate content + const uniqueResults = finalResults.reduce((acc, current) => { + const existingResult = acc.find(item => item.id === current.id); + if (!existingResult) { + acc.push(current); + } + return acc; + }, [] as typeof finalResults); + data.appendMessageAnnotation( - finalResults.map((r) => ({ + uniqueResults.map((r) => ({ id: r.id, content: r.content, type: r.type, @@ -414,7 +423,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() .orderBy(sql`RANDOM()`) .limit(7); - if (recentLearnings.length === 0) { + if (recentLearnings.length === 0 || recentLearnings.length < 3) { return c.json({ suggestedLearnings: [] }); } @@ -425,7 +434,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() const model = openai(c.env).chat("gpt-4o-mini-2024-07-18"); const prompt = `Generate a concise topic recall card for this document. The card should: - Have a clear title that captures the main topic - - Include a brief "Last week, you saved notes on..." intro (do something different every time.) + - based on when the document was saved, include a brief "Last (week/month/...), you saved notes on..." intro (do something different every time.) - List 2-3 key points from the content in simple bullet points - Keep the total length under 280 characters - Focus on the core concepts worth remembering diff --git a/apps/backend/src/routes/spaces.ts b/apps/backend/src/routes/spaces.ts index aa9c905d..2ca2e461 100644 --- a/apps/backend/src/routes/spaces.ts +++ b/apps/backend/src/routes/spaces.ts @@ -118,6 +118,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>() canRead: true, canEdit, isOwner: space[0].ownerId === user?.id, + isPublic: space[0].isPublic, }, }); } @@ -156,6 +157,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>() canRead: true, canEdit, isOwner: space[0].ownerId === user.id, + isPublic: space[0].isPublic, }, }); }) @@ -254,16 +256,8 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>() error instanceof Error && error.message.includes("saved_spaces_user_space_idx") ) { - // Space is already favorited - remove it - await db - .delete(savedSpaces) - .where( - and( - eq(savedSpaces.userId, user.id), - eq(savedSpaces.spaceId, space[0].id) - ) - ); - return c.json({ message: "Space unfavorited successfully" }); + // Space is already favorited + return c.json({ message: "Space already favorited" }); } throw error; } @@ -524,6 +518,28 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>() return c.json({ success: true }); } - ); + ).delete("/:spaceId", async (c) => { + const user = c.get("user"); + if (!user) { + return c.json({ error: "Unauthorized" }, 401); + } + + const { spaceId } = c.req.param(); + const db = database(c.env.HYPERDRIVE.connectionString); + + const space = await db + .select() + .from(spaces) + .where(eq(spaces.uuid, spaceId)) + .limit(1); + + if (space.length === 0) { + return c.json({ error: "Space not found" }, 404); + } + + await db.delete(spaces).where(eq(spaces.uuid, spaceId)); + + return c.json({ success: true }); + }); export default spacesRoute; diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json index d51d27b4..98d42318 100644 --- a/apps/extension/manifest.json +++ b/apps/extension/manifest.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/chrome-manifest.json", "manifest_version": 3, - "version": "5.0.12", + "version": "5.0.135", "homepage_url": "https://supermemory.ai", "name": "Supermemory", "description": "An extension for https://supermemory.ai - an AI hub for all your knowledge.", diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 7d96feff..b02d027f 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -14,7 +14,7 @@ const tabStates = new Map<number, TabState>(); const checkIfLoggedIn = async () => { const baseURL = await getBaseURL(); - const response = await fetch(`${baseURL}/v1/session`); + const response = await fetch(`${baseURL}/backend/v1/session`); return response.status == 200; }; 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" diff --git a/apps/web/app/lib/hooks/use-chat-stream.ts b/apps/web/app/lib/hooks/use-chat-stream.ts index f47df3df..4cf25111 100644 --- a/apps/web/app/lib/hooks/use-chat-stream.ts +++ b/apps/web/app/lib/hooks/use-chat-stream.ts @@ -22,7 +22,7 @@ export const useChatStream = (initialMessages: CoreMessage[], initialThreadUuid? }, keepLastMessageOnError: true, onError: (e) => { - alert(`Error in chat: ${e}`); + console.error(e); }, body: { threadId: threadUuid, diff --git a/apps/web/app/lib/hooks/use-live-transcript.tsx b/apps/web/app/lib/hooks/use-live-transcript.tsx new file mode 100644 index 00000000..05c3436a --- /dev/null +++ b/apps/web/app/lib/hooks/use-live-transcript.tsx @@ -0,0 +1,132 @@ + +import { useState, useEffect, useCallback } from "react"; +import { useQueue } from "@uidotdev/usehooks"; +import { LiveClient, LiveTranscriptionEvents, createClient } from "@deepgram/sdk"; + +export function useLiveTranscript() { + + const { add, remove, first, size, queue } = useQueue<any>([]); + const [apiKey, _] = useState<string | null>(""); + const [connection, setConnection] = useState<LiveClient | null>(null); + const [isListening, setIsListening] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isProcessing, setIsProcessing] = useState(false); + const [micOpen, setMicOpen] = useState(false); + const [microphone, setMicrophone] = useState<MediaRecorder | null>(null); + const [userMedia, setUserMedia] = useState<MediaStream | null>(null); + const [caption, setCaption] = useState<string>(""); + const [status, setStatus] = useState<string>("Not Connected"); + // Initialize Deepgram connection + const initializeConnection = useCallback(() => { + if (!apiKey) return null; + + const deepgram = createClient(apiKey); + const connection = deepgram.listen.live({ + model: "nova-3", + language: "en", + smart_format: true, + interim_results: true, + punctuate: true, + diarize: true, + utterances: true, + }); + + connection.on(LiveTranscriptionEvents.Open, () => { + setStatus("Connected"); + setIsListening(true); + }); + + connection.on(LiveTranscriptionEvents.Close, () => { + setStatus("Not Connected"); + setIsListening(false); + }); + + connection.on(LiveTranscriptionEvents.Error, (error) => { + console.error("Deepgram error:", error); + setStatus("Error occurred"); + }); + + connection.on(LiveTranscriptionEvents.Transcript, (data) => { + const transcript = data.channel.alternatives[0].transcript; + if (data.is_final) { + if (transcript && transcript.trim() !== "") { + setCaption((prev) => prev + " " + transcript); + } + } + }); + + return connection; + }, [apiKey]); + + const toggleMicrophone = useCallback(async () => { + if (microphone && userMedia) { + setUserMedia(null); + setMicrophone(null); + microphone.stop(); + if (connection) { + connection.finish(); + setConnection(null); + } + } else { + const userMedia = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + + const microphone = new MediaRecorder(userMedia); + microphone.start(250); + + microphone.onstart = () => { + setMicOpen(true); + // Create new connection when starting microphone + const newConnection = initializeConnection(); + if (newConnection) { + setConnection(newConnection); + } + }; + + microphone.onstop = () => { + setMicOpen(false); + }; + + microphone.ondataavailable = (e) => { + add(e.data); + }; + + setUserMedia(userMedia); + setMicrophone(microphone); + } + }, [add, microphone, userMedia, connection, initializeConnection]); + + useEffect(() => { + setIsLoading(false); + }, []); + + useEffect(() => { + const processQueue = async () => { + if (size > 0 && !isProcessing && isListening && connection) { + setIsProcessing(true); + try { + const blob = first; + if (blob) { + connection.send(blob); + } + remove(); + } catch (error) { + console.error("Error processing audio:", error); + } + setIsProcessing(false); + } + }; + + const interval = setInterval(processQueue, 100); + return () => clearInterval(interval); + }, [connection, queue, remove, first, size, isProcessing, isListening]); + + return { + toggleMicrophone, + caption, + status, + isListening, + isLoading, + }; +}
\ No newline at end of file diff --git a/apps/web/app/lib/hooks/use-spaces.tsx b/apps/web/app/lib/hooks/use-spaces.tsx index 827c7c79..14ffb56c 100644 --- a/apps/web/app/lib/hooks/use-spaces.tsx +++ b/apps/web/app/lib/hooks/use-spaces.tsx @@ -7,6 +7,7 @@ export type ExtraSpaceMetaData = { canRead: boolean; canEdit: boolean; isOwner: boolean; + isPublic: boolean; }; owner: { id: string; diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx index 8512cd69..b8c69a39 100644 --- a/apps/web/app/routes/_index.tsx +++ b/apps/web/app/routes/_index.tsx @@ -14,6 +14,7 @@ import Navbar from "~/components/Navbar"; import Suggestions from "~/components/Suggestions"; import { IntegrationModals } from "~/components/memories/Integrations"; import SuggestionsSkeleton from "~/components/skeletons/SuggestionsSkeleton"; +import { Theme, useTheme } from "~/lib/theme-provider"; const MemoriesPage = lazy(() => import("~/components/memories/MemoriesPage")); const Reminders = lazy(() => import("~/components/Reminders")); @@ -40,7 +41,14 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => { const integration = searchParams.get("integration"); if (!user) { - return redirect("/signin"); + return { + user: null, + signinUrl, + greeting, + recommendedQuestions: null, + success, + integration, + }; } try { @@ -80,15 +88,21 @@ const HomePage = memo(function HomePage() { const [input, setInput] = useState(""); const fetcher = useFetcher(); const [scrollOpacity, setScrollOpacity] = useState(1); + const [theme, setTheme] = useTheme(); useEffect(() => { + if (!user) { + setTheme(Theme.LIGHT); + return; + } + const handleScroll = () => { const scrollPosition = window.scrollY; const opacity = Math.max(0, 1 - scrollPosition / 200); // Adjust 200 to control fade speed setScrollOpacity(opacity); }; - if (!recommendedQuestions || recommendedQuestions === null) { + if ((!recommendedQuestions || recommendedQuestions === null) && !user) { toast.error("Something went wrong. Please try again later."); alert("Something went wrong. Please try again later."); } @@ -126,7 +140,7 @@ const HomePage = memo(function HomePage() { }, []); if (!user) { - return <div>Loading...</div>; + return <Landing />; } return ( diff --git a/apps/web/app/routes/editor.tsx b/apps/web/app/routes/editor.tsx index 02cd277e..264db784 100644 --- a/apps/web/app/routes/editor.tsx +++ b/apps/web/app/routes/editor.tsx @@ -1,19 +1,13 @@ import { lazy, memo } from "react"; -import { OpenAIProvider } from "~/components/editor/use-chat"; - -const PlateEditorImport = lazy(() => - import("~/components/editor/plate-editor").then((mod) => ({ default: mod.PlateEditor })), +const WritingPlaygroundImport = lazy(() => + import("~/components/editor/writing-playground").then((mod) => ({ + default: mod.WritingPlayground, + })), ); -const PlateEditor = memo(PlateEditorImport); +const WritingPlayground = memo(WritingPlaygroundImport); export default function Page() { - return ( - <div className="h-screen w-full" data-registry="plate"> - <OpenAIProvider> - <PlateEditor /> - </OpenAIProvider> - </div> - ); + return <WritingPlayground />; } diff --git a/apps/web/app/routes/onboarding.privacy.tsx b/apps/web/app/routes/onboarding.privacy.tsx index 49055095..5a03c998 100644 --- a/apps/web/app/routes/onboarding.privacy.tsx +++ b/apps/web/app/routes/onboarding.privacy.tsx @@ -28,7 +28,7 @@ export default function Onboarding() { className="flex flex-col min-h-screen items-center pt-20 relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 bg-opacity-40" > {/* Neural network background pattern */} - <div className="absolute inset-0 overflow-hidden"> + <div className="absolute inset-0 overflow-hidden pointer-events-none"> {/* Subtle gradient orbs */} {[...Array(4)].map((_, i) => ( <motion.div @@ -110,7 +110,7 @@ export default function Onboarding() { initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.8 }} - className="flex flex-col gap-2 items-center font-geist italic text-5xl tracking-tight text-white" + className="flex flex-col gap-2 items-center font-geist italic text-5xl tracking-tight text-white relative z-10" > <Logo className="h-24 w-24" /> supermemory </motion.div> @@ -119,29 +119,29 @@ export default function Onboarding() { initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.8, delay: 0.2 }} - className="flex flex-col gap-2 items-center mt-8 text-white" + className="flex flex-col gap-2 items-center mt-8 text-white relative z-10" > <h1 className="text-xl font-geist font-medium">Your privacy is our priority</h1> <div className="flex flex-col gap-4 text-base font-geist max-w-md text-center font-light text-gray-200"> - <p> + <p className="select-text"> We use Cloudflare's "bindings" architecture, meaning encryption keys are never exposed - not even to our developers. Your data remains encrypted and inaccessible without your authorization. </p> - <p> + <p className="select-text"> Our entire codebase is open source and available for security review at{" "} - <a href="https://git.new/memory" className="text-blue-400 hover:text-blue-300"> + <a href="https://git.new/memory" className="text-blue-400 hover:text-blue-300 cursor-pointer"> git.new/memory </a> . We believe transparency builds trust. </p> - <p> + <p className="select-text"> Your data is encrypted at rest and in transit, and we use industry-standard encryption. You maintain full control over your data, including the right to export or delete it at any time. </p> - <p className="text-sm mt-2"> - <a href="/privacy" className="text-blue-400 hover:text-blue-300"> + <p className="text-sm mt-2 select-text"> + <a href="/privacy" className="text-blue-400 hover:text-blue-300 cursor-pointer"> Read our detailed privacy policy → </a> </p> @@ -153,7 +153,7 @@ export default function Onboarding() { initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.8, delay: 0.4 }} - className="mt-16 relative border-none bg-transparent p-0 cursor-pointer outline-offset-4 transition-[filter] duration-250 select-none" + className="mt-16 relative border-none bg-transparent p-0 cursor-pointer outline-offset-4 transition-[filter] duration-250 select-none z-10" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} href="/onboarding/add" diff --git a/apps/web/app/routes/privacy.tsx b/apps/web/app/routes/privacy.tsx index d480834e..84676609 100644 --- a/apps/web/app/routes/privacy.tsx +++ b/apps/web/app/routes/privacy.tsx @@ -3,7 +3,7 @@ import Markdown from "react-markdown"; function Page() { return ( <div className="flex flex-col items-center justify-center mt-8"> - <div className="max-w-3xl prose prose-invert"> + <div className="max-w-3xl prose md:prose-lg dark:prose-invert"> <Markdown> {` Privacy Policy for Supermemory.ai @@ -34,7 +34,7 @@ The extension has the capability to see all websites that users visit. However, - **Current page data**: Upon activation (click) by the user, Supermemory.ai stores data from the current HTML page. This data is used to provide relevant contextual information based on the content of the page you are viewing. ## Data Storage and Security -All collected data is securely stored in a SQLite database hosted on [Cloudflare D1](https://developers.cloudflare.com/d1/), [Cloudflare vectorize](https://developers.cloudflare.com/vectorize/), and [Cloudflare KV](https://developers.cloudflare.com/kv). +All collected data is securely stored in a Database hosted on [Hetzner](https://www.hetzner.com/). We employ industry-standard security measures to protect your information from unauthorized access, alteration, disclosure, or destruction. Despite our efforts, no method of transmission over the Internet or method of electronic storage is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security. When you chat with the app, your queries may be sent to OpenAI GPT-4 API, Google Gemini API or other third-party services to provide you with relevant information. These services may store your queries and responses for training purposes. @@ -58,8 +58,8 @@ Supermemory.ai reserves the right to update this privacy policy at any time. Whe # Contact Us If you have any questions about this Privacy Policy, the practices of this site, or your dealings with this site, please contact us at: -Email: [email protected] -This document was last updated on July 4, 2024. +Email: [email protected] +This document was last updated on February 14, 2025. `} </Markdown> diff --git a/apps/web/app/routes/space.$spaceId.tsx b/apps/web/app/routes/space.$spaceId.tsx index 65ac6e3f..88f817c9 100644 --- a/apps/web/app/routes/space.$spaceId.tsx +++ b/apps/web/app/routes/space.$spaceId.tsx @@ -4,7 +4,7 @@ import { LoaderFunctionArgs, json } from "@remix-run/cloudflare"; import { useLoaderData, useNavigate } from "@remix-run/react"; import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session"; -import { Clipboard, Share, Star, UserPlus } from "lucide-react"; +import { Clipboard, DeleteIcon, Share, Star, Trash, UserPlus } from "lucide-react"; import { proxy } from "server/proxy"; import { toast } from "sonner"; import Navbar from "~/components/Navbar"; @@ -155,11 +155,9 @@ export default function SpacePage() { } catch (err) { // Fallback to clipboard if share fails or is cancelled await navigator.clipboard.writeText(shareUrl); - toast.success("Link copied to clipboard!"); } } else { await navigator.clipboard.writeText(shareUrl); - toast.success("Link copied to clipboard!"); } }; @@ -169,6 +167,20 @@ export default function SpacePage() { toast.success("Link copied to clipboard!"); }; + const handleDelete = async () => { + const response = await fetch(`/backend/v1/spaces/${space.uuid}`, { + method: "DELETE", + credentials: "include", + }); + + if (!response.ok) { + toast.error("Failed to delete space"); + } else { + navigate("/"); + toast.success("Space deleted successfully"); + } + }; + return ( <div> <Navbar user={user} /> @@ -241,6 +253,10 @@ export default function SpacePage() { <Button variant="outline" size="sm" onClick={handleCopyLink}> <Clipboard className="h-4 w-4" /> </Button> + <Button variant="outline" size="sm" onClick={handleDelete}> + <Trash className="h-4 w-4" /> + </Button> + {space.isPublic && user && !space.permissions.isOwner && diff --git a/apps/web/public/product-of-the-day.png b/apps/web/public/product-of-the-day.png Binary files differnew file mode 100644 index 00000000..9381c6a6 --- /dev/null +++ b/apps/web/public/product-of-the-day.png diff --git a/apps/web/server/index.ts b/apps/web/server/index.ts index 1343be53..09cd6af0 100644 --- a/apps/web/server/index.ts +++ b/apps/web/server/index.ts @@ -4,7 +4,7 @@ import * as cheerio from "cheerio"; import { createOpenAI } from "@ai-sdk/openai"; import { zValidator } from "@hono/zod-validator"; import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session"; -import { convertToCoreMessages, generateText, streamText } from "ai"; +import { convertToCoreMessages, generateObject, generateText, streamText } from "ai"; import { putEncryptedKV } from "encrypt-workers-kv"; import { Hono } from "hono"; import { z } from "zod"; @@ -97,7 +97,7 @@ app.all("/backend/*", async (c) => { try { // Use tee() to create a copy of the body stream const [stream1, stream2] = c.req.raw.body.tee(); - + const reader = stream2.getReader(); const chunks = []; let done = false; @@ -125,21 +125,27 @@ app.all("/backend/*", async (c) => { body = JSON.stringify(parsedBody); } catch (e) { console.error("Invalid JSON in request body:", bodyText); - return c.json({ - error: "Invalid JSON in request body", - details: bodyText.substring(0, 100) + "..." // Show partial body for debugging - }, 400); + return c.json( + { + error: "Invalid JSON in request body", + details: bodyText.substring(0, 100) + "...", // Show partial body for debugging + }, + 400, + ); } } else { body = bodyText; } } catch (error) { console.error("Error reading request body:", error); - return c.json({ - error: "Failed to process request body", - details: error instanceof Error ? error.message : "Unknown error", - path: path - }, 400); + return c.json( + { + error: "Failed to process request body", + details: error instanceof Error ? error.message : "Unknown error", + path: path, + }, + 400, + ); } } @@ -197,11 +203,14 @@ app.all("/backend/*", async (c) => { } } catch (error) { console.error("Error reading response:", error); - return c.json({ - error: "Failed to read backend response", - details: error instanceof Error ? error.message : "Unknown error", - path: path - }, 502); + return c.json( + { + error: "Failed to read backend response", + details: error instanceof Error ? error.message : "Unknown error", + path: path, + }, + 502, + ); } return new Response(JSON.stringify(responseBody), { @@ -214,19 +223,22 @@ app.all("/backend/*", async (c) => { }); } catch (error) { console.error("Error proxying request:", error); - return c.json({ - error: "Failed to proxy request to backend", - details: error instanceof Error ? error.message : "Unknown error", - path: path, - url: url - }, 502); + return c.json( + { + error: "Failed to proxy request to backend", + details: error instanceof Error ? error.message : "Unknown error", + path: path, + url: url, + }, + 502, + ); } }); app.post("/api/ai/command", async (c) => { - const { apiKey: key, messages, model = "gpt-4o-mini", system } = await c.req.json(); + const { messages, model = "gpt-4o-mini", system } = await c.req.json(); - const apiKey = key || c.env.OPENAI_API_KEY; + const apiKey = c.env.OPENAI_API_KEY; if (!apiKey) { return c.json({ error: "Missing OpenAI API key." }, 401); @@ -280,6 +292,60 @@ app.post("/api/ai/copilot", async (c) => { } }); +app.post("/api/ai/update", async (c) => { + const { caption, document } = await c.req.json(); + + const apiKey = c.env.OPENAI_API_KEY; + + if (!apiKey) { + return c.json({ error: "Missing OpenAI API key." }, 401); + } + + const openai = createOpenAI({ apiKey }); + + const result = await generateObject({ + model: openai("gpt-4o-mini"), + schema: z.object({ + action: z.enum(["edit", "delete", "append", "ignore"]), + blockId: z.string().optional(), + content: z.string().optional(), + }), + prompt: `You are a professional technical document editor. + +You are given a document and a new caption that was transcribed from a recording. + +Your job is to analyze how to update the document to reflect the new caption. + +Given this document structure: +${JSON.stringify(document)} + +And this new caption that was transcribed: +${caption} + +Analyze how to update the document. Choose one: +1. Edit an existing block (provide blockId and new content) +2. Delete a block (provide blockId) +3. Append new content (provide content) +4. Ignore the new content if it doesn't meaningfully improve the document + +You can be strict in ignoring. if the transcript is not related to the content of the document, you should ignore it. + +Sometimes you may need to edit things below the headers, or move things around. Do what you think is best. That's all right. + +For eg, if I am talking about the features, add it to the block ID BELOW the features header. Don't change the heading blogs + +I will first talk about "what supermemory is", you need to add it to the block id that is right below the heading "what is supermemory?". +Then, I will talk about our charter. I will say somethuing like "we want to help companies write and read better documentation". now, you need to add it to the block id that is right below the heading "our charter". +I will talk about features like "you can actually edit the docs along with AI, seamlessly integrate it into your workflow". now, you need to add it to the block id that is right below the heading "features". +I will also talk about connections with existing documentation platforms. now, you need to add it to the block id that is right below the heading "how we will do it". + +there should not be any repetitive stuff. it should be professional. +Make sure that the document you write is a good, accurate, and up-to-date document.`, + }); + + return c.json(result.object); +}); + app.all("/auth/notion/callback", zValidator("query", z.object({ code: z.string() })), async (c) => { const { code } = c.req.valid("query"); diff --git a/package.json b/package.json index ef25819d..4a97e761 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ ], "dependencies": { "@cloudflare/workers-types": "^4.20241112.0", + "@deepgram/sdk": "^3.10.1", + "@heroicons/react": "^2.2.0", "@hono-rate-limiter/cloudflare": "^0.2.2", "@hono/zod-validator": "^0.4.1", "@llm-ui/code": "^0.13.3", @@ -48,6 +50,8 @@ "@types/postlight__mercury-parser": "^2.2.7", "@types/showdown": "^2.0.6", "@types/turndown": "^5.0.5", + "@udecode/plate": "^44.0.1", + "@uidotdev/usehooks": "^2.4.1", "ai": "4.0.18", "autoevals": "^0.0.106", "aws4fetch": "^1.0.20", |