Merge remote-tracking branch 'upstream/dev' into fix/tabs

This commit is contained in:
Anish Sarkar 2026-07-22 00:02:26 +05:30
commit 9fd5d3571c
263 changed files with 17289 additions and 1525 deletions

View file

@ -11,7 +11,7 @@ import { useAnnouncements } from "@/hooks/use-announcements";
export function AnnouncementsDialog() {
const [open, setOpen] = useAtom(announcementsDialogAtom);
const { announcements, markAllRead } = useAnnouncements();
const { announcements, markAllRead } = useAnnouncements({ includeExpired: true });
// Auto-mark all visible announcements as read when the dialog opens
useEffect(() => {

View file

@ -34,7 +34,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
MCP Connectors
</DialogTitle>
<DialogDescription className="text-xs sm:text-base text-muted-foreground/80 mt-1 sm:mt-1.5">
Connect external tools and services through MCP.
Connect external tools and services through MCP.
</DialogDescription>
</DialogHeader>

View file

@ -233,4 +233,4 @@ Image.Preview = ImagePreview;
Image.Filename = ImageFilename;
Image.Zoom = ImageZoom;
export { Image, ImageRoot, ImagePreview, ImageFilename, ImageZoom, imageVariants };
export { Image, ImageFilename, ImagePreview, ImageRoot, ImageZoom, imageVariants };

View file

@ -116,7 +116,7 @@ import {
} from "../new-chat/document-mention-picker";
const COMPOSER_PLACEHOLDER =
"Track competitors, scrape platforms, automate briefs. Use / for prompts, @ for docs";
"Research the live web, scrape platforms, automate briefs. Use / for prompts, @ for docs";
type ComposerSuggestionAnchorPoint = {
left: number;

View file

@ -277,7 +277,7 @@ export function ConnectorPage({ content }: { content: ConnectorPageContent }) {
<p className="mx-auto mt-3 max-w-xl text-muted-foreground leading-relaxed">
The {content.name} connector is one of many in the SurfSense{" "}
<Link href="/" className="font-medium text-foreground underline underline-offset-4">
competitive intelligence platform
open web research platform
</Link>
. Start free, no credit card required.
</p>

View file

@ -221,9 +221,7 @@ export const DocumentNode = React.memo(function DocumentNode({
className={cn(
"absolute inset-0 flex items-center justify-center transition-opacity",
canMention &&
(isMentioned
? "opacity-0"
: "max-sm:opacity-0 group-hover/item:opacity-0")
(isMentioned ? "opacity-0" : "max-sm:opacity-0 group-hover/item:opacity-0")
)}
>
{getDocumentTypeIcon(

View file

@ -390,75 +390,75 @@ export const FolderNode = React.memo(function FolderNode({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{isWatched && onRescan && (
{isWatched && onRescan && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleRescan();
}}
>
<RefreshCw className={cn("mr-2 h-4 w-4", isRescanning && "animate-spin")} />
Re-scan
</DropdownMenuItem>
)}
{isWatched && onStopWatching && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onStopWatching(folder);
}}
>
<EyeOff className="mr-2 h-4 w-4" />
Stop watching
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleRescan();
onCreateSubfolder(folder.id);
}}
>
<RefreshCw className={cn("mr-2 h-4 w-4", isRescanning && "animate-spin")} />
Re-scan
<FolderPlus className="mr-2 h-4 w-4" />
New subfolder
</DropdownMenuItem>
)}
{isWatched && onStopWatching && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onStopWatching(folder);
startRename();
}}
>
<EyeOff className="mr-2 h-4 w-4" />
Stop watching
<Pencil className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onCreateSubfolder(folder.id);
}}
>
<FolderPlus className="mr-2 h-4 w-4" />
New subfolder
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
startRename();
}}
>
<Pencil className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onMove(folder);
}}
>
<Move className="mr-2 h-4 w-4" />
Move to...
</DropdownMenuItem>
{onExportFolder && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onExportFolder(folder);
onMove(folder);
}}
>
<Download className="mr-2 h-4 w-4" />
Export folder
<Move className="mr-2 h-4 w-4" />
Move to...
</DropdownMenuItem>
{onExportFolder && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onExportFolder(folder);
}}
>
<Download className="mr-2 h-4 w-4" />
Export folder
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete(folder);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete(folder);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

View file

@ -1,36 +1,61 @@
import { Reveal } from "@/components/connectors-marketing/reveal";
import { MarketingSection } from "@/components/marketing/section";
const COLUMNS = [
{ label: "Browser agents", examples: "Browserbase, Browser Use" },
{ label: "Scraping APIs", examples: "Firecrawl" },
{ label: "Search APIs", examples: "Exa, Tavily, Parallel" },
{ label: "Scraper marketplaces", examples: "Apify" },
];
const ROWS = [
{
feature: "Who does the work",
suites: "Analysts read human dashboards",
scrapers: "You build everything on raw data",
surfsense: "Your AI agents gather, analyze, and alert",
feature: "Built for",
browser: "Web tasks that need clicking, logins, and forms",
scraping: "Turning individual pages into LLM-ready content",
search: "Finding and reading pages about a topic",
marketplace: "Thousands of community-built scrapers, one per site",
surfsense: "Live platform data as research primitives for agents",
},
{
feature: "Intelligence layer",
suites: "Curated but slow, human-in-the-loop",
scrapers: "None; data only",
surfsense: "Agent harness turns live data into briefs",
feature: "How retrieval works",
browser: "An LLM drives a real browser, page by page",
scraping: "Fetch a URL, get markdown or schema-extracted JSON",
search: "Query an index, get ranked results and page content",
marketplace: "Pick an actor per site, learn its input, run it",
surfsense: "One typed REST call per platform, no LLM in the retrieval loop",
},
{
feature: "Platform data (comment trees, transcripts, reviews)",
browser: "Whatever the LLM extracts from rendered pages",
scraping: "Page-level extraction; social platforms aren't the focus",
search: "Page text and snippets, not structured platform items",
marketplace: "Yes, but schema and quality vary per actor",
surfsense: "Native items: posts, comment trees, transcripts, reviews, SERPs",
},
{
feature: "Consistency",
browser: "Depends on the model and the page",
scraping: "One API, you define schemas per page type",
search: "One API, snippet and page-content shapes",
marketplace: "A different schema and quality bar per actor",
surfsense: "One API and one schema style across every connector",
},
{
feature: "Research workspace & knowledge base",
browser: "No",
scraping: "No",
search: "No",
marketplace: "No",
surfsense: "Cited briefs, knowledge base, scheduled automations, deliverables",
},
{
feature: "Pricing",
suites: "Enterprise contracts, annual quotes",
scrapers: "Usage-priced infrastructure",
surfsense: "Self-serve, pay per item, free tier",
},
{
feature: "Developer surface",
suites: "No agent surface",
scrapers: "APIs, but no MCP or harness",
surfsense: "REST API with your API key, plus an MCP server for agents",
},
{
feature: "Open source",
suites: "No",
scrapers: "No",
surfsense: "Yes, self-hostable",
browser: "Per browser minute (1-minute session minimum) plus the LLM tokens driving it",
scraping: "Credits per page; schema extraction costs extra credits",
search: "Per search request",
marketplace: "Per event or result, set by each actor",
surfsense: "Per item returned; failed calls never billed",
},
];
@ -40,24 +65,23 @@ export function CompareTable() {
<Reveal>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">How SurfSense compares</h2>
<p className="mt-3 max-w-2xl text-muted-foreground leading-relaxed">
CI suites sell dashboards to analysts. Scraper APIs sell raw data to engineers. SurfSense
gives AI agents the data and the harness in one open-source platform.
SurfSense is the only open-source product that combines a NotebookLM-style research
workspace for people with live-data primitives for agents. Here is how that stacks up
against each class of tool.
</p>
</Reveal>
<Reveal>
<div className="mt-8 overflow-x-auto rounded-xl border bg-card">
<table className="w-full min-w-2xl text-sm">
<table className="w-full min-w-4xl text-sm">
<thead>
<tr className="border-b bg-muted/40 text-left">
<th className="p-4 font-medium">Feature</th>
<th className="p-4 font-medium text-muted-foreground">
CI suites
<span className="block text-xs font-normal">Crayon, Klue</span>
</th>
<th className="p-4 font-medium text-muted-foreground">
Scraper APIs
<span className="block text-xs font-normal">Bright Data, Apify</span>
</th>
{COLUMNS.map((col) => (
<th key={col.label} className="p-4 font-medium text-muted-foreground">
{col.label}
<span className="block text-xs font-normal">{col.examples}</span>
</th>
))}
<th className="p-4 font-medium text-brand">SurfSense</th>
</tr>
</thead>
@ -67,8 +91,10 @@ export function CompareTable() {
<th scope="row" className="p-4 text-left font-medium">
{row.feature}
</th>
<td className="p-4 text-muted-foreground">{row.suites}</td>
<td className="p-4 text-muted-foreground">{row.scrapers}</td>
<td className="p-4 text-muted-foreground">{row.browser}</td>
<td className="p-4 text-muted-foreground">{row.scraping}</td>
<td className="p-4 text-muted-foreground">{row.search}</td>
<td className="p-4 text-muted-foreground">{row.marketplace}</td>
<td className="bg-brand/5 p-4 text-foreground">{row.surfsense}</td>
</tr>
))}

