mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-10 22:41:17 +00:00
auto-provision changes
This commit is contained in:
parent
9256283183
commit
65ffa81f1a
4 changed files with 589 additions and 147 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
||||
<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>
|
||||
);
|
||||
|
||||
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<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(() => {
|
||||
// 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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed inset-0 w-full h-full"
|
||||
// style={{ background: "rgb(17, 24, 39)" }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col lg:flex-row relative font-geistSans overflow-hidden items-center justify-between min-h-screen">
|
||||
<Turnstile siteKey="0x4AAAAAAAakohhUeXc99J7E" onSuccess={(token) => setToken(token)} />
|
||||
<div className="relative w-full lg:w-1/2 flex items-center min-h-[60vh] lg:min-h-screen bg-page-gradient p-4 lg:p-8 border-b-[1px] lg:border-b-0 lg:border-r-[1px] border-black/5 dark:border-white/5">
|
||||
<div className="absolute top-0 left-0 p-4 lg:p-8 text-black dark:text-white inline-flex gap-2 items-center">
|
||||
<Logo />
|
||||
<span className="text-lg lg:text-xl">supermemory.ai</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 opacity-5 w-full bg-transparent bg-[linear-gradient(to_right,#1a1a1a_1px,transparent_1px),linear-gradient(to_bottom,#1a1a1a_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:6rem_4rem] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
|
||||
<div className="flex flex-col relative font-geistSans overflow-hidden items-center justify-between min-h-screen ">
|
||||
<SupermemoryBackground />
|
||||
|
||||
<div className="pl-2 lg:pl-4 z-20 mt-16 lg:mt-0">
|
||||
<h1 className="text-3xl lg:text-5xl text-black dark:text-white mb-4 lg:mb-8 tracking-tighter">
|
||||
Hello, <span className="magicText">human</span>{" "}
|
||||
</h1>
|
||||
<p className="text-black dark:text-white mb-6 lg:mb-8 text-base lg:text-lg tracking-tighter">
|
||||
Write, ideate, and learn with all the wisdom of your bookmarks, notes, tweets and
|
||||
everything else, all in one place.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="relative flex flex-col sm:flex-row items-start sm:items-center gap-3 w-full">
|
||||
<div className="relative flex-1 w-full sm:min-w-[320px]">
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => 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 && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||
{success && (
|
||||
<p className="text-green-500 text-sm mt-2">Successfully joined waitlist!</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !email || !token}
|
||||
className="relative h-12 w-full sm:w-auto text-white flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-500 hover:opacity-90 transition-opacity duration-200 px-6 font-medium shadow-lg shadow-indigo-500/25 whitespace-nowrap disabled:opacity-50"
|
||||
>
|
||||
<span>{loading ? "Joining..." : "Join waitlist"}</span>
|
||||
<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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-5 h-5 text-blue-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 lg:w-1/2 flex flex-col items-center justify-center min-h-[40vh] lg:min-h-screen bg-page-gradient p-4 lg:p-8">
|
||||
<span className="text-xl lg:text-3xl leading-relaxed tracking-tighter mb-4 lg:mb-8 text-center text-black dark:text-gray-200">
|
||||
Ready for your <span className="text-black dark:text-white font-bold">Second brain</span>?
|
||||
</span>
|
||||
|
||||
<iframe
|
||||
src="https://customer-5xczlbkyq4f9ejha.cloudflarestream.com/111c4828c3587348bc703e67bfca9682/watch"
|
||||
frameBorder="0"
|
||||
className="w-full max-w-2xl rounded-2xl shadow-2xl shadow-indigo-500/20 aspect-video"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 <Landing />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<MemoizedNavbar user={user ?? undefined} />
|
||||
|
|
|
|||
164
docs/api/search.md
Normal file
164
docs/api/search.md
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue