mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-10 20:35:17 +02:00
Merge commit '61adc80615' into dev
This commit is contained in:
commit
6d1d00ebbc
7 changed files with 136 additions and 46 deletions
18
surfsense_web/app/(home)/free/layout.tsx
Normal file
18
surfsense_web/app/(home)/free/layout.tsx
Normal file
|
|
@ -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
|
||||
* <AdSenseScript /> here loads adsbygoogle.js across the entire /free route
|
||||
* tree, which is what powers both the manual <AdUnit /> 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 (
|
||||
<>
|
||||
<AdSenseScript />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="min-h-screen pt-20">
|
||||
<AdSenseScript />
|
||||
<JsonLd
|
||||
data={{
|
||||
"@context": "https://schema.org",
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ import type { Context } from "@/types/zero";
|
|||
import { queries } from "@/zero/queries";
|
||||
import { schema } from "@/zero/schema";
|
||||
|
||||
function getBackendBaseUrl() {
|
||||
const base = process.env.FASTAPI_BACKEND_INTERNAL_URL || "http://localhost:8000";
|
||||
return base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
}
|
||||
|
||||
const backendURL = getBackendBaseUrl();
|
||||
// This route is invoked server-to-server by zero-cache (via ZERO_QUERY_URL),
|
||||
// so it must reach the backend over the internal Docker network
|
||||
// (e.g. http://backend:8000). The browser-facing NEXT_PUBLIC_FASTAPI_BACKEND_URL
|
||||
// (e.g. http://localhost:8929) does NOT resolve from inside the frontend
|
||||
// container and would make every authenticated Zero query fail with a 503.
|
||||
const backendURL = (
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
BACKEND_URL ||
|
||||
"http://localhost:8000"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
async function authenticateRequest(
|
||||
request: Request
|
||||
|
|
|
|||
|
|
@ -254,13 +254,15 @@ const ThreadWelcome: FC = () => {
|
|||
|
||||
return (
|
||||
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
||||
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
||||
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-[2.625rem] select-none">
|
||||
{greeting}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
|
||||
<Composer />
|
||||
<div className="my-auto flex w-full flex-col items-center gap-6 py-6 sm:contents sm:my-0 sm:gap-0 sm:py-0">
|
||||
<div className="aui-thread-welcome-message flex flex-col items-center text-center sm:absolute sm:bottom-[calc(50%+5rem)] sm:left-0 sm:right-0">
|
||||
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-[2.625rem] select-none">
|
||||
{greeting}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full flex items-start justify-center sm:absolute sm:top-[calc(50%-3.5rem)] sm:left-0 sm:right-0">
|
||||
<Composer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<FreeModelSelector />
|
||||
</div>
|
||||
|
||||
<RemoveAdsBanner />
|
||||
|
||||
{captchaRequired && TURNSTILE_SITE_KEY && (
|
||||
<div className="flex justify-center border-b bg-muted/30 px-4 py-4">
|
||||
<Alert className="w-auto max-w-md">
|
||||
|
|
|
|||
77
surfsense_web/components/free-chat/remove-ads-banner.tsx
Normal file
77
surfsense_web/components/free-chat/remove-ads-banner.tsx
Normal file
|
|
@ -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 (
|
||||
<div className={cn("shrink-0 border-b bg-muted/30 px-4 py-3", className)}>
|
||||
<Alert className="relative mx-auto w-full max-w-2xl pr-10">
|
||||
<Sparkles />
|
||||
<AlertTitle>Go ad-free with a free account</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<Button asChild size="sm" className="mt-1">
|
||||
<Link href="/login">Create Free Account</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss"
|
||||
className="absolute top-2 right-2 size-6"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -76,36 +76,24 @@ export function ChatExamplePrompts({ onSelect }: ChatExamplePromptsProps) {
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeCategory ? (
|
||||
<div className="overflow-hidden rounded-lg border border-input bg-muted shadow-sm shadow-black/5 dark:shadow-black/10 sm:rounded-xl">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 sm:gap-3 sm:px-4 sm:py-3">
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground sm:text-sm">
|
||||
<span className="truncate">{activeCategory.label}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setActiveCategoryId(null)}
|
||||
aria-label="Close example prompts"
|
||||
className="size-7 shrink-0 rounded-full text-muted-foreground hover:bg-foreground/10 hover:text-foreground sm:size-8"
|
||||
>
|
||||
<X aria-hidden="true" className="size-3.5 sm:size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="max-h-52 sm:max-h-64">
|
||||
<ul className="divide-y px-2 pb-2 sm:px-3 sm:pb-3">
|
||||
{activeCategory.prompts.map((prompt) => (
|
||||
<li key={prompt} className="py-0.5 sm:py-1">
|
||||
<ExamplePromptButton prompt={prompt} onSelect={onSelect} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : null}
|
||||
{CHAT_EXAMPLE_CATEGORIES.map((category) => (
|
||||
<TabsContent
|
||||
key={category.id}
|
||||
value={category.id}
|
||||
className="mt-3 focus-visible:outline-none"
|
||||
>
|
||||
<ScrollArea className="h-[clamp(7.5rem,26vh,12rem)]">
|
||||
<ul className="flex flex-col gap-2 pr-3">
|
||||
{category.prompts.map((prompt) => (
|
||||
<li key={prompt}>
|
||||
<ExamplePromptButton prompt={prompt} onSelect={onSelect} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue