auto-provision changes

This commit is contained in:
Dhravya Shah 2025-01-30 18:31:46 -07:00
parent 9256283183
commit 65ffa81f1a
4 changed files with 589 additions and 147 deletions

View file

@ -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;

View file

@ -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>
);

View file

@ -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
View 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