Merge remote-tracking branch 'upstream/dev' into fix/tabs
|
|
@ -5,7 +5,7 @@
|
|||
# Optional packaged-client override. Leave unset in Docker so browser requests
|
||||
# use same-origin relative URLs behind Caddy. Set it for packaged clients
|
||||
# (e.g. Electron) or local dev that talks to a separate backend origin.
|
||||
# NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
||||
|
||||
# Server-only. Internal backend URL used by Next.js server code (RSC / route
|
||||
# handlers). Cannot be a relative URL.
|
||||
|
|
@ -40,7 +40,7 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
|||
# Zero cache (real-time sync). Leave unset in Docker to use the same-origin
|
||||
# "/zero" endpoint behind Caddy. Set it for local dev or packaged clients.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
||||
# Server-only shared secret that authorizes zero-cache when it calls
|
||||
# /api/zero/query. Leave unset during the compatibility rollout, then set it
|
||||
# once every zero-cache instance sends X-Api-Key.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useAnnouncements } from "@/hooks/use-announcements";
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function AnnouncementsPage() {
|
||||
const { announcements, markAllRead } = useAnnouncements();
|
||||
const { announcements, markAllRead } = useAnnouncements({ includeExpired: true });
|
||||
|
||||
// Auto-mark all visible announcements as read when the page is opened
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getAllConnectors } from "@/lib/connectors-marketing";
|
|||
const canonicalUrl = "https://www.surfsense.com/connectors";
|
||||
|
||||
const metaDescription =
|
||||
"Platform-native scraper APIs for AI agents. Pull live data from the platforms your market uses through one typed API or the SurfSense MCP server. Explore every connector.";
|
||||
"Platform-native scraper APIs for AI agents. Pull live, structured data from the platforms where answers live through one typed API or the SurfSense MCP server. Explore every connector.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Scraper APIs for AI Agents: All Connectors | SurfSense",
|
||||
|
|
@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
|||
"scraper api for ai agents",
|
||||
"data connectors",
|
||||
"mcp server",
|
||||
"competitive intelligence platform",
|
||||
"open web research platform",
|
||||
],
|
||||
alternates: { canonical: canonicalUrl },
|
||||
openGraph: {
|
||||
|
|
@ -38,13 +38,13 @@ export default function ConnectorsIndexPage() {
|
|||
<div className="mx-auto w-full max-w-7xl px-2 md:px-8 xl:px-0">
|
||||
<header className="max-w-2xl">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-balance sm:text-4xl lg:text-5xl">
|
||||
Connectors for every platform your market uses
|
||||
Connectors for every platform where answers live
|
||||
</h1>
|
||||
<p className="mt-5 text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
Each connector is a platform-native scraper API your AI agents can call directly, or
|
||||
through the SurfSense MCP server. They are the live data behind the SurfSense{" "}
|
||||
<Link href="/" className="font-medium text-foreground underline underline-offset-4">
|
||||
competitive intelligence platform
|
||||
open web research platform
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ export default function ExternalMcpConnectorsPage() {
|
|||
<p className="mt-3 max-w-2xl text-muted-foreground leading-relaxed">
|
||||
These apps run on their official hosted MCP servers. Sign in once with OAuth and
|
||||
SurfSense manages the tokens and curates each tool list, so your agents can search
|
||||
Notion, read Slack threads, or file Jira issues alongside your market intelligence.
|
||||
Notion, read Slack threads, or file Jira issues alongside your live web research.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal>
|
||||
|
|
@ -330,7 +330,7 @@ export default function ExternalMcpConnectorsPage() {
|
|||
<p className="mx-auto mt-3 max-w-xl text-muted-foreground leading-relaxed">
|
||||
External MCP connectors are part of 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>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type { FaqItem } from "@/lib/connectors-marketing/types";
|
|||
const canonicalUrl = "https://www.surfsense.com/mcp-server";
|
||||
|
||||
const metaDescription =
|
||||
"The SurfSense MCP server gives Claude, Cursor, and any MCP client native tools for your workspace: scrape Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, and the web, plus full knowledge base access. One API key.";
|
||||
"The SurfSense MCP server gives Claude, Cursor, and any MCP client native tools for your workspace: scrape Reddit, YouTube, Instagram, TikTok, Amazon, Google Maps, Google Search, and the web, plus full knowledge base access. One API key.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SurfSense MCP Server: Scraper APIs and Knowledge Base as Agent Tools",
|
||||
|
|
@ -102,6 +102,7 @@ const TOOL_GROUPS = [
|
|||
"surfsense_google_maps_scrape",
|
||||
"surfsense_google_maps_reviews",
|
||||
"surfsense_google_search",
|
||||
"surfsense_amazon_scrape",
|
||||
"surfsense_web_crawl",
|
||||
"surfsense_list_scraper_runs",
|
||||
"surfsense_get_scraper_run",
|
||||
|
|
@ -133,7 +134,7 @@ const FAQ: FaqItem[] = [
|
|||
{
|
||||
question: "What is the SurfSense MCP server?",
|
||||
answer:
|
||||
"It is a Model Context Protocol server that exposes your SurfSense workspace to MCP clients like Claude Code, Cursor, and Claude Desktop. Your agents get native tools for every scraper API (Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, web crawl) and for searching, reading, and writing your knowledge base.",
|
||||
"It is a Model Context Protocol server that exposes your SurfSense workspace to MCP clients like Claude Code, Cursor, and Claude Desktop. Your agents get native tools for every scraper API (Reddit, YouTube, Instagram, TikTok, Amazon, Google Maps, Google Search, web crawl) and for searching, reading, and writing your knowledge base.",
|
||||
},
|
||||
{
|
||||
question: "Which MCP clients does it work with?",
|
||||
|
|
@ -221,9 +222,9 @@ export default function McpServerPage() {
|
|||
</h1>
|
||||
<p className="mt-5 max-w-xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
The SurfSense MCP server hands Claude, Cursor, or any MCP client the whole platform:
|
||||
scrape Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, and the open
|
||||
web, and search, read, and write your knowledge base. One API key, typed tools, pay
|
||||
as you go.
|
||||
scrape Reddit, YouTube, Instagram, TikTok, Amazon, Google Maps, Google Search, and
|
||||
the open web, and search, read, and write your knowledge base. One API key, typed
|
||||
tools, pay as you go.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||
<Button asChild size="lg">
|
||||
|
|
@ -369,12 +370,12 @@ export default function McpServerPage() {
|
|||
<Reveal>
|
||||
<div className="rounded-2xl border bg-card p-8 text-center sm:p-12">
|
||||
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
|
||||
Put live market data inside your agents
|
||||
Put live web data inside your agents
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-muted-foreground leading-relaxed">
|
||||
The MCP server is part of the SurfSense{" "}
|
||||
<Link href="/" className="font-medium text-foreground underline underline-offset-4">
|
||||
competitive intelligence platform
|
||||
open web research platform
|
||||
</Link>
|
||||
. Start with $5 of free credit, no credit card required.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -4,19 +4,17 @@ import { CompareTable } from "@/components/homepage/compare-table";
|
|||
import { ConnectorGrid } from "@/components/homepage/connector-grid";
|
||||
import { HeroSection } from "@/components/homepage/hero-section";
|
||||
import { HomeFaq } from "@/components/homepage/home-faq";
|
||||
import { HowItWorks } from "@/components/homepage/how-it-works";
|
||||
import { PersonaPaths } from "@/components/homepage/persona-paths";
|
||||
import { UseCasesRow } from "@/components/homepage/use-cases";
|
||||
import { LogoCloud } from "@/components/homepage/logo-cloud";
|
||||
import { SocialProof } from "@/components/homepage/social-proof";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
|
||||
<AuthRedirect />
|
||||
<HeroSection />
|
||||
<PersonaPaths />
|
||||
<LogoCloud />
|
||||
<SocialProof />
|
||||
<ConnectorGrid />
|
||||
<HowItWorks />
|
||||
<UseCasesRow />
|
||||
<CompareTable />
|
||||
<HomeFaq />
|
||||
<CommunityStrip />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
|||
"open source ai agent platform",
|
||||
"self-hosted ai workspace",
|
||||
"ai automation pricing",
|
||||
"competitive intelligence pricing",
|
||||
"web scraping api pricing",
|
||||
],
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function BasicsSection({ name, description, errors, onChange }: BasicsSec
|
|||
id="automation-name"
|
||||
value={name}
|
||||
maxLength={200}
|
||||
placeholder="Weekly competitor digest"
|
||||
placeholder="Weekly research digest"
|
||||
onChange={(e) => onChange({ name: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Server component
|
||||
import type React from "react";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import type React from "react";
|
||||
import { DashboardClientLayout } from "./client-layout";
|
||||
|
||||
const PLAYGROUND_SIDEBAR_COLLAPSED_COOKIE = "surfsense_playground_sidebar_collapsed";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ import { AppError } from "@/lib/error";
|
|||
import { findVerb } from "@/lib/playground/catalog";
|
||||
import { formatCost, formatDuration, formatPricing } from "@/lib/playground/format";
|
||||
import { buildPayload, initialFormValues, parseSchemaFields } from "@/lib/playground/json-schema";
|
||||
import {
|
||||
AmazonMarketplaceHint,
|
||||
getAmazonFieldOptions,
|
||||
hasAmazonFranceValue,
|
||||
} from "@/lib/playground/platform-overrides/amazon";
|
||||
import { ApiReference } from "./api-reference";
|
||||
import { OutputViewer } from "./output-viewer";
|
||||
import { RunProgressPanel } from "./run-progress-panel";
|
||||
|
|
@ -171,6 +176,8 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
|
|||
[run.detail]
|
||||
);
|
||||
const endpoint = `POST /workspaces/${workspaceId}/scrapers/${platform}/${verb}`;
|
||||
const isAmazonScrape = platform === "amazon" && verb === "scrape";
|
||||
const hasAmazonFranceUrl = useMemo(() => hasAmazonFranceValue(values), [values]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousStatus = previousStatusRef.current;
|
||||
|
|
@ -260,7 +267,9 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn
|
|||
values={values}
|
||||
onChange={handleChange}
|
||||
disabled={isRunning}
|
||||
getFieldOptions={isAmazonScrape ? getAmazonFieldOptions : undefined}
|
||||
/>
|
||||
{isAmazonScrape ? <AmazonMarketplaceHint showFranceWarning={hasAmazonFranceUrl} /> : null}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" onClick={handleRun} disabled={isRunning} className="relative">
|
||||
|
|
|
|||
|
|
@ -17,11 +17,19 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import type { FormField } from "@/lib/playground/json-schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface FieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type FieldOptionsResolver = (field: FormField) => FieldOption[] | undefined;
|
||||
|
||||
interface SchemaFormProps {
|
||||
fields: FormField[];
|
||||
values: Record<string, unknown>;
|
||||
onChange: (name: string, value: unknown) => void;
|
||||
disabled?: boolean;
|
||||
getFieldOptions?: FieldOptionsResolver;
|
||||
/** Field names flagged by a 422 response, shown with error styling. */
|
||||
fieldErrors?: Record<string, string>;
|
||||
}
|
||||
|
|
@ -32,12 +40,14 @@ function FieldControl({
|
|||
onChange,
|
||||
disabled,
|
||||
invalid,
|
||||
options,
|
||||
}: {
|
||||
field: FormField;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
disabled?: boolean;
|
||||
invalid?: boolean;
|
||||
options?: FieldOption[];
|
||||
}) {
|
||||
const id = `field-${field.name}`;
|
||||
|
||||
|
|
@ -68,6 +78,27 @@ function FieldControl({
|
|||
);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return (
|
||||
<Select
|
||||
value={value ? String(value) : options[0]?.value}
|
||||
onValueChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger id={id} className={cn("w-full", invalid && "border-destructive")}>
|
||||
<SelectValue placeholder="Select…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label} ({option.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.kind === "string_array") {
|
||||
return (
|
||||
<Textarea
|
||||
|
|
@ -116,12 +147,14 @@ function FieldRow({
|
|||
onChange,
|
||||
disabled,
|
||||
error,
|
||||
options,
|
||||
}: {
|
||||
field: FormField;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
options?: FieldOption[];
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
|
|
@ -143,13 +176,21 @@ function FieldRow({
|
|||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
invalid={!!error}
|
||||
options={options}
|
||||
/>
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SchemaForm({ fields, values, onChange, disabled, fieldErrors }: SchemaFormProps) {
|
||||
export function SchemaForm({
|
||||
fields,
|
||||
values,
|
||||
onChange,
|
||||
disabled,
|
||||
getFieldOptions,
|
||||
fieldErrors,
|
||||
}: SchemaFormProps) {
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const { primary, advanced } = useMemo(() => {
|
||||
|
|
@ -168,6 +209,7 @@ export function SchemaForm({ fields, values, onChange, disabled, fieldErrors }:
|
|||
onChange={(value) => onChange(field.name, value)}
|
||||
disabled={disabled}
|
||||
error={fieldErrors?.[field.name]}
|
||||
options={getFieldOptions?.(field)}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
@ -197,6 +239,7 @@ export function SchemaForm({ fields, values, onChange, disabled, fieldErrors }:
|
|||
onChange={(value) => onChange(field.name, value)}
|
||||
disabled={disabled}
|
||||
error={fieldErrors?.[field.name]}
|
||||
options={getFieldOptions?.(field)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,27 +50,26 @@ export const metadata: Metadata = {
|
|||
alternates: {
|
||||
canonical: "https://www.surfsense.com",
|
||||
},
|
||||
title: "SurfSense - NotebookLM for Competitive Intelligence",
|
||||
title: "SurfSense - Open-Source NotebookLM Alternative for Web Research",
|
||||
description:
|
||||
"SurfSense is an open-source competitive intelligence platform. Your AI agents monitor competitors, track rankings, and listen to your market through one API or MCP server.",
|
||||
"The open-source NotebookLM for open web research. Your AI agents pull live, structured data from Reddit, YouTube, Amazon, and any page via one API or MCP server.",
|
||||
keywords: [
|
||||
"competitive intelligence platform",
|
||||
"competitive intelligence software",
|
||||
"market intelligence tools",
|
||||
"competitor monitoring",
|
||||
"competitor price monitoring",
|
||||
"brand monitoring",
|
||||
"rank tracking",
|
||||
"social listening",
|
||||
"open web research platform",
|
||||
"web research for AI agents",
|
||||
"live web data for agents",
|
||||
"web scraping API",
|
||||
"reddit scraper api",
|
||||
"youtube scraper api",
|
||||
"deep research agent",
|
||||
"mcp server",
|
||||
"agent harness",
|
||||
"open source competitive intelligence",
|
||||
"open source NotebookLM alternative",
|
||||
"SurfSense",
|
||||
],
|
||||
openGraph: {
|
||||
title: "SurfSense - NotebookLM for Competitive Intelligence",
|
||||
title: "SurfSense - NotebookLM for Open Web Research",
|
||||
description:
|
||||
"SurfSense is an open-source competitive intelligence platform. Your AI agents monitor competitors, track rankings, and listen to your market through one API or MCP server.",
|
||||
"The open-source NotebookLM alternative for AI agents. Research the live web with structured data from Reddit, YouTube, Amazon, Google Maps, and Google Search, through one API or MCP server.",
|
||||
url: "https://www.surfsense.com",
|
||||
siteName: "SurfSense",
|
||||
type: "website",
|
||||
|
|
@ -79,16 +78,16 @@ export const metadata: Metadata = {
|
|||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "SurfSense - Competitive Intelligence Platform for AI Agents",
|
||||
alt: "SurfSense, open-source NotebookLM alternative for AI agents",
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "SurfSense - NotebookLM for Competitive Intelligence",
|
||||
title: "SurfSense - NotebookLM for Open Web Research",
|
||||
description:
|
||||
"SurfSense is an open-source competitive intelligence platform. Your AI agents monitor competitors, track rankings, and listen to your market through one API or MCP server.",
|
||||
"The open-source NotebookLM alternative for AI agents. Research the live web with structured data from Reddit, YouTube, Amazon, Google Maps, and Google Search, through one API or MCP server.",
|
||||
creator: "@SurfSenseAI",
|
||||
site: "@SurfSenseAI",
|
||||
images: [
|
||||
|
|
@ -96,7 +95,7 @@ export const metadata: Metadata = {
|
|||
url: "/og-image-twitter.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "SurfSense - Competitive Intelligence Platform for AI Agents",
|
||||
alt: "SurfSense, open-source NotebookLM alternative for AI agents",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
date: "2026-07-05"
|
||||
---
|
||||
|
||||
## SurfSense's Next Chapter: Competitive Intelligence for AI Agents
|
||||
## SurfSense's Next Chapter: Open Web Research for AI Agents
|
||||
|
||||
Happy Independence Day to everyone celebrating in the United States! We hope you had a wonderful Fourth of July. It felt like the right weekend for a declaration of our own.
|
||||
|
||||
Today we are pointing SurfSense at a single mission: **give your AI agents competitive intelligence.**
|
||||
Today we are pointing SurfSense at a single mission: **give your AI agents the primitives to research the open web.**
|
||||
|
||||
### How we got here
|
||||
|
||||
|
|
@ -14,20 +14,20 @@ For the past couple of months we set out to build the best general research agen
|
|||
|
||||
That same period settled a bigger question. Agentic tools like Claude, OpenCode, Hermes, and OpenClaw have proven themselves. Agents that plan, use tools, and finish multi-step work on their own are no longer a demo, they are how work gets done. General research assistance is becoming something every capable agent does out of the box.
|
||||
|
||||
So we asked what these agents still lack. Not reasoning. Not tools. **Live market data and the workflows around it.** An agent asked "what are competitors charging this week?" or "what is Reddit saying about us since the launch?" has nowhere trustworthy to look. We want to be one of the first, and hopefully the definitive, open-source competitive intelligence agent platforms. This focus also lets our team pour all of our energy into the high-value workflows that genuinely need AI automation.
|
||||
So we asked what these agents still lack. Not reasoning. Not tools. **Live data from the places where answers actually live, and the workflows around it.** An agent asked "what is Reddit saying about this product since launch?" or "what do the reviews of these ten places actually complain about?" has nowhere trustworthy to look. Official platform APIs are rate-limited or missing, scraping plumbing is brittle, and driving a browser with an LLM burns minutes and tokens per page. We want to be the open-source platform that gives agents those primitives.
|
||||
|
||||
### What SurfSense is now
|
||||
|
||||
- **Platform-native connectors for live market data.** [Reddit](/reddit), [YouTube](/youtube), [Google Maps](/google-maps), [Google Search](/google-search), and the [open web](/web-crawl). Each one is a typed REST endpoint you can call from any language with your SurfSense API key.
|
||||
- **Platform-native connectors for live web data.** [Reddit](/reddit), [YouTube](/youtube), [Google Maps](/google-maps), [Google Search](/google-search), and the [open web](/web-crawl). Each one is a typed REST endpoint returning structured JSON — posts, comments, transcripts, reviews, SERPs, pages — that you can call from any language with your SurfSense API key. And the catalog keeps growing beyond social platforms and search.
|
||||
- **An MCP server that makes every connector an agent tool.** Add the [SurfSense MCP server](/mcp-server) to Claude, Cursor, or your own agent framework. And with [external MCP connectors](/external-mcp-connectors), your SurfSense agents can use any MCP server too, with one-click OAuth for apps like Notion, Slack, and Jira.
|
||||
- **An agent harness, not just raw data.** Retries, structured output, and credit metering are built in, so agents go from a question to a brief without you building the plumbing.
|
||||
- **Automations for the workflows that matter.** Scheduled and event-triggered agents watch competitor pricing, track brand mentions, monitor rankings and reviews, then deliver briefs and alerts.
|
||||
- **An agent harness, not just raw data.** Retries, structured output, and credit metering are built in, so agents go from a question to a cited brief without you building the plumbing.
|
||||
- **Automations for the workflows that matter.** Scheduled and event-triggered agents watch pages, track mentions, monitor rankings and reviews, then deliver briefs and alerts.
|
||||
- **Simpler pricing.** Self-hosting stays free with no metering. Cloud is pay as you go: start with $5 of free credit, connectors bill per item returned, crawls per page fetched, models at provider cost. Failed calls are never billed. Details on the [pricing page](/pricing).
|
||||
|
||||
### To our existing users: nothing you rely on is going away
|
||||
|
||||
This is an addition, not a replacement. Your knowledge base, chat with citations, reports, podcasts, presentations, automations, and collaborative chats all keep working exactly as they do today. You can still upload files and sync Google Drive, OneDrive, and Dropbox. Self-hosting stays free and open source, and your existing deployments keep running without any action from you. What changes is where we invest: expect deeper connectors, sharper agent workflows, and broader market coverage.
|
||||
This is an addition, not a replacement. Your knowledge base, chat with citations, reports, podcasts, presentations, automations, and collaborative chats all keep working exactly as they do today. You can still upload files and sync Google Drive, OneDrive, and Dropbox. Self-hosting stays free and open source, and your existing deployments keep running without any action from you. What changes is where we invest: expect deeper connectors, sharper agent workflows, and broader coverage of the open web.
|
||||
|
||||
If you are building agents, [grab an API key](/login) or add the MCP server, and start with the [connector catalog](/connectors). Tell us what you are building on [Discord](https://discord.gg/ejRNvftDp9) or [GitHub](https://github.com/MODSetter/SurfSense).
|
||||
|
||||
We think the next great competitive intelligence tool will not be a dashboard. It will be an agent that already knows your market. We are building the platform it runs on.
|
||||
We think the next great research tool will not be another dashboard or another chat window. It will be an agent that can actually reach the live web. We are building the platform it runs on.
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
111
surfsense_web/components/homepage/logo-cloud.tsx
Normal 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: "UW–Madison", 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
298
surfsense_web/components/homepage/social-proof.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }} />
|
||||
|
|
|
|||
|
|
@ -181,13 +181,13 @@ function ComposerSuggestionSkeleton({
|
|||
}
|
||||
|
||||
export {
|
||||
ComposerSuggestionPopoverContent,
|
||||
ComposerSuggestionList,
|
||||
ComposerSuggestionGroup,
|
||||
ComposerSuggestionGroupHeading,
|
||||
ComposerSuggestionHeader,
|
||||
ComposerSuggestionItem,
|
||||
ComposerSuggestionSeparator,
|
||||
ComposerSuggestionList,
|
||||
ComposerSuggestionMessage,
|
||||
ComposerSuggestionPopoverContent,
|
||||
ComposerSuggestionSeparator,
|
||||
ComposerSuggestionSkeleton,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ export const GenerateImageToolUI = ({
|
|||
};
|
||||
|
||||
export {
|
||||
GenerateImageArgsSchema,
|
||||
GenerateImageResultSchema,
|
||||
type GenerateImageArgs,
|
||||
GenerateImageArgsSchema,
|
||||
type GenerateImageResult,
|
||||
GenerateImageResultSchema,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ export const UpdateMemoryToolUI = ({
|
|||
// ============================================================================
|
||||
|
||||
export {
|
||||
UpdateMemoryArgsSchema,
|
||||
UpdateMemoryResultSchema,
|
||||
type UpdateMemoryArgs,
|
||||
UpdateMemoryArgsSchema,
|
||||
type UpdateMemoryResult,
|
||||
UpdateMemoryResultSchema,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -157,4 +157,4 @@ export const WriteTodosToolUI = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { WriteTodosSchema, type WriteTodosData };
|
||||
export { type WriteTodosData, WriteTodosSchema };
|
||||
|
|
|
|||
|
|
@ -61,4 +61,4 @@ function AccordionContent({
|
|||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,4 +59,4 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
|||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
|
|
|
|||
|
|
@ -556,4 +556,4 @@ const TabsContent = forwardRef<
|
|||
});
|
||||
TabsContent.displayName = "TabsContent";
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
|
|
|
|||
|
|
@ -55,4 +55,4 @@ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">)
|
|||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount };
|
||||
export { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ function CollapsibleContent({
|
|||
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|
||||
|
|
|
|||
|
|
@ -150,11 +150,11 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">)
|
|||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -91,4 +91,4 @@ const ExpandedGifOverlay = ExpandedMediaOverlay;
|
|||
/** @deprecated Use useExpandedMedia instead */
|
||||
const useExpandedGif = useExpandedMedia;
|
||||
|
||||
export { ExpandedMediaOverlay, useExpandedMedia, ExpandedGifOverlay, useExpandedGif };
|
||||
export { ExpandedGifOverlay, ExpandedMediaOverlay, useExpandedGif, useExpandedMedia };
|
||||
|
|
|
|||
|
|
@ -140,12 +140,12 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -96,9 +96,9 @@ function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span"
|
|||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -122,11 +122,11 @@ function SheetDescription({
|
|||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -52,4 +52,4 @@ const TabsContent = React.forwardRef<
|
|||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
|
|
|
|||
|
|
@ -82,4 +82,4 @@ function TooltipContent({
|
|||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Connectors bring data into SurfSense — either as searchable knowledge or as li
|
|||
<Card
|
||||
icon={<Zap />}
|
||||
title="Native Connectors"
|
||||
description="SurfSense's built-in scraper APIs: Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, and Web Crawl — usable in chat, the API Playground, or via REST"
|
||||
description="SurfSense's built-in scraper APIs: Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, Indeed, Amazon, and Web Crawl — usable in chat, the API Playground, or via REST"
|
||||
href="/docs/connectors/native"
|
||||
/>
|
||||
<Card
|
||||
|
|
|
|||
67
surfsense_web/content/docs/connectors/native/amazon.mdx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: Amazon
|
||||
description: Scrape public Amazon product data as structured JSON
|
||||
---
|
||||
|
||||
The Amazon scraper returns public product data as structured JSON: price and list price, rating and review breakdown, availability, marketplace offers, sellers, best-seller ranks, product variants, and on-page reviews. It only uses public, anonymous data — no login or seller account. Give it search terms or Amazon product, search, category, or best-seller URLs.
|
||||
|
||||
Marketplace support is inferred from the Amazon URL or `domain` you provide. Supported public marketplaces include US (`amazon.com`), UK (`amazon.co.uk`), Germany (`amazon.de`), Italy (`amazon.it`), and Spain (`amazon.es`). France (`amazon.fr`) is supported in the scraper but remains best-effort because Amazon France is more sensitive to proxy-pool WAF challenges.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```bash
|
||||
POST /api/v1/workspaces/{workspace_id}/scrapers/amazon/scrape
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `urls` | — | Amazon product, search, category, best-seller, or short (`a.co` / `amzn.to`) URLs. Marketplace is inferred from the URL, e.g. `amazon.co.uk`, `amazon.de`, `amazon.it`, or `amazon.es`. Provide `urls` or `search_terms` |
|
||||
| `search_terms` | — | Search phrases run on the selected `domain`, e.g. `wireless earbuds`. Provide `search_terms` or `urls` |
|
||||
| `max_items` | `10` | Max products per search term or category/best-seller URL (1–100) |
|
||||
| `domain` | `www.amazon.com` | Amazon marketplace domain for `search_terms`, e.g. `www.amazon.co.uk`, `www.amazon.de`, `www.amazon.it`, or `www.amazon.es`. `www.amazon.fr` is best-effort due to Amazon WAF sensitivity |
|
||||
| `include_details` | `true` | Fetch full product detail pages; `false` returns faster card-only results |
|
||||
| `max_offers` | `0` | Extra marketplace offers per product (0–100); `0` = featured offer only |
|
||||
| `include_sellers` | `false` | Enrich the product and each offer with the seller's public profile |
|
||||
| `max_variants` | `0` | Product variants to return as separate results (0–100) |
|
||||
| `include_variant_prices` | `false` | Attach per-variant prices (one extra request per variant) |
|
||||
| `country_code` | — | Two-letter delivery country for localized pricing, e.g. `us` |
|
||||
| `zip_code` | — | Delivery ZIP/postal code for localized availability, e.g. `10001` |
|
||||
| `language` | — | Content language for the domain, e.g. `en` |
|
||||
|
||||
At least one of `urls` or `search_terms` is required, with up to 20 combined sources per call.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/workspaces/1/scrapers/amazon/scrape" \
|
||||
-H "Authorization: Bearer $SURFSENSE_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"search_terms": ["mechanical keyboard"],
|
||||
"max_items": 5,
|
||||
"max_offers": 3
|
||||
}'
|
||||
```
|
||||
|
||||
## Marketplace examples
|
||||
|
||||
```json
|
||||
{
|
||||
"urls": [
|
||||
"https://www.amazon.co.uk/s?k=usb+c+cable",
|
||||
"https://www.amazon.de/s?k=mechanische+tastatur",
|
||||
"https://www.amazon.it/s?k=cavo+usb+c",
|
||||
"https://www.amazon.es/s?k=cable+usb+c"
|
||||
],
|
||||
"max_items": 5,
|
||||
"include_details": false
|
||||
}
|
||||
```
|
||||
|
||||
For `search_terms`, set `domain` to the marketplace you want to search. For direct URLs, the scraper infers the marketplace from each URL.
|
||||
|
||||
The response is `{ "items": [...] }` — one item per product, plus structured error items for any input that could not be resolved. Billing is per returned product; error items are never billed.
|
||||
|
||||
For the full input and output JSON schemas and generated code snippets in your language, open **API Playground → Amazon → Scrape** in your workspace.
|
||||
51
surfsense_web/content/docs/connectors/native/indeed.mdx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: Indeed
|
||||
description: Scrape public Indeed job postings by search, company, or URL
|
||||
---
|
||||
|
||||
The Indeed scraper pulls structured public job postings from Indeed. Give it search terms and/or URLs (a search page, a company jobs page, or a single job), and it returns jobs with title, company, location, salary, job types, benefits, remote/hybrid flag, posting age, and description.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```bash
|
||||
POST /api/v1/workspaces/{workspace_id}/scrapers/indeed/scrape
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
At least one of `urls` or `search_queries` is required.
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `urls` | — | Indeed URLs: a search page (`/jobs?q=&l=`), a company jobs page (`/cmp/<slug>/jobs`), or a single job (`/viewjob?jk=`) (max 20 sources per call, combined with queries) |
|
||||
| `search_queries` | — | Job search terms, e.g. `["data analyst"]` |
|
||||
| `country` | `us` | Country code selecting the Indeed domain, e.g. `us`, `gb`, `de` |
|
||||
| `location` | — | Where to search, e.g. `Remote`, `New York, NY` |
|
||||
| `radius` | — | Search radius in miles/km around `location` |
|
||||
| `job_type` | — | `fulltime`, `parttime`, `contract`, `internship`, `temporary`, `permanent`, `seasonal`, or `freelance` |
|
||||
| `level` | — | `entry_level`, `mid_level`, or `senior_level` |
|
||||
| `remote` | — | `remote` or `hybrid` |
|
||||
| `from_days` | — | Only return jobs posted within the last N days |
|
||||
| `sort` | `relevance` | `relevance` or `date` |
|
||||
| `scrape_job_details` | `false` | Fetch each job's detail page for the full description (slower: one extra page load per job) |
|
||||
| `max_items` | `25` | Max total jobs returned across all sources (hard cap 100) |
|
||||
| `max_items_per_query` | `25` | Max jobs per search/company target |
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/workspaces/1/scrapers/indeed/scrape" \
|
||||
-H "Authorization: Bearer $SURFSENSE_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"search_queries": ["data analyst"],
|
||||
"location": "Remote",
|
||||
"remote": "remote",
|
||||
"sort": "date",
|
||||
"max_items": 30
|
||||
}'
|
||||
```
|
||||
|
||||
The response is `{ "items": [...] }` — one item per job posting. Billing is per returned job.
|
||||
|
||||
For the full input and output JSON schemas and generated code snippets in your language, open **API Playground → Indeed → Scrape** in your workspace.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Native Connectors
|
||||
description: SurfSense's built-in scraper APIs for Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, and the web
|
||||
description: SurfSense's built-in scraper APIs for Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, Indeed, Amazon, and the web
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card';
|
||||
|
|
@ -38,6 +38,16 @@ Native connectors are SurfSense's own scraper APIs — built into the platform,
|
|||
description="Structured SERPs: organic results, people-also-ask, AI overviews"
|
||||
href="/docs/connectors/native/google-search"
|
||||
/>
|
||||
<Card
|
||||
title="Indeed"
|
||||
description="Job postings by search, company, or URL — with salary and full description"
|
||||
href="/docs/connectors/native/indeed"
|
||||
/>
|
||||
<Card
|
||||
title="Amazon"
|
||||
description="Public product data: prices, ratings, offers, sellers, and best-seller ranks"
|
||||
href="/docs/connectors/native/amazon"
|
||||
/>
|
||||
<Card
|
||||
title="Web Crawl"
|
||||
description="Scrape any page or spider a whole site into clean markdown"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
"tiktok",
|
||||
"google-maps",
|
||||
"google-search",
|
||||
"indeed",
|
||||
"amazon",
|
||||
"web-crawl"
|
||||
],
|
||||
"defaultOpen": false
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ curl -X POST "$BASE_URL/api/v1/workspaces/1/scrapers/web/crawl" \
|
|||
|
||||
One item per fetched page (in crawl order) with its markdown, metadata (title, description), crawl provenance (depth, referrer), links classified as internal/external/social/email/tel, and page-level contacts. The response also includes a site-wide `contacts` summary that deduplicates every email, phone, and social profile found — `siteWide: true` marks header/footer values (the company's own contacts) versus page-local finds like individual team members.
|
||||
|
||||
Contact details often live on about/contact/privacy pages, so crawl with `maxCrawlDepth >= 1` to surface them — useful for lead generation and competitive intelligence.
|
||||
Contact details often live on about/contact/privacy pages, so crawl with `maxCrawlDepth >= 1` to surface them — useful for lead generation and company research.
|
||||
|
||||
Billing is per successfully fetched page.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
|||
|
||||
# SurfSense MCP Server
|
||||
|
||||
The SurfSense MCP server exposes your workspace to any [Model Context Protocol](https://modelcontextprotocol.io/) client. Your agent gets 24 native, typed tools: every scraper (Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, web crawl), full knowledge-base access (search, read, add, upload, update, delete), and a workspace selector.
|
||||
The SurfSense MCP server exposes your workspace to any [Model Context Protocol](https://modelcontextprotocol.io/) client. Your agent gets 26 native, typed tools: every scraper (Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, Indeed, Amazon, web crawl), full knowledge-base access (search, read, add, upload, update, delete), and a workspace selector.
|
||||
|
||||
Connect it two ways: the **hosted** server at `https://mcp.surfsense.com/mcp` (nothing to install — just an API key), or run it yourself over **stdio** against any SurfSense backend, cloud or self-hosted.
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ Add to `~/.cursor/mcp.json` (global — keeps the key out of your repo) or a pro
|
|||
}
|
||||
```
|
||||
|
||||
Then open **Cursor Settings → MCP** and refresh the `surfsense` server; its 24 tools should appear with a green dot.
|
||||
Then open **Cursor Settings → MCP** and refresh the `surfsense` server; its 25 tools should appear with a green dot.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Claude Desktop">
|
||||
|
|
@ -257,14 +257,14 @@ For self-host (stdio), all settings are environment variables passed by the clie
|
|||
- **401 errors** — the API key is wrong or expired; create a new one.
|
||||
- **403 errors** — API access is disabled for the workspace; toggle **API key access** on under **API Playground → API Keys**.
|
||||
- **"Could not reach SurfSense"** — the backend isn't running or `SURFSENSE_BASE_URL` is wrong.
|
||||
- **Server won't start** — run `uv run python -m mcp_server.selfcheck` inside `surfsense_mcp`; it verifies all 24 tools register without needing a backend.
|
||||
- **Server won't start** — run `uv run python -m mcp_server.selfcheck` inside `surfsense_mcp`; it verifies all 25 tools register without needing a backend.
|
||||
|
||||
## Tools reference
|
||||
|
||||
| Group | Tools |
|
||||
|-------|-------|
|
||||
| Workspaces | `surfsense_list_workspaces`, `surfsense_select_workspace` |
|
||||
| Scrapers | `surfsense_reddit_scrape`, `surfsense_youtube_scrape`, `surfsense_youtube_comments`, `surfsense_instagram_scrape`, `surfsense_instagram_details`, `surfsense_tiktok_scrape`, `surfsense_tiktok_comments`, `surfsense_tiktok_user_search`, `surfsense_tiktok_trending`, `surfsense_google_maps_scrape`, `surfsense_google_maps_reviews`, `surfsense_google_search`, `surfsense_web_crawl`, `surfsense_list_scraper_runs`, `surfsense_get_scraper_run` |
|
||||
| Scrapers | `surfsense_reddit_scrape`, `surfsense_youtube_scrape`, `surfsense_youtube_comments`, `surfsense_instagram_scrape`, `surfsense_instagram_details`, `surfsense_tiktok_scrape`, `surfsense_tiktok_comments`, `surfsense_tiktok_user_search`, `surfsense_tiktok_trending`, `surfsense_google_maps_scrape`, `surfsense_google_maps_reviews`, `surfsense_google_search`, `surfsense_indeed_scrape`, `surfsense_web_crawl`, `surfsense_list_scraper_runs`, `surfsense_get_scraper_run` |
|
||||
| Knowledge base | `surfsense_search_knowledge_base`, `surfsense_list_documents`, `surfsense_get_document`, `surfsense_add_document`, `surfsense_upload_file`, `surfsense_update_document`, `surfsense_delete_document` |
|
||||
|
||||
Usage is billed exactly like the REST API — scraper tools are metered per returned item, and every call is recorded under **API Playground → Runs**.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "@/lib/announcements/announcements-storage";
|
||||
import {
|
||||
getActiveAnnouncements,
|
||||
getPublishedAnnouncements,
|
||||
msUntilNextTransition,
|
||||
} from "@/lib/announcements/announcements-utils";
|
||||
import { isAuthenticated } from "@/lib/auth-utils";
|
||||
|
|
@ -56,10 +57,16 @@ export interface AnnouncementWithState extends Announcement {
|
|||
interface UseAnnouncementsOptions {
|
||||
/** Filter by category */
|
||||
category?: AnnouncementCategory;
|
||||
/**
|
||||
* Include announcements whose visibility window has expired (archive
|
||||
* views). Defaults to false so spotlights, toasts, and unread badges
|
||||
* only consider currently-active announcements.
|
||||
*/
|
||||
includeExpired?: boolean;
|
||||
}
|
||||
|
||||
export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
|
||||
const { category } = options;
|
||||
const { category, includeExpired = false } = options;
|
||||
|
||||
// Subscribe to state changes (re-renders when localStorage state is bumped)
|
||||
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
|
|
@ -71,12 +78,13 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
|
|||
const enriched: AnnouncementWithState[] = useMemo(() => {
|
||||
const authed = isAuthenticated();
|
||||
const now = new Date();
|
||||
let items: AnnouncementWithState[] = getActiveAnnouncements(announcements, authed, now).map(
|
||||
(a) => ({
|
||||
...a,
|
||||
isRead: isAnnouncementRead(a.id),
|
||||
})
|
||||
);
|
||||
const visible = includeExpired
|
||||
? getPublishedAnnouncements(announcements, authed, now)
|
||||
: getActiveAnnouncements(announcements, authed, now);
|
||||
let items: AnnouncementWithState[] = visible.map((a) => ({
|
||||
...a,
|
||||
isRead: isAnnouncementRead(a.id),
|
||||
}));
|
||||
|
||||
if (category) {
|
||||
items = items.filter((a) => a.category === category);
|
||||
|
|
@ -86,7 +94,7 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
|
|||
|
||||
return items;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [category, stateVersion, tick]);
|
||||
}, [category, includeExpired, stateVersion, tick]);
|
||||
|
||||
// Schedule a re-render when the next announcement starts or expires
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import type { Announcement } from "@/contracts/types/announcement.types";
|
|||
export const announcements: Announcement[] = [
|
||||
{
|
||||
id: "2026-07-05-competitive-intelligence-direction",
|
||||
title: "SurfSense's Next Chapter: Competitive Intelligence for AI Agents",
|
||||
title: "SurfSense's Next Chapter: Open Web Research for AI Agents",
|
||||
description:
|
||||
"Happy Independence Day to everyone celebrating in the United States! We picked this week to share some big news: SurfSense is now the open-source competitive intelligence agent platform. Your agents monitor competitors, track rankings, and listen to your market with live data from Reddit, YouTube, Google Maps, Google Search, and the open web, through one REST API or MCP server. Everything you rely on today keeps working, and self-hosting stays free.",
|
||||
"Happy Independence Day to everyone celebrating in the United States! We picked this week to share some big news: SurfSense is now the open-source open web research platform for AI agents. Your agents research the live web with structured data from Reddit, YouTube, Google Maps, Google Search, and any page on the open web, through one REST API or MCP server. Everything you rely on today keeps working, and self-hosting stays free.",
|
||||
category: "update",
|
||||
date: "2026-07-05T00:00:00Z",
|
||||
startTime: "2026-07-05T00:00:00Z",
|
||||
|
|
@ -27,7 +27,7 @@ export const announcements: Announcement[] = [
|
|||
spotlight: true,
|
||||
image: {
|
||||
src: "/announcements/competitive-intelligence.png",
|
||||
alt: "Platform data tiles for social, video, maps, search, and the web flowing into a central AI core that outputs market charts and alerts.",
|
||||
alt: "Platform data tiles for social, video, maps, search, and the web flowing into a central AI core that outputs research briefs and alerts.",
|
||||
},
|
||||
link: {
|
||||
label: "Read the full announcement",
|
||||
|
|
@ -54,28 +54,17 @@ export const announcements: Announcement[] = [
|
|||
url: "/changelog",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "announcement-1",
|
||||
title: "Introducing What's New",
|
||||
description: "All major product updates will be posted here.",
|
||||
category: "feature",
|
||||
date: "2026-02-17T00:00:00Z",
|
||||
startTime: "2026-02-17T00:00:00Z",
|
||||
endTime: "2026-02-20T00:00:00Z",
|
||||
audience: "all",
|
||||
isImportant: false,
|
||||
},
|
||||
{
|
||||
id: "announcement-6",
|
||||
title: "Past Test Announcement",
|
||||
description: "This should be seen by nobody, because it's in the past.",
|
||||
category: "maintenance",
|
||||
date: "2026-02-17T00:00:00Z",
|
||||
startTime: "2026-02-15T23:23:00Z",
|
||||
endTime: "2026-02-16T00:00:00Z",
|
||||
audience: "users",
|
||||
isImportant: true,
|
||||
},
|
||||
// {
|
||||
// id: "announcement-1",
|
||||
// title: "Introducing What's New",
|
||||
// description: "All major product updates will be posted here.",
|
||||
// category: "feature",
|
||||
// date: "2026-02-17T00:00:00Z",
|
||||
// startTime: "2026-02-17T00:00:00Z",
|
||||
// endTime: "2026-02-20T00:00:00Z",
|
||||
// audience: "all",
|
||||
// isImportant: false,
|
||||
// },
|
||||
// {
|
||||
// id: "2026-02-10-podcast-improvements",
|
||||
// title: "Podcast Generation Improvements",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ export function announcementMatchesAudience(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the announcement has been published (startTime has
|
||||
* passed), regardless of whether its visibility window has expired.
|
||||
* Used for archive views like the announcements page.
|
||||
*/
|
||||
export function isAnnouncementPublished(announcement: Announcement, now = new Date()): boolean {
|
||||
const start = new Date(announcement.startTime).getTime();
|
||||
if (Number.isNaN(start)) return false;
|
||||
return now.getTime() >= start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter announcements to all published ones (including expired) targeted
|
||||
* at the given audience. Powers archive views; use getActiveAnnouncements
|
||||
* for toasts, spotlights, and unread badges.
|
||||
*/
|
||||
export function getPublishedAnnouncements(
|
||||
announcements: Announcement[],
|
||||
isAuthenticated: boolean,
|
||||
now = new Date()
|
||||
): Announcement[] {
|
||||
return announcements.filter(
|
||||
(a) => isAnnouncementPublished(a, now) && announcementMatchesAudience(a, isAuthenticated)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter announcements to only those that are currently active and
|
||||
* targeted at the given audience.
|
||||
|
|
|
|||
|
|
@ -426,6 +426,6 @@ export interface ObsidianStats {
|
|||
last_sync_at: string | null;
|
||||
}
|
||||
|
||||
export type { SlackChannel, DiscordChannel };
|
||||
export type { DiscordChannel, SlackChannel };
|
||||
|
||||
export const connectorsApiService = new ConnectorsApiService();
|
||||
|
|
|
|||
|
|
@ -43,10 +43,7 @@ class ModelConnectionsApiService {
|
|||
};
|
||||
|
||||
getLlmSetupStatus = async (workspaceId: number): Promise<LlmSetupStatus> => {
|
||||
return baseApiService.get(
|
||||
`/api/v1/workspaces/${workspaceId}/llm-setup-status`,
|
||||
llmSetupStatus
|
||||
);
|
||||
return baseApiService.get(`/api/v1/workspaces/${workspaceId}/llm-setup-status`, llmSetupStatus);
|
||||
};
|
||||
|
||||
getModelProviders = async (): Promise<ModelProviderRead[]> => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ const PUBLIC_ROUTE_PREFIXES = [
|
|||
"/youtube",
|
||||
"/google-maps",
|
||||
"/google-search",
|
||||
"/indeed",
|
||||
"/web-crawl",
|
||||
"/amazon",
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -20,43 +20,44 @@ export interface ChatExampleCategory {
|
|||
|
||||
export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
|
||||
{
|
||||
id: "monitor",
|
||||
label: "Monitor Competitors",
|
||||
id: "research",
|
||||
label: "Research the Web",
|
||||
prompts: [
|
||||
"Extract every plan, price, and limit from [competitor]'s pricing page",
|
||||
"Crawl [competitor]'s changelog and brief me on what they shipped this month",
|
||||
"Track who ranks and runs ads for [keyword] in the US",
|
||||
"Which of our target keywords trigger an AI Overview, and who gets cited?",
|
||||
"Research [topic] across the live web and give me a cited brief",
|
||||
"Map who ranks for [keyword], crawl each result, and compare their claims in one table",
|
||||
"Pull the Google Maps reviews for [business] and summarize the top complaints",
|
||||
"Which queries about [topic] trigger an AI Overview, and who gets cited?",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "listen",
|
||||
label: "Market Listening",
|
||||
label: "Community Listening",
|
||||
prompts: [
|
||||
"Find 20 Reddit posts where people ask for an alternative to [product]",
|
||||
"Analyze the comments on [channel]'s last 10 videos and cluster the complaints",
|
||||
"Pull the Google Maps reviews for [business] and summarize the top complaints",
|
||||
"Find people actively looking to switch away from [competitor] this month",
|
||||
"What is Reddit saying about [topic] this week?",
|
||||
"Pull the top TikTok videos for [hashtag] and summarize the trend",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "workflows",
|
||||
label: "Multi-Connector",
|
||||
id: "monitor",
|
||||
label: "Monitor Competitors",
|
||||
prompts: [
|
||||
"Extract every plan, price, and limit from [competitor]'s pricing page",
|
||||
"Track the Amazon price, rating, and offers for [product] and its top rivals",
|
||||
"Crawl [competitor]'s changelog and brief me on what they shipped this month",
|
||||
"Measure the reaction to [competitor]'s launch across search, Reddit, and YouTube",
|
||||
"Find the top-rated [category] businesses in [city], crawl their sites, and build a lead list with contacts",
|
||||
"Map who ranks for [keyword], crawl each result, and compare their pricing in one table",
|
||||
"Build a 360 on [competitor]: site changes, rank movements, Reddit sentiment, and YouTube reaction",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "automate",
|
||||
label: "Automate",
|
||||
prompts: [
|
||||
"Re-check [competitor]'s pricing page daily and alert me on any change",
|
||||
"Every Monday, crawl our competitors' changelogs and send me a brief",
|
||||
"Watch [url] daily and alert me on any change",
|
||||
"Track our brand mentions on Reddit daily and tag buying intent",
|
||||
"Send me a weekly report on [keyword] rankings and AI Overview citations",
|
||||
"Every Monday, crawl [site]'s changelog and send me a brief",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
320
surfsense_web/lib/connectors-marketing/amazon.tsx
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import { IconBrandAmazon } from "@tabler/icons-react";
|
||||
import type { ConnectorPageContent } from "./types";
|
||||
|
||||
export const amazon: ConnectorPageContent = {
|
||||
slug: "amazon",
|
||||
name: "Amazon",
|
||||
cardTitle: "Amazon Product API",
|
||||
icon: IconBrandAmazon,
|
||||
|
||||
metaTitle: "Amazon Product API for Price and Review Data | SurfSense",
|
||||
metaDescription:
|
||||
"Scrape public Amazon product data as structured JSON across US, UK, Germany, Italy, and Spain: prices, ratings, offers, sellers, and ranks. Start free.",
|
||||
keywords: [
|
||||
"amazon product api",
|
||||
"amazon scraper api",
|
||||
"amazon price scraper",
|
||||
"scrape amazon product data",
|
||||
"amazon product data api",
|
||||
"amazon review scraper",
|
||||
"amazon best sellers api",
|
||||
"amazon asin lookup",
|
||||
"amazon price tracking api",
|
||||
"amazon competitor monitoring",
|
||||
"amazon offers scraper",
|
||||
"ecommerce product api",
|
||||
],
|
||||
|
||||
h1: "Amazon Product API for Price, Review, and Offer Data",
|
||||
heroLede:
|
||||
"The SurfSense Amazon Product API scrapes public Amazon listings as structured JSON: price and list price, rating and review breakdown, availability, marketplace offers, sellers, best-seller ranks, and on-page reviews. Point your AI agents at a search term or product URL and track prices across US, UK, German, Italian, and Spanish marketplaces — no login, only public data.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Track the price and rating of the top mechanical keyboards on Amazon",
|
||||
toolCall:
|
||||
'amazon.scrape({ search_terms: ["mechanical keyboard"],\n max_items: 5, max_offers: 3 })',
|
||||
rows: [
|
||||
{
|
||||
primary: "Keychron K8 Pro — $89.99 (was $99.99)",
|
||||
secondary: "4.7 stars · 12,431 ratings · In Stock",
|
||||
tag: "-10%",
|
||||
},
|
||||
{
|
||||
primary: "3 marketplace offers, cheapest $84.50 used",
|
||||
secondary: "sold by 2 third-party sellers",
|
||||
tag: "offers",
|
||||
},
|
||||
{
|
||||
primary: "#2 in Computer Keyboards best-sellers",
|
||||
secondary: "Electronics › Accessories › Keyboards",
|
||||
tag: "rank",
|
||||
},
|
||||
],
|
||||
resultSummary: "5 products · 14 offers · 5 best-seller ranks · surfaced in 3.1s",
|
||||
},
|
||||
|
||||
extractIntro:
|
||||
"Give the API a list of product/search/category/best-seller URLs or search terms and a domain. Marketplace is inferred from each Amazon URL, or from the domain used with search terms. Supported marketplaces include US, UK, Germany, Italy, and Spain; France is best-effort because Amazon France is more WAF-sensitive.",
|
||||
extractFields: [
|
||||
{
|
||||
label: "Product core",
|
||||
description:
|
||||
"Title, ASIN, brand, price, list price, availability, condition, and canonical URL for every product.",
|
||||
},
|
||||
{
|
||||
label: "Ratings & reviews",
|
||||
description:
|
||||
"Star rating, review count, the 5-to-1-star histogram, and on-page customer reviews with text, author, and date.",
|
||||
},
|
||||
{
|
||||
label: "Marketplace offers",
|
||||
description:
|
||||
"Additional buy-box offers with price, condition, delivery, and the third-party seller behind each.",
|
||||
},
|
||||
{
|
||||
label: "Sellers",
|
||||
description:
|
||||
"Public seller profile summaries — name, rating, and feedback count — for the featured and offer sellers.",
|
||||
},
|
||||
{
|
||||
label: "Best-seller ranks",
|
||||
description:
|
||||
"Category rank positions and best-seller list placements, for demand and category monitoring.",
|
||||
},
|
||||
{
|
||||
label: "Variants & media",
|
||||
description:
|
||||
"Variant ASINs and attributes (color, size), per-variant prices, gallery and high-resolution images.",
|
||||
},
|
||||
],
|
||||
|
||||
useCasesHeading: "What teams do with the Amazon Product API",
|
||||
useCases: [
|
||||
{
|
||||
title: "Price and buy-box tracking",
|
||||
description:
|
||||
"Watch your own and competitors' prices, list prices, and marketplace offers across US, UK, German, Italian, and Spanish marketplaces. Feed each run to an agent that diffs prices and alerts you when a competitor undercuts or the buy box changes hands.",
|
||||
},
|
||||
{
|
||||
title: "Review mining and product research",
|
||||
description:
|
||||
"Pull ratings, the star histogram, and on-page reviews to understand what customers love and complain about, then brief an agent to summarize themes and inform your roadmap or listing copy.",
|
||||
},
|
||||
{
|
||||
title: "Best-seller and category monitoring",
|
||||
description:
|
||||
"Track best-seller ranks in the categories you care about to spot rising products and demand shifts before they show up anywhere else.",
|
||||
},
|
||||
{
|
||||
title: "Catalog and seller intelligence",
|
||||
description:
|
||||
"Enrich a catalog by ASIN with structured attributes, variants, and images, and see which third-party sellers are winning offers on the products that matter to you.",
|
||||
},
|
||||
],
|
||||
|
||||
comparison: {
|
||||
heading: "An Amazon scraper API built for agents",
|
||||
intro:
|
||||
"Most Amazon data APIs bill per request, meter add-ons separately, and leave the discovery-to-detail logic to you. Here is how SurfSense compares.",
|
||||
columnLabel: "Typical Amazon data API",
|
||||
rows: [
|
||||
{
|
||||
feature: "Pricing",
|
||||
official: "Per-request pricing that climbs fast at scale",
|
||||
surfsense: "Pay per product returned, with a free tier to start",
|
||||
},
|
||||
{
|
||||
feature: "Discovery",
|
||||
official: "Separate search and detail endpoints you stitch together",
|
||||
surfsense: "One verb takes search terms, product, category, or best-seller URLs",
|
||||
},
|
||||
{
|
||||
feature: "Offers & sellers",
|
||||
official: "Often separate paid endpoints",
|
||||
surfsense: "Offers and seller profiles enriched inline on the product",
|
||||
},
|
||||
{
|
||||
feature: "Data access",
|
||||
official: "Requires accounts, keys, or approval for many providers",
|
||||
surfsense: "Public, anonymous data only — no login or seller account",
|
||||
},
|
||||
{
|
||||
feature: "Agent-ready",
|
||||
official: "No; you wire the harness yourself",
|
||||
surfsense: "MCP server exposes amazon.scrape as a native tool",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
api: {
|
||||
platform: "amazon",
|
||||
verb: "scrape",
|
||||
mcpTool: "amazon.scrape",
|
||||
requestBody: {
|
||||
search_terms: ["mechanical keyboard"],
|
||||
max_items: 5,
|
||||
max_offers: 3,
|
||||
},
|
||||
},
|
||||
|
||||
schema: {
|
||||
requestNote:
|
||||
"Provide urls or search_terms (at least one). Marketplace is inferred from Amazon URLs, or from domain for search terms. Up to 20 combined sources per call.",
|
||||
request: [
|
||||
{
|
||||
name: "urls",
|
||||
type: "string[]",
|
||||
description:
|
||||
"Amazon product, search, category, best-seller, or short (a.co / amzn.to) URLs. Supports amazon.com, amazon.co.uk, amazon.de, amazon.it, and amazon.es; amazon.fr is best-effort. Provide urls or search_terms.",
|
||||
},
|
||||
{
|
||||
name: "search_terms",
|
||||
type: "string[]",
|
||||
description:
|
||||
"Search phrases run on the Amazon domain, e.g. 'wireless earbuds'. Provide search_terms or urls.",
|
||||
},
|
||||
{
|
||||
name: "max_items",
|
||||
type: "integer",
|
||||
defaultValue: "10",
|
||||
description: "Max products per search term or category/best-seller URL, 1 to 100.",
|
||||
},
|
||||
{
|
||||
name: "domain",
|
||||
type: "string",
|
||||
defaultValue: '"www.amazon.com"',
|
||||
description:
|
||||
"Amazon marketplace domain for search_terms, e.g. 'www.amazon.co.uk', 'www.amazon.de', 'www.amazon.it', or 'www.amazon.es'. 'www.amazon.fr' is best-effort due to WAF sensitivity.",
|
||||
},
|
||||
{
|
||||
name: "include_details",
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
description: "Fetch full product detail pages. false returns faster card-only results.",
|
||||
},
|
||||
{
|
||||
name: "max_offers",
|
||||
type: "integer",
|
||||
defaultValue: "0",
|
||||
description:
|
||||
"Extra marketplace offers to fetch per product, 0 to 100. 0 returns the featured offer only.",
|
||||
},
|
||||
{
|
||||
name: "include_sellers",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
description: "Enrich the product and each offer with the seller's public profile.",
|
||||
},
|
||||
{
|
||||
name: "max_variants",
|
||||
type: "integer",
|
||||
defaultValue: "0",
|
||||
description: "Product variants to return as separate results, 0 to 100.",
|
||||
},
|
||||
{
|
||||
name: "include_variant_prices",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
description: "Attach per-variant prices (one extra request per variant).",
|
||||
},
|
||||
{
|
||||
name: "country_code",
|
||||
type: "string",
|
||||
description: "Two-letter delivery country for localized pricing, e.g. 'us'.",
|
||||
},
|
||||
{
|
||||
name: "zip_code",
|
||||
type: "string",
|
||||
description: "Delivery ZIP/postal code for localized availability, e.g. '10001'.",
|
||||
},
|
||||
{
|
||||
name: "language",
|
||||
type: "string",
|
||||
description: "Content language for the domain, e.g. 'en'.",
|
||||
},
|
||||
],
|
||||
responseNote:
|
||||
"The response is { items: [...] } with one item per product. One returned product is one billable unit; error items are never billed.",
|
||||
response: [
|
||||
{
|
||||
name: "title / asin / brand",
|
||||
type: "string",
|
||||
description: "Product identity: display title, ASIN, and brand.",
|
||||
},
|
||||
{
|
||||
name: "price / listPrice",
|
||||
type: "object",
|
||||
description: "Current price and strike-through list price, each with value and currency.",
|
||||
},
|
||||
{
|
||||
name: "stars / reviewsCount / starsBreakdown",
|
||||
type: "object",
|
||||
description:
|
||||
"Average rating, total review count, and the 5-to-1-star distribution as fractions.",
|
||||
},
|
||||
{
|
||||
name: "offers",
|
||||
type: "object[]",
|
||||
description:
|
||||
"Marketplace offers with price, condition, delivery, seller, and pinned-offer flag.",
|
||||
},
|
||||
{
|
||||
name: "seller",
|
||||
type: "object",
|
||||
description: "Featured seller profile: id, name, url, rating, and feedback count.",
|
||||
},
|
||||
{
|
||||
name: "bestsellerRanks",
|
||||
type: "object[]",
|
||||
description: "Category rank positions and best-seller list placements.",
|
||||
},
|
||||
{
|
||||
name: "variantAsins / variantDetails / variantAttributes",
|
||||
type: "object[]",
|
||||
description: "Related variant ASINs and their attributes and per-variant prices.",
|
||||
},
|
||||
{
|
||||
name: "productPageReviews",
|
||||
type: "object[]",
|
||||
description: "On-page customer reviews with text, author, rating, and date.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
faq: [
|
||||
{
|
||||
question: "What is an Amazon product API?",
|
||||
answer:
|
||||
"An Amazon product API returns a listing's data as structured JSON instead of raw HTML. The SurfSense Amazon API scrapes public product pages and gives you price, rating, review breakdown, offers, sellers, and best-seller ranks as clean JSON your agents can read.",
|
||||
},
|
||||
{
|
||||
question: "Can I track Amazon prices with it?",
|
||||
answer:
|
||||
"Yes. Each product returns its current price, list price, and marketplace offers. Run the same URLs or search terms on a schedule and diff the prices to build price and buy-box tracking, without maintaining scrapers or proxies yourself.",
|
||||
},
|
||||
{
|
||||
question: "Does it only use public data?",
|
||||
answer:
|
||||
"It does. The scraper collects public, anonymous product data only — no login, seller account, or authenticated APIs. That covers product details, ratings, on-page reviews, offers, public seller profiles, and best-seller rankings.",
|
||||
},
|
||||
{
|
||||
question: "How do I look up a product by ASIN or search term?",
|
||||
answer:
|
||||
"Pass a product URL (which contains the ASIN) in urls, or a phrase in search_terms to discover products on the domain. You can also pass search, category, and best-seller URLs. One verb, amazon.scrape, handles all of them.",
|
||||
},
|
||||
{
|
||||
question: "Which Amazon marketplaces are supported?",
|
||||
answer:
|
||||
"Marketplace routing is inferred from the Amazon URL or domain. SurfSense supports public Amazon data from US, UK, Germany, Italy, and Spain. France is wired in and retried like the others, but remains best-effort because Amazon France is more sensitive to proxy-pool WAF challenges.",
|
||||
},
|
||||
],
|
||||
|
||||
related: [
|
||||
{ label: "Google Search API", href: "/google-search" },
|
||||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "Reddit API", href: "/reddit" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
{ label: "Read the docs", href: "/docs" },
|
||||
],
|
||||
};
|
||||
|
|
@ -21,9 +21,9 @@ export const googleMaps: ConnectorPageContent = {
|
|||
"google maps scraper python",
|
||||
],
|
||||
|
||||
h1: "Google Maps Scraper API for Lead Generation and Local Market Intelligence",
|
||||
h1: "Google Maps Scraper API for Lead Generation and Local Business Research",
|
||||
heroLede:
|
||||
"The SurfSense Google Maps API turns Maps into a B2B lead engine. Extract business names, categories, phones, websites, ratings, and reviews at scale, then point your AI agents at any territory or category to build lead lists and map a local market in minutes.",
|
||||
"The SurfSense Google Maps API turns Maps into a B2B lead engine. Extract business names, categories, phones, websites, ratings, and reviews at scale, then point your AI agents at any territory or category to build lead lists and map any local category in minutes.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Find every dentist in Austin without a website",
|
||||
|
|
@ -306,6 +306,7 @@ export const googleMaps: ConnectorPageContent = {
|
|||
{ label: "Instagram API", href: "/instagram" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
{ label: "Read the docs", href: "/docs" },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ export const googleSearch: ConnectorPageContent = {
|
|||
cardTitle: "SERP API",
|
||||
icon: IconBrandGoogle,
|
||||
|
||||
metaTitle: "SERP API for Rank Tracking and Competitor Intel | SurfSense",
|
||||
metaTitle: "SERP API for Rank Tracking and AI Overview Monitoring | SurfSense",
|
||||
metaDescription:
|
||||
"Track rankings, competitor ads, and Google AI Overviews with the SurfSense SERP API. Scrape Google Search results as structured JSON via API or MCP. Start free.",
|
||||
"Track rankings, ads, and Google AI Overviews with the SurfSense SERP API. Scrape Google Search results as structured JSON via API or MCP. Start free.",
|
||||
keywords: [
|
||||
"serp api",
|
||||
"serp api alternative",
|
||||
|
|
@ -25,9 +25,9 @@ export const googleSearch: ConnectorPageContent = {
|
|||
"serp analysis",
|
||||
],
|
||||
|
||||
h1: "SERP API for Rank Tracking and Competitor SERP Intelligence",
|
||||
h1: "SERP API for Rank Tracking and AI Overview Monitoring",
|
||||
heroLede:
|
||||
"The SurfSense SERP API scrapes Google Search results as structured JSON: organic rankings, paid ads, AI Overviews, and People Also Ask. Point your AI agents at the queries that matter to your market and know the moment a rank moves, an ad appears, or an AI answer starts citing a competitor.",
|
||||
"The SurfSense SERP API scrapes Google Search results as structured JSON: organic rankings, paid ads, AI Overviews, and People Also Ask. Point your AI agents at the queries you care about and know the moment a rank moves, an ad appears, or an AI answer changes its sources.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Track who's ranking and running ads for 'competitor pricing' in the US",
|
||||
|
|
@ -77,8 +77,7 @@ export const googleSearch: ConnectorPageContent = {
|
|||
},
|
||||
{
|
||||
label: "Related searches",
|
||||
description:
|
||||
"Suggested and related queries, to map how your market actually phrases its search.",
|
||||
description: "Suggested and related queries, to map how people actually phrase the search.",
|
||||
},
|
||||
{
|
||||
label: "SERP metadata",
|
||||
|
|
@ -261,6 +260,7 @@ export const googleSearch: ConnectorPageContent = {
|
|||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "Reddit API", href: "/reddit" },
|
||||
{ label: "Instagram API", href: "/instagram" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
{ label: "Read the docs", href: "/docs" },
|
||||
],
|
||||
|
|
|
|||
327
surfsense_web/lib/connectors-marketing/indeed.tsx
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import { IconBriefcase } from "@tabler/icons-react";
|
||||
import type { ConnectorPageContent } from "./types";
|
||||
|
||||
export const indeed: ConnectorPageContent = {
|
||||
slug: "indeed",
|
||||
name: "Indeed",
|
||||
icon: IconBriefcase,
|
||||
|
||||
metaTitle: "Indeed Scraper API for Jobs and Hiring Data | SurfSense",
|
||||
metaDescription:
|
||||
"Scrape public Indeed job postings with the SurfSense Indeed Scraper API: titles, companies, salaries, and full descriptions by search or company. No Indeed API. Start free.",
|
||||
keywords: [
|
||||
"indeed scraper",
|
||||
"indeed scraper api",
|
||||
"indeed api",
|
||||
"indeed jobs api",
|
||||
"scrape indeed",
|
||||
"indeed job scraper",
|
||||
"job posting scraper",
|
||||
"salary data api",
|
||||
"hiring data api",
|
||||
"indeed mcp",
|
||||
"labor market data",
|
||||
"recruiting data tool",
|
||||
],
|
||||
|
||||
h1: "Indeed Scraper API for Job Postings and Hiring Data",
|
||||
heroLede:
|
||||
"The SurfSense Indeed API extracts public job postings, salaries, companies, and full descriptions by search query, company page, or job URL, without Indeed's official API. Give your AI agents a live feed of who is hiring, for what, at what pay, so you track the labor market as it moves.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Find remote data analyst roles posted this week and what they pay",
|
||||
toolCall:
|
||||
'indeed.scrape({ search_queries: ["data analyst"], location: "Remote",\n remote: "remote", from_days: 7, sort: "date", max_items: 30 })',
|
||||
rows: [
|
||||
{
|
||||
primary: "Senior Data Analyst · Acme Corp",
|
||||
secondary: "Remote (US) · $120k–$145k/year · posted 2 days ago",
|
||||
tag: "salary listed",
|
||||
},
|
||||
{
|
||||
primary: "Data Analyst, Growth · Globex",
|
||||
secondary: "Remote · $95k–$110k/year · Indeed Apply",
|
||||
tag: "buying signal",
|
||||
},
|
||||
{
|
||||
primary: "Marketing Data Analyst · Initech",
|
||||
secondary: "Remote (US) · estimated $88k–$102k · posted today",
|
||||
tag: "new today",
|
||||
},
|
||||
],
|
||||
resultSummary: "30 jobs · 22 with salary · surfaced in 3.4s",
|
||||
},
|
||||
|
||||
extractIntro:
|
||||
"Every call returns structured job items. Point the API at a search query, an Indeed search or company page, or a single job URL, and set scrape_job_details for the full description per job.",
|
||||
extractFields: [
|
||||
{
|
||||
label: "Job",
|
||||
description: "Title, job key, listing URL, apply URL, and whether Indeed Apply is enabled.",
|
||||
},
|
||||
{
|
||||
label: "Company",
|
||||
description: "Company name, profile URL, star rating, and review count where available.",
|
||||
},
|
||||
{
|
||||
label: "Location",
|
||||
description: "Formatted location, city, state, postal code, country, and remote or hybrid flags.",
|
||||
},
|
||||
{
|
||||
label: "Salary",
|
||||
description:
|
||||
"Pay text, min and max bounds, currency, period, and whether the figure is an Indeed estimate.",
|
||||
},
|
||||
{
|
||||
label: "Description",
|
||||
description:
|
||||
"Listing snippet by default; the full text and HTML description with scrape_job_details.",
|
||||
},
|
||||
{
|
||||
label: "Signals",
|
||||
description:
|
||||
"Job types, benefits, sponsored, urgently hiring, new, and expired flags, plus post age.",
|
||||
},
|
||||
],
|
||||
|
||||
useCasesHeading: "What teams do with the Indeed API",
|
||||
useCases: [
|
||||
{
|
||||
title: "Competitor hiring intelligence",
|
||||
description:
|
||||
"Track what your competitors are hiring for and where. A spike in sales or ML roles is a roadmap signal months before it ships. Feed the stream to an agent that flags the moves that matter.",
|
||||
},
|
||||
{
|
||||
title: "Salary and compensation benchmarking",
|
||||
description:
|
||||
"Pull real posted salaries for a title in a location and benchmark your own bands against the live market, instead of a survey that is a year stale.",
|
||||
},
|
||||
{
|
||||
title: "Labor market and sector research",
|
||||
description:
|
||||
"Measure hiring demand for a role, skill, or sector over time. Turn thousands of postings into a demand index your analysts and clients can act on.",
|
||||
},
|
||||
{
|
||||
title: "Recruiting and lead sourcing",
|
||||
description:
|
||||
"Find companies actively hiring for a role and reach them while the need is hot. Job postings are a public, timely buying signal for staffing and B2B sales.",
|
||||
},
|
||||
],
|
||||
|
||||
comparison: {
|
||||
heading: "An Indeed API alternative built for agents",
|
||||
intro:
|
||||
"Indeed retired its public Publisher jobs API and gates data behind partner programs. If you cannot get access or need clean structured jobs now, here is how SurfSense compares.",
|
||||
columnLabel: "DIY Indeed scraping",
|
||||
rows: [
|
||||
{
|
||||
feature: "Access",
|
||||
official: "Publisher API retired; partner-gated and approval-only",
|
||||
surfsense: "One API key; scrape public postings without an approval process",
|
||||
},
|
||||
{
|
||||
feature: "Anti-bot",
|
||||
official: "You fight Cloudflare, fingerprinting, and CAPTCHAs yourself",
|
||||
surfsense: "Warmed, rotated sessions managed for you; no proxy plumbing",
|
||||
},
|
||||
{
|
||||
feature: "Pricing",
|
||||
official: "Proxy, browser, and maintenance costs you own",
|
||||
surfsense: "Pay per job returned, with a free tier to start",
|
||||
},
|
||||
{
|
||||
feature: "Descriptions",
|
||||
official: "Extra page fetch and parsing you build and maintain",
|
||||
surfsense: "Full description per job with one scrape_job_details flag",
|
||||
},
|
||||
{
|
||||
feature: "Agent-ready",
|
||||
official: "No; you build the harness yourself",
|
||||
surfsense: "MCP server exposes indeed.scrape as a native tool",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
api: {
|
||||
platform: "indeed",
|
||||
verb: "scrape",
|
||||
mcpTool: "indeed.scrape",
|
||||
requestBody: {
|
||||
search_queries: ["data analyst"],
|
||||
location: "Remote",
|
||||
remote: "remote",
|
||||
from_days: 7,
|
||||
sort: "date",
|
||||
max_items: 30,
|
||||
},
|
||||
},
|
||||
|
||||
schema: {
|
||||
requestNote:
|
||||
"Provide at least one source: urls or search_queries. Up to 20 sources per call.",
|
||||
request: [
|
||||
{
|
||||
name: "urls",
|
||||
type: "string[]",
|
||||
defaultValue: "[]",
|
||||
description:
|
||||
"Indeed URLs: a search page (/jobs?q=&l=), a company jobs page (/cmp/<slug>/jobs), or a single job (/viewjob?jk=...). Max 20.",
|
||||
},
|
||||
{
|
||||
name: "search_queries",
|
||||
type: "string[]",
|
||||
defaultValue: "[]",
|
||||
description:
|
||||
"Job search terms. Each returns up to max_items_per_query results, shaped by the filters below. Max 20.",
|
||||
},
|
||||
{
|
||||
name: "country",
|
||||
type: "string",
|
||||
defaultValue: '"us"',
|
||||
description: "Country code selecting the Indeed domain, e.g. 'us', 'gb', 'de'.",
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
type: "string",
|
||||
description: "Where to search, e.g. 'Remote', 'New York, NY'.",
|
||||
},
|
||||
{
|
||||
name: "radius",
|
||||
type: "integer",
|
||||
description: "Search radius in miles or km around location.",
|
||||
},
|
||||
{
|
||||
name: "job_type",
|
||||
type: "string",
|
||||
description: "Employment type: fulltime, parttime, contract, internship, and more.",
|
||||
},
|
||||
{
|
||||
name: "level",
|
||||
type: "string",
|
||||
description: "Experience level: entry_level, mid_level, or senior_level.",
|
||||
},
|
||||
{
|
||||
name: "remote",
|
||||
type: "string",
|
||||
description: "Work model filter: remote or hybrid.",
|
||||
},
|
||||
{
|
||||
name: "from_days",
|
||||
type: "integer",
|
||||
description: "Only return jobs posted within the last N days.",
|
||||
},
|
||||
{
|
||||
name: "sort",
|
||||
type: "string",
|
||||
defaultValue: '"relevance"',
|
||||
description: "Result ordering: relevance or date.",
|
||||
},
|
||||
{
|
||||
name: "scrape_job_details",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
description:
|
||||
"Fetch each job's detail page for the full description. Slower: one extra page load per job.",
|
||||
},
|
||||
{
|
||||
name: "max_items",
|
||||
type: "integer",
|
||||
defaultValue: "25",
|
||||
description: "Max total jobs to return across all sources. 1 to 100.",
|
||||
},
|
||||
{
|
||||
name: "max_items_per_query",
|
||||
type: "integer",
|
||||
defaultValue: "25",
|
||||
description: "Max jobs to pull per search or company target.",
|
||||
},
|
||||
],
|
||||
responseNote:
|
||||
"The response is { items: [...] } with one flat item per job. Fields Indeed omits are null. One returned job is one billable unit.",
|
||||
response: [
|
||||
{
|
||||
name: "jobKey / jobUrl / applyUrl",
|
||||
type: "string",
|
||||
description: "Indeed job key, listing URL, and third-party apply URL.",
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
type: "string",
|
||||
description: "The job title as posted.",
|
||||
},
|
||||
{
|
||||
name: "company / companyUrl",
|
||||
type: "string",
|
||||
description: "Company name and its Indeed profile URL.",
|
||||
},
|
||||
{
|
||||
name: "companyRating / companyReviewCount",
|
||||
type: "number / integer",
|
||||
description: "Employer star rating and number of reviews, where Indeed shows them.",
|
||||
},
|
||||
{
|
||||
name: "formattedLocation / isRemote / remoteType",
|
||||
type: "string / boolean",
|
||||
description: "Location string plus remote and hybrid flags.",
|
||||
},
|
||||
{
|
||||
name: "salary",
|
||||
type: "object",
|
||||
description:
|
||||
"salaryText, salaryMin, salaryMax, currency, period, and isEstimated when the pay is an Indeed estimate.",
|
||||
},
|
||||
{
|
||||
name: "jobTypes / benefits",
|
||||
type: "string[]",
|
||||
description: "Employment types and listed benefits parsed from the posting.",
|
||||
},
|
||||
{
|
||||
name: "descriptionText / descriptionHtml",
|
||||
type: "string",
|
||||
description: "Snippet by default; the full description when scrape_job_details is set.",
|
||||
},
|
||||
{
|
||||
name: "sponsored / urgentlyHiring / isNew / expired",
|
||||
type: "boolean",
|
||||
description: "Listing flags for ranking and filtering.",
|
||||
},
|
||||
{
|
||||
name: "age / datePublished / scrapedAt",
|
||||
type: "string",
|
||||
description: "Relative post age, ISO publish date, and when the job was scraped.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
faq: [
|
||||
{
|
||||
question: "Is scraping Indeed legal?",
|
||||
answer:
|
||||
"SurfSense reads only public Indeed job postings, the same listings any logged-out visitor can see. It never logs in and cannot access private or applicant data. As always, review Indeed's terms and your own compliance needs before you run at scale.",
|
||||
},
|
||||
{
|
||||
question: "Does Indeed have an official jobs API?",
|
||||
answer:
|
||||
"Indeed retired its public Publisher jobs API and now gates job data behind partner and approval programs. SurfSense is an independent alternative: you call one API, or add the MCP server to your agent, and get structured public postings back.",
|
||||
},
|
||||
{
|
||||
question: "Can I get the full job description?",
|
||||
answer:
|
||||
"Yes. By default each job returns the listing snippet, which is fast. Set scrape_job_details to true and SurfSense fetches each job's detail page for the full description text and HTML, at the cost of one extra page load per job.",
|
||||
},
|
||||
{
|
||||
question: "What are the rate limits?",
|
||||
answer:
|
||||
"Each call returns up to 100 jobs across all sources, with up to 20 URLs or search queries per request. SurfSense manages the anti-bot request budget for you, so you scale reads without running proxies or a headless browser yourself.",
|
||||
},
|
||||
],
|
||||
|
||||
related: [
|
||||
{ label: "Reddit API", href: "/reddit" },
|
||||
{ label: "YouTube API", href: "/youtube" },
|
||||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
],
|
||||
};
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { amazon } from "./amazon";
|
||||
import { googleMaps } from "./google-maps";
|
||||
import { googleSearch } from "./google-search";
|
||||
import { indeed } from "./indeed";
|
||||
import { instagram } from "./instagram";
|
||||
import { reddit } from "./reddit";
|
||||
import { tiktok } from "./tiktok";
|
||||
|
|
@ -17,6 +19,8 @@ const CONNECTOR_LIST: ConnectorPageContent[] = [
|
|||
tiktok,
|
||||
googleMaps,
|
||||
googleSearch,
|
||||
indeed,
|
||||
amazon,
|
||||
webCrawl,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const instagram: ConnectorPageContent = {
|
|||
name: "Instagram",
|
||||
icon: IconBrandInstagram,
|
||||
|
||||
metaTitle: "Instagram Scraper API for Profiles and Reels | SurfSense",
|
||||
metaTitle: "Instagram API for Profiles, Posts, and Reels | SurfSense",
|
||||
metaDescription:
|
||||
"Instagram scraper API for public profiles, posts, and reels. No login or Graph API review. Structured data for AI agents, plus a free tier. Start now.",
|
||||
keywords: [
|
||||
|
|
@ -29,7 +29,7 @@ export const instagram: ConnectorPageContent = {
|
|||
"social listening",
|
||||
],
|
||||
|
||||
h1: "Instagram Scraper API for Profiles, Posts, and Reels",
|
||||
h1: "Instagram API for Profiles, Posts, and Reels",
|
||||
heroLede:
|
||||
"The SurfSense Instagram scraper extracts public profiles, posts, and reels without logging in or registering for the Instagram Graph API. Give your AI agents a live feed of what creators post, so you spot trends and shifts in engagement first.",
|
||||
|
||||
|
|
@ -288,6 +288,7 @@ export const instagram: ConnectorPageContent = {
|
|||
{ label: "Reddit API", href: "/reddit" },
|
||||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ export const reddit: ConnectorPageContent = {
|
|||
name: "Reddit",
|
||||
icon: IconBrandReddit,
|
||||
|
||||
metaTitle: "Reddit Scraper API for Brand Monitoring | SurfSense",
|
||||
metaTitle: "Reddit API for Brand Monitoring | SurfSense",
|
||||
metaDescription:
|
||||
"Track every brand and competitor mention across Reddit with the SurfSense Reddit Scraper API. No official API rate limits or pricing, plus a free tier. Start now.",
|
||||
"Track every mention of any brand, product, or topic across Reddit with the SurfSense Reddit Scraper API. No official API rate limits or pricing, plus a free tier. Start now.",
|
||||
keywords: [
|
||||
"reddit scraper",
|
||||
"reddit scraper api",
|
||||
|
|
@ -24,28 +24,28 @@ export const reddit: ConnectorPageContent = {
|
|||
"brand monitoring tool",
|
||||
],
|
||||
|
||||
h1: "Reddit Scraper API for Brand Monitoring and Market Research",
|
||||
h1: "Reddit API for Brand Monitoring and Community Research",
|
||||
heroLede:
|
||||
"The SurfSense Reddit API extracts posts, comments, subreddits, and user data without the official Reddit API's rate limits or per-call pricing. Give your AI agents a live feed of what your market says about your brand, your competitors, and your category, so you hear it first.",
|
||||
"The SurfSense Reddit API extracts posts, comments, subreddits, and user data without the official Reddit API's rate limits or per-call pricing. Give your AI agents a live feed of what Reddit actually says about any brand, product, or topic, so you hear it first.",
|
||||
|
||||
transcript: {
|
||||
prompt: "See what r/webscraping is saying about our competitor this week",
|
||||
prompt: "See what r/SaaS is saying about our competitor this week",
|
||||
toolCall:
|
||||
'reddit.scrape({ search_queries: ["competitor"], community: "webscraping",\n sort: "top", time_filter: "week", max_comments: 50 })',
|
||||
'reddit.scrape({ search_queries: ["competitor"], community: "SaaS",\n sort: "top", time_filter: "week", max_comments: 50 })',
|
||||
rows: [
|
||||
{
|
||||
primary: "Switched off Competitor after the API price hike",
|
||||
secondary: "r/webscraping · 312 upvotes · 0.94 ratio",
|
||||
secondary: "r/SaaS · 312 upvotes · 0.94 ratio",
|
||||
tag: "pricing pain",
|
||||
},
|
||||
{
|
||||
primary: "Anyone got a Competitor alternative that does comments?",
|
||||
secondary: "r/webscraping · 148 upvotes · 61 comments",
|
||||
secondary: "r/SaaS · 148 upvotes · 61 comments",
|
||||
tag: "buying intent",
|
||||
},
|
||||
{
|
||||
primary: "Competitor keeps rate-limiting my agent, help",
|
||||
secondary: "r/webscraping · 89 upvotes · 44 comments",
|
||||
secondary: "r/SaaS · 89 upvotes · 44 comments",
|
||||
tag: "churn signal",
|
||||
},
|
||||
],
|
||||
|
|
@ -97,7 +97,7 @@ export const reddit: ConnectorPageContent = {
|
|||
{
|
||||
title: "Reddit sentiment analysis",
|
||||
description:
|
||||
"Pull comment trees at depth and score them for sentiment, so you can measure how a launch, a price change, or a competitor's move actually landed with your market.",
|
||||
"Pull comment trees at depth and score them for sentiment, so you can measure how a launch, a price change, or an announcement actually landed with the community.",
|
||||
},
|
||||
{
|
||||
title: "Community research for go-to-market",
|
||||
|
|
@ -146,7 +146,7 @@ export const reddit: ConnectorPageContent = {
|
|||
mcpTool: "reddit.scrape",
|
||||
requestBody: {
|
||||
search_queries: ["your brand"],
|
||||
community: "webscraping",
|
||||
community: "SaaS",
|
||||
sort: "top",
|
||||
time_filter: "week",
|
||||
max_comments: 50,
|
||||
|
|
@ -315,6 +315,7 @@ export const reddit: ConnectorPageContent = {
|
|||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const tiktok: ConnectorPageContent = {
|
|||
name: "TikTok",
|
||||
icon: IconBrandTiktok,
|
||||
|
||||
metaTitle: "TikTok Scraper API for Videos and Comments | SurfSense",
|
||||
metaTitle: "TikTok API for Videos, Comments, and Trends | SurfSense",
|
||||
metaDescription:
|
||||
"TikTok scraper API for public videos, comments, hashtags, and profiles. No Research API approval. Structured data for AI agents, plus a free tier. Start now.",
|
||||
keywords: [
|
||||
|
|
@ -30,9 +30,9 @@ export const tiktok: ConnectorPageContent = {
|
|||
"influencer research tool",
|
||||
],
|
||||
|
||||
h1: "TikTok Scraper API for Videos, Comments, and Trend Research",
|
||||
h1: "TikTok API for Videos, Comments, and Trend Research",
|
||||
heroLede:
|
||||
"The SurfSense TikTok scraper extracts public videos by hashtag, creator profile, or URL, plus comment threads and trending feeds, without TikTok's approval-gated Research API. Give your AI agents a live feed of what your market watches and shares, so you catch a trend while it is still rising.",
|
||||
"The SurfSense TikTok scraper extracts public videos by hashtag, creator profile, or URL, plus comment threads and trending feeds, without TikTok's approval-gated Research API. Give your AI agents a live feed of what people watch and share, so you catch a trend while it is still rising.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Find trending TikToks about meal prep this week",
|
||||
|
|
@ -284,6 +284,7 @@ export const tiktok: ConnectorPageContent = {
|
|||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const webCrawl: ConnectorPageContent = {
|
|||
|
||||
metaTitle: "Web Scraping and Crawl API for AI Agents | SurfSense",
|
||||
metaDescription:
|
||||
"Point your agents at any URL and get clean, LLM-ready markdown, links, and contacts back. The SurfSense Web Crawl API for deep research and competitor monitoring. Start free.",
|
||||
"Point your agents at any URL and get clean, LLM-ready markdown, links, and contacts back. The SurfSense Web Crawl API for deep research and site monitoring. Start free.",
|
||||
keywords: [
|
||||
"web crawl api",
|
||||
"web scraping api",
|
||||
|
|
@ -20,7 +20,7 @@ export const webCrawl: ConnectorPageContent = {
|
|||
"agentic web scraping",
|
||||
"deep research api",
|
||||
"web scraping mcp",
|
||||
"competitor price monitoring",
|
||||
"website change monitoring",
|
||||
"crawl website for ai",
|
||||
"website to markdown",
|
||||
"web data for ai",
|
||||
|
|
@ -283,6 +283,7 @@ export const webCrawl: ConnectorPageContent = {
|
|||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "Reddit API", href: "/reddit" },
|
||||
{ label: "Instagram API", href: "/instagram" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
{ label: "Read the docs", href: "/docs" },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const youtube: ConnectorPageContent = {
|
|||
name: "YouTube",
|
||||
icon: IconBrandYoutube,
|
||||
|
||||
metaTitle: "YouTube Scraper API for Audience Sentiment | SurfSense",
|
||||
metaTitle: "YouTube Data API for Comments and Transcripts | SurfSense",
|
||||
metaDescription:
|
||||
"Scrape YouTube videos, comments, and transcripts at scale with the SurfSense YouTube Scraper API. No Data API quotas, plus a free tier for AI agents. Start now.",
|
||||
keywords: [
|
||||
|
|
@ -23,9 +23,9 @@ export const youtube: ConnectorPageContent = {
|
|||
"audience research tools",
|
||||
],
|
||||
|
||||
h1: "YouTube Scraper API for Audience Sentiment and Content Intelligence",
|
||||
h1: "YouTube Data API for Comments, Transcripts, and Audience Sentiment",
|
||||
heroLede:
|
||||
"The SurfSense YouTube API pulls videos, channel stats, comments, and transcripts at scale, without YouTube Data API quotas. Feed your AI agents every comment and transcript that matters to your market, so you know what people actually think about your product and your competitors' content.",
|
||||
"The SurfSense YouTube API pulls videos, channel stats, comments, and transcripts at scale, without YouTube Data API quotas. Feed your AI agents every comment and transcript that matters to your research, so you know what audiences actually think about any product, topic, or channel.",
|
||||
|
||||
transcript: {
|
||||
prompt: "Summarize how people reacted in the comments on our launch video",
|
||||
|
|
@ -88,9 +88,9 @@ export const youtube: ConnectorPageContent = {
|
|||
"Read every comment on your launch video or a competitor's ad, not a sample. Score product feedback and ad reaction so your team knows what actually resonated with the audience.",
|
||||
},
|
||||
{
|
||||
title: "Competitor channel analysis",
|
||||
title: "Channel analysis",
|
||||
description:
|
||||
"Pull a rival channel's full catalog with views, likes, and cadence to see which content wins in your niche, then brief an agent to spot the patterns you should copy.",
|
||||
"Pull any channel's full catalog with views, likes, and cadence to see which content wins in a niche, then brief an agent to spot the patterns worth copying.",
|
||||
},
|
||||
{
|
||||
title: "Influencer vetting",
|
||||
|
|
@ -275,6 +275,7 @@ export const youtube: ConnectorPageContent = {
|
|||
{ label: "Google Maps API", href: "/google-maps" },
|
||||
{ label: "SERP API", href: "/google-search" },
|
||||
{ label: "Web Crawl API", href: "/web-crawl" },
|
||||
{ label: "Indeed API", href: "/indeed" },
|
||||
{ label: "SurfSense MCP Server", href: "/mcp-server" },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import type { ComponentType } from "react";
|
||||
import {
|
||||
AmazonIcon,
|
||||
GoogleMapsIcon,
|
||||
GoogleSearchIcon,
|
||||
IndeedIcon,
|
||||
InstagramIcon,
|
||||
RedditIcon,
|
||||
TikTokIcon,
|
||||
|
|
@ -89,6 +91,18 @@ export const PLAYGROUND_PLATFORMS: PlaygroundPlatform[] = [
|
|||
icon: GoogleSearchIcon,
|
||||
verbs: [{ name: "google_search.scrape", verb: "scrape", label: "Scrape" }],
|
||||
},
|
||||
{
|
||||
id: "indeed",
|
||||
label: "Indeed",
|
||||
icon: IndeedIcon,
|
||||
verbs: [{ name: "indeed.scrape", verb: "scrape", label: "Scrape" }],
|
||||
},
|
||||
{
|
||||
id: "amazon",
|
||||
label: "Amazon",
|
||||
icon: AmazonIcon,
|
||||
verbs: [{ name: "amazon.scrape", verb: "scrape", label: "Scrape" }],
|
||||
},
|
||||
{
|
||||
id: "web",
|
||||
label: "Web",
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ function brandIcon(src: string, alt: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export const AmazonIcon = brandIcon("/connectors/amazon.svg", "Amazon");
|
||||
export const RedditIcon = brandIcon("/connectors/reddit.svg", "Reddit");
|
||||
export const YouTubeIcon = brandIcon("/connectors/youtube.svg", "YouTube");
|
||||
export const InstagramIcon = brandIcon("/connectors/instagram.svg", "Instagram");
|
||||
export const TikTokIcon = brandIcon("/connectors/tiktok.svg", "TikTok");
|
||||
export const GoogleMapsIcon = brandIcon("/connectors/google-maps.svg", "Google Maps");
|
||||
export const GoogleSearchIcon = brandIcon("/connectors/google-search.svg", "Google Search");
|
||||
export const IndeedIcon = brandIcon("/connectors/indeed.svg", "Indeed");
|
||||
export const WebIcon = brandIcon("/connectors/web.svg", "Web");
|
||||
|
|
|
|||
38
surfsense_web/lib/playground/platform-overrides/amazon.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Info } from "lucide-react";
|
||||
import type { FieldOption } from "@/app/dashboard/[workspace_id]/playground/components/schema-form";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import type { FormField } from "@/lib/playground/json-schema";
|
||||
|
||||
export const AMAZON_DOMAIN_OPTIONS: FieldOption[] = [
|
||||
{ label: "US", value: "www.amazon.com" },
|
||||
{ label: "UK", value: "www.amazon.co.uk" },
|
||||
{ label: "Germany", value: "www.amazon.de" },
|
||||
{ label: "Italy", value: "www.amazon.it" },
|
||||
{ label: "Spain", value: "www.amazon.es" },
|
||||
{ label: "France, best effort", value: "www.amazon.fr" },
|
||||
];
|
||||
|
||||
const AMAZON_SUPPORTED_COUNTRIES = "US, UK, Germany, Italy, Spain, France";
|
||||
|
||||
export function getAmazonFieldOptions(field: FormField): FieldOption[] | undefined {
|
||||
return field.name === "domain" ? AMAZON_DOMAIN_OPTIONS : undefined;
|
||||
}
|
||||
|
||||
export function hasAmazonFranceValue(values: Record<string, unknown>): boolean {
|
||||
return JSON.stringify(values).toLowerCase().includes("amazon.fr");
|
||||
}
|
||||
|
||||
export function AmazonMarketplaceHint({ showFranceWarning }: { showFranceWarning: boolean }) {
|
||||
return (
|
||||
<Alert>
|
||||
<Info />
|
||||
<AlertDescription className="flex flex-wrap items-baseline gap-x-1">
|
||||
<span className="font-medium text-foreground">Supported countries: </span>
|
||||
<span>{AMAZON_SUPPORTED_COUNTRIES}</span>
|
||||
{showFranceWarning
|
||||
? " France is more WAF-sensitive. If this run returns no results, retry later or use another marketplace."
|
||||
: null}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "surfsense_web",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.34",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.26.0",
|
||||
"description": "SurfSense Frontend",
|
||||
|
|
@ -138,6 +138,7 @@
|
|||
"react-dom": "^19.2.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
|
|
|
|||
196
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -335,6 +335,9 @@ importers:
|
|||
react-hook-form:
|
||||
specifier: ^7.61.1
|
||||
version: 7.71.2(react@19.2.4)
|
||||
react-social-media-embed:
|
||||
specifier: ^2.5.18
|
||||
version: 2.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-syntax-highlighter:
|
||||
specifier: ^15.6.1
|
||||
version: 15.6.6(react@19.2.4)
|
||||
|
|
@ -1155,28 +1158,24 @@ packages:
|
|||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.4.6':
|
||||
resolution: {integrity: sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.4.6':
|
||||
resolution: {integrity: sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.4.6':
|
||||
resolution: {integrity: sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.4.6':
|
||||
resolution: {integrity: sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==}
|
||||
|
|
@ -1864,105 +1863,89 @@ packages:
|
|||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
|
|
@ -2082,35 +2065,30 @@ packages:
|
|||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
|
||||
resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
||||
|
|
@ -2154,28 +2132,24 @@ packages:
|
|||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.6':
|
||||
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.6':
|
||||
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.6':
|
||||
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.6':
|
||||
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
|
||||
|
|
@ -2827,56 +2801,48 @@ packages:
|
|||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.45.0':
|
||||
resolution: {integrity: sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==}
|
||||
|
|
@ -2931,42 +2897,36 @@ packages:
|
|||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||
|
|
@ -4287,79 +4247,66 @@ packages:
|
|||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
|
|
@ -4550,28 +4497,24 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.15.13':
|
||||
resolution: {integrity: sha512-SmZ9m+XqCB35NddHCctvHFLqPZDAs5j8IgD36GoutufDJmeq2VNfgk5rQoqNqKmAK3Y7iFdEmI76QoHIWiCLyw==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.15.13':
|
||||
resolution: {integrity: sha512-5rij+vB9a29aNkHq72EXI2ihDZPszJb4zlApJY4aCC/q6utgqFA6CkrfTfIb+O8hxtG3zP5KERETz8mfFK6A0A==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.15.13':
|
||||
resolution: {integrity: sha512-OlSlaOK9JplQ5qn07WiBLibkOw7iml2++ojEXhhR3rbWrNEKCD7sd8+6wSavsInyFdw4PhLA+Hy6YyDBIE23Yw==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.15.13':
|
||||
resolution: {integrity: sha512-zwQii5YVdsfG8Ti9gIKgBKZg8qMkRZxl+OlYWUT5D93Jl4NuNBRausP20tfEkQdAPSRrMCSUZBM6FhW7izAZRg==}
|
||||
|
|
@ -4655,28 +4598,24 @@ packages:
|
|||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
||||
|
|
@ -4976,6 +4915,9 @@ packages:
|
|||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@types/youtube-player@5.5.11':
|
||||
resolution: {integrity: sha512-pM41CDBqJqBmTeJWnF7NOGz82IQoYOhqzMYXv5vKCXBqGiYSLldxMtpCk6KAEtADTy49S45AriYaCaZyeUX38Q==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.0':
|
||||
resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
|
@ -5100,49 +5042,41 @@ packages:
|
|||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
|
|
@ -5830,6 +5764,14 @@ packages:
|
|||
dayjs@1.11.21:
|
||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -7255,28 +7197,24 @@ packages:
|
|||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
|
|
@ -7297,6 +7235,9 @@ packages:
|
|||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
load-script@1.0.0:
|
||||
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -7617,6 +7558,9 @@ packages:
|
|||
react-dom:
|
||||
optional: true
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
|
|
@ -8169,6 +8113,12 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-html-props@2.1.1:
|
||||
resolution: {integrity: sha512-tM+YCYlr90m3JontKUAa+gNVU2zkyprlCS7OQ9aa3z2MfyJjAioJzrSmi1Vef/+UCTE6CQlPqLX4ebdLIJDKxw==}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
|
|
@ -8227,6 +8177,12 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-social-media-embed@2.5.18:
|
||||
resolution: {integrity: sha512-+PkzLRGAwnySkxKajaiK5VD+EjOhlFsh/vjNxgHsDfKBTseDpFxPrMXXQWkk6BRCwFBNVWX+V1HZ9AU0y54Wgw==}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
|
||||
react-style-singleton@2.2.3:
|
||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -8237,6 +8193,12 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-sub-unsub@2.2.8:
|
||||
resolution: {integrity: sha512-o3tmiOOZPdQUCmRhkdCHXRFLOHnCwdz/N3QZ1JQ14fQGA2HysKMF0kWu56ERnQUCK7wYVCQzI8pFbnivAYNQ+A==}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||
|
||||
react-syntax-highlighter@15.6.6:
|
||||
resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==}
|
||||
peerDependencies:
|
||||
|
|
@ -8261,11 +8223,24 @@ packages:
|
|||
react-native:
|
||||
optional: true
|
||||
|
||||
react-twitter-embed@4.0.4:
|
||||
resolution: {integrity: sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react-wrap-balancer@1.1.1:
|
||||
resolution: {integrity: sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0 || ^17.0.0 || ^18'
|
||||
|
||||
react-youtube@10.1.0:
|
||||
resolution: {integrity: sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==}
|
||||
engines: {node: '>= 14.x'}
|
||||
peerDependencies:
|
||||
react: '>=0.14.1'
|
||||
|
||||
react@19.2.4:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -8489,6 +8464,9 @@ packages:
|
|||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
scriptjs@2.5.9:
|
||||
resolution: {integrity: sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==}
|
||||
|
||||
scroll-into-view-if-needed@3.1.0:
|
||||
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
|
||||
|
||||
|
|
@ -8574,6 +8552,9 @@ packages:
|
|||
simple-swizzle@0.2.4:
|
||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||
|
||||
sister@3.0.2:
|
||||
resolution: {integrity: sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==}
|
||||
|
||||
slate-dom@0.119.0:
|
||||
resolution: {integrity: sha512-foc8a2NkE+1SldDIYaoqjhVKupt8RSuvHI868rfYOcypD4we5TT7qunjRKJ852EIRh/Ql8sSTepXgXKOUJnt1w==}
|
||||
peerDependencies:
|
||||
|
|
@ -9240,6 +9221,9 @@ packages:
|
|||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
youtube-player@5.5.2:
|
||||
resolution: {integrity: sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==}
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
|
|
@ -13989,6 +13973,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.19.33
|
||||
|
||||
'@types/youtube-player@5.5.11': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
|
|
@ -14881,6 +14867,10 @@ snapshots:
|
|||
|
||||
dayjs@1.11.21: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
|
@ -16535,6 +16525,8 @@ snapshots:
|
|||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
load-script@1.0.0: {}
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
|
@ -17136,6 +17128,8 @@ snapshots:
|
|||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mutative@1.1.0: {}
|
||||
|
|
@ -17802,6 +17796,11 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
react-html-props@2.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-lifecycles-compat@3.0.4: {}
|
||||
|
|
@ -17870,6 +17869,19 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
react-social-media-embed@2.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@types/youtube-player': 5.5.11
|
||||
classnames: 2.5.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
react-html-props: 2.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-sub-unsub: 2.2.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-twitter-embed: 4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-youtube: 10.1.0(react@19.2.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
|
|
@ -17878,6 +17890,11 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
react-sub-unsub@2.2.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
react-syntax-highlighter@15.6.6(react@19.2.4):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
|
|
@ -17906,10 +17923,25 @@ snapshots:
|
|||
optionalDependencies:
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
react-twitter-embed@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
scriptjs: 2.5.9
|
||||
|
||||
react-wrap-balancer@1.1.1(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
react-youtube@10.1.0(react@19.2.4):
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.4
|
||||
youtube-player: 5.5.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
|
|
@ -18250,6 +18282,8 @@ snapshots:
|
|||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
scriptjs@2.5.9: {}
|
||||
|
||||
scroll-into-view-if-needed@3.1.0:
|
||||
dependencies:
|
||||
compute-scroll-into-view: 3.1.1
|
||||
|
|
@ -18383,6 +18417,8 @@ snapshots:
|
|||
dependencies:
|
||||
is-arrayish: 0.3.4
|
||||
|
||||
sister@3.0.2: {}
|
||||
|
||||
slate-dom@0.119.0(slate@0.120.0):
|
||||
dependencies:
|
||||
'@juggle/resize-observer': 3.4.0
|
||||
|
|
@ -19095,6 +19131,14 @@ snapshots:
|
|||
|
||||
yoctocolors@2.1.2: {}
|
||||
|
||||
youtube-player@5.5.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
load-script: 1.0.0
|
||||
sister: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
|
|
|||
22
surfsense_web/public/connectors/amazon.svg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>Amazon-color</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Color-" transform="translate(-601.000000, -560.000000)">
|
||||
<g id="Amazon" transform="translate(601.000000, 560.000000)">
|
||||
<path d="M25.4026553,25.9595294 C24.660417,27.4418824 23.3876054,28.3962353 22.0103725,28.7181176 C21.8015298,28.7181176 21.4826213,28.8225882 21.1637129,28.8225882 C18.835399,28.8225882 17.458166,27.0211765 17.458166,24.3727059 C17.458166,20.9788235 19.4703937,19.392 22.0103725,18.6465882 C23.3876054,18.3303529 24.9793255,18.2230588 26.5682233,18.2230588 L26.5682233,19.4964706 C26.5682233,21.9331765 26.6726447,23.8390588 25.4026553,25.9595294 L25.4026553,25.9595294 Z M26.5682233,13.3524706 C25.1909904,13.4569412 23.5992703,13.5614118 22.0103725,13.7703529 C19.574815,14.0922353 17.1392576,14.5157647 15.1298521,15.4701176 C11.2098182,17.0597647 8.55977364,20.4508235 8.55977364,25.4287059 C8.55977364,31.6856471 12.5842289,34.8621176 17.6726531,34.8621176 C19.3659723,34.8621176 20.7432053,34.6475294 22.0103725,34.3341176 C24.0282445,33.696 25.7187415,32.5298824 27.7309692,30.4094118 C28.8965372,31.9990588 29.2182679,32.7444706 31.2276733,34.4385882 C31.7582467,34.6475294 32.28882,34.6475294 32.7093276,34.3341176 C33.9821392,33.2724706 36.208854,31.3637647 37.3715998,30.3049412 C37.9021732,29.8814118 37.7977518,29.2432941 37.4760212,28.7181176 C36.3132753,27.2329412 35.1448851,25.9595294 35.1448851,23.0992941 L35.1448851,13.5614118 C35.1448851,9.53505882 35.4666157,5.82494118 32.5004849,3.072 C30.0649275,0.849882353 26.2493149,0 23.2831841,0 L22.0103725,0 C16.6115064,0.313411765 10.8937319,2.64564706 9.61809814,9.32329412 C9.40643324,10.1731765 10.0442501,10.4894118 10.4675799,10.5938824 L16.3998415,11.3364706 C17.0348362,11.2291765 17.3537447,10.6983529 17.458166,10.1731765 C17.9859172,7.84094118 19.8937235,6.67482353 22.0103725,6.46023529 L22.4365245,6.46023529 C23.7093361,6.46023529 25.086569,6.99105882 25.8259851,8.05270588 C26.6726447,9.32329412 26.5682233,11.0202353 26.5682233,12.5054118 L26.5682233,13.3524706 L26.5682233,13.3524706 Z" fill="#343B45">
|
||||
|
||||
</path>
|
||||
<path d="M47.9943556,35.9463529 L47.9943556,35.9435294 C47.971778,35.4437647 47.8673567,35.0625882 47.658514,34.7463529 L47.6359364,34.7152941 L47.6105366,34.6842353 C47.3988717,34.4527059 47.1956734,34.3651765 46.9755419,34.2691765 C46.3179696,34.0150588 45.3612442,33.8795294 44.2097872,33.8767059 C43.382883,33.8767059 42.4713128,33.9557647 41.5540982,34.1562353 L41.551276,34.0941176 L40.6284171,34.4018824 L40.6114839,34.4103529 L40.0893771,34.5797647 L40.0893771,34.6023529 C39.47696,34.8564706 38.9209869,35.1727059 38.4045245,35.5482353 C38.0827939,35.7882353 37.8175072,36.1072941 37.8033962,36.5957647 C37.7949296,36.8611765 37.9303952,37.1661176 38.1533489,37.3468235 C38.3763025,37.5275294 38.6359448,37.5896471 38.8645429,37.5896471 C38.9181647,37.5896471 38.9689643,37.5868235 39.0141194,37.5783529 L39.0592746,37.5755294 L39.093141,37.5698824 C39.5446928,37.4738824 40.2022651,37.4089412 40.9727253,37.3016471 C41.6331198,37.2282353 42.3330251,37.1745882 42.9397978,37.1745882 C43.368772,37.1717647 43.7554132,37.2028235 44.0206999,37.2592941 C44.1533432,37.2875294 44.2521202,37.3214118 44.3057419,37.3496471 C44.3254973,37.3552941 44.3396083,37.3637647 44.3480749,37.3694118 C44.3593637,37.4061176 44.3762969,37.5021176 44.3734747,37.6348235 C44.3791191,38.1430588 44.164632,39.0861176 43.8683012,40.0065882 C43.5804369,40.9270588 43.2304843,41.8503529 42.999064,42.4630588 C42.94262,42.6042353 42.9059314,42.7595294 42.9059314,42.9289412 C42.900287,43.1745882 43.0018862,43.4738824 43.2163733,43.6715294 C43.425216,43.8691765 43.696147,43.9482353 43.9219229,43.9482353 L43.9332117,43.9482353 C44.2718756,43.9454118 44.5597398,43.8098824 44.8080933,43.6150588 C47.1505182,41.5087059 47.9661336,38.1430588 48,36.2484706 L47.9943556,35.9463529 Z M41.0489247,38.8658824 C40.8090378,38.8630588 40.5635065,38.9195294 40.3349084,39.0268235 C40.0780883,39.1284706 39.8156239,39.2470588 39.5672704,39.3515294 L39.2032068,39.504 L38.7290774,39.6931765 L38.7290774,39.6988235 C33.5785648,41.7882353 28.16841,43.0136471 23.1618295,43.1209412 C22.9783866,43.1265882 22.7921215,43.1265882 22.614323,43.1265882 C14.7403887,43.1322353 8.31706456,39.4785882 1.83729642,35.8785882 C1.61152053,35.76 1.37727804,35.6978824 1.15150215,35.6978824 C0.860815683,35.6978824 0.561662624,35.808 0.344353327,36.0112941 C0.12704403,36.2174118 -0.00277710907,36.5138824 4.50895989e-05,36.816 C-0.00277710907,37.2084706 0.208887791,37.5698824 0.505218651,37.8042353 C6.58705678,43.0870588 13.25309,47.9943529 22.2192152,48 C22.3941915,48 22.57199,47.9943529 22.7497885,47.9915294 C28.453452,47.8644706 34.902176,45.936 39.9087564,42.7905882 L39.9398006,42.7708235 C40.5945507,42.3783529 41.2493008,41.9322353 41.8673623,41.4381176 C42.2511813,41.1529412 42.516468,40.7068235 42.516468,40.2437647 C42.4995348,39.4221176 41.8024517,38.8658824 41.0489247,38.8658824 Z" id="Fill-237" fill="#FF9A00">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="#fff" d="M195.368 60.632H60.632v134.736h134.736z"/><path fill="#ea4335" d="M195.368 256L256 195.368l-30.316-5.172l-30.316 5.172l-5.533 27.73z"/><path fill="#188038" d="M0 195.368v40.421C0 246.956 9.044 256 20.21 256h40.422l6.225-30.316l-6.225-30.316l-33.033-5.172z"/><path fill="#1967d2" d="M256 60.632V20.21C256 9.044 246.956 0 235.79 0h-40.422q-5.532 22.554-5.533 33.196q0 10.641 5.533 27.436q20.115 5.76 30.316 5.76T256 60.631"/><path fill="#fbbc04" d="M256 60.632h-60.632v134.736H256z"/><path fill="#34a853" d="M195.368 195.368H60.632V256h134.736z"/><path fill="#4285f4" d="M195.368 0H20.211C9.044 0 0 9.044 0 20.21v175.158h60.632V60.632h134.736z"/><path fill="#4285f4" d="M88.27 165.154c-5.036-3.402-8.523-8.37-10.426-14.94l11.689-4.816q1.59 6.063 5.558 9.398c2.627 2.223 5.827 3.318 9.566 3.318q5.734 0 9.852-3.487c2.746-2.324 4.127-5.288 4.127-8.875q0-5.508-4.345-8.994c-2.897-2.324-6.535-3.486-10.88-3.486h-6.754v-11.57h6.063q5.608 0 9.448-3.033c2.56-2.02 3.84-4.783 3.84-8.303c0-3.132-1.145-5.625-3.435-7.494c-2.29-1.87-5.188-2.813-8.708-2.813c-3.436 0-6.164.91-8.185 2.745a16.1 16.1 0 0 0-4.413 6.754l-11.57-4.817c1.532-4.345 4.345-8.185 8.471-11.503s9.398-4.985 15.798-4.985c4.733 0 8.994.91 12.767 2.745c3.772 1.836 6.736 4.379 8.875 7.613c2.14 3.25 3.2 6.888 3.2 10.93c0 4.126-.993 7.613-2.98 10.476s-4.43 5.052-7.327 6.585v.69a22.25 22.25 0 0 1 9.398 7.327c2.442 3.284 3.672 7.208 3.672 11.79c0 4.58-1.163 8.673-3.487 12.26c-2.324 3.588-5.54 6.417-9.617 8.472c-4.092 2.055-8.69 3.1-13.793 3.1c-5.912.016-11.369-1.685-16.405-5.087m71.797-58.005l-12.833 9.28l-6.417-9.734l23.023-16.607h8.825v78.333h-12.598z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none"><path fill="#bbe2ff" d="M32 36.8C32 20.894 44.894 8 60.8 8h70.4C147.106 8 160 20.894 160 36.8v30.4c0 15.906-12.894 28.8-28.8 28.8H60.8C44.894 96 32 83.106 32 67.2z"/><path fill="#3c90ff" d="M19.867 49.392C17.818 33.82 29.94 20 45.645 20h100.71c15.706 0 27.827 13.82 25.778 29.392L166 96l6.133 46.608C174.182 158.18 162.061 172 146.355 172H45.645c-15.706 0-27.827-13.82-25.778-29.392L26 96z"/><mask id="a" width="154" height="152" x="19" y="20" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#3c90ff" d="M19.867 49.392C17.818 33.82 29.94 20 45.645 20h100.71c15.706 0 27.827 13.82 25.778 29.392L166 96l6.133 46.608C174.182 158.18 162.061 172 146.355 172H45.645c-15.706 0-27.827-13.82-25.778-29.392L26 96z"/></mask><g mask="url(#a)"><path fill="url(#b)" d="M0 0h166v76H0z" transform="matrix(1 0 0 -1 13 172)"/></g><mask id="c" width="154" height="152" x="19" y="20" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#3186ff" d="M19.867 49.392C17.818 33.82 29.94 20 45.645 20h100.71c15.706 0 27.827 13.82 25.778 29.392L166 96l6.133 46.608C174.182 158.18 162.061 172 146.355 172H45.645c-15.706 0-27.827-13.82-25.778-29.392L26 96z"/></mask><g mask="url(#c)"><path fill="url(#d)" d="M32 27.2C32 16.596 40.596 8 51.2 8h89.6c10.604 0 19.2 8.596 19.2 19.2V96H32z" filter="url(#e)"/></g><path fill="#fff" d="M75.353 133.336q-6.282 0-10.777-2.043t-7.61-5.465q-3.065-3.474-4.342-6.793T51.603 115a2.07 2.07 0 0 1 1.021-1.124l5.67-2.247q.714-.357 1.43-.102.714.204 1.685 2.349 1.022 2.145 2.86 4.546a14.3 14.3 0 0 0 4.495 3.728q2.606 1.328 6.435 1.328 6.18 0 9.807-3.575 3.677-3.575 3.677-9.091 0-5.976-3.882-9.194-3.881-3.269-10.266-3.269h-5.362a1.9 1.9 0 0 1-1.328-.51q-.51-.562-.511-1.277v-5.465q0-.767.51-1.277a1.82 1.82 0 0 1 1.329-.562h4.647q5.721 0 9.194-3.116t3.473-8.07q0-4.902-3.116-7.916t-8.58-3.014q-3.065 0-5.312 1.022a11.5 11.5 0 0 0-3.882 2.86 22.7 22.7 0 0 0-2.809 3.78q-1.174 1.941-1.89 2.145-.714.153-1.379-.255l-5.363-2.605q-.664-.358-.868-1.124t1.226-3.575q1.481-2.86 4.494-5.823a21 21 0 0 1 7.049-4.597q4.035-1.635 9.398-1.634 9.96 0 15.782 5.26 5.823 5.21 5.823 13.791 0 5.925-2.86 10.266-2.81 4.34-7.968 6.13v.204q6.231 1.838 9.806 6.741 3.627 4.853 3.626 11.594 0 9.654-6.742 15.834-6.74 6.18-17.57 6.18zm51.25-1.175q-.868 0-1.533-.664a2.25 2.25 0 0 1-.612-1.583V73.118l-11.492 8.274q-.614.46-1.431.307a1.96 1.96 0 0 1-1.225-.766l-3.32-4.7a1.98 1.98 0 0 1-.358-1.43q.153-.816.817-1.276l20.379-14.557q.256-.204.562-.306.307-.153.715-.153h4.291q.868 0 1.379.613.562.56.562 1.43v69.36q0 .92-.664 1.583a2 2 0 0 1-1.533.664z"/><defs><linearGradient id="b" x1="83" x2="83" y1="76" gradientUnits="userSpaceOnUse"><stop stop-color="#4fa0ff"/><stop offset="1" stop-color="#3186ff"/></linearGradient><linearGradient id="d" x1="89.06" x2="89.06" y1="21.75" y2="96.39" gradientUnits="userSpaceOnUse"><stop stop-color="#a9a8ff"/><stop offset=".8" stop-color="#3c90ff"/></linearGradient><filter id="e" width="152" height="112" x="20" y="-4" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_37330_7673" stdDeviation="6"/></filter></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.3 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="229" viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none"><mask id="a" width="168" height="154" x="12" y="18" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#b43333" d="M63.09 37c14.626-25.333 51.193-25.334 65.819 0l45.033 78c14.626 25.334-3.657 57.001-32.91 57.001H50.967c-29.253 0-47.536-31.667-32.91-57.001z"/></mask><g mask="url(#a)"><path fill="url(#b)" d="M206.905 172.02h-91.888l-19.015-32.934 45.944-79.578z"/><path fill="url(#c)" d="M-14.919 172.006 50.04 59.494v.002L31.032 92.422h38.02L115 172.004l-129.918.001z"/><path fill="url(#d)" d="M96.007-20.085 141.954 59.5l-19.011 32.928H31.048z"/></g><defs><linearGradient id="b" x1="193.6" x2="103.09" y1="165.6" y2="111.21" gradientUnits="userSpaceOnUse"><stop offset=".09" stop-color="#ffe921"/><stop offset="1" stop-color="#fec700"/></linearGradient><linearGradient id="c" x1="114.4" x2="15.53" y1="181.61" y2="121.8" gradientUnits="userSpaceOnUse"><stop offset=".15" stop-color="#a9a8ff"/><stop offset=".33" stop-color="#6d97ff"/><stop offset=".48" stop-color="#3186ff"/></linearGradient><linearGradient id="d" x1="128.88" x2="28.7" y1="37.88" y2="84.64" gradientUnits="userSpaceOnUse"><stop offset=".55" stop-color="#0ebc5f"/><stop offset=".85" stop-color="#78c9ff"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="193" viewBox="0 0 256 193"><path fill="#4285f4" d="M58.182 192.05V93.14L27.507 65.077L0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455z"/><path fill="#34a853" d="M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837l-27.026 25.798z"/><path fill="#ea4335" d="m58.182 93.14l-4.174-38.647l4.174-36.989L128 69.868l69.818-52.364l4.669 34.992l-4.669 40.644L128 145.504z"/><path fill="#fbbc04" d="M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945z"/><path fill="#c5221f" d="m0 49.504l26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none" viewBox="0 0 192 192"><path fill="url(#a)" d="M146 44h38v110c0 6.627-5.373 12-12 12h-20a6 6 0 0 1-6-6z"/><path fill="#fc413d" d="M46 44H8v110c0 6.627 5.373 12 12 12h20a6 6 0 0 0 6-6z"/><path fill="url(#b)" d="M39.226 30.456c-8.033-6.752-20.018-5.714-26.77 2.319-6.752 8.032-5.714 20.017 2.319 26.77l76.078 63.949a8 8 0 0 0 10.295 0l76.078-63.95c8.032-6.752 9.07-18.737 2.318-26.77-6.752-8.032-18.737-9.07-26.769-2.318L96 78.18z"/><defs><linearGradient id="a" x1="165" x2="165" y1="44" y2="166" gradientUnits="userSpaceOnUse"><stop stop-color="#60d673"/><stop offset=".17" stop-color="#42c868"/><stop offset=".39" stop-color="#0ebc5f"/><stop offset=".62" stop-color="#00a9bb"/><stop offset=".86" stop-color="#3c90ff"/><stop offset="1" stop-color="#3186ff"/></linearGradient><linearGradient id="b" x1="8" x2="184" y1="46.13" y2="46.13" gradientUnits="userSpaceOnUse"><stop offset=".08" stop-color="#ff63a0"/><stop offset=".3" stop-color="#fc413d"/><stop offset=".5" stop-color="#fc413d"/><stop offset=".65" stop-color="#fc413d"/><stop offset=".72" stop-color="#fc5c30"/><stop offset=".86" stop-color="#feb10c"/><stop offset=".91" stop-color="#fec700"/><stop offset=".96" stop-color="#ffdb0f"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 671 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 4.1 MiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><radialGradient id="prefix__b" cx="1.479" cy="12.788" fx="1.479" fy="12.788" r="9.655" gradientTransform="matrix(.8032 0 0 1.0842 2.459 -.293)" gradientUnits="userSpaceOnUse"><stop offset=".368" stop-color="#ffcf09"/><stop offset=".718" stop-color="#ffcf09" stop-opacity=".7"/><stop offset="1" stop-color="#ffcf09" stop-opacity="0"/></radialGradient><radialGradient id="prefix__c" cx="14.295" cy="23.291" fx="14.295" fy="23.291" r="11.878" gradientTransform="matrix(1.3272 0 0 1.0073 -3.434 -.672)" gradientUnits="userSpaceOnUse"><stop offset=".383" stop-color="#34a853"/><stop offset=".706" stop-color="#34a853" stop-opacity=".7"/><stop offset="1" stop-color="#34a853" stop-opacity="0"/></radialGradient><linearGradient id="prefix__d" x1="23.558" y1="6.286" x2="12.148" y2="20.299" gradientUnits="userSpaceOnUse"><stop offset=".671" stop-color="#4285f4"/><stop offset=".885" stop-color="#4285f4" stop-opacity="0"/></linearGradient><clipPath id="prefix__a"><path d="M22.36 10H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h-.013l.013-.01c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09c.87-2.6 3.3-4.53 6.16-4.53 1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07 1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93v.01C3.99 20.53 7.7 23 12 23c2.97 0 5.46-.98 7.28-2.66 2.08-1.92 3.28-4.74 3.28-8.09 0-.78-.07-1.53-.2-2.25z" fill="none"/></clipPath></defs><path d="M22.36 10H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h-.013l.013-.01c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09c.87-2.6 3.3-4.53 6.16-4.53 1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07 1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93v.01C3.99 20.53 7.7 23 12 23c2.97 0 5.46-.98 7.28-2.66 2.08-1.92 3.28-4.74 3.28-8.09 0-.78-.07-1.53-.2-2.25z" fill="#fc4c53"/><g clip-path="url(#prefix__a)"><ellipse cx="3.646" cy="13.572" rx="7.755" ry="10.469" fill="url(#prefix__b)"/><ellipse cx="15.538" cy="22.789" rx="15.765" ry="11.965" transform="rotate(-7.12 15.539 22.789)" fill="url(#prefix__c)"/><path fill="url(#prefix__d)" d="M11.105 8.28l.491 5.596.623 3.747 7.362 6.848 8.607-15.897-17.083-.294z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 2.2 KiB |
1
surfsense_web/public/connectors/indeed.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Indeed" viewBox="0 0 24 24" width="24" height="24" fill="#003A9B"><title>Indeed</title><path d="M11.566 21.5633v-8.762c.2553.0231.5009.0346.758.0346 1.2225 0 2.3739-.3206 3.3506-.8928v9.6182c0 .8219-.1957 1.4287-.5757 1.8338-.378.4033-.8808.6049-1.491.6049-.6007 0-1.0766-.2016-1.468-.6183-.3781-.4032-.5739-1.01-.5739-1.8184zM11.589.5659c2.5447-.8929 5.4424-.8449 7.6186.987.405.3687.8673.8334 1.0515 1.3806.2207.6913-.7695-.073-.9057-.167-.71-.4532-1.4182-.8334-2.2127-1.0946C12.8614.3873 8.8122 2.709 6.2945 6.315c-1.0516 1.5939-1.7367 3.2721-2.299 5.1174-.0614.2017-.1094.4647-.2207.6413-.1113.2036-.048-.5453-.048-.5702.0845-.7623.2438-1.4997.4414-2.237C5.3292 5.3375 7.897 2.0655 11.5891.5658zm4.9281 7.0587c0 1.6686-1.353 3.0224-3.0205 3.0224-1.6677 0-3.0186-1.3538-3.0186-3.0224 0-1.6687 1.351-3.0224 3.0186-3.0224 1.6676 0 3.0205 1.3518 3.0205 3.0224Z"/></svg>
|
||||
|
After Width: | Height: | Size: 933 B |
|
|
@ -1,6 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<path fill="#212121" fill-rule="evenodd" d="M10.904,6h26.191C39.804,6,42,8.196,42,10.909v26.191C42,39.804,39.804,42,37.096,42H10.904C8.196,42,6,39.804,6,37.091V10.909C6,8.196,8.196,6,10.904,6z" clip-rule="evenodd"/>
|
||||
<path fill="#ec407a" fill-rule="evenodd" d="M29.208,20.607c1.576,1.126,3.507,1.788,5.592,1.788v-4.011c-0.395,0-0.788-0.041-1.174-0.123v3.157c-2.085,0-4.015-0.663-5.592-1.788v8.184c0,4.094-3.321,7.413-7.417,7.413c-1.528,0-2.949-0.462-4.129-1.254c1.347,1.376,3.225,2.23,5.303,2.23c4.096,0,7.417-3.319,7.417-7.413L29.208,20.607L29.208,20.607z M30.657,16.561c-0.805-0.879-1.334-2.016-1.449-3.273v-0.516h-1.113C28.375,14.369,29.331,15.734,30.657,16.561L30.657,16.561z M19.079,30.832c-0.45-0.59-0.693-1.311-0.692-2.053c0-1.873,1.519-3.391,3.393-3.391c0.349,0,0.696,0.053,1.029,0.159v-4.1c-0.389-0.053-0.781-0.076-1.174-0.068v3.191c-0.333-0.106-0.68-0.16-1.03-0.159c-1.874,0-3.392,1.518-3.392,3.392C17.213,29.156,17.972,30.31,19.079,30.832z" clip-rule="evenodd"/>
|
||||
<path fill="#fff" fill-rule="evenodd" d="M28.034,19.63c1.576,1.126,3.507,1.788,5.592,1.788v-3.157c-1.164-0.248-2.194-0.856-2.969-1.701c-1.326-0.827-2.282-2.191-2.561-3.789h-2.923v16.018c-0.007,1.867-1.523,3.379-3.393,3.379c-1.102,0-2.081-0.525-2.701-1.338c-1.107-0.522-1.866-1.678-1.866-3.011c0-1.874,1.518-3.392,3.392-3.392c0.359,0,0.705,0.056,1.03,0.159v-3.191c-4.024,0.083-7.26,3.369-7.26,7.411c0,2.018,0.806,3.847,2.114,5.183c1.18,0.792,2.601,1.254,4.129,1.254c4.096,0,7.417-3.319,7.417-7.413L28.034,19.63z" clip-rule="evenodd"/>
|
||||
<path fill="#81ecec" fill-rule="evenodd" d="M33.626,18.261v-0.854c-1.05,0.002-2.078-0.292-2.969-0.848C31.445,17.423,32.483,18.018,33.626,18.261z M28.095,12.771c-0.027-0.153-0.047-0.306-0.061-0.461v-0.516h-4.036v16.019c-0.006,1.867-1.522,3.379-3.393,3.379c-0.549,0-1.068-0.13-1.527-0.362c0.62,0.813,1.599,1.338,2.701,1.338c1.87,0,3.386-1.512,3.393-3.379V12.771H28.095z M21.35,21.657v-0.909c-0.337-0.046-0.677-0.069-1.017-0.069c-4.097,0-7.418,3.319-7.418,7.413c0,2.567,1.305,4.829,3.289,6.159c-1.308-1.336-2.114-3.165-2.114-5.183C14.09,25.026,17.326,21.74,21.35,21.657z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" id="tiktok">
|
||||
<path fill="#070201" fill-rule="evenodd" d="m60,12c0-4.42-3.58-8-8-8H12C7.58,4,4,7.58,4,12v40c0,4.42,3.58,8,8,8h40c4.42,0,8-3.58,8-8V12h0Z"></path>
|
||||
<path fill="#fe2c55" fill-rule="evenodd" d="m47.64,27.65c0,.3-.13.58-.37.77-.23.19-.53.22-.83.21-1.86-.05-4.77-.69-6.14-2v12.54c0,5.78-4.68,10.46-10.46,10.46h-1.08c-2.42,0-4.72-.88-6.58-2.39,1.43.71,3,1.13,4.63,1.13h1.08c5.78,0,10.46-4.68,10.46-10.46v-12.54c1.37,1.3,4.28,1.95,6.14,2,.3,0,.6-.02.83-.21.23-.19.37-.47.37-.77,0-1.42,0-3.78,0-5.02.38.11.77.2,1.16.29.46.1.79.5.79.97,0,1.24,0,3.61,0,5.02Zm-19.58,4.43c-.19.19-.44.29-.71.29-2.76,0-5,2.24-5,5,0,2.17,1.39,4,3.33,4.69-.85-.9-1.38-2.1-1.38-3.43,0-2.76,2.24-5,5-5,.27,0,.52-.1.71-.29.19-.19.29-.44.29-.71v-3c0-.26-.11-.52-.29-.71s-.44-.29-.71-.29h-.54c-.14,0-.27.04-.41.04v2.7c0,.27-.11.52-.29.71Zm13.67-12.69c-.83-.75-1.53-1.74-2.03-3.09-.15-.39-.52-.67-.94-.67h-.74c.87,1.91,2.16,3.03,3.71,3.75Z"></path>
|
||||
<path fill="#25f4ee" fill-rule="evenodd" d="m21.37,46.57c.25.25.55.45.82.67-1.01-.5-1.96-1.12-2.77-1.93-1.96-1.96-3.06-4.62-3.06-7.4v-.08c0-5.78,4.68-10.46,10.46-10.46h.54c.27,0,.52.11.71.29.19.19.29.44.29.71v.3c-5.58.22-10.05,4.78-10.05,10.42v.08c0,2.77,1.1,5.43,3.06,7.4Zm24.31-25.2h0c0-.47-.33-.88-.79-.97-1.12-.25-2.19-.55-3.17-1.01,1.11,1,2.47,1.57,3.95,1.98Zm-16.38,22.27c1.33,0,2.6-.53,3.54-1.46s1.46-2.21,1.46-3.54v-22c0-.55.45-1,1-1h2.72c-.09-.2-.19-.38-.27-.6-.15-.39-.52-.67-.94-.67h-3.46c-.55,0-1,.45-1,1v22c0,1.33-.53,2.6-1.46,3.54s-2.21,1.46-3.54,1.46c-.59,0-1.15-.12-1.67-.31.91.96,2.19,1.57,3.62,1.57Z"></path>
|
||||
<path fill="#fff" fill-rule="evenodd" d="m27.9,48.37c5.78,0,10.46-4.68,10.46-10.46v-12.54c1.37,1.3,4.28,1.95,6.14,2,.3,0,.6-.02.83-.21.23-.19.37-.47.37-.77,0-1.42,0-3.78,0-5.02-1.48-.41-2.84-.98-3.95-1.98-1.54-.72-2.84-1.85-3.71-3.75h-2.72c-.55,0-1,.45-1,1v22c0,1.33-.53,2.6-1.46,3.54s-2.21,1.46-3.54,1.46c-1.43,0-2.71-.61-3.62-1.57-1.93-.69-3.33-2.52-3.33-4.69,0-2.76,2.24-5,5-5,.27,0,.52-.1.71-.29.19-.19.29-.44.29-.71v-2.7c-5.58.22-10.05,4.78-10.05,10.42v.08c0,2.77,1.1,5.43,3.06,7.4.25.25.55.45.82.67,1.43.71,3,1.13,4.63,1.13h1.08Z"></path>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
874
surfsense_web/public/logos/berkeley.svg
Normal file
|
After Width: | Height: | Size: 119 KiB |