diff --git a/surfsense_web/app/(home)/free/layout.tsx b/surfsense_web/app/(home)/free/layout.tsx new file mode 100644 index 000000000..9447d34dd --- /dev/null +++ b/surfsense_web/app/(home)/free/layout.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; +import { AdSenseScript } from "@/components/ads/adsense-script"; + +/** + * Wraps the /free hub and all /free/[model_slug] subpages. Mounting + * here loads adsbygoogle.js across the entire /free route + * tree, which is what powers both the manual slots and AdSense + * Auto ads. Because the script lives here (not in the root layout), Auto ads + * is naturally scoped to /free and its subpages only. + */ +export default function FreeSectionLayout({ children }: { children: ReactNode }) { + return ( + <> + + {children} + + ); +} diff --git a/surfsense_web/app/(home)/free/page.tsx b/surfsense_web/app/(home)/free/page.tsx index 5cea9b6d2..89a4735ae 100644 --- a/surfsense_web/app/(home)/free/page.tsx +++ b/surfsense_web/app/(home)/free/page.tsx @@ -3,7 +3,6 @@ import type { Metadata } from "next"; import Link from "next/link"; import { AdUnit } from "@/components/ads/ad-unit"; import { ADSENSE_SLOTS } from "@/components/ads/adsense-config"; -import { AdSenseScript } from "@/components/ads/adsense-script"; import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav"; import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld"; import { Badge } from "@/components/ui/badge"; @@ -160,7 +159,6 @@ export default async function FreeHubPage() { return (
- { return (
-
-

- {greeting} -

-
-
- +
+
+

+ {greeting} +

+
+
+ +
); diff --git a/surfsense_web/components/free-chat/free-chat-page.tsx b/surfsense_web/components/free-chat/free-chat-page.tsx index 2ee026cc3..b28b1e0a1 100644 --- a/surfsense_web/components/free-chat/free-chat-page.tsx +++ b/surfsense_web/components/free-chat/free-chat-page.tsx @@ -37,6 +37,7 @@ import { BACKEND_URL } from "@/lib/env-config"; import { trackAnonymousChatMessageSent } from "@/lib/posthog/events"; import { FreeModelSelector } from "./free-model-selector"; import { FreeThread } from "./free-thread"; +import { RemoveAdsBanner } from "./remove-ads-banner"; // Render all tool calls via ToolFallback; backend keeps persisted // payloads bounded by summarising / truncating outputs. @@ -135,7 +136,7 @@ export function FreeChatPage() { pendingRetryRef.current = null; }, [resetKey, modelSlug, tokenUsageStore]); - const cancelRun = useCallback(() => { + const cancelRun = useCallback(async () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; @@ -487,6 +488,8 @@ export function FreeChatPage() {
+ + {captchaRequired && TURNSTILE_SITE_KEY && (
diff --git a/surfsense_web/components/free-chat/remove-ads-banner.tsx b/surfsense_web/components/free-chat/remove-ads-banner.tsx new file mode 100644 index 000000000..143609c6b --- /dev/null +++ b/surfsense_web/components/free-chat/remove-ads-banner.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Sparkles, X } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +const ADSENSE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID; + +// Versioned key so the copy can change later without resurfacing for users who +// already dismissed an older variant (bump the version to re-show). +const DISMISS_KEY = "surfsense:remove-ads-banner-dismissed:v1"; + +/** + * Dismissible promo shown on the free /free/[model_slug] chat pages, nudging + * anonymous users to sign up to remove ads. Dismissal is persisted in + * localStorage so it stays hidden across reloads and navigations. The free + * chat keeps working whether or not the banner is dismissed. + * + * Renders nothing when AdSense is not configured (dev/preview), since there are + * no ads to remove in that case. + */ +export function RemoveAdsBanner({ className }: { className?: string }) { + // Default hidden so dismissed users never see a flash before the stored + // value is read on the client (avoids a hydration/flicker mismatch). + const [dismissed, setDismissed] = useState(true); + + useEffect(() => { + try { + setDismissed(localStorage.getItem(DISMISS_KEY) === "1"); + } catch { + // localStorage can throw in private browsing / when disabled. + setDismissed(false); + } + }, []); + + const handleDismiss = () => { + setDismissed(true); + try { + localStorage.setItem(DISMISS_KEY, "1"); + } catch { + // Ignore: dismissal just won't persist across reloads. + } + }; + + if (!ADSENSE_CLIENT_ID || dismissed) return null; + + return ( +
+ + + Go ad-free with a free account + +

+ Create a free SurfSense account to remove ads, unlock $5 of premium credit, and save + your chat history. You can keep chatting for free either way. +

+ +
+ +
+
+ ); +} diff --git a/surfsense_web/components/new-chat/chat-example-prompts.tsx b/surfsense_web/components/new-chat/chat-example-prompts.tsx index f969aa61b..ee2e86daf 100644 --- a/surfsense_web/components/new-chat/chat-example-prompts.tsx +++ b/surfsense_web/components/new-chat/chat-example-prompts.tsx @@ -76,36 +76,24 @@ export function ChatExamplePrompts({ onSelect }: ChatExamplePromptsProps) { })}
- )} - - {activeCategory ? ( -
-
-
- {activeCategory.label} -
- -
- -
    - {activeCategory.prompts.map((prompt) => ( -
  • - -
  • - ))} -
-
-
- ) : null} + {CHAT_EXAMPLE_CATEGORIES.map((category) => ( + + +
    + {category.prompts.map((prompt) => ( +
  • + +
  • + ))} +
+
+
+ ))} + ); } \ No newline at end of file