diff --git a/common/constants.go b/common/constants.go index 04e203f898c2..dee974f6f600 100644 --- a/common/constants.go +++ b/common/constants.go @@ -120,7 +120,14 @@ var TurnstileSecretKey = "" var TelegramBotToken = "" var TelegramBotName = "" -var QuotaForNewUser = 0 +// QuotaForNewUser is the trial credit granted to every new account at +// signup time (consumed by model/user.go:410 inside User.Insert()). +// +// 500_000 = $1 USD = ~500K tokens at the standard 1:500_000 ratio. This is +// enough for a casual user to chat ~100 turns with gpt-4o-mini and verify +// the service end-to-end before topping up. Operators who don't want +// trial credits can override to 0 in System Settings → Operations. +var QuotaForNewUser = 500_000 var QuotaForInviter = 0 var QuotaForInvitee = 0 var ChannelDisableThreshold = 5.0 diff --git a/web/default/src/features/onboarding/index.tsx b/web/default/src/features/onboarding/index.tsx new file mode 100644 index 000000000000..e6c5857b11d0 --- /dev/null +++ b/web/default/src/features/onboarding/index.tsx @@ -0,0 +1,187 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { useMemo, useState } from 'react' +import { Link, useParams } from '@tanstack/react-router' +import { + ArrowLeft, + Check, + Code, + Copy, + MessageCircle, + PlayCircle, + Sparkles, + Terminal, + type LucideIcon, +} from 'lucide-react' +import { useTranslation } from 'react-i18next' +import i18next from 'i18next' +import { toast } from 'sonner' +import { useStatus } from '@/hooks/use-status' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Markdown } from '@/components/ui/markdown' +import { getTutorial } from './tutorials/registry' + +type IconKey = 'cherry' | 'chat' | 'lobe' | 'cursor' | 'terminal' | 'code' + +const ICONS: Record = { + cherry: Sparkles, + chat: MessageCircle, + lobe: MessageCircle, + cursor: Code, + terminal: Terminal, + code: Code, +} + +function defaultBaseUrl(): string { + if (typeof window === 'undefined') return 'https://deeprouter.ai/v1' + const { protocol, host } = window.location + return `${protocol}//${host}/v1` +} + +export function OnboardingTutorial() { + const { t } = useTranslation() + const params = useParams({ strict: false }) as { slug?: string } + const slug = params?.slug ?? '' + const tutorial = getTutorial(slug) + const { status: _status } = useStatus() + + const baseUrl = defaultBaseUrl() + const modelName = 'deeprouter' + + const content = useMemo(() => { + if (!tutorial) return null + const body = tutorial.content({ baseUrl, modelName }) + const lang = i18next.language?.toLowerCase().startsWith('zh') ? 'zh' : 'en' + return body[lang] ?? body.en + }, [tutorial, baseUrl]) + + if (!tutorial || !content) { + return ( +
+

{t('Tutorial not found')}

+

+ {t("We don't have a guide for this client yet.")} +