View file

@ -15,7 +15,7 @@ export function ConnectorGrid() {
<Reveal>
<div className="flex flex-wrap items-center gap-3">
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
Connectors for every platform your market uses
Connectors for every platform where answers live
</h2>
<Badge variant="outline" className="py-1">
{connectors.length} connectors and growing

View file

@ -106,7 +106,7 @@ export function FooterNew() {
];
return (
<div className="border-t border-neutral-100 dark:border-white/[0.1] px-8 py-20 bg-white dark:bg-neutral-950 w-full relative overflow-hidden">
<div className="max-w-7xl mx-auto text-sm text-neutral-500 flex sm:flex-row flex-col justify-between items-start md:px-8">
<div className="max-w-7xl mx-auto text-sm text-neutral-600 dark:text-neutral-400 flex sm:flex-row flex-col justify-between items-start md:px-8">
<div>
<div className="mr-0 md:mr-4 md:flex mb-4">
<Logo className="h-6 w-6 rounded-md mr-2" />

View file

@ -28,7 +28,7 @@ export type HeroChatDemoScript = {
type Stage = "typing" | "steps" | "answer" | "done";
const PLACEHOLDER =
"Track competitors, scrape platforms, automate briefs. Use / for prompts, @ for docs";
"Research the live web, scrape platforms, automate briefs. Use / for prompts, @ for docs";
/** Blinking caret for the typewriter (overlay only, never inside the real input). */
function Caret() {
@ -77,6 +77,7 @@ function DemoTimeline({
<Button
variant="ghost"
type="button"
tabIndex={-1}
onClick={() => setIsOpen((prev) => !prev)}
className="h-auto w-full justify-start gap-1.5 p-0 text-left text-sm font-normal text-muted-foreground transition-colors hover:bg-transparent hover:text-accent-foreground"
>

View file

@ -1,15 +1,27 @@
"use client";
import { ChevronDown, Download } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import Link from "next/link";
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer";
import { HeroChatDemo, type HeroChatDemoScript } from "@/components/homepage/hero-chat-demo";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
GITHUB_RELEASES_URL,
getAssetLabel,
usePrimaryDownload,
} from "@/lib/desktop-download-utils";
import { buildBackendUrl } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@ -58,7 +70,6 @@ type HeroCategory = {
};
const HERO_TUTORIAL = "/homepage/hero_tutorial";
const HERO_REALTIME = "/homepage/hero_realtime";
/*
* Every scripted demo below mirrors a task the SurfSense agent has actually run
@ -67,8 +78,152 @@ const HERO_REALTIME = "/homepage/hero_realtime";
*/
const CATEGORIES: HeroCategory[] = [
{
id: "connector-workflows",
label: "Multi-Connector Workflows",
id: "live-research",
label: "Live Web Research",
useCases: [
{
id: "deep-research",
title: "Deep Research on the Live Web",
description:
"The agent crawls dozens of live sources on a question and synthesizes a cited answer, not a stale index.",
src: null,
demo: {
prompt: "Research the AI note-taking market and build a landscape brief with citations.",
steps: [
{
title: "Research",
items: ["Crawling 38 live sources", "Vendor sites, reviews, pricing pages"],
},
{
title: "Generate report",
items: ["Landscape brief · 24 inline citations"],
},
],
rows: [
{
primary: "23 vendors mapped across 4 segments",
secondary: "consumer, prosumer, team, developer-first",
},
{
primary: "Pricing clusters at $10 and $20/mo",
secondary: "3 vendors moved upmarket this quarter",
},
],
summary: "Landscape brief saved · 24 inline citations you can check",
},
},
{
id: "academic-research",
title: "Academic Literature Scan",
description:
"Sweep recent papers, preprints, and technical blogs on a topic and get the main approaches mapped with citations.",
src: null,
demo: {
prompt:
"Survey the last year of research on LLM hallucination detection and map the main approaches.",
steps: [
{
title: "Google Search",
items: ["12 SERPs · arXiv, ACL, technical blogs"],
},
{
title: "Web Crawler",
items: ["Reading 21 papers and posts", "Extracting methods and benchmarks"],
},
{
title: "Generate report",
items: ["Literature brief · grouped by approach"],
},
],
rows: [
{
primary: "4 approach families mapped",
secondary: "self-consistency, retrieval grounding, probes, uncertainty",
},
{
primary: "Benchmarks converge on 2 suites",
secondary: "used in 9 of the 21 papers reviewed",
},
],
summary: "Literature brief saved · every claim links to its paper",
},
},
{
id: "financial-research",
title: "Financial & Market Research",
description:
"Pull earnings coverage, analyst breakdowns, and retail sentiment on any company into one cited brief.",
src: null,
demo: {
prompt:
"Summarize the reaction to NVIDIA's latest earnings across news, YouTube, and Reddit.",
steps: [
{
title: "Google Search",
items: ["10 SERPs · earnings coverage and recaps"],
},
{
title: "Youtube",
items: ["8 analyst breakdowns · transcripts pulled"],
},
{
title: "Reddit",
items: ["r/investing + r/stocks · 31 threads"],
},
],
rows: [
{
primary: "Data-center revenue beat leads coverage",
secondary: "cited in 7 of 10 top results",
},
{
primary: "Analyst take split on guidance",
secondary: "transcripts: 5 bullish · 3 cautious",
},
{
primary: "Retail sentiment net positive",
secondary: "top threads focus on supply constraints",
},
],
summary: "Cited earnings brief saved to your workspace",
},
},
{
id: "geo-monitoring",
title: "AI Overview & GEO Tracking",
description:
"Capture when Google's AI Overviews answer the queries you care about, and exactly which sources they cite.",
src: null,
demo: {
prompt: "Which of our target keywords trigger an AI Overview, and who gets cited?",
steps: [
{
title: "Google Search",
items: ["Scraping 25 SERPs", "Capturing AI Overviews and citations"],
},
{
title: "Plan tasks",
items: ["Map citations to competitors", "Compute your citation gap"],
},
],
rows: [
{
primary: "9 of 25 keywords trigger an AI Overview",
secondary: "up from 6 last month",
},
{
primary: "Competitor A cited on 4 · you on 1",
secondary: "their listicle wins 3 of those citations",
},
],
summary: "Citation gap report saved · weekly re-check scheduled",
},
},
],
},
{
id: "ci-workflows",
label: "Competitive Intelligence Workflows",
useCases: [
{
id: "launch-impact",
@ -154,60 +309,6 @@ const CATEGORIES: HeroCategory[] = [
summary: "Maps + Crawler + Search in one run · teardown saved",
},
},
{
id: "competitor-360",
title: "Competitor 360, on a Schedule",
description:
"An automation chains four connectors every week: site changes, rank movements, Reddit sentiment, and YouTube reaction.",
src: null,
demo: {
prompt:
"Every Monday, build me a 360 on our top competitor: site changes, rankings, Reddit, and YouTube.",
steps: [
{
title: "Web Crawler",
items: ["pricing + changelog pages · 2 changes detected"],
},
{
title: "Google Search",
items: ["12 shared keywords · rank movements captured"],
},
{
title: "Reddit",
items: ["18 mentions this week · sentiment tagged"],
},
{
title: "Youtube",
items: ["2 new videos · comments and transcripts pulled"],
},
{
title: "Create automation",
items: ["Weekly 360 brief · Mondays 8:00"],
},
],
rows: [
{
primary: "Shipped: usage-based pricing page",
secondary: "pricing + changelog diff · detected Jul 3",
},
{
primary: 'Took #2 on "competitive intelligence api"',
secondary: "you hold #4 · gap widened two weeks in a row",
},
{
primary: "Reddit sentiment down 12 pts since the change",
secondary: "churn signals in 5 threads · quotes linked",
},
],
summary: "4 connectors, 1 automation · first brief lands Monday 8:00",
},
},
],
},
{
id: "competitor-monitoring",
label: "Competitor Monitoring",
useCases: [
{
id: "pricing-watch",
title: "Competitor Pricing Watch",
@ -290,7 +391,7 @@ const CATEGORIES: HeroCategory[] = [
id: "serp-watch",
title: "Rank & Ad Monitoring",
description:
"Automations track the Google rankings, paid ads, and AI Overview citations your market actually sees.",
"Automations track the Google rankings, paid ads, and AI Overview citations your audience actually sees.",
src: null,
demo: {
prompt: "Track who ranks and runs ads for our top 10 keywords in the US.",
@ -310,7 +411,7 @@ const CATEGORIES: HeroCategory[] = [
],
rows: [
{
primary: '"competitive intelligence tools" — you #4, ↓1',
primary: '"ai research tools" — you #4, ↓1',
secondary: "Competitor A took #3 · runs 2 sponsored ads",
},
{
@ -321,202 +422,6 @@ const CATEGORIES: HeroCategory[] = [
summary: "10 SERPs captured · 3 movements flagged · daily automation on",
},
},
],
},
{
id: "lead-generation",
label: "B2B Lead Generation",
useCases: [
{
id: "local-leads",
title: "Local Business Leads",
description:
"Turn a category and a territory into a lead list with phones, websites, ratings, and decision-maker contacts.",
src: null,
demo: {
prompt:
"Find the top 5 burger places in San Jose and pull staff contacts from their websites.",
steps: [
{
title: "Google Maps",
items: ['Searching "burger san jose" · top 5 by rating and reviews'],
},
{
title: "Web Crawler",
items: ["Visiting 5 business sites", "Extracting staff and contact pages"],
},
{
title: "Save document",
items: ["Lead list with phones, sites, contacts"],
},
],
rows: [
{
primary: "The Counter — 4.5★ (2,310 reviews)",
secondary: "+1 408-423-9200 · thecounter.com · 2 contacts found",
},
{
primary: "Paper Plane — 4.6★ (1,882 reviews)",
secondary: "site crawled · owner + events email found",
},
{
primary: "Smoking Pig BBQ — 4.4★ (3,041 reviews)",
secondary: "+1 408-380-4784 · catering contact found",
},
],
summary: "5 places · 9 contacts with provenance · saved as lead list",
},
},
{
id: "team-rosters",
title: "Team Rosters & Contacts",
description:
"Spider any company site and pull the full team with emails, socials, and provenance, exported to CSV.",
src: null,
demo: {
prompt: "Get the complete a16z team roster and save it as a CSV in my workspace.",
steps: [
{
title: "Web Crawler",
items: ["a16z.com/team · following profile links", "142 profiles found"],
},
{
title: "Plan tasks",
items: ["Enrich each profile with email or LinkedIn"],
},
{
title: "Write file",
items: ["a16z-team.csv · saved to workspace"],
},
],
rows: [
{
primary: "142 team profiles extracted",
secondary: "name, role, focus area, profile URL",
},
{
primary: "89 enriched with email or LinkedIn",
secondary: "pulled from bios and linked pages",
},
{ primary: "a16z-team.csv", secondary: "saved to your workspace · ready to export" },
],
summary: "Full roster in 94s · every row cites its source page",
},
},
{
id: "portfolio-mapping",
title: "Portfolio & Market Mapping",
description:
"Map an investor's portfolio or a whole category, then enrich every company with pricing and contacts.",
src: null,
demo: {
prompt: "Get the a16z team and their portfolio companies.",
steps: [
{
title: "Web Crawler",
items: ["a16z.com/team · 142 profiles", "a16z.com/portfolio · 312 companies"],
},
{
title: "Plan tasks",
items: [
"Match partners to their portfolio boards",
"Enrich your category with pricing",
],
},
],
rows: [
{
primary: "312 portfolio companies mapped",
secondary: "sector, stage, website, one-line pitch",
},
{
primary: "48 in your category",
secondary: "enriched with pricing page + team size",
},
],
summary: "Market map saved · new investments auto-added by automation",
},
},
],
},
{
id: "brand-listening",
label: "Brand & Market Listening",
useCases: [
{
id: "reddit-listening",
title: "Reddit Brand Monitoring",
description:
"Hear what your market says about you, your competitors, and your category in the threads where buyers speak candidly.",
src: null,
demo: {
prompt: "Give me 20 posts on Reddit where people ask for an alternative to NotebookLM.",
steps: [
{
title: "Reddit",
items: [
'Searching "notebooklm alternative" across Reddit',
"37 posts found · ranking by relevance and upvotes",
],
},
{
title: "Plan tasks",
items: ["Tag each post: buying intent, churn signal, question"],
},
],
rows: [
{
primary: "Best NotebookLM alternative that's open source?",
secondary: "r/selfhosted · 214 upvotes · buying intent",
},
{
primary: "NotebookLM keeps losing my sources, what else?",
secondary: "r/artificial · 96 upvotes · churn signal",
},
{
primary: "Anything like NotebookLM but with an API?",
secondary: "r/LocalLLaMA · 71 upvotes · developer intent",
},
],
summary: "20 posts · 8 with buying intent · daily tracking automation on",
},
},
{
id: "youtube-sentiment",
title: "YouTube Audience Sentiment",
description:
"Pull videos, transcripts, and comments at scale to mine what audiences praise and complain about.",
src: null,
demo: {
prompt:
"Analyze the comments on our competitor's last 10 videos and cluster the complaints.",
steps: [
{
title: "Youtube",
items: ["Fetching last 10 videos", "4,812 comments pulled"],
},
{
title: "Plan tasks",
items: ["Cluster complaints by theme", "Score sentiment per cluster"],
},
],
rows: [
{
primary: "Pricing complaints — 31% of negative comments",
secondary: '"went from free to $20/mo overnight"',
},
{
primary: "Export limits — 22%",
secondary: '"can\'t get my own data out"',
},
{
primary: "Praise: onboarding — 18% positive",
secondary: "worth copying in your messaging",
},
],
summary: "4,812 comments clustered · sentiment report saved",
},
},
{
id: "switcher-mining",
title: "Switcher & Intent Mining",
@ -554,79 +459,95 @@ const CATEGORIES: HeroCategory[] = [
],
},
{
id: "market-research",
label: "Market Research",
id: "artifacts",
label: "Artifacts (Podcasts, Videos & More)",
useCases: [
{
id: "deep-research",
title: "Deep Research on the Live Web",
description:
"The agent crawls dozens of live sources on a question and synthesizes a cited answer, not a stale index.",
src: null,
demo: {
prompt: "Research the AI note-taking market and build a landscape brief with citations.",
steps: [
{
title: "Research",
items: ["Crawling 38 live sources", "Vendor sites, reviews, pricing pages"],
},
{
title: "Generate report",
items: ["Landscape brief · 24 inline citations"],
},
],
rows: [
{
primary: "23 vendors mapped across 4 segments",
secondary: "consumer, prosumer, team, developer-first",
},
{
primary: "Pricing clusters at $10 and $20/mo",
secondary: "3 vendors moved upmarket this quarter",
},
],
summary: "Landscape brief saved · 24 inline citations you can check",
},
id: "report",
title: "AI Report Generator",
description: "Turn your research into cited reports, then export to PDF or Markdown.",
src: `${HERO_TUTORIAL}/ReportGenGif_compressed.mp4`,
},
{
id: "geo-monitoring",
title: "AI Overview & GEO Tracking",
id: "podcast",
title: "AI Podcast Generator",
description: "Turn any brief or folder into a two-host AI podcast in under 20 seconds.",
src: `${HERO_TUTORIAL}/PodcastGenGif.mp4`,
},
{
id: "presentation",
title: "AI Presentation & Video Maker",
description: "Create editable slide decks and narrated video overviews from your findings.",
src: `${HERO_TUTORIAL}/video_gen_surf.mp4`,
},
{
id: "image-gen",
title: "AI Image Generation",
description: "Generate images inside your workspace for decks, briefs, and posts.",
src: `${HERO_TUTORIAL}/ImageGenGif.mp4`,
},
],
},
{
id: "automations",
label: "Automations",
useCases: [
{
id: "competitor-360",
title: "Competitor 360, on a Schedule",
description:
"Capture when Google's AI Overviews answer your market's queries, and exactly which sources they cite.",
"An automation chains four connectors every week: site changes, rank movements, Reddit sentiment, and YouTube reaction.",
src: null,
demo: {
prompt: "Which of our target keywords trigger an AI Overview, and who gets cited?",
prompt:
"Every Monday, build me a 360 on our top competitor: site changes, rankings, Reddit, and YouTube.",
steps: [
{
title: "Google Search",
items: ["Scraping 25 SERPs", "Capturing AI Overviews and citations"],
title: "Web Crawler",
items: ["pricing + changelog pages · 2 changes detected"],
},
{
title: "Plan tasks",
items: ["Map citations to competitors", "Compute your citation gap"],
title: "Google Search",
items: ["12 shared keywords · rank movements captured"],
},
{
title: "Reddit",
items: ["18 mentions this week · sentiment tagged"],
},
{
title: "Youtube",
items: ["2 new videos · comments and transcripts pulled"],
},
{
title: "Create automation",
items: ["Weekly 360 brief · Mondays 8:00"],
},
],
rows: [
{
primary: "9 of 25 keywords trigger an AI Overview",
secondary: "up from 6 last month",
primary: "Shipped: usage-based pricing page",
secondary: "pricing + changelog diff · detected Jul 3",
},
{
primary: "Competitor A cited on 4 · you on 1",
secondary: "their listicle wins 3 of those citations",
primary: 'Took #2 on "reddit scraper api"',
secondary: "you hold #4 · gap widened two weeks in a row",
},
{
primary: "Reddit sentiment down 12 pts since the change",
secondary: "churn signals in 5 threads · quotes linked",
},
],
summary: "Citation gap report saved · weekly re-check scheduled",
summary: "4 connectors, 1 automation · first brief lands Monday 8:00",
},
},
{
id: "cited-briefs",
title: "Cited Briefs & Alerts",
title: "Scheduled Briefs & Alerts",
description:
"Everything the agents gather lands in your workspace as briefs and alerts with sources you can check.",
src: null,
demo: {
prompt: "Send me a Monday brief of every competitor change detected last week.",
prompt: "Send me a Monday brief of every change my agents detected last week.",
steps: [
{
title: "Plan tasks",
@ -650,50 +571,68 @@ const CATEGORIES: HeroCategory[] = [
summary: "Automation created · first brief lands Monday 8:00",
},
},
{
id: "event-triggers",
title: "Event-Triggered Workflows",
description:
"Automations can fire on events, not just schedules: a document landing in a folder kicks off the workflow.",
src: null,
demo: {
prompt:
"Whenever a new file lands in my Research folder, summarize it and post the summary to Slack.",
steps: [
{
title: "Create automation",
items: [
"Trigger: new document in Research folder",
"Action: summarize → post to #research",
],
},
],
rows: [
{
primary: "Automation armed on the Research folder",
secondary: "fires the moment a document lands",
},
{
primary: "First run: competitor-teardown.pdf",
secondary: "summary posted to #research · 42s after upload",
},
],
summary: "Event-triggered automation live · no schedule needed",
},
},
],
},
{
id: "platform-agent",
label: "SurfSense Agent",
id: "desktop-app",
label: "Desktop App",
useCases: [
{
id: "chat-workspace",
title: "Chat With Everything You Gather",
id: "general-assist",
title: "General Assist",
description:
"Ask questions across every crawl, mention, and document in your workspace and get answers with inline citations.",
src: `${HERO_TUTORIAL}/BQnaGif_compressed.mp4`,
"Launch SurfSense from any application on your computer with a global shortcut.",
src: `${HERO_TUTORIAL}/general_assist.mp4`,
},
{
id: "report",
title: "AI Report Generator",
id: "quick-assist",
title: "Quick Assist",
description: "Select text anywhere, then ask AI to explain, rewrite, or act on it.",
src: `${HERO_TUTORIAL}/quick_assist.mp4`,
},
{
id: "screenshot-assist",
title: "Screenshot Assist",
description: "Capture any region of your screen and ask AI about it.",
src: `${HERO_TUTORIAL}/screenshot_assist.mp4`,
},
{
id: "folder-watch",
title: "Watch Local Folder",
description:
"Turn your intelligence into cited research reports, then export to PDF or Markdown.",
src: `${HERO_TUTORIAL}/ReportGenGif_compressed.mp4`,
},
{
id: "podcast",
title: "AI Podcast Generator",
description: "Turn any brief or folder into a two-host AI podcast in under 20 seconds.",
src: `${HERO_TUTORIAL}/PodcastGenGif.mp4`,
},
{
id: "presentation",
title: "AI Presentation & Video Maker",
description: "Create editable slide decks and narrated video overviews from your findings.",
src: `${HERO_TUTORIAL}/video_gen_surf.mp4`,
},
{
id: "connect",
title: "Build Your Knowledge Base",
description:
"Upload files or sync Google Drive, OneDrive, and Dropbox into one searchable knowledge base alongside everything your agents gather.",
src: `${HERO_TUTORIAL}/ConnectorFlowGif.mp4`,
},
{
id: "collab",
title: "Collaborative AI Chat",
description: "Work on AI conversations with your team in real time.",
src: `${HERO_REALTIME}/RealTimeChatGif.mp4`,
"Auto-sync a local folder to your knowledge base. Point it at your Obsidian vault to keep your notes searchable.",
src: `${HERO_TUTORIAL}/folder_watch.mp4`,
},
],
},
@ -708,7 +647,7 @@ export function HeroSection() {
"relative mt-4 max-w-4xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl dark:text-neutral-50"
)}
>
<Balancer>NotebookLM for competitive intelligence research.</Balancer>
<Balancer>The open-source NotebookLM alternative for open web research.</Balancer>
</h1>
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 md:mt-8 md:flex-row md:items-end md:gap-10">
<div>
@ -717,14 +656,15 @@ export function HeroSection() {
"relative mb-8 max-w-2xl text-left text-sm text-neutral-600 antialiased sm:text-base md:text-lg dark:text-neutral-400"
)}
>
SurfSense is an open-source competitive intelligence platform, like NotebookLM but
with live scraping connectors. Your AI agents monitor competitors, track rankings, and
listen to your market with live data from platforms like Reddit, YouTube, Instagram,
TikTok, Google Maps, Google Search, and the open web.
SurfSense is the open-source NotebookLM alternative for AI agents, an open web
research platform with live data connectors. Your AI agents research the live web with
structured data from Reddit, YouTube, Instagram, TikTok, Amazon, Google Maps, Google
Search, and any page on the open web.
</p>
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
<GetStartedButton />
<DownloadButton />
</div>
</div>
</div>
@ -767,6 +707,89 @@ function GetStartedButton() {
);
}
function DownloadButton() {
const { os, primary, alternatives, isMobileOS } = usePrimaryDownload();
const fallbackUrl = GITHUB_RELEASES_URL;
const mobileDisabledLabel = "Desktop app unavailable on mobile";
if (isMobileOS) {
return (
<Button
type="button"
variant="ghost"
disabled
className="h-14 w-full gap-2 rounded-lg border border-neutral-200 bg-white text-center text-base font-medium text-neutral-700 shadow-sm transition duration-150 sm:w-auto sm:px-6 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200"
>
<Download className="size-4" />
{mobileDisabledLabel}
</Button>
);
}
if (!primary) {
return (
<Button
asChild
variant="ghost"
className="h-14 w-full gap-2 rounded-lg border border-neutral-200 bg-white text-center text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-98 hover:bg-neutral-50 sm:w-auto sm:px-6 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
>
<a href={fallbackUrl} target="_blank" rel="noopener noreferrer">
<Download className="size-4" />
Download for {os}
</a>
</Button>
);
}
return (
<div className="flex h-14 w-full items-stretch sm:w-auto">
<Button
asChild
variant="ghost"
className="h-auto flex-1 gap-2 rounded-l-lg rounded-r-none border border-r-0 border-neutral-200 bg-white px-5 text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-[0.99] hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
>
<a href={primary.url}>
<Download className="size-4 shrink-0" />
Download for {os}
</a>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
aria-label="More download options"
className="h-auto rounded-l-none rounded-r-lg border border-neutral-200 bg-white px-2.5 text-neutral-500 shadow-sm transition duration-150 hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800"
>
<ChevronDown className="size-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
{alternatives.map((asset) => (
<DropdownMenuItem key={asset.name} asChild>
<a href={asset.url} className="cursor-pointer">
<Download className="mr-2 size-3.5" />
{getAssetLabel(asset.name)}
</a>
</DropdownMenuItem>
))}
<DropdownMenuItem asChild>
<a
href={fallbackUrl}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer"
>
All downloads
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
const TabVideo = memo(function TabVideo({
src,
title,
@ -847,9 +870,9 @@ const UseCasePane = memo(function UseCasePane({
<div className="relative overflow-hidden rounded-tl-xl rounded-tr-xl bg-white shadow-sm ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-950">
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
<div className="min-w-0">
<h3 className="truncate text-base font-semibold text-neutral-900 sm:text-lg dark:text-white">
<h2 className="truncate text-base font-semibold text-neutral-900 sm:text-lg dark:text-white">
{useCase.title}
</h3>
</h2>
<p className="text-sm text-neutral-500 text-pretty dark:text-neutral-400">
{useCase.description}
</p>

View file

@ -6,9 +6,9 @@ import { FAQJsonLd } from "@/components/seo/json-ld";
/** Answers are 40-60 words, written as quotable definitions for AI Overviews. */
export const HOME_FAQ = [
{
question: "What is competitive intelligence?",
question: "What is open web research?",
answer:
"Competitive intelligence is the practice of gathering and analyzing public information about competitors and your market to make better decisions. It covers pricing, product moves, rankings, reviews, and what customers say online. SurfSense automates it: AI agents collect the live data and turn it into briefs and alerts.",
"Open web research is gathering and analyzing live public information from across the web: search results, community discussions, reviews, videos, and any page. Unlike asking a chatbot that reasons over a stale index, it works from what the web says right now. SurfSense automates it: AI agents collect the live data and turn it into cited briefs and alerts.",
},
{
question: "What is an MCP server?",
@ -28,7 +28,7 @@ export const HOME_FAQ = [
{
question: "Can I self-host SurfSense?",
answer:
"Yes. SurfSense is open source and self-hostable, so you can run the entire platform on your own infrastructure and keep sensitive competitive research in-house. Use the cloud version to start in minutes, or deploy from the GitHub repository when you need full control.",
"Yes. SurfSense is open source and self-hostable, so you can run the entire platform on your own infrastructure and keep sensitive research in-house. Use the cloud version to start in minutes, or deploy from the GitHub repository when you need full control.",
},
];

View file

@ -0,0 +1,111 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { Reveal } from "@/components/connectors-marketing/reveal";
import { MarketingSection } from "@/components/marketing/section";
/**
* Real signups pulled from prod (Google-auth), curated to the most recognizable
* names. These are self-serve users, not signed enterprise accounts, so the
* heading stays honest ("Used by people at") rather than implying contracts.
*
* Logos live in `public/logos/` official marks from Wikimedia Commons /
* Wikipedia (universities use their seals). Bosta, Devoteam, and Leverage Edu
* have no clean Wikimedia logo, so they fall back to a brand favicon. Each item
* also falls back to a text wordmark on image error.
*/
const COMPANIES: { title: string; file: string }[] = [
{ title: "UC Berkeley", file: "berkeley.svg" },
{ title: "USC", file: "usc.svg" },
{ title: "Texas A&M", file: "tamu.svg" },
{ title: "UWMadison", file: "wisc.svg" },
{ title: "Pitt", file: "pitt.svg" },
{ title: "Korean Air", file: "koreanair.svg" },
{ title: "Iron Mountain", file: "ironmountain.svg" },
{ title: "Globant", file: "globant.svg" },
{ title: "Devoteam", file: "devoteam.png" },
{ title: "VNG", file: "vng.svg" },
{ title: "TPBank", file: "tpbank.svg" },
{ title: "OpenGov", file: "opengov.png" },
{ title: "WeLab", file: "welab.png" },
{ title: "Leverage Edu", file: "leverageedu.png" },
{ title: "Zopper", file: "zopper.png" },
{ title: "Tec de Monterrey", file: "tec.svg" },
{ title: "Chulalongkorn", file: "chula.svg" },
{ title: "Univ. of Bristol", file: "bristol.svg" },
{ title: "Nutresa", file: "nutresa.svg" },
{ title: "Bosta", file: "bosta.png" },
];
const LOGOS_PER_SET = 10;
const TOTAL_SETS = Math.ceil(COMPANIES.length / LOGOS_PER_SET);
function LogoItem({ title, file }: { title: string; file: string }) {
const [failed, setFailed] = useState(false);
if (failed) {
return (
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">{title}</span>
);
}
return (
// biome-ignore lint/performance/noImgElement: swapped in/out by the cycling animation, next/image adds no value here
<img
src={`/logos/${file}`}
alt={title}
title={title}
width={130}
height={40}
loading="lazy"
onError={() => setFailed(true)}
// dark mode: dark-on-transparent marks would vanish, so render every logo as a
// uniform light silhouette (brightness-0 + invert) instead of relying on its own color
className="h-10 w-auto max-w-[130px] object-contain opacity-60 grayscale transition duration-300 hover:opacity-100 hover:grayscale-0 dark:opacity-70 dark:brightness-0 dark:invert dark:hover:opacity-100"
/>
);
}
export function LogoCloud() {
const [currentSet, setCurrentSet] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentSet((prev) => (prev + 1) % TOTAL_SETS);
}, 3000);
return () => clearInterval(interval);
}, []);
const startIndex = currentSet * LOGOS_PER_SET;
const currentLogos = Array.from(
{ length: LOGOS_PER_SET },
(_, i) => COMPANIES[(startIndex + i) % COMPANIES.length]
);
return (
<MarketingSection>
<Reveal>
<h2 className="mx-auto max-w-xl text-center text-lg font-medium text-neutral-600 dark:text-neutral-400">
Used by people at
</h2>
</Reveal>
<div className="mx-auto mt-10 grid max-w-4xl grid-cols-3 gap-8 sm:grid-cols-5">
<AnimatePresence mode="popLayout">
{currentLogos.map((logo, index) => (
<motion.div
key={`${logo.title}-${currentSet}-${index}`}
initial={{ x: 40, opacity: 0, filter: "blur(8px)" }}
animate={{ x: 0, opacity: 1, filter: "blur(0px)" }}
exit={{ x: -40, opacity: 0, filter: "blur(8px)" }}
transition={{ duration: 0.4, ease: "easeOut", delay: index * 0.05 }}
className="flex items-center justify-center"
>
<LogoItem title={logo.title} file={logo.file} />
</motion.div>
))}
</AnimatePresence>
</div>
</MarketingSection>
);
}

View file

@ -250,17 +250,22 @@ const DesktopNav = ({ navItems, isScrolled, scrolledBgClassName }: DesktopNavPro
href="https://discord.gg/ejRNvftDp9"
target="_blank"
rel="noopener noreferrer"
aria-label="SurfSense on Discord"
className="hidden rounded-full p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors md:flex items-center justify-center"
>
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
<IconBrandDiscord
className="h-5 w-5 text-neutral-600 dark:text-neutral-300"
aria-hidden
/>
</Link>
<Link
href="https://www.reddit.com/r/SurfSense/"
target="_blank"
rel="noopener noreferrer"
aria-label="SurfSense on Reddit"
className="hidden rounded-full p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors md:flex items-center justify-center"
>
<IconBrandReddit className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
<IconBrandReddit className="h-5 w-5 text-neutral-600 dark:text-neutral-300" aria-hidden />
</Link>
<NavbarGitHubStars className="hidden md:flex" />
<ThemeTogglerComponent />
@ -362,17 +367,25 @@ const MobileNav = ({ navItems, isScrolled, scrolledBgClassName }: MobileNavProps
href="https://discord.gg/ejRNvftDp9"
target="_blank"
rel="noopener noreferrer"
aria-label="SurfSense on Discord"
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
<IconBrandDiscord
className="h-5 w-5 text-neutral-600 dark:text-neutral-300"
aria-hidden
/>
</Link>
<Link
href="https://www.reddit.com/r/SurfSense/"
target="_blank"
rel="noopener noreferrer"
aria-label="SurfSense on Reddit"
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandReddit className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
<IconBrandReddit
className="h-5 w-5 text-neutral-600 dark:text-neutral-300"
aria-hidden
/>
</Link>
<NavbarGitHubStars className="rounded-lg" />
<ThemeTogglerComponent />

View file

@ -21,9 +21,9 @@ const PATHS: {
icon: Megaphone,
art: "chat",
eyebrow: "For founders & marketing teams",
title: "Competitor and market intelligence without the enterprise price tag",
title: "Live web research without the enterprise price tag",
description:
"Ask for a competitor teardown, a lead list, or a weekly market brief in plain English. The agent gathers live data, cites its sources, and automations keep watch so you hear about changes first. Start free, pay only for what you use.",
"Ask for a research brief, a lead list, or a competitor teardown in plain English. The agent gathers live data, cites its sources, and automations keep watch so you hear about changes first. Start free, pay only for what you use.",
links: [
{ label: "See what teams build", href: "/connectors" },
{ label: "Pricing", href: "/pricing" },
@ -35,7 +35,7 @@ const PATHS: {
eyebrow: "For developers & agents",
title: "The whole platform is programmable",
description:
"Everything SurfSense agents can do is a typed REST API: scrape Reddit, YouTube, TikTok, Google Maps, Google Search, and the open web, search the knowledge base, run automations. One key, JSON in and out, $5 free credit, pay as you go. Already running agents in Claude, Cursor, or your own harness? The SurfSense MCP server hands them the same tools natively.",
"Everything SurfSense agents can do is a typed REST API: scrape Reddit, YouTube, TikTok, Amazon, Google Maps, Google Search, and the open web, search the knowledge base, run automations. One key, JSON in and out, $5 free credit, pay as you go. Already running agents in Claude, Cursor, or your own harness? The SurfSense MCP server hands them the same tools natively.",
links: [
{ label: "Read the docs", href: "/docs" },
{ label: "SurfSense MCP server", href: "/mcp-server" },

View file

@ -0,0 +1,298 @@
"use client";
import {
IconBrandLinkedin,
IconBrandTiktok,
IconBrandX,
IconBrandYoutube,
} from "@tabler/icons-react";
import { Component, type ReactNode, useEffect, useRef, useState } from "react";
import { LinkedInEmbed, TikTokEmbed, XEmbed, YouTubeEmbed } from "react-social-media-embed";
import { Reveal } from "@/components/connectors-marketing/reveal";
type Post =
| { kind: "youtube"; url: string; title: string; channel: string }
| { kind: "x"; url: string }
| { kind: "linkedin"; url: string; postUrl: string }
| { kind: "tiktok"; url: string };
/**
* Organic SurfSense coverage real posts embedded live with react-social-media-embed,
* grouped into three platform-uniform marquee rows (heights match within a row).
*
* Note: LinkedIn only renders when the author enabled embedding on the post; if
* disabled, the EmbedBoundary swaps in a link card to the original.
*
* ponytail: three rows, each list duplicated once for a seamless CSS loop. Ceiling:
* heavy third-party embeds. Mitigated by lazy-mounting the whole section on scroll
* (IntersectionObserver). Upgrade path: per-card virtualization, or swap YouTube
* players for thumbnail links.
*/
const YOUTUBE: Post[] = [
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=i9AJ7PHGSGg",
title: "SurfSense vs NotebookLM",
channel: "rezasaad plus",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=VBOwuD6xVK0",
title: "NotebookLM Is Great… Until You See SurfSense",
channel: "Thomas AI",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=UaekqjhUiJM",
title: "NotebookLM vs SurfSense en 6 pruebas reales (sorprendente)",
channel: "NextGen IA Hub",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=QGjKpZJJ9aw",
title: "Gana DINERO configurando “Cerebros de IA” privados con SurfSense",
channel: "Creando Con La IA",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=cfNAIQtNbKY",
title: "¿Adiós NotebookLM? Probé SurfSense y es BRUTAL (IA Gratis)",
channel: "NextGen IA Hub",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=pIWOKSHhf38",
title: "¿Superaron a NotebookLM? SurfSense es Open Source, privada y GRATIS",
channel: "academIArtificial",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=K5xx-J_mQZ8",
title: "¿Mejor que NotebookLM? IA GRATIS con modo local",
channel: "Migue Baena IA",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=AKxM3RUBFsc",
title: "¿Nueva IA GRATIS destroza a NotebookLM de Google? (OPEN SOURCE)",
channel: "Inteligencia Artificial Top",
},
{
kind: "youtube",
url: "https://www.youtube.com/watch?v=jCAgeaVgPDA",
title: "¿Nueva Herramienta IA GRATIS Destroza a NotebookLM? (OPEN SOURCE)",
channel: "Joaquín Barberá",
},
];
const TWEETS: Post[] = [
{ kind: "x", url: "https://x.com/LangChain/status/1853133037019562434" },
{ kind: "x", url: "https://x.com/MoureDev/status/1976279289780740448" },
{ kind: "x", url: "https://x.com/GithubProjects/status/2004892541590929490" },
{ kind: "x", url: "https://x.com/GitHub_Daily/status/1920418408736436438" },
{ kind: "x", url: "https://x.com/tom_doerr/status/2066062170173977088" },
{ kind: "x", url: "https://x.com/itsharmanjot/status/2066118517905354816" },
{ kind: "x", url: "https://x.com/JulianGoldieSEO/status/2011085275133604095" },
{ kind: "x", url: "https://x.com/L_go_mrk/status/2066482853232115847" },
{ kind: "x", url: "https://x.com/semihdev/status/2006275500952736028" },
{ kind: "x", url: "https://x.com/shao__meng/status/1919912860957999494" },
{ kind: "x", url: "https://x.com/LangChain/status/1840406184316342561" },
];
const SOCIAL: Post[] = [
{
kind: "linkedin",
url: "https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:7448203908834938881",
postUrl:
"https://www.linkedin.com/posts/vikas-singh-546643206_most-ai-tools-live-in-your-browser-and-ugcPost-7448203908834938881-gR6y",
},
{
kind: "linkedin",
url: "https://www.linkedin.com/embed/feed/update/urn:li:share:7448351685409701889",
postUrl:
"https://www.linkedin.com/posts/neha-jain-279b80118_ive-been-using-a-lot-of-ai-tools-daily-share-7448351685409701889-JvFP",
},
{
kind: "tiktok",
url: "https://www.tiktok.com/@alejavirivera/video/7603064928114625814",
},
];
const CARD = "mr-4 shrink-0 overflow-hidden rounded-xl border bg-card";
const SIZE: Record<Post["kind"], string> = {
youtube: "h-[262px] w-[340px]",
x: "h-[440px] w-[340px]",
linkedin: "h-[540px] w-[340px]",
tiktok: "h-[540px] w-[340px]",
};
const META: Record<Post["kind"], { Icon: typeof IconBrandX; label: string }> = {
youtube: { Icon: IconBrandYoutube, label: "YouTube" },
x: { Icon: IconBrandX, label: "X" },
linkedin: { Icon: IconBrandLinkedin, label: "LinkedIn" },
tiktok: { Icon: IconBrandTiktok, label: "TikTok" },
};
/**
* Some embeds (notably Facebook, and LinkedIn when the author disabled embedding)
* throw at render/mount instead of degrading gracefully. This boundary stops one
* bad embed from taking down the whole marquee it swaps in a link card instead.
*/
class EmbedBoundary extends Component<
{ fallback: ReactNode; children: ReactNode },
{ failed: boolean }
> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
render() {
return this.state.failed ? this.props.fallback : this.props.children;
}
}
function FallbackCard({ post }: { post: Post }) {
const { Icon, label } = META[post.kind];
const href = post.kind === "linkedin" ? post.postUrl : post.url;
return (
<div className={`${CARD} ${SIZE[post.kind]}`}>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex h-full w-full flex-col items-center justify-center gap-3 p-6 text-center text-sm font-medium text-muted-foreground transition-colors hover:text-brand"
>
<Icon className="size-8" aria-hidden />
<span>View this post on {label}</span>
</a>
</div>
);
}
function Card({ post }: { post: Post }) {
switch (post.kind) {
case "youtube":
return (
<div className={`${CARD} ${SIZE.youtube} flex flex-col`}>
<YouTubeEmbed url={post.url} width={340} height={191} />
<div className="flex flex-1 flex-col gap-1.5 p-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<IconBrandYoutube className="size-4 shrink-0 text-red-500" aria-hidden />
<span className="truncate">{post.channel}</span>
</div>
<a
href={post.url}
target="_blank"
rel="noopener noreferrer"
className="line-clamp-2 text-sm font-medium leading-snug hover:text-brand"
>
{post.title}
</a>
</div>
</div>
);
case "x":
return (
<div className={`${CARD} ${SIZE.x}`}>
<XEmbed url={post.url} width={340} />
</div>
);
case "linkedin":
return (
<div className={`${CARD} ${SIZE.linkedin}`}>
<LinkedInEmbed url={post.url} postUrl={post.postUrl} width={340} height={540} />
</div>
);
case "tiktok":
return (
<div className={`${CARD} ${SIZE.tiktok}`}>
<TikTokEmbed url={post.url} width={340} />
</div>
);
}
}
function Row({
posts,
animation,
duration,
}: {
posts: Post[];
animation: "ss-marquee-l" | "ss-marquee-r";
duration: number;
}) {
return (
<div className="group flex overflow-hidden">
{/* Track holds the list twice; the -50% shift wraps seamlessly (margins, not gap). */}
<div
className="flex w-max shrink-0 group-hover:paused motion-reduce:paused"
style={{ animation: `${animation} ${duration}s linear infinite` }}
>
{[...posts, ...posts].map((post, i) => (
<EmbedBoundary
key={`${post.kind}-${post.url}-${i}`}
fallback={<FallbackCard post={post} />}
>
<Card post={post} />
</EmbedBoundary>
))}
</div>
</div>
);
}
export function SocialProof() {
// Third-party embeds are heavy; only mount them once the section scrolls near view.
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setVisible(true);
io.disconnect();
}
},
{ rootMargin: "300px" }
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<section className="overflow-hidden py-12 sm:py-16">
<Reveal>
<div className="mx-auto max-w-2xl px-4 text-center">
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
Loved across the internet
</h2>
</div>
</Reveal>
<div
ref={ref}
className="mt-10 flex min-h-[1360px] flex-col gap-4"
style={{
maskImage: "linear-gradient(to right, transparent, black 6%, black 94%, transparent)",
WebkitMaskImage:
"linear-gradient(to right, transparent, black 6%, black 94%, transparent)",
}}
>
{visible ? (
<>
<Row posts={YOUTUBE} animation="ss-marquee-l" duration={60} />
<Row posts={TWEETS} animation="ss-marquee-r" duration={75} />
{/* Only 3 social cards — pre-double so one set spans wide viewports (no loop gap). */}
<Row posts={[...SOCIAL, ...SOCIAL]} animation="ss-marquee-l" duration={70} />
</>
) : null}
</div>
<style>{`
@keyframes ss-marquee-l { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes ss-marquee-r { from { transform: translateX(-50%); } to { transform: translateX(0); } }
`}</style>
</section>
);
}

View file

@ -13,17 +13,17 @@ const USE_CASES: {
art: UseCaseArtVariant;
}[] = [
{
title: "Competitor price monitoring",
title: "Search & AI answer research",
description:
"Crawl competitor pricing and product pages on a schedule and get an alert the day something changes, not the quarter after.",
href: "/web-crawl",
anchor: "Web Crawl API",
art: "price",
"Watch the rankings, ads, and AI Overviews people actually see for the queries you care about, and know the moment they change.",
href: "/google-search",
anchor: "SERP API",
art: "serp",
},
{
title: "Brand monitoring",
title: "Community & brand listening",
description:
"Track every mention of your brand, your competitors, and your category across the communities where buyers speak candidly.",
"Track every mention of your brand, your competitors, and your category across the communities where people speak candidly.",
href: "/reddit",
anchor: "Reddit API",
art: "brand",
@ -31,7 +31,7 @@ const USE_CASES: {
{
title: "Social sentiment mining",
description:
"Pull public posts, reels, and full comment threads from any creator or competitor, then score how audiences actually react to launches and campaigns.",
"Pull public posts, reels, and full comment threads from any creator or brand, then score how audiences actually react to launches and campaigns.",
href: "/instagram",
anchor: "Instagram API",
art: "chat",
@ -45,12 +45,12 @@ const USE_CASES: {
art: "leads",
},
{
title: "Market research",
title: "Competitor price monitoring",
description:
"Watch the rankings, ads, and AI answers your market actually sees, and mine audience sentiment at scale.",
href: "/google-search",
anchor: "SERP API",
art: "serp",
"Crawl competitor pricing and product pages on a schedule and get an alert the day something changes, not the quarter after.",
href: "/web-crawl",
anchor: "Web Crawl API",
art: "price",
},
];

View file

@ -82,9 +82,7 @@ export function CreateWorkspaceDialog({ open, onOpenChange }: CreateWorkspaceDia
// which is the authoritative net regardless.
const isInitialSetup = result.llm_setup?.stage === "initial_setup";
router.push(
isInitialSetup
? `/dashboard/${result.id}/onboard`
: `/dashboard/${result.id}/new-chat`
isInitialSetup ? `/dashboard/${result.id}/onboard` : `/dashboard/${result.id}/new-chat`
);
} catch (error) {
console.error("Failed to create workspace:", error);

View file

@ -96,7 +96,7 @@ export function AgentSetupTabs({ options }: { options?: Partial<McpSnippetOption
const snippet = client[transport];
const config = snippet.build(resolved);
return (
<TabsContent key={client.id} value={client.id} className="min-w-0 space-y-3">
<TabsContent key={client.id} value={client.id} className="min-w-0 space-y-3">
<ol className="list-decimal space-y-1 pl-5 text-sm leading-relaxed text-muted-foreground">
{snippet.steps.map((step) => (
<li key={step}>{step}</li>

View file

@ -45,7 +45,8 @@ export function ConnectAgentDialog({ className }: { className?: string }) {
<DialogHeader>
<DialogTitle>Connect your coding agent to SurfSense</DialogTitle>
<DialogDescription>
Give your coding agent access to SurfSense scrapers and your knowledge base. Create an API key under API Keys, choose your agent, then paste the config.
Give your coding agent access to SurfSense scrapers and your knowledge base. Create an
API key under API Keys, choose your agent, then paste the config.
</DialogDescription>
</DialogHeader>
<AgentSetupTabs options={{ baseUrl: BACKEND_URL || undefined }} />

View file

@ -181,13 +181,13 @@ function ComposerSuggestionSkeleton({
}
export {
ComposerSuggestionPopoverContent,
ComposerSuggestionList,
ComposerSuggestionGroup,
ComposerSuggestionGroupHeading,
ComposerSuggestionHeader,
ComposerSuggestionItem,
ComposerSuggestionSeparator,
ComposerSuggestionList,
ComposerSuggestionMessage,
ComposerSuggestionPopoverContent,
ComposerSuggestionSeparator,
ComposerSuggestionSkeleton,
};

View file

@ -35,7 +35,7 @@ const demoPlans = [
billingText: "Your first $5 of credit is free. No subscription, ever",
features: [
"$5 of free credit to start, one balance for everything",
"Platform connectors: Reddit, YouTube, TikTok, Google Maps, Google Search, and the open web",
"Platform connectors: Reddit, YouTube, TikTok, Amazon, Google Maps, Google Search, and the open web",
"Call every connector as a REST API with your key or through the MCP server",
"Pay per item returned and per page crawled. Failed calls are never billed",
"Premium models like GPT-5.5, Claude Sonnet 5, Gemini 3.1 Pro billed at provider cost",
@ -145,12 +145,12 @@ const faqData: FAQSection[] = [
{
question: "Do agents and automations cost extra?",
answer:
"No. There is no add-on fee for agents or automations. A scheduled competitor brief or an event-triggered alert draws from the same credit balance: connector items and crawled pages at their per-unit rates, and model usage at provider cost. A workflow that uses free models and no scraping costs nothing.",
"No. There is no add-on fee for agents or automations. A scheduled research brief or an event-triggered alert draws from the same credit balance: connector items and crawled pages at their per-unit rates, and model usage at provider cost. A workflow that uses free models and no scraping costs nothing.",
},
{
question: "What can the agents actually do?",
answer:
"You describe the job in plain English and SurfSense sets up the agent, no code needed. Agents can watch competitor pricing pages, track brand mentions on Reddit and YouTube, monitor Google rankings and Maps reviews, then turn what they find into briefs and alerts, and write results back to Notion, Slack, Linear, and Jira.",
"You describe the job in plain English and SurfSense sets up the agent, no code needed. Agents can watch any page for changes, track mentions on Reddit and YouTube, monitor Google rankings and Maps reviews, then turn what they find into briefs and alerts, and write results back to Notion, Slack, Linear, and Jira.",
},
],
},
@ -363,7 +363,7 @@ function PricingBasic() {
<Pricing
plans={demoPlans}
title="SurfSense Pricing"
description="Give your agents competitive intelligence. Self-host for free, or start with $5 of credit and pay as you go. No subscriptions."
description="Give your agents the live web. Self-host for free, or start with $5 of credit and pay as you go. No subscriptions."
/>
<PricingFAQ />
</>

View file

@ -19,7 +19,7 @@ export function OrganizationJsonLd() {
url: "https://www.surfsense.com",
logo: "https://www.surfsense.com/logo.png",
description:
"SurfSense is an open-source competitive intelligence platform. AI agents monitor competitors, track rankings, and listen to your market through one API or MCP server.",
"SurfSense is an open-source NotebookLM alternative for AI agents. It researches the live web with structured data through one API or MCP server.",
sameAs: [
"https://github.com/MODSetter/SurfSense",
"https://discord.gg/ejRNvftDp9",
@ -45,7 +45,7 @@ export function WebSiteJsonLd() {
name: "SurfSense",
url: "https://www.surfsense.com",
description:
"SurfSense is an open-source competitive intelligence platform for AI agents, with live data connectors served through one API or MCP server.",
"SurfSense is an open-source NotebookLM alternative for AI agents, an open web research platform with live data connectors served through one API or MCP server.",
potentialAction: {
"@type": "SearchAction",
target: {
@ -76,14 +76,14 @@ export function SoftwareApplicationJsonLd() {
"Free self-hosted from the open-source repo; cloud starts with $5 of free credit, then pay as you go",
},
description:
"SurfSense is an open-source competitive intelligence platform. AI agents monitor competitors, track rankings, and listen to your market with platform-native connectors for Reddit, YouTube, TikTok, Google Maps, Google Search, and the open web, through one API or MCP server.",
"SurfSense is an open-source NotebookLM alternative for AI agents. It researches the live web with platform-native connectors for Reddit, YouTube, TikTok, Amazon, Google Maps, Google Search, and any page on the open web, through one API or MCP server.",
url: "https://www.surfsense.com",
downloadUrl: "https://github.com/MODSetter/SurfSense/releases",
featureList: [
"Platform-native connectors: Reddit, YouTube, TikTok, Google Maps, Google Search, Web Crawl",
"Platform-native connectors: Reddit, YouTube, TikTok, Amazon, Google Maps, Google Search, Web Crawl",
"MCP server that exposes every connector as a native agent tool",
"Agent harness with retries, structured output, and credit metering",
"Competitor, brand, and rank monitoring with briefs and alerts",
"Live web research with cited briefs and alerts",
"AI automations and agents (scheduled and event-triggered workflows)",
"AI-powered semantic search across connected tools and documents",
"Knowledge base with file uploads and Google Drive, OneDrive, and Dropbox sync",

View file

@ -135,8 +135,8 @@ export const GenerateImageToolUI = ({
};
export {
GenerateImageArgsSchema,
GenerateImageResultSchema,
type GenerateImageArgs,
GenerateImageArgsSchema,
type GenerateImageResult,
GenerateImageResultSchema,
};

View file

@ -418,4 +418,4 @@ export const SandboxExecuteToolUI = ({
return <ExecuteCompleted command={command} parsed={parsed} threadId={threadId} />;
};
export { ExecuteArgsSchema, ExecuteResultSchema, type ExecuteArgs, type ExecuteResult };
export { type ExecuteArgs, ExecuteArgsSchema, type ExecuteResult, ExecuteResultSchema };

View file

@ -90,8 +90,8 @@ export const UpdateMemoryToolUI = ({
// ============================================================================
export {
UpdateMemoryArgsSchema,
UpdateMemoryResultSchema,
type UpdateMemoryArgs,
UpdateMemoryArgsSchema,
type UpdateMemoryResult,
UpdateMemoryResultSchema,
};

View file

@ -157,4 +157,4 @@ export const WriteTodosToolUI = ({
);
};
export { WriteTodosSchema, type WriteTodosData };
export { type WriteTodosData, WriteTodosSchema };

View file

@ -61,4 +61,4 @@ function AccordionContent({
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View file

@ -121,14 +121,14 @@ function AlertDialogCancel({
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};

View file

@ -59,4 +59,4 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
);
}
export { Alert, AlertTitle, AlertDescription };
export { Alert, AlertDescription, AlertTitle };

View file

@ -556,4 +556,4 @@ const TabsContent = forwardRef<
});
TabsContent.displayName = "TabsContent";
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -55,4 +55,4 @@ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">)
);
}
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount };
export { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };

View file

@ -52,4 +52,4 @@ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View file

@ -18,4 +18,4 @@ function CollapsibleContent({
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
export { Collapsible, CollapsibleContent, CollapsibleTrigger };

View file

@ -150,11 +150,11 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">)
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandShortcut,
CommandList,
CommandSeparator,
CommandShortcut,
};

View file

@ -207,18 +207,18 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
ContextMenuTrigger,
};

View file

@ -92,13 +92,13 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View file

@ -102,14 +102,14 @@ DrawerHandle.displayName = "DrawerHandle";
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerOverlay,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
};

View file

@ -211,18 +211,18 @@ function DropdownMenuSubContent({
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};

View file

@ -91,4 +91,4 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
);
}
export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle };

View file

@ -91,4 +91,4 @@ const ExpandedGifOverlay = ExpandedMediaOverlay;
/** @deprecated Use useExpandedMedia instead */
const useExpandedGif = useExpandedMedia;
export { ExpandedMediaOverlay, useExpandedMedia, ExpandedGifOverlay, useExpandedGif };
export { ExpandedGifOverlay, ExpandedMediaOverlay, useExpandedGif, useExpandedMedia };

View file

@ -140,12 +140,12 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
};

View file

@ -96,9 +96,9 @@ function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span"
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View file

@ -39,4 +39,4 @@ function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitiv
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };

View file

@ -122,11 +122,11 @@ function SheetDescription({
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
};

View file

@ -89,4 +89,4 @@ function TableCaption({ className, ...props }: React.ComponentProps<"caption">)
);
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };

View file

@ -52,4 +52,4 @@ const TabsContent = React.forwardRef<
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -82,4 +82,4 @@ function TooltipContent({
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };