mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-30 21:59:46 +02:00
Merge remote-tracking branch 'upstream/dev' into fix/changelogs
This commit is contained in:
commit
1c3f4cc6ac
28 changed files with 2461 additions and 785 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 { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -159,7 +158,6 @@ export default async function FreeHubPage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen pt-20">
|
||||
<AdSenseScript />
|
||||
<JsonLd
|
||||
data={{
|
||||
"@context": "https://schema.org",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import { mustGetQuery } from "@rocicorp/zero";
|
||||
import { handleQueryRequest } from "@rocicorp/zero/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import type { Context } from "@/types/zero";
|
||||
import { queries } from "@/zero/queries";
|
||||
import { schema } from "@/zero/schema";
|
||||
|
||||
const backendURL = BACKEND_URL;
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||
import { useFolderSync } from "@/hooks/use-folder-sync";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { isLlmOnboardingComplete } from "@/lib/onboarding";
|
||||
|
||||
export function DashboardClientLayout({
|
||||
children,
|
||||
|
|
@ -47,9 +48,8 @@ export function DashboardClientLayout({
|
|||
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
|
||||
|
||||
const isOnboardingComplete = useCallback(() => {
|
||||
// Check that the Agent LLM ID is set, including 0 for Auto mode.
|
||||
return preferences.agent_llm_id !== null && preferences.agent_llm_id !== undefined;
|
||||
}, [preferences.agent_llm_id]);
|
||||
return isLlmOnboardingComplete(preferences.agent_llm_id, globalConfigs.length > 0);
|
||||
}, [preferences.agent_llm_id, globalConfigs.length]);
|
||||
|
||||
const { data: access = null, isLoading: accessLoading } = useAtomValue(myAccessAtom);
|
||||
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { isLlmOnboardingComplete } from "@/lib/onboarding";
|
||||
|
||||
export default function OnboardPage() {
|
||||
const router = useRouter();
|
||||
|
|
@ -52,15 +53,16 @@ export default function OnboardPage() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Check if onboarding is already complete (including 0 for Auto mode)
|
||||
const isOnboardingComplete =
|
||||
preferences.agent_llm_id !== null && preferences.agent_llm_id !== undefined;
|
||||
const isOnboardingComplete = isLlmOnboardingComplete(
|
||||
preferences.agent_llm_id,
|
||||
globalConfigs.length > 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!preferencesLoading && isOnboardingComplete) {
|
||||
if (!preferencesLoading && globalConfigsLoaded && isOnboardingComplete) {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
}
|
||||
}, [preferencesLoading, isOnboardingComplete, router, searchSpaceId]);
|
||||
}, [preferencesLoading, globalConfigsLoaded, isOnboardingComplete, router, searchSpaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoConfigureWithGlobal = async () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ All configuration lives in a single `docker/.env` file (or `surfsense/.env` if y
|
|||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `SURFSENSE_VERSION` | Image tag to deploy. Use `latest`, a clean version (e.g. `0.0.14`), or a specific build (e.g. `0.0.14.1`) | `latest` |
|
||||
| `SURFSENSE_VARIANT` | Backend image variant. Leave empty for CPU, set `cuda` for CUDA 12.8, or `cuda126` for CUDA 12.6. | *(empty)* |
|
||||
| `AUTH_TYPE` | Authentication method: `LOCAL` (email/password) or `GOOGLE` (OAuth) | `LOCAL` |
|
||||
| `ETL_SERVICE` | Document parsing: `DOCLING` (local), `UNSTRUCTURED`, or `LLAMACLOUD` | `DOCLING` |
|
||||
| `EMBEDDING_MODEL` | Embedding model for vector search | `sentence-transformers/all-MiniLM-L6-v2` |
|
||||
|
|
@ -42,6 +43,62 @@ All configuration lives in a single `docker/.env` file (or `surfsense/.env` if y
|
|||
| `STT_SERVICE` | Speech-to-text provider for audio files | `local/base` |
|
||||
| `REGISTRATION_ENABLED` | Allow new user registrations | `TRUE` |
|
||||
|
||||
### Image Variants
|
||||
|
||||
SurfSense publishes CPU and CUDA backend image variants. The frontend image is not variant-specific.
|
||||
|
||||
| Backend tag | Use case | `SURFSENSE_VARIANT` |
|
||||
|-------------|----------|---------------------|
|
||||
| `:latest` | CPU-only default | *(empty)* |
|
||||
| `:latest-cuda` | NVIDIA CUDA 12.8 backend image | `cuda` |
|
||||
| `:latest-cuda126` | NVIDIA CUDA 12.6 backend image for older driver stacks | `cuda126` |
|
||||
|
||||
All backend variants are published for `linux/amd64` and `linux/arm64`. CUDA on `linux/arm64` is best-effort.
|
||||
|
||||
<Callout type="info">
|
||||
GPU acceleration needs two settings: `SURFSENSE_VARIANT` selects the CUDA image, and `COMPOSE_FILE` enables the GPU device overlay. The host must have the NVIDIA Container Toolkit installed.
|
||||
</Callout>
|
||||
|
||||
### NVIDIA GPU Acceleration
|
||||
|
||||
For most NVIDIA systems, add these values to `.env` to use the CUDA 12.8 image:
|
||||
|
||||
```dotenv
|
||||
SURFSENSE_VARIANT=cuda
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.gpu.yml
|
||||
SURFSENSE_GPU_COUNT=1
|
||||
```
|
||||
|
||||
Use `SURFSENSE_VARIANT=cuda126` for older NVIDIA driver stacks or older GPUs that need the CUDA 12.6 fallback image.
|
||||
|
||||
On Windows, use `;` instead of `:` in `COMPOSE_FILE` inside `.env`:
|
||||
|
||||
```dotenv
|
||||
COMPOSE_FILE=docker-compose.yml;docker-compose.gpu.yml
|
||||
```
|
||||
|
||||
To switch variants later, edit `SURFSENSE_VARIANT` and `COMPOSE_FILE` in `.env`, then run:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
### Automatic Updates
|
||||
|
||||
Manual Docker Compose installs do not start Watchtower automatically. To enable external automatic updates, run Watchtower separately:
|
||||
|
||||
```bash
|
||||
docker run -d --name watchtower \
|
||||
--restart unless-stopped \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
nickfedor/watchtower \
|
||||
--label-enable \
|
||||
--interval 86400
|
||||
```
|
||||
|
||||
SurfSense containers are labeled for Watchtower, so `--label-enable` limits updates to the SurfSense services.
|
||||
|
||||
### Ports
|
||||
|
||||
| Variable | Description | Default |
|
||||
|
|
@ -270,11 +327,13 @@ Symptom (in `docker compose logs zero-cache`):
|
|||
Error: Unknown or invalid publications. Specified: [zero_publication]. Found: []
|
||||
```
|
||||
|
||||
This means `zero-cache` started before `zero_publication` was created. With
|
||||
the current compose files this should be impossible. The `migrations`
|
||||
service blocks `zero-cache` from starting. If you see it, your stack
|
||||
predates the fix or you brought up `zero-cache` manually with `docker
|
||||
compose up zero-cache` before the migrations service ran.
|
||||
This means `zero-cache` started before `zero_publication` was created or the
|
||||
publication does not match SurfSense's canonical Zero shape. With the current
|
||||
compose files this should be impossible: the `migrations` service blocks
|
||||
`zero-cache` from starting and verifies the publication before exiting
|
||||
successfully. If you see it, your stack predates the fix or you brought up
|
||||
`zero-cache` manually with `docker compose up zero-cache` before the migrations
|
||||
service ran.
|
||||
|
||||
Recovery:
|
||||
|
||||
|
|
@ -284,18 +343,13 @@ docker volume rm surfsense-zero-cache # wipe half-built SQLite replica
|
|||
docker compose up -d # migrations runs first, then zero-cache
|
||||
```
|
||||
|
||||
The install script (`install.ps1` / `install.sh`) detects this case
|
||||
automatically: if it finds a `surfsense-zero-cache` volume from a previous
|
||||
install with no matching `surfsense-zero-init` volume, it removes the stale
|
||||
volume before bringing the stack up.
|
||||
|
||||
### Zero-cache crashes with `_zero.tableMetadata` errors
|
||||
|
||||
This indicates a half-initialized SQLite replica left behind by a previous
|
||||
crash. The `migrations` service writes a marker file on a shared volume
|
||||
(`surfsense-zero-init`) when the publication oid changes; zero-cache wipes
|
||||
its replica and re-syncs on next start. If the marker mechanism somehow did
|
||||
not trigger, run the recovery one-liner above.
|
||||
crash. Zero's own event triggers and `ZERO_AUTO_RESET` handle schema and
|
||||
replication halts automatically. If the local SQLite replica is wedged, run the
|
||||
recovery one-liner above to wipe `surfsense-zero-cache`; zero-cache will
|
||||
re-sync from Postgres on the next start.
|
||||
|
||||
### Ensuring `wal_level = logical`
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ title: One-Line Install Script
|
|||
description: One-command installation of SurfSense using Docker
|
||||
---
|
||||
|
||||
Downloads the compose files, generates a `SECRET_KEY`, starts all services, and sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) for automatic daily updates.
|
||||
Downloads the compose files, generates a `SECRET_KEY`, starts all services with `docker compose up -d --wait`, and starts [Watchtower](https://github.com/nicholas-fedor/watchtower) as an external updater for automatic daily updates.
|
||||
|
||||
**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) must be installed and running.
|
||||
|
||||
|
|
@ -19,9 +19,38 @@ curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scr
|
|||
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
This creates a `./surfsense/` directory with `docker-compose.yml` and `.env`, then runs `docker compose up -d`.
|
||||
This creates a `./surfsense/` directory with `docker-compose.yml`, `docker-compose.gpu.yml`, and `.env`, then runs `docker compose up -d --wait`.
|
||||
|
||||
To skip Watchtower (e.g. in production where you manage updates yourself):
|
||||
If an NVIDIA GPU and NVIDIA Container Toolkit are detected, the installer asks whether to use GPU acceleration and chooses the compatible backend image automatically. Non-interactive installs default to CPU unless you pass an explicit flag.
|
||||
|
||||
Interactive installs also ask whether to enable automatic daily updates with Watchtower, noting that updates may download several GB in the background.
|
||||
|
||||
### GPU options
|
||||
|
||||
Linux/macOS:
|
||||
|
||||
```bash
|
||||
# CUDA 12.8
|
||||
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash -s -- --variant=cuda
|
||||
|
||||
# CUDA 12.6 fallback for older driver stacks
|
||||
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash -s -- --variant=cuda126
|
||||
|
||||
# Reserve all available GPUs
|
||||
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash -s -- --gpu --gpu-count=all
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
# Save the script locally first when passing PowerShell parameters.
|
||||
.\install.ps1 -Variant cuda
|
||||
.\install.ps1 -Variant cuda126 -GpuCount all
|
||||
```
|
||||
|
||||
The installer writes the same `.env` settings you would configure manually: `SURFSENSE_VARIANT` selects the backend image and `COMPOSE_FILE` enables the GPU overlay.
|
||||
|
||||
To skip Watchtower (e.g. in production where you manage updates yourself, or to avoid large background image downloads):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash -s -- --no-watchtower
|
||||
|
|
@ -29,6 +58,16 @@ curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scr
|
|||
|
||||
To customise the check interval (default 24h), use `--watchtower-interval=SECONDS`.
|
||||
|
||||
Manual updates use the same compose state stored in `.env`, so GPU overlays and variants are preserved:
|
||||
|
||||
```bash
|
||||
cd surfsense
|
||||
docker compose pull
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
If Watchtower is enabled, it preserves the running image variant tag automatically. Because SurfSense images are large, use `--no-watchtower` when you prefer to manage update timing yourself.
|
||||
|
||||
---
|
||||
|
||||
## Access SurfSense
|
||||
|
|
|
|||
8
surfsense_web/lib/onboarding.ts
Normal file
8
surfsense_web/lib/onboarding.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function isLlmOnboardingComplete(
|
||||
agentLlmId: number | null | undefined,
|
||||
hasGlobalConfigs: boolean
|
||||
): boolean {
|
||||
if (agentLlmId === null || agentLlmId === undefined) return false;
|
||||
if (agentLlmId === 0) return hasGlobalConfigs;
|
||||
return true;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue