diff --git a/apps/backend/src/routes/actions.ts b/apps/backend/src/routes/actions.ts index 1ac02fdd..142d2a01 100644 --- a/apps/backend/src/routes/actions.ts +++ b/apps/backend/src/routes/actions.ts @@ -469,7 +469,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() try { // Generate embedding for the search query - const embeddings = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text: query, }); @@ -503,7 +502,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() ) .orderBy( sql`1 - (embeddings <=> ${JSON.stringify(embeddings.data[0])}::vector) desc` - ) //figure out a better way to do order by my brain isn't working at this time. but youcan't do vector search twice + ) .limit(limit); return c.json({ @@ -623,7 +622,24 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() .limit(1); if (!space[0]) { - return { spaceId, allowed: false, error: "Space not found" }; + // create a new space for the user with the given id + const newSpace = await db + .insert(spaceInDb) + .values({ + uuid: spaceId, + name: spaceId, + isPublic: false, + ownerId: user.id, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return { + spaceId: newSpace[0].id, + allowed: true, + error: null, + }; } const spaceData = space[0] as Space; diff --git a/apps/web/app/components/Landing.tsx b/apps/web/app/components/Landing.tsx index 8a756f1e..3ed8fa7c 100644 --- a/apps/web/app/components/Landing.tsx +++ b/apps/web/app/components/Landing.tsx @@ -1,162 +1,429 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { Logo } from "./icons/Logo"; -import { Input } from "./ui/input"; -import { Turnstile } from "@marsidev/react-turnstile"; -import posthog from "posthog-js"; +import { motion, useMotionTemplate, useScroll, useSpring, useTransform } from "framer-motion"; -const gradientStyles = ` - @keyframes background-pan { - from { - background-position: 0% center; - } - to { - background-position: -200% center; - } - } +// Interfaces +interface Memory { + x: number; + y: number; + size: number; + type: "bookmark" | "note" | "tweet" | "doc"; + color: string; + orbitRadius: number; + orbitSpeed: number; + orbitOffset: number; + opacity: number; +} - .magicText { - --purple: rgba(72, 130, 244, 0.868); - --violet: #9a80f7; - --pink: #f7f7f7; - animation: background-pan 3s linear infinite; - background: linear-gradient( - to right, - var(--purple), - var(--violet), - var(--pink), - var(--purple) - ); - background-size: 200%; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - white-space: nowrap; - } -`; +// Components +const ProductHuntBadge = () => ( + + Supermemory - #1 Product of the Day on Product Hunt + +); -export default function Landing() { - const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(false); - const [token, setToken] = useState(""); +const SupermemoryBackground = () => { + const canvasRef = useRef(null); + const memoriesRef = useRef([]); + const rafRef = useRef(); + const centerRef = useRef({ x: 0, y: 0 }); + const circleRadiusRef = useRef(150); + const targetRadiusRef = useRef(0); useEffect(() => { - // Inject styles - const styleSheet = document.createElement("style"); - styleSheet.textContent = gradientStyles; - document.head.appendChild(styleSheet); + const canvas = canvasRef.current; + if (!canvas) return; - posthog.reset(); + 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 () => { - document.head.removeChild(styleSheet); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + window.removeEventListener("resize", resize); }; }, []); - const handleSubmit = async () => { - setLoading(true); - setError(""); - try { - const response = await fetch(`/backend/waitlist`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email, token }) - }); + return ( + + ); +}; - if (!response.ok) { - throw new Error("Failed to join waitlist"); - } - - setSuccess(true); - setEmail(""); - } catch (err) { - setError("Failed to join waitlist. Please try again."); - } finally { - setLoading(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && email && token && !loading) { - handleSubmit(); - } - }; +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 ( -
- setToken(token)} /> -
-
- - supermemory.ai -
-
+
+ -
-

- Hello, human{" "} -

-

- Write, ideate, and learn with all the wisdom of your bookmarks, notes, tweets and - everything else, all in one place. -

-
-
-
- setEmail(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="your@email.com" - className="w-full h-12 rounded-xl bg-black/5 dark:bg-white/5 border-black/10 dark:border-white/10 text-black dark:text-white placeholder:text-black/50 dark:placeholder:text-white/50 focus:border-black/20 dark:focus:border-white/20 focus:ring-black/20 dark:focus:ring-white/20 px-4" - /> - {error &&

{error}

} - {success && ( -

Successfully joined waitlist!

- )} -
- -
-
+ Chrome extension for one-click saving +
+
+ + + + AI-powered search across all your content +
+
+ + + + Integrates with Notion, Twitter, and more +
+ */} + + + Try it free + + + + + + + + + Star on GitHub + + + + Medium + Notion + Reddit + Twitter + +
-
- - Ready for your Second brain? - - +
+ +
+

+ All your knowledge in one place +

+

+ Supermemory intelligently organizes and connects your saved content, making it easy to + find and use when you need it. +

