feat: no login experience and prem tokens
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-15 17:02:00 -07:00
parent 87452bb315
commit ff4e0f9b62
68 changed files with 5914 additions and 121 deletions

View file

@ -0,0 +1,74 @@
"use client";
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
export interface AnonymousModeContextValue {
isAnonymous: true;
modelSlug: string;
setModelSlug: (slug: string) => void;
uploadedDoc: { filename: string; sizeBytes: number } | null;
setUploadedDoc: (doc: { filename: string; sizeBytes: number } | null) => void;
resetKey: number;
resetChat: () => void;
}
interface AuthenticatedContextValue {
isAnonymous: false;
}
type ContextValue = AnonymousModeContextValue | AuthenticatedContextValue;
const DEFAULT_VALUE: AuthenticatedContextValue = { isAnonymous: false };
const AnonymousModeContext = createContext<ContextValue>(DEFAULT_VALUE);
export function AnonymousModeProvider({
initialModelSlug,
children,
}: {
initialModelSlug: string;
children: ReactNode;
}) {
const [modelSlug, setModelSlug] = useState(initialModelSlug);
const [uploadedDoc, setUploadedDoc] = useState<{ filename: string; sizeBytes: number } | null>(
null
);
const [resetKey, setResetKey] = useState(0);
const resetChat = () => setResetKey((k) => k + 1);
useEffect(() => {
anonymousChatApiService
.getDocument()
.then((doc) => {
if (doc) {
setUploadedDoc({ filename: doc.filename, sizeBytes: doc.size_bytes });
}
})
.catch(() => {});
}, []);
const value = useMemo<AnonymousModeContextValue>(
() => ({
isAnonymous: true,
modelSlug,
setModelSlug,
uploadedDoc,
setUploadedDoc,
resetKey,
resetChat,
}),
[modelSlug, uploadedDoc, resetKey]
);
return <AnonymousModeContext.Provider value={value}>{children}</AnonymousModeContext.Provider>;
}
export function useAnonymousMode(): ContextValue {
return useContext(AnonymousModeContext);
}
export function useIsAnonymous(): boolean {
return useContext(AnonymousModeContext).isAnonymous;
}

View file

@ -0,0 +1,84 @@
"use client";
import Link from "next/link";
import { createContext, type ReactNode, useCallback, useContext, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useIsAnonymous } from "./anonymous-mode";
interface LoginGateContextValue {
gate: (feature: string) => void;
}
const LoginGateContext = createContext<LoginGateContextValue>({
gate: () => {},
});
export function LoginGateProvider({ children }: { children: ReactNode }) {
const isAnonymous = useIsAnonymous();
const [feature, setFeature] = useState<string | null>(null);
const gate = useCallback(
(feat: string) => {
if (isAnonymous) {
setFeature(feat);
}
},
[isAnonymous]
);
const close = () => setFeature(null);
return (
<LoginGateContext.Provider value={{ gate }}>
{children}
<Dialog open={feature !== null} onOpenChange={(open) => !open && close()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create a free account to {feature}</DialogTitle>
<DialogDescription>
Get 5 million tokens, save chat history, upload documents, use all AI tools, and
connect 30+ integrations.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex flex-col gap-2 sm:flex-row">
<Button asChild>
<Link href="/register">Create Free Account</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/login">Log In</Link>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</LoginGateContext.Provider>
);
}
export function useLoginGate(): LoginGateContextValue {
return useContext(LoginGateContext);
}
/**
* Returns a click handler that triggers the login gate when anonymous,
* or calls the original handler when authenticated.
*/
export function useGatedHandler(handler: (() => void) | undefined, feature: string): () => void {
const { gate } = useLoginGate();
const isAnonymous = useIsAnonymous();
return useCallback(() => {
if (isAnonymous) {
gate(feature);
} else {
handler?.();
}
}, [isAnonymous, gate, feature, handler]);
}