+ +
+ ) + } + + const Icon = ICONS[tutorial.icon] + + return ( +
+ {/* Breadcrumb back to /keys */} +
+ +
+ + {/* Header card */} +
+
+ +
+
+
+

+ {tutorial.label} +

+ {tutorial.recommended && ( + + {t('Recommended')} + + )} +
+

+ {t(tutorial.descriptionKey)} +

+
+ +
+
+
+ + {/* Connection values — top-of-page copy-block for the most common need */} +
+ + +
+ {t( + 'Use these values in the steps below. Your API Key was shown when you created it on the Keys page.' + )} +
+
+ + {/* Markdown body */} + {content} +
+ ) +} + +function CopyField({ label, value }: { label: string; value: string }) { + const { t } = useTranslation() + const [copied, setCopied] = useState(false) + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value) + setCopied(true) + window.setTimeout(() => setCopied(false), 1500) + } catch { + toast.error(t('Copy failed')) + } + } + return ( +
+

{label}

+
+ + {value} + + +
+
+ ) +} diff --git a/web/default/src/features/onboarding/tutorials/chatbox.ts b/web/default/src/features/onboarding/tutorials/chatbox.ts new file mode 100644 index 000000000000..63363ecf38ce --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/chatbox.ts @@ -0,0 +1,78 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const CHATBOX: Tutorial = { + slug: 'chatbox', + label: 'Chatbox', + descriptionKey: 'Cross-platform chat client (web + desktop).', + icon: 'chat', + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Connect DeepRouter to Chatbox + +[Chatbox](https://chatboxai.app) is an open-source AI chat client — works on web, desktop, mobile. + +### 1. Open Chatbox +Use the web app at [web.chatboxai.app](https://web.chatboxai.app) or install the desktop app. + +### 2. Add the AI provider +- Click **Settings** (top-right) +- **Model Provider** → choose **OpenAI API** + +### 3. Fill these fields + +| Field | Value | +|---|---| +| API Key | the key from DeepRouter (starts with \`sk-\`) | +| API Host | \`${vars.baseUrl}\` | +| Model | \`${vars.modelName}\` | + +### 4. Save and chat. + +### Troubleshooting +- **"401 Unauthorized"** — re-copy the API key. Avoid trailing spaces. +- **Model not responding** — try \`gpt-4o-mini\` or another model in the dropdown. +`, + zh: `## 在 Chatbox 里接入 DeepRouter + +[Chatbox](https://chatboxai.app) 是开源 AI 客户端 —— 网页 / 桌面 / 移动端都有。 + +### 1. 打开 Chatbox +用网页版 [web.chatboxai.app](https://web.chatboxai.app) 或装桌面端。 + +### 2. 添加 AI 服务商 +- 点右上 **设置** +- **模型提供方** 选 **OpenAI API** + +### 3. 填以下字段 + +| 字段 | 内容 | +|---|---| +| API 密钥 | DeepRouter 创建的 Key(\`sk-\` 开头) | +| API 地址 | \`${vars.baseUrl}\` | +| 模型 | \`${vars.modelName}\` | + +### 4. 保存,开聊。 + +### 常见问题 +- **"401 未授权"** — 重新复制 Key,注意前后不要带空格。 +- **模型无响应** — 下拉里换 \`gpt-4o-mini\` 试试。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/cherry-studio.ts b/web/default/src/features/onboarding/tutorials/cherry-studio.ts new file mode 100644 index 000000000000..a82053cc4960 --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/cherry-studio.ts @@ -0,0 +1,93 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const CHERRY_STUDIO: Tutorial = { + slug: 'cherry-studio', + label: 'Cherry Studio', + descriptionKey: 'Friendly desktop client — recommended for chat and writing.', + icon: 'cherry', + recommended: true, + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Connect DeepRouter to Cherry Studio + +Cherry Studio is a free desktop AI client — the easiest way to start using DeepRouter without writing code. + +### 1. Install Cherry Studio +Download from [cherry-ai.com](https://cherry-ai.com) and install. Available for macOS, Windows and Linux. + +### 2. Add DeepRouter as a provider +- Open Cherry Studio +- Click the **Settings** ⚙️ icon (bottom-left) +- Go to **Model Providers** → click **+ Add provider** +- Pick **OpenAI-compatible** as the provider type + +### 3. Fill in these fields + +| Field | Value | +|---|---| +| Provider Name | DeepRouter | +| API Key | the key you just created (starts with \`sk-\`) | +| API URL | \`${vars.baseUrl}\` | +| Model | \`${vars.modelName}\` | + +### 4. Start chatting +Pick **DeepRouter** in the model dropdown and say hi. That's it. + +### Troubleshooting +- **"Connection refused"** — double-check the API URL. It should end with \`/v1\`. +- **"Invalid model"** — try \`gpt-4o\` or \`claude-sonnet-4-7\` instead of \`${vars.modelName}\`. The virtual alias is best for Simple-mode keys. +- **"Insufficient balance"** — top up at the [Wallet](/wallet) page. + +> Screenshots coming soon. If you get stuck, ask on our community forum. +`, + zh: `## 在 Cherry Studio 里接入 DeepRouter + +Cherry Studio 是一款免费桌面端 AI 客户端 —— 最适合非技术用户起步,不用写代码就能用上 DeepRouter。 + +### 1. 安装 Cherry Studio +从 [cherry-ai.com](https://cherry-ai.com) 下载安装。macOS / Windows / Linux 都有。 + +### 2. 添加 DeepRouter 模型供应商 +- 打开 Cherry Studio +- 点左下角 **设置** ⚙️ 图标 +- 进入 **模型服务** → 点 **+ 添加** +- 类型选 **OpenAI 兼容** + +### 3. 填以下字段 + +| 字段 | 内容 | +|---|---| +| 服务商名称 | DeepRouter | +| API 密钥 | 你刚刚创建的 Key(\`sk-\` 开头) | +| API 地址 | \`${vars.baseUrl}\` | +| 默认模型 | \`${vars.modelName}\` | + +### 4. 开聊 +在模型下拉里选 **DeepRouter**,发"你好"。完事。 + +### 常见问题 +- **"连接失败"** — 检查 API 地址,确保以 \`/v1\` 结尾。 +- **"模型无效"** — 试试 \`gpt-4o\` 或 \`claude-sonnet-4-7\`,虚拟别名适合 Simple Key。 +- **"余额不足"** — 去 [钱包](/wallet) 充值。 + +> 截图教程整理中。卡住了可以来社区问。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/claude-code.ts b/web/default/src/features/onboarding/tutorials/claude-code.ts new file mode 100644 index 000000000000..23748f133795 --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/claude-code.ts @@ -0,0 +1,92 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const CLAUDE_CODE: Tutorial = { + slug: 'claude-code', + label: 'Claude Code', + descriptionKey: "Anthropic's CLI agent — route via DeepRouter using Anthropic-format keys.", + icon: 'terminal', + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Use DeepRouter with Claude Code + +Claude Code talks the Anthropic Messages API. DeepRouter exposes an Anthropic-compatible endpoint at \`${vars.baseUrl.replace(/\/v1\/?$/, '')}/v1/messages\`. + +### 1. Install Claude Code +\`\`\`bash +npm install -g @anthropic-ai/claude-code +\`\`\` + +### 2. Configure environment variables +Set these in your shell rc file (\`~/.zshrc\`, \`~/.bashrc\`, etc): + +\`\`\`bash +export ANTHROPIC_API_KEY="" +export ANTHROPIC_BASE_URL="${vars.baseUrl.replace(/\/v1\/?$/, '')}" +\`\`\` + +Reload your shell. + +### 3. Run claude +\`\`\`bash +cd your-project +claude +\`\`\` + +### 4. Pick a model +At the prompt, \`/model\` then pick \`claude-sonnet-4-7\` or \`${vars.modelName}\`. + +### Troubleshooting +- **"Invalid API key"** — verify the env var is set in the same shell session. +- **"Rate limit / quota exceeded"** — top up at [Wallet](/wallet). +`, + zh: `## 用 Claude Code 走 DeepRouter + +Claude Code 用的是 Anthropic Messages API。DeepRouter 暴露了 Anthropic 兼容端点:\`${vars.baseUrl.replace(/\/v1\/?$/, '')}/v1/messages\`。 + +### 1. 安装 Claude Code +\`\`\`bash +npm install -g @anthropic-ai/claude-code +\`\`\` + +### 2. 配置环境变量 +加进 shell rc 文件(\`~/.zshrc\` / \`~/.bashrc\` 等): + +\`\`\`bash +export ANTHROPIC_API_KEY="<你的 deeprouter key>" +export ANTHROPIC_BASE_URL="${vars.baseUrl.replace(/\/v1\/?$/, '')}" +\`\`\` + +重新加载 shell。 + +### 3. 跑 claude +\`\`\`bash +cd your-project +claude +\`\`\` + +### 4. 选模型 +进入交互后 \`/model\`,选 \`claude-sonnet-4-7\` 或 \`${vars.modelName}\`。 + +### 常见问题 +- **"Invalid API key"** — 确保环境变量在当前 shell session 里生效。 +- **"Rate limit / quota exceeded"** — 去 [钱包](/wallet) 充值。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/code.ts b/web/default/src/features/onboarding/tutorials/code.ts new file mode 100644 index 000000000000..3eee17d82e21 --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/code.ts @@ -0,0 +1,140 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const CODE: Tutorial = { + slug: 'code', + label: 'Python / Node code', + descriptionKey: 'Call DeepRouter directly from your application.', + icon: 'code', + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Call DeepRouter from code + +The endpoint is OpenAI-compatible, so use the official OpenAI client library. + +### Python +\`\`\`python +from openai import OpenAI + +client = OpenAI( + api_key="", + base_url="${vars.baseUrl}", +) + +resp = client.chat.completions.create( + model="${vars.modelName}", + messages=[{"role": "user", "content": "Hello"}], +) +print(resp.choices[0].message.content) +\`\`\` + +### Node.js +\`\`\`js +import OpenAI from 'openai' + +const client = new OpenAI({ + apiKey: '', + baseURL: '${vars.baseUrl}', +}) + +const resp = await client.chat.completions.create({ + model: '${vars.modelName}', + messages: [{ role: 'user', content: 'Hello' }], +}) +console.log(resp.choices[0].message.content) +\`\`\` + +### cURL +\`\`\`bash +curl ${vars.baseUrl}/chat/completions \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer " \\ + -d '{ + "model": "${vars.modelName}", + "messages": [{"role": "user", "content": "Hello"}] + }' +\`\`\` + +### Streaming +Add \`stream: true\` to the request. Both Python and Node SDKs support \`for chunk in stream\` patterns. + +### Common errors +- **401** — bad API key. Re-copy from DeepRouter. +- **404** — bad base URL. Should end with \`/v1\` (no trailing slash). +- **429** — rate limit. Wait or upgrade your plan. +- **402** — out of quota. Top up at [Wallet](/wallet). +`, + zh: `## 在代码里调用 DeepRouter + +端点是 OpenAI 兼容的,直接用官方 OpenAI 客户端库即可。 + +### Python +\`\`\`python +from openai import OpenAI + +client = OpenAI( + api_key="<你的 deeprouter key>", + base_url="${vars.baseUrl}", +) + +resp = client.chat.completions.create( + model="${vars.modelName}", + messages=[{"role": "user", "content": "你好"}], +) +print(resp.choices[0].message.content) +\`\`\` + +### Node.js +\`\`\`js +import OpenAI from 'openai' + +const client = new OpenAI({ + apiKey: '<你的 deeprouter key>', + baseURL: '${vars.baseUrl}', +}) + +const resp = await client.chat.completions.create({ + model: '${vars.modelName}', + messages: [{ role: 'user', content: '你好' }], +}) +console.log(resp.choices[0].message.content) +\`\`\` + +### cURL +\`\`\`bash +curl ${vars.baseUrl}/chat/completions \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer <你的 deeprouter key>" \\ + -d '{ + "model": "${vars.modelName}", + "messages": [{"role": "user", "content": "你好"}] + }' +\`\`\` + +### 流式返回 +请求里加 \`stream: true\`。Python 和 Node SDK 都支持 \`for chunk in stream\` 这种用法。 + +### 常见错误 +- **401** — Key 错误。从 DeepRouter 重新复制。 +- **404** — base URL 错。应该以 \`/v1\` 结尾(不带斜杠)。 +- **429** — 限流。等一会或升级套餐。 +- **402** — 余额不足。去 [钱包](/wallet) 充值。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/cursor.ts b/web/default/src/features/onboarding/tutorials/cursor.ts new file mode 100644 index 000000000000..2daaaafa4aac --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/cursor.ts @@ -0,0 +1,74 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const CURSOR: Tutorial = { + slug: 'cursor', + label: 'Cursor', + descriptionKey: 'AI-powered code editor — use DeepRouter for completions and chat.', + icon: 'cursor', + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Connect DeepRouter to Cursor + +[Cursor](https://cursor.sh) supports custom OpenAI-compatible backends. + +### 1. Open Cursor → Settings (⌘,) + +### 2. Models tab +- Scroll to **OpenAI API Key** +- Paste your DeepRouter key +- Click **Override OpenAI Base URL** → enter \`${vars.baseUrl}\` +- Click **Verify** + +### 3. Add your model name +- In the **Model Names** field, add: \`${vars.modelName}\` (and any others you want, e.g. \`gpt-4o\`, \`claude-sonnet-4-7\`) +- Click **Add Model** + +### 4. Use it +\`Cmd+L\` or \`Cmd+K\` → DeepRouter routes the call. + +### Notes +- Cursor's auto-complete (Cmd+→) uses its own internal model — **not** DeepRouter. Only Chat (⌘L) and Edit (⌘K) route through your custom backend. +- For agent mode and tab-completion, use the DeepRouter key inside Cursor's "Composer" agent settings instead. +`, + zh: `## 在 Cursor 里接入 DeepRouter + +[Cursor](https://cursor.sh) 支持自定义 OpenAI 兼容后端。 + +### 1. 打开 Cursor → 设置(⌘,) + +### 2. Models 标签页 +- 滚到 **OpenAI API Key** +- 粘贴 DeepRouter 创建的 Key +- 点 **Override OpenAI Base URL** → 填 \`${vars.baseUrl}\` +- 点 **Verify** + +### 3. 添加模型名 +- 在 **Model Names** 字段里加 \`${vars.modelName}\`(也可以多加几个 \`gpt-4o\`、\`claude-sonnet-4-7\`) +- 点 **Add Model** + +### 4. 使用 +\`Cmd+L\` 或 \`Cmd+K\` → DeepRouter 路由调用。 + +### 注意 +- Cursor 的代码自动补全(Cmd+→)用自家内部模型 —— **不走** DeepRouter。只有 Chat(⌘L)和 Edit(⌘K)走自定义后端。 +- 想 agent 模式 / tab 补全也走 DeepRouter?要在 Cursor 的 "Composer" agent 设置里再配一遍。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/lobechat.ts b/web/default/src/features/onboarding/tutorials/lobechat.ts new file mode 100644 index 000000000000..0d6a0ac065df --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/lobechat.ts @@ -0,0 +1,78 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { Tutorial, TutorialContent, TutorialVars } from './registry' + +export const LOBECHAT: Tutorial = { + slug: 'lobechat', + label: 'LobeChat', + descriptionKey: 'Open-source AI chat with plugins and multi-modal support.', + icon: 'lobe', + content: (vars: TutorialVars): TutorialContent => ({ + en: `## Connect DeepRouter to LobeChat + +[LobeChat](https://lobehub.com/chat) supports OpenAI-compatible endpoints and ships with a rich plugin ecosystem. + +### 1. Open LobeChat +Either the hosted [lobechat.com](https://lobechat.com) or self-hosted Docker. + +### 2. Settings → Language Model +Click your avatar → **Settings** → **Language Model** → **OpenAI**. + +### 3. Configure + +| Field | Value | +|---|---| +| API Key | DeepRouter key | +| API Proxy Address | \`${vars.baseUrl}\` | +| Custom Model Name | \`${vars.modelName}\` | + +Enable the toggle for **Use Client-Side Request** if you're running self-hosted and want direct calls. + +### 4. Pick **OpenAI** as the chat provider in the chat session, choose your custom model, send a message. + +### Troubleshooting +- **CORS errors (self-hosted)** — enable "Use Client-Side Request" or proxy through your LobeChat backend. +`, + zh: `## 在 LobeChat 里接入 DeepRouter + +[LobeChat](https://lobehub.com/chat) 支持 OpenAI 兼容接口,自带丰富的插件生态。 + +### 1. 打开 LobeChat +托管版 [lobechat.com](https://lobechat.com) 或自部署 Docker。 + +### 2. 设置 → 语言模型 +点头像 → **设置** → **语言模型** → **OpenAI**。 + +### 3. 填配置 + +| 字段 | 内容 | +|---|---| +| API Key | DeepRouter 创建的 Key | +| API 代理地址 | \`${vars.baseUrl}\` | +| 自定义模型名 | \`${vars.modelName}\` | + +如果是自部署版本,建议打开"客户端发送请求"开关。 + +### 4. 在对话里选 **OpenAI**,选你的自定义模型,发消息。 + +### 常见问题 +- **CORS 报错(自部署)** — 打开"客户端发送请求",或者通过 LobeChat 后端转发。 +`, + }), +} diff --git a/web/default/src/features/onboarding/tutorials/registry.ts b/web/default/src/features/onboarding/tutorials/registry.ts new file mode 100644 index 000000000000..12a42cf18666 --- /dev/null +++ b/web/default/src/features/onboarding/tutorials/registry.ts @@ -0,0 +1,96 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { CHATBOX } from './chatbox' +import { CHERRY_STUDIO } from './cherry-studio' +import { CLAUDE_CODE } from './claude-code' +import { CODE } from './code' +import { CURSOR } from './cursor' +import { LOBECHAT } from './lobechat' + +/** + * The slug is what appears in the URL (`/onboarding/`) and the + * `` field in the API Key success dialog. Keep in sync with + * `api-key-success-dialog.tsx::CLIENT_LINKS`. + */ +export type TutorialSlug = + | 'cherry-studio' + | 'chatbox' + | 'lobechat' + | 'cursor' + | 'claude-code' + | 'code' + +/** Per-language markdown body. Frontend picks `zh` when the user's i18next + * language starts with "zh", otherwise falls back to `en`. */ +export type TutorialContent = Record<'en' | 'zh', string> + +export type Tutorial = { + slug: TutorialSlug + /** Display name shown in breadcrumb and as the page header. Not + * translated per-language — these are product names. */ + label: string + /** One-line description, i18n key (translated by useTranslation). */ + descriptionKey: string + /** Lucide icon name (resolved client-side); keep as string so the + * registry stays a plain TS module. */ + icon: 'cherry' | 'chat' | 'lobe' | 'cursor' | 'terminal' | 'code' + /** Whether this client is "Recommended for non-tech" — surfaces a + * badge on the tutorial page header. Cherry Studio is the default + * recommendation for casual persona. */ + recommended?: boolean + /** Markdown body. Built as a function so callers can interpolate the + * current base URL + model name at render time. */ + content: (vars: TutorialVars) => TutorialContent +} + +export type TutorialVars = { + /** Pulled from useStatus() at render time. */ + baseUrl: string + /** What model name to suggest in the client config. Defaults to + * `deeprouter` (the virtual alias). */ + modelName: string +} + +export const TUTORIALS: Record = { + 'cherry-studio': CHERRY_STUDIO, + chatbox: CHATBOX, + lobechat: LOBECHAT, + cursor: CURSOR, + 'claude-code': CLAUDE_CODE, + code: CODE, +} + +export const TUTORIAL_SLUGS: TutorialSlug[] = [ + 'cherry-studio', + 'chatbox', + 'lobechat', + 'cursor', + 'claude-code', + 'code', +] + +export function isTutorialSlug(slug: string): slug is TutorialSlug { + return TUTORIAL_SLUGS.includes(slug as TutorialSlug) +} + +export function getTutorial(slug: string): Tutorial | undefined { + if (!isTutorialSlug(slug)) return undefined + return TUTORIALS[slug] +} diff --git a/web/default/src/features/wallet/components/recharge-form-card.tsx b/web/default/src/features/wallet/components/recharge-form-card.tsx index f7e4a3b54b5f..ec8a8d5aad53 100644 --- a/web/default/src/features/wallet/components/recharge-form-card.tsx +++ b/web/default/src/features/wallet/components/recharge-form-card.tsx @@ -20,6 +20,7 @@ import { useState, useEffect } from 'react' import { Gift, ExternalLink, Loader2, Receipt, WalletCards } from 'lucide-react' import { useTranslation } from 'react-i18next' import { formatNumber } from '@/lib/format' +import { estimateChats, formatCount } from '@/lib/usage-estimate' import { cn } from '@/lib/utils' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' @@ -263,6 +264,22 @@ export function RechargeFormCard({ )} + {/* Human-friendly "how many chats" hint — derived + * from the preset's quota value (estimateChats + * uses a mid-tier model average; see + * lib/usage-estimate.ts). Helps non-technical + * users gauge value at a glance. */} + {(() => { + const chats = estimateChats(preset.value) + if (chats <= 0) return null + return ( +
+ {t('≈ {{count}} chats', { + count: formatCount(chats), + })} +
+ ) + })()} ) })} diff --git a/web/default/src/features/wallet/components/wallet-stats-card.tsx b/web/default/src/features/wallet/components/wallet-stats-card.tsx index cecdee6a42d6..6882567666b8 100644 --- a/web/default/src/features/wallet/components/wallet-stats-card.tsx +++ b/web/default/src/features/wallet/components/wallet-stats-card.tsx @@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import { Activity, BarChart3, WalletCards } from 'lucide-react' import { useTranslation } from 'react-i18next' import { formatQuota } from '@/lib/format' +import { estimateChats, formatCount } from '@/lib/usage-estimate' import { Skeleton } from '@/components/ui/skeleton' import type { UserWalletData } from '../types' @@ -45,23 +46,28 @@ export function WalletStatsCard(props: WalletStatsCardProps) { ) } + const balanceQuota = props.user?.quota ?? 0 + const chats = estimateChats(balanceQuota) const stats = [ { label: t('Current Balance'), - value: formatQuota(props.user?.quota ?? 0), - description: t('Remaining quota'), + value: formatQuota(balanceQuota), + description: + chats > 0 + ? t('≈ {{count}} chats remaining', { count: formatCount(chats) }) + : t('Top up to start using AI models'), icon: WalletCards, }, { label: t('Total Usage'), value: formatQuota(props.user?.used_quota ?? 0), - description: t('Total consumed quota'), + description: t('Spent so far'), icon: BarChart3, }, { label: t('API Requests'), value: (props.user?.request_count ?? 0).toLocaleString(), - description: t('Total requests made'), + description: t('Total calls made'), icon: Activity, }, ] @@ -81,7 +87,7 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
{item.value}
-
+
{item.description}
diff --git a/web/default/src/features/wallet/index.tsx b/web/default/src/features/wallet/index.tsx index 664ded8b1648..658943435ebc 100644 --- a/web/default/src/features/wallet/index.tsx +++ b/web/default/src/features/wallet/index.tsx @@ -262,7 +262,9 @@ export function Wallet(props: WalletProps) { {t('Wallet')} - {t('Manage your balance and payment methods')} + {t( + 'Top up to call AI models. Your trial credit covers your first few requests so you can try things out.' + )}
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 4727c377511e..1907a93a3448 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -70,6 +70,7 @@ "AI model testing environment": "AI model testing environment", "AI models": "AI models", "AI models supported": "AI models supported", + "AI-powered code editor — use DeepRouter for completions and chat.": "AI-powered code editor — use DeepRouter for completions and chat.", "AIGC2D": "AIGC2D", "AILS": "AILS", "AK/SK mode: use AccessKey|SecretAccessKey|Region": "AK/SK mode: use AccessKey|SecretAccessKey|Region", @@ -318,6 +319,7 @@ "Answer": "Answer", "Answers for common access and billing questions": "Answers for common access and billing questions", "Anthropic": "Anthropic", + "Anthropic's CLI agent — route via DeepRouter using Anthropic-format keys.": "Anthropic's CLI agent — route via DeepRouter using Anthropic-format keys.", "Any Match (OR)": "Any Match (OR)", "App": "App", "App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.": "App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.", @@ -426,6 +428,7 @@ "Avg. latency": "Avg. latency", "Azure": "Azure", "Back": "Back", + "Back to API Keys": "Back to API Keys", "Back to Home": "Back to Home", "Back to Models": "Back to Models", "Back to login": "Back to login", @@ -563,6 +566,7 @@ "Calculating...": "Calculating...", "Call Count Distribution": "Call Count Distribution", "Call Count Ranking": "Call Count Ranking", + "Call DeepRouter directly from your application.": "Call DeepRouter directly from your application.", "Call Proportion": "Call Proportion", "Call Trend": "Call Trend", "Callback Caller IP": "Callback Caller IP", @@ -984,6 +988,7 @@ "Creem products must be a JSON array": "Creem products must be a JSON array", "Cross-group": "Cross-group", "Cross-group retry": "Cross-group retry", + "Cross-platform chat client (web + desktop).": "Cross-platform chat client (web + desktop).", "Curate quick links to your different Domains": "Curate quick links to your different Domains", "Currency": "Currency", "Currency & Display": "Currency & Display", @@ -1691,6 +1696,7 @@ "Forward requests directly to upstream providers without any post-processing.": "Forward requests directly to upstream providers without any post-processing.", "Frames per second": "Frames per second", "Free: {{free}} / Total: {{total}}": "Free: {{free}} / Total: {{total}}", + "Friendly desktop client — recommended for chat and writing.": "Friendly desktop client — recommended for chat and writing.", "Friendly name to identify this channel": "Friendly name to identify this channel", "From Address": "From Address", "From IO.NET deployment": "From IO.NET deployment", @@ -2564,6 +2570,7 @@ "Open the io.net console API Keys page": "Open the io.net console API Keys page", "Open theme settings": "Open theme settings", "Open weights": "Open weights", + "Open-source AI chat with plugins and multi-modal support.": "Open-source AI chat with plugins and multi-modal support.", "OpenAI": "OpenAI", "OpenAI Compatible": "OpenAI Compatible", "OpenAI Organization": "OpenAI Organization", @@ -3503,6 +3510,7 @@ "Special ratios override the token group ratio for specific user group and token group combinations.": "Special ratios override the token group ratio for specific user group and token group combinations.", "Special usable group rules": "Special usable group rules", "Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Special usable group rules can add, remove, or append selectable token groups for a specific user group.", + "Spent so far": "Spent so far", "Standard": "Standard", "Standard price": "Standard price", "Start": "Start", @@ -3837,6 +3845,8 @@ "Top model": "Top model", "Top models": "Top models", "Top up balance and view billing history.": "Top up balance and view billing history.", + "Top up to call AI models. Your trial credit covers your first few requests so you can try things out.": "Top up to call AI models. Your trial credit covers your first few requests so you can try things out.", + "Top up to start using AI models": "Top up to start using AI models", "Top vendors": "Top vendors", "Top {{count}}": "Top {{count}}", "Top-Up Link": "Top-Up Link", @@ -3855,6 +3865,7 @@ "Total Quota": "Total Quota", "Total Tokens": "Total Tokens", "Total Usage": "Total Usage", + "Total calls made": "Total calls made", "Total check-ins": "Total check-ins", "Total consumed": "Total consumed", "Total consumed quota": "Total consumed quota", @@ -3893,8 +3904,10 @@ "Trusted by developers shipping production AI features.": "Trusted by developers shipping production AI features.", "Try adjusting your search": "Try adjusting your search", "Try adjusting your search to locate a missing model.": "Try adjusting your search to locate a missing model.", + "Try in Playground": "Try in Playground", "Tune selection priority, testing, status handling, and request overrides.": "Tune selection priority, testing, status handling, and request overrides.", "Turnstile is enabled but site key is empty.": "Turnstile is enabled but site key is empty.", + "Tutorial not found": "Tutorial not found", "Tutoring, learning aids, assessment": "Tutoring, learning aids, assessment", "Two-Factor Authentication": "Two-Factor Authentication", "Two-Step Verification": "Two-Step Verification", @@ -4022,6 +4035,7 @@ "Use sidebar shortcut": "Use sidebar shortcut", "Use the full-width table to scan prices, then select a row to edit it here.": "Use the full-width table to scan prices, then select a row to edit it here.", "Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.": "Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.", + "Use these values in the steps below. Your API Key was shown when you created it on the Keys page.": "Use these values in the steps below. Your API Key was shown when you created it on the Keys page.", "Use this in your client. We route it to the right model based on this key.": "Use this in your client. We route it to the right model based on this key.", "Use this token for API authentication": "Use this token for API authentication", "Use your Passkey": "Use your Passkey", @@ -4163,6 +4177,7 @@ "Warning: This action is permanent and irreversible!": "Warning: This action is permanent and irreversible!", "We apologize for the inconvenience.": "We apologize for the inconvenience.", "We could not load the setup status.": "We could not load the setup status.", + "We don't have a guide for this client yet.": "We don't have a guide for this client yet.", "We will prompt your device to confirm using biometrics or your hardware key.": "We will prompt your device to confirm using biometrics or your hardware key.", "We'll be back online shortly.": "We'll be back online shortly.", "WeChat": "WeChat", @@ -4497,6 +4512,8 @@ "{{value}}ms": "{{value}}ms", "{{value}}s": "{{value}}s", "| Based on": "| Based on", - "© 2025 Your Company. All rights reserved.": "© 2025 Your Company. All rights reserved." + "© 2025 Your Company. All rights reserved.": "© 2025 Your Company. All rights reserved.", + "≈ {{count}} chats": "≈ {{count}} chats", + "≈ {{count}} chats remaining": "≈ {{count}} chats remaining" } } diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 311d66c42ebe..15fae5c46ed9 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -70,6 +70,7 @@ "AI model testing environment": "AI模型测试环境", "AI models": "AI 模型", "AI models supported": "支持的 AI 模型", + "AI-powered code editor — use DeepRouter for completions and chat.": "AI 代码编辑器 —— 用 DeepRouter 走对话和编辑。", "AIGC2D": "AIGC2D", "AILS": "AILS", "AK/SK mode: use AccessKey|SecretAccessKey|Region": "AK/SK 模式:使用 AccessKey|SecretAccessKey|Region", @@ -318,6 +319,7 @@ "Answer": "答案", "Answers for common access and billing questions": "访问与计费常见问题解答", "Anthropic": "Anthropic", + "Anthropic's CLI agent — route via DeepRouter using Anthropic-format keys.": "Anthropic CLI agent —— 通过 DeepRouter 用 Anthropic 接口调用。", "Any Match (OR)": "任一满足(OR)", "App": "应用", "App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.": "此处展示的应用排行为预览模拟数据,待后端对接完成后将替换为真实数据。", @@ -426,6 +428,7 @@ "Avg. latency": "平均延迟", "Azure": "Azure", "Back": "返回", + "Back to API Keys": "返回 API 密钥", "Back to Home": "返回主页", "Back to Models": "返回模型", "Back to login": "返回登录", @@ -563,6 +566,7 @@ "Calculating...": "计算中...", "Call Count Distribution": "调用次数分布", "Call Count Ranking": "调用次数排行", + "Call DeepRouter directly from your application.": "在自己的代码里直接调用 DeepRouter。", "Call Proportion": "调用比例", "Call Trend": "调用趋势", "Callback Caller IP": "回调调用者 IP", @@ -984,6 +988,7 @@ "Creem products must be a JSON array": "Creem 产品必须是 JSON 数组", "Cross-group": "跨分组", "Cross-group retry": "跨分组重试", + "Cross-platform chat client (web + desktop).": "跨平台 AI 客户端(网页 / 桌面)。", "Curate quick links to your different Domains": "整理到不同域的快速链接", "Currency": "货币", "Currency & Display": "货币与展示", @@ -1691,6 +1696,7 @@ "Forward requests directly to upstream providers without any post-processing.": "将请求直接转发给上游提供商,不进行任何后处理。", "Frames per second": "帧率", "Free: {{free}} / Total: {{total}}": "可用空间: {{free}} / 总空间: {{total}}", + "Friendly desktop client — recommended for chat and writing.": "新手友好的桌面客户端 —— 适合聊天和写作。", "Friendly name to identify this channel": "用于识别此渠道的友好名称", "From Address": "发件地址", "From IO.NET deployment": "来自 IO.NET 部署", @@ -2564,6 +2570,7 @@ "Open the io.net console API Keys page": "打开 io.net 控制台 API 密钥页面", "Open theme settings": "打开主题设置", "Open weights": "开放权重", + "Open-source AI chat with plugins and multi-modal support.": "开源 AI 客户端,支持插件和多模态。", "OpenAI": "OpenAI", "OpenAI Compatible": "兼容 OpenAI", "OpenAI Organization": "OpenAI 组织", @@ -3503,6 +3510,7 @@ "Special ratios override the token group ratio for specific user group and token group combinations.": "特殊倍率会针对特定用户分组和令牌分组组合覆盖令牌分组倍率。", "Special usable group rules": "特殊可用分组规则", "Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "特殊可用分组规则可以为特定用户分组添加、移除或追加可选令牌分组。", + "Spent so far": "累计消费", "Standard": "标准", "Standard price": "标准价格", "Start": "开始", @@ -3837,6 +3845,8 @@ "Top model": "领头模型", "Top models": "热门模型", "Top up balance and view billing history.": "充值余额并查看账单历史。", + "Top up to call AI models. Your trial credit covers your first few requests so you can try things out.": "充值后调用 AI 模型。注册赠送的额度够你试用前几次请求。", + "Top up to start using AI models": "充值后开始使用 AI 模型", "Top vendors": "热门厂商", "Top {{count}}": "前 {{count}}", "Top-Up Link": "充值链接", @@ -3855,6 +3865,7 @@ "Total Quota": "总额度", "Total Tokens": "总 Token 数", "Total Usage": "总用量", + "Total calls made": "累计调用次数", "Total check-ins": "累计签到", "Total consumed": "总消耗", "Total consumed quota": "总消耗额度", @@ -3893,8 +3904,10 @@ "Trusted by developers shipping production AI features.": "被一线 AI 开发者信赖。", "Try adjusting your search": "请尝试调整搜索条件", "Try adjusting your search to locate a missing model.": "尝试调整您的搜索以找到缺失的模型。", + "Try in Playground": "在试玩里试试", "Tune selection priority, testing, status handling, and request overrides.": "调整选择优先级、测试、状态处理和请求覆盖。", "Turnstile is enabled but site key is empty.": "Turnstile 已启用但站点密钥为空。", + "Tutorial not found": "教程未找到", "Tutoring, learning aids, assessment": "辅导、学习辅助与测评", "Two-Factor Authentication": "两步验证", "Two-Step Verification": "两步验证", @@ -4022,6 +4035,7 @@ "Use sidebar shortcut": "使用侧边栏快捷方式", "Use the full-width table to scan prices, then select a row to edit it here.": "先在表格中快速浏览价格,然后选择一行在这里编辑。", "Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.": "使用定价分组表管理倍率,以及该分组是否出现在创建令牌的下拉框中。", + "Use these values in the steps below. Your API Key was shown when you created it on the Keys page.": "下面步骤中会用到这些值。API Key 在你创建时已经显示过。", "Use this in your client. We route it to the right model based on this key.": "在客户端填这个名字,我们会根据 Key 路由到合适的模型。", "Use this token for API authentication": "使用此令牌进行 API 身份验证", "Use your Passkey": "使用您的通行密钥", @@ -4163,6 +4177,7 @@ "Warning: This action is permanent and irreversible!": "警告:此操作是永久且不可逆的!", "We apologize for the inconvenience.": "对于由此造成的不便,我们深表歉意。", "We could not load the setup status.": "我们无法加载设置状态。", + "We don't have a guide for this client yet.": "暂无该客户端的接入指南。", "We will prompt your device to confirm using biometrics or your hardware key.": "我们将提示您的设备使用生物识别或硬件密钥进行确认。", "We'll be back online shortly.": "我们将很快恢复在线。", "WeChat": "微信", @@ -4497,6 +4512,8 @@ "{{value}}ms": "{{value}} 毫秒", "{{value}}s": "{{value}} 秒", "| Based on": "| 基于", - "© 2025 Your Company. All rights reserved.": "© 2025 您的公司。保留所有权利。" + "© 2025 Your Company. All rights reserved.": "© 2025 您的公司。保留所有权利。", + "≈ {{count}} chats": "≈ 聊 {{count}} 次", + "≈ {{count}} chats remaining": "≈ 还能聊 {{count}} 次" } } diff --git a/web/default/src/lib/usage-estimate.ts b/web/default/src/lib/usage-estimate.ts new file mode 100644 index 000000000000..900b8e9fb9cd --- /dev/null +++ b/web/default/src/lib/usage-estimate.ts @@ -0,0 +1,85 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +/** + * Quota → human-friendly "how many chats can I afford" estimates. + * + * Quota is stored in units where 500,000 = $1 USD. We assume a mid-tier + * chat costs ~$0.005 (avg between gpt-4o-mini and gpt-4o): + * chats ≈ (quota / 500_000) / 0.005 = quota / 2_500 + * + * Image / video / TTS estimates use coarser per-unit costs sourced from + * setting/alias_setting/seed/aliases.yaml YAML defaults. + * + * These are *order-of-magnitude* estimates for marketing-friendly UI + * copy — never treat them as billing-grade. Actual cost depends on the + * model the user invokes and conversation length. + */ + +const QUOTA_PER_USD = 500_000 +const AVG_CHAT_COST_USD = 0.005 // mid-tier mix of gpt-4o-mini ($0.0004) and gpt-4o ($0.006) +const AVG_IMAGE_COST_USD = 0.04 // DALL·E-3 standard +const AVG_VIDEO_COST_USD = 0.6 // Veo-3 5-sec clip +const AVG_MINUTE_COST_USD = 0.006 // Whisper-1 / TTS midpoint + +/** + * Estimate how many average chat turns a quota balance covers. + * Returns a non-negative integer; 0 when input is invalid or zero. + */ +export function estimateChats(quota: number | null | undefined): number { + if (!quota || !Number.isFinite(quota) || quota <= 0) return 0 + const usd = quota / QUOTA_PER_USD + return Math.max(0, Math.floor(usd / AVG_CHAT_COST_USD)) +} + +/** Estimate images at DALL·E-3 standard price. */ +export function estimateImages(quota: number | null | undefined): number { + if (!quota || !Number.isFinite(quota) || quota <= 0) return 0 + const usd = quota / QUOTA_PER_USD + return Math.max(0, Math.floor(usd / AVG_IMAGE_COST_USD)) +} + +/** Estimate ~5s video clips at Veo-3 price. */ +export function estimateVideoClips(quota: number | null | undefined): number { + if (!quota || !Number.isFinite(quota) || quota <= 0) return 0 + const usd = quota / QUOTA_PER_USD + return Math.max(0, Math.floor(usd / AVG_VIDEO_COST_USD)) +} + +/** Estimate audio minutes (TTS/STT) at Whisper midpoint. */ +export function estimateAudioMinutes( + quota: number | null | undefined +): number { + if (!quota || !Number.isFinite(quota) || quota <= 0) return 0 + const usd = quota / QUOTA_PER_USD + return Math.max(0, Math.floor(usd / AVG_MINUTE_COST_USD)) +} + +/** + * Format an integer count with the appropriate "k" / "万" suffix for + * marketing copy. Returns "1.2k" / "12k" / "120k" / "1.2m" style strings. + * For zh-leaning UI you may prefer to render with 万 (ten-thousand) — + * this helper stays language-neutral; callers wrap with their own i18n. + */ +export function formatCount(n: number): string { + if (n < 1000) return String(n) + if (n < 10_000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k` + if (n < 1_000_000) return `${Math.floor(n / 1000)}k` + return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}m` +} diff --git a/web/default/src/routeTree.gen.ts b/web/default/src/routeTree.gen.ts index 3fddc8342bf0..6adf861224cb 100644 --- a/web/default/src/routeTree.gen.ts +++ b/web/default/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as SetupIndexRouteImport } from './routes/setup/index' import { Route as RankingsIndexRouteImport } from './routes/rankings/index' import { Route as PricingIndexRouteImport } from './routes/pricing/index' import { Route as AboutIndexRouteImport } from './routes/about/index' +import { Route as OnboardingSlugRouteImport } from './routes/onboarding/$slug' import { Route as OauthProviderRouteImport } from './routes/oauth/$provider' import { Route as ConsoleTopupRouteImport } from './routes/console/topup' import { Route as ConsoleLogRouteImport } from './routes/console/log' @@ -112,6 +113,11 @@ const AboutIndexRoute = AboutIndexRouteImport.update({ path: '/about/', getParentRoute: () => rootRouteImport, } as any) +const OnboardingSlugRoute = OnboardingSlugRouteImport.update({ + id: '/onboarding/$slug', + path: '/onboarding/$slug', + getParentRoute: () => rootRouteImport, +} as any) const OauthProviderRoute = OauthProviderRouteImport.update({ id: '/oauth/$provider', path: '/oauth/$provider', @@ -413,6 +419,7 @@ export interface FileRoutesByFullPath { '/console/log': typeof ConsoleLogRoute '/console/topup': typeof ConsoleTopupRoute '/oauth/$provider': typeof OauthProviderRoute + '/onboarding/$slug': typeof OnboardingSlugRoute '/about/': typeof AboutIndexRoute '/pricing/': typeof PricingIndexRoute '/rankings/': typeof RankingsIndexRoute @@ -471,6 +478,7 @@ export interface FileRoutesByTo { '/console/log': typeof ConsoleLogRoute '/console/topup': typeof ConsoleTopupRoute '/oauth/$provider': typeof OauthProviderRoute + '/onboarding/$slug': typeof OnboardingSlugRoute '/about': typeof AboutIndexRoute '/pricing': typeof PricingIndexRoute '/rankings': typeof RankingsIndexRoute @@ -533,6 +541,7 @@ export interface FileRoutesById { '/console/log': typeof ConsoleLogRoute '/console/topup': typeof ConsoleTopupRoute '/oauth/$provider': typeof OauthProviderRoute + '/onboarding/$slug': typeof OnboardingSlugRoute '/about/': typeof AboutIndexRoute '/pricing/': typeof PricingIndexRoute '/rankings/': typeof RankingsIndexRoute @@ -594,6 +603,7 @@ export interface FileRouteTypes { | '/console/log' | '/console/topup' | '/oauth/$provider' + | '/onboarding/$slug' | '/about/' | '/pricing/' | '/rankings/' @@ -652,6 +662,7 @@ export interface FileRouteTypes { | '/console/log' | '/console/topup' | '/oauth/$provider' + | '/onboarding/$slug' | '/about' | '/pricing' | '/rankings' @@ -713,6 +724,7 @@ export interface FileRouteTypes { | '/console/log' | '/console/topup' | '/oauth/$provider' + | '/onboarding/$slug' | '/about/' | '/pricing/' | '/rankings/' @@ -767,6 +779,7 @@ export interface RootRouteChildren { ConsoleLogRoute: typeof ConsoleLogRoute ConsoleTopupRoute: typeof ConsoleTopupRoute OauthProviderRoute: typeof OauthProviderRoute + OnboardingSlugRoute: typeof OnboardingSlugRoute AboutIndexRoute: typeof AboutIndexRoute PricingIndexRoute: typeof PricingIndexRoute RankingsIndexRoute: typeof RankingsIndexRoute @@ -839,6 +852,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AboutIndexRouteImport parentRoute: typeof rootRouteImport } + '/onboarding/$slug': { + id: '/onboarding/$slug' + path: '/onboarding/$slug' + fullPath: '/onboarding/$slug' + preLoaderRoute: typeof OnboardingSlugRouteImport + parentRoute: typeof rootRouteImport + } '/oauth/$provider': { id: '/oauth/$provider' path: '/oauth/$provider' @@ -1336,6 +1356,7 @@ const rootRouteChildren: RootRouteChildren = { ConsoleLogRoute: ConsoleLogRoute, ConsoleTopupRoute: ConsoleTopupRoute, OauthProviderRoute: OauthProviderRoute, + OnboardingSlugRoute: OnboardingSlugRoute, AboutIndexRoute: AboutIndexRoute, PricingIndexRoute: PricingIndexRoute, RankingsIndexRoute: RankingsIndexRoute, diff --git a/web/default/src/routes/onboarding/$slug.tsx b/web/default/src/routes/onboarding/$slug.tsx new file mode 100644 index 000000000000..a5cf47dfb39b --- /dev/null +++ b/web/default/src/routes/onboarding/$slug.tsx @@ -0,0 +1,24 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { createFileRoute } from '@tanstack/react-router' +import { OnboardingTutorial } from '@/features/onboarding' + +export const Route = createFileRoute('/onboarding/$slug')({ + component: OnboardingTutorial, +})