+
); diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx index 6aa8d553..84373771 100644 --- a/apps/web/app/routes/_index.tsx +++ b/apps/web/app/routes/_index.tsx @@ -1,7 +1,7 @@ import { Suspense, lazy, memo, useCallback, useEffect, useState } from "react"; -import { useFetcher, useNavigate, useRouteError } from "@remix-run/react"; import { LoaderFunctionArgs, defer, json, redirect } from "@remix-run/cloudflare"; +import { useFetcher, useNavigate, useRouteError } from "@remix-run/react"; import { Await, useLoaderData } from "@remix-run/react"; import { getSignInUrl } from "@supermemory/authkit-remix-cloudflare"; @@ -39,9 +39,6 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => { const success = searchParams.get("success"); const integration = searchParams.get("integration"); - if (!user) { - return redirect("/signin"); - } try { const recommendedQuestionsPromise = proxy("/api/recommended-questions", {}, request, context) @@ -58,7 +55,7 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => { greeting, recommendedQuestions: recommendedQuestionsPromise, success, - integration + integration, }); } catch (error) { console.error("Error in loader:", error); @@ -100,12 +97,6 @@ const HomePage = memo(function HomePage() { const [isModalOpen, setIsModalOpen] = useState(false); const navigate = useNavigate(); - useEffect(() => { - if (!user) { - navigate("/signin"); - } - }, [user]); - useEffect(() => { if (success && integration && integration === "notion") { setIsModalOpen(true); @@ -131,6 +122,10 @@ const HomePage = memo(function HomePage() { window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); }, []); + if (!user) { + return ; + } + return (
diff --git a/docs/api/search.md b/docs/api/search.md new file mode 100644 index 00000000..2f3273fe --- /dev/null +++ b/docs/api/search.md @@ -0,0 +1,164 @@ +# Search + +This endpoint provides semantic search capabilities across your saved memories using state-of-the-art embeddings. It returns relevant content ranked by similarity. + +```http +POST /api/search +``` + +## Request Body + +```typescript +{ + // The search query to find relevant content + query: string, + + // Maximum number of results to return (1-50, default: 10) + limit?: number, + + // Minimum similarity threshold (0-1, default: 0) + threshold?: number +} +``` + +### Field Descriptions + +| Field | Type | Required | Description | +| ----------- | ------ | -------- | --------------------------------------------- | +| `query` | string | Yes | Search query text (minimum 1 character) | +| `limit` | number | No | Maximum number of results (1-50, default: 10) | +| `threshold` | number | No | Minimum similarity score (0-1, default: 0) | + +## Response + +The endpoint returns an array of search results, sorted by relevance: + +```typescript +{ + results: Array<{ + // Document identifiers + id: string; + uuid: string; + + // Content fields + content: string; // Full document content + chunkContent: string; // Matching content chunk + + // Metadata + createdAt: string; // ISO timestamp + + // Relevance score (0-1) + similarity: number; // Rounded to 4 decimal places + }>; +} +``` + +### Error Responses + +#### 400 Bad Request + +```json +{ + "error": "Search query cannot be empty" +} +``` + +#### 401 Unauthorized + +```json +{ + "error": "Unauthorized" +} +``` + +#### 500 Internal Server Error + +```json +{ + "error": "Search failed", + "details": "Error details (in development mode)" +} +``` + +## Features + +### Semantic Search + +- Uses BAAI BGE base embeddings model +- Computes cosine similarity between query and content +- Returns similarity scores between 0 and 1 +- Supports partial matching and semantic understanding + +### Performance + +- Results limited to specified threshold +- Efficient vector search using PostgreSQL +- Chunked content for better matching +- Optimized similarity calculations + +### Content Processing + +- Automatic query embedding +- Smart content chunking +- Relevance scoring +- Result deduplication + +## Examples + +### Basic Search + +```json +{ + "query": "machine learning concepts" +} +``` + +### Advanced Search with Filters + +```json +{ + "query": "python programming", + "limit": 20, + "threshold": 0.5 +} +``` + +### Response Example + +```json +{ + "results": [ + { + "id": "doc-123", + "uuid": "abc-456", + "content": "Python is a versatile programming language...", + "chunkContent": "...particularly useful for machine learning...", + "createdAt": "2024-03-20T12:00:00Z", + "similarity": 0.8754 + }, + { + "id": "doc-124", + "uuid": "def-789", + "content": "Programming basics include...", + "chunkContent": "...Python syntax is straightforward...", + "createdAt": "2024-03-19T15:30:00Z", + "similarity": 0.7123 + } + ] +} +``` + +## Notes + +1. Authentication is required +2. Results are sorted by similarity score (descending) +3. Similarity scores are normalized between 0 and 1 +4. Content is automatically chunked for better matching +5. Performance optimizations include: + - Vector indexing + - Similarity thresholding + - Result limiting + - Score normalization +6. The API uses the BGE base embeddings model for semantic understanding +7. Supports both exact and semantic matching +8. Results include both full content and relevant chunks