"use client";
import { IconPointerFilled } from "@tabler/icons-react";
import { Check, X } from "lucide-react";
import { motion, useInView } from "motion/react";
import { useRef, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
const cards = [
{
title: "Unlimited & Self-Hosted",
description:
"No caps on sources, notebooks, or file sizes. Deploy on your own infra and your data never leaves your control.",
skeleton: ,
},
{
title: "100+ LLMs, Zero Lock-in",
description:
"Swap between 100+ LLMs via OpenAI spec and LiteLLM, or run fully private with vLLM, Ollama, and more.",
skeleton: ,
},
{
title: "Real-Time Multiplayer",
description:
"RBAC with Owner, Admin, Editor, and Viewer roles plus real-time chat and comment threads. Built for teams.",
skeleton: ,
},
];
export function WhySurfSense() {
return (
Why SurfSense
Everything NotebookLM should have been
Open source. No data limits. No vendor lock-in. Built for teams that care about privacy
and flexibility.
{cards.map((card) => (
))}
);
}
function UnlimitedSkeleton({ className }: { className?: string }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
const items = [
{ label: "Sources", notebookLm: "50-600", surfSense: "Unlimited", icon: "📄" },
{ label: "Notebooks", notebookLm: "100-500", surfSense: "Unlimited", icon: "📓" },
{ label: "File size", notebookLm: "200 MB", surfSense: "No limit", icon: "📦" },
{ label: "Self-host", notebookLm: "No", surfSense: "Yes", icon: "🏠" },
];
return (
{items.map((item, index) => (
{item.icon}
{item.label}
{item.notebookLm}
{item.surfSense}
))}
);
}
function LLMFlexibilitySkeleton({ className }: { className?: string }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
const [selected, setSelected] = useState(0);
const models = [
{ name: "GPT-4o", provider: "OpenAI", color: "bg-green-500" },
{ name: "Claude 4", provider: "Anthropic", color: "bg-orange-500" },
{ name: "Gemini 2.5", provider: "Google", color: "bg-blue-500" },
{ name: "Llama 4", provider: "Local", color: "bg-purple-500" },
{ name: "DeepSeek R1", provider: "DeepSeek", color: "bg-cyan-500" },
];
return (
{models.map((model, index) => (
setSelected(index)}
initial={{ opacity: 0, x: 12 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.3, delay: 0.1 + index * 0.1 }}
className={cn(
"flex w-full cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-all",
selected === index ? "bg-background shadow-sm ring-1 ring-border" : "hover:bg-accent"
)}
>
{model.name}
{model.provider}
{selected === index && (
)}
))}
);
}
function MultiplayerSkeleton({ className }: { className?: string }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
const collaborators = [
{
id: 1,
name: "Alice",
role: "Editor",
color: "#3b82f6",
path: [
{ x: 15, y: 10 },
{ x: 80, y: 40 },
{ x: 40, y: 80 },
{ x: 15, y: 10 },
],
},
{
id: 2,
name: "Bob",
role: "Viewer",
color: "#10b981",
path: [
{ x: 115, y: 70 },
{ x: 55, y: 20 },
{ x: 95, y: 50 },
{ x: 115, y: 70 },
],
},
];
const codeLines = [
{ indent: 0, width: "60%", color: "bg-chart-4/60" },
{ indent: 1, width: "75%", color: "bg-muted-foreground/20" },
{ indent: 1, width: "50%", color: "bg-chart-1/60" },
{ indent: 2, width: "80%", color: "bg-muted-foreground/20" },
{ indent: 2, width: "45%", color: "bg-chart-2/60" },
{ indent: 1, width: "30%", color: "bg-muted-foreground/20" },
{ indent: 0, width: "20%", color: "bg-chart-4/60" },
];
return (
{codeLines.map((line, index) => (
))}
{collaborators.map((collaborator, index) => (
p.x),
y: collaborator.path.map((p) => p.y),
}
: {}
}
transition={{
opacity: { duration: 0.3, delay: 0.5 + index * 0.2 },
x: {
duration: 6,
delay: 0.5 + index * 0.3,
repeat: Infinity,
ease: "easeInOut",
},
y: {
duration: 6,
delay: 0.5 + index * 0.3,
repeat: Infinity,
ease: "easeInOut",
},
}}
>
{collaborator.name[0]}
{collaborator.name}
{collaborator.role}
))}
);
}
function FeatureCard({
title,
description,
skeleton,
}: {
title: string;
description: string;
skeleton: React.ReactNode;
}) {
return (
);
}
const comparisonRows: {
feature: string;
notebookLm: string | boolean;
surfSense: string | boolean;
}[] = [
{
feature: "Sources per Notebook",
notebookLm: "50-600",
surfSense: "Unlimited",
},
{
feature: "LLM Support",
notebookLm: "Gemini only",
surfSense: "100+ LLMs",
},
{
feature: "Self-Hostable",
notebookLm: false,
surfSense: true,
},
{
feature: "Open Source",
notebookLm: false,
surfSense: true,
},
{
feature: "External Connectors",
notebookLm: "Limited",
surfSense: "27+",
},
{
feature: "Desktop App",
notebookLm: false,
surfSense: true,
},
{
feature: "Agentic Architecture",
notebookLm: false,
surfSense: true,
},
{
feature: "AI File Sorting",
notebookLm: false,
surfSense: true,
},
];
function ComparisonStrip() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-80px" });
return (
Feature
NotebookLM
SurfSense
{comparisonRows.map((row, index) => (
{row.feature}
{typeof row.notebookLm === "boolean" ? (
row.notebookLm ? (
) : (
)
) : (
{row.notebookLm}
)}
{typeof row.surfSense === "boolean" ? (
row.surfSense ? (
) : (
)
) : (
{row.surfSense}
)}
{index !== comparisonRows.length - 1 && }
))}
);
}