Merge upstream/dev into feature/multi-agent

This commit is contained in:
CREDO23 2026-05-05 01:44:46 +02:00
commit 5119915f4f
278 changed files with 34669 additions and 8970 deletions

View file

@ -127,7 +127,7 @@ const FAQ_ITEMS = [
{
question: "What happens after I use my free tokens?",
answer:
"After your free tokens, create a free SurfSense account to unlock 3 million more premium tokens. Additional tokens can be purchased at $1 per million. Non-premium models remain unlimited for registered users.",
"After your free tokens, create a free SurfSense account to unlock $5 of premium credit. Additional credit can be topped up at $1 for $1 of credit, billed at the actual provider cost. Non-premium models remain unlimited for registered users.",
},
{
question: "Is Claude AI available without login?",
@ -329,7 +329,7 @@ export default async function FreeHubPage() {
<section className="max-w-3xl mx-auto text-center">
<h2 className="text-2xl font-bold mb-3">Want More Features?</h2>
<p className="text-muted-foreground mb-6 leading-relaxed">
Create a free SurfSense account to unlock 3 million tokens, document uploads with
Create a free SurfSense account to unlock $5 of premium credit, document uploads with
citations, team collaboration, and integrations with Slack, Google Drive, Notion, and
30+ more tools.
</p>

View file

@ -5,7 +5,7 @@ import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
export const metadata: Metadata = {
title: "Pricing | SurfSense - Free AI Search Plans",
description:
"Explore SurfSense plans and pricing. Start free with 500 pages & 3M premium tokens. Use ChatGPT, Claude AI, and premium AI models. Pay-as-you-go tokens at $1 per million.",
"Explore SurfSense plans and pricing. Start free with 500 pages & $5 in premium credits. Use ChatGPT, Claude AI, and premium AI models. Pay as you go at provider cost.",
alternates: {
canonical: "https://surfsense.com/pricing",
},

View file

@ -8,7 +8,7 @@ import { cn } from "@/lib/utils";
const TABS = [
{ id: "pages", label: "Pages" },
{ id: "tokens", label: "Premium Tokens" },
{ id: "tokens", label: "Premium Credit" },
] as const;
type TabId = (typeof TABS)[number]["id"];

View file

@ -1,11 +1,8 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
import { CheckCircle2 } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useEffect } from "react";
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
import { Button } from "@/components/ui/button";
import {
Card,
@ -18,14 +15,8 @@ import {
export default function PurchaseSuccessPage() {
const params = useParams();
const queryClient = useQueryClient();
const searchSpaceId = String(params.search_space_id ?? "");
useEffect(() => {
void queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
void queryClient.invalidateQueries({ queryKey: ["token-status"] });
}, [queryClient]);
return (
<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4 py-8">
<Card className="w-full max-w-lg">

View file

@ -178,6 +178,19 @@ const FLAG_GROUPS: FlagGroup[] = [
},
],
},
{
id: "desktop",
title: "Desktop",
subtitle: "Desktop-only capabilities exposed by the backend deployment.",
flags: [
{
key: "enable_desktop_local_filesystem",
label: "Local filesystem",
description: "Allow Desktop chat sessions to operate directly on selected local folders.",
envVar: "ENABLE_DESKTOP_LOCAL_FILESYSTEM",
},
],
},
];
function FlagRow({ def, value }: { def: FlagDef; value: boolean }) {

View file

@ -28,6 +28,12 @@ type UnifiedPurchase = {
kind: PurchaseKind;
created_at: string;
status: PagePurchaseStatus;
/**
* Granted units. Interpretation depends on ``kind``:
* - ``"pages"`` integer number of indexed pages.
* - ``"tokens"`` integer micro-USD of credit (1_000_000 = $1.00).
* The ``Granted`` column formats accordingly.
*/
granted: number;
amount_total: number | null;
currency: string | null;
@ -58,7 +64,7 @@ const KIND_META: Record<
iconClass: "text-sky-500",
},
tokens: {
label: "Premium Tokens",
label: "Premium Credit",
icon: Coins,
iconClass: "text-amber-500",
},
@ -97,12 +103,25 @@ function normalizeTokenPurchase(p: TokenPurchase): UnifiedPurchase {
kind: "tokens",
created_at: p.created_at,
status: p.status,
granted: p.tokens_granted,
granted: p.credit_micros_granted,
amount_total: p.amount_total,
currency: p.currency,
};
}
function formatGranted(p: UnifiedPurchase): string {
if (p.kind === "tokens") {
const dollars = p.granted / 1_000_000;
// Premium credit packs are always whole dollars at the moment, but
// future fractional grants (refunds, partial top-ups) shouldn't
// silently round to "$0".
if (dollars >= 1) return `$${dollars.toFixed(2)} of credit`;
if (dollars > 0) return `$${dollars.toFixed(3)} of credit`;
return "$0 of credit";
}
return p.granted.toLocaleString();
}
export function PurchaseHistoryContent() {
const results = useQueries({
queries: [
@ -143,7 +162,7 @@ export function PurchaseHistoryContent() {
<ReceiptText className="h-8 w-8 text-muted-foreground" />
<p className="text-sm font-medium">No purchases yet</p>
<p className="text-xs text-muted-foreground">
Your page and premium token purchases will appear here after checkout.
Your page and premium credit purchases will appear here after checkout.
</p>
</div>
);
@ -177,7 +196,7 @@ export function PurchaseHistoryContent() {
</div>
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{p.granted.toLocaleString()}
{formatGranted(p)}
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{formatAmount(p.amount_total, p.currency)}

View file

@ -132,8 +132,8 @@ export default function DesktopPermissionsPage() {
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">System Permissions</h1>
<p className="text-sm text-muted-foreground">
SurfSense needs two macOS permissions for Screenshot Assist and for desktop features that
require focusing the app or the active application.
SurfSense needs two macOS permissions for Screenshot Assist and for desktop features
that require focusing the app or the active application.
</p>
</div>
</div>