Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
187 changes: 187 additions & 0 deletions web/default/src/features/onboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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<IconKey, LucideIcon> = {
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 (
<div className='mx-auto max-w-3xl px-4 py-16 text-center'>
<h1 className='text-2xl font-semibold'>{t('Tutorial not found')}</h1>
<p className='text-muted-foreground mt-2'>
{t("We don't have a guide for this client yet.")}
</p>
<Button className='mt-6' render={<Link to='/keys' />}>
<ArrowLeft className='mr-1.5 h-4 w-4' />
{t('Back to API Keys')}
</Button>
</div>
)
}

const Icon = ICONS[tutorial.icon]

return (
<div className='mx-auto max-w-3xl px-4 py-6 sm:py-10'>
{/* Breadcrumb back to /keys */}
<div className='mb-4 flex items-center gap-2 text-sm'>
<Button
variant='ghost'
size='sm'
render={<Link to='/keys' />}
className='text-muted-foreground hover:text-foreground -ml-2'
>
<ArrowLeft className='mr-1.5 h-4 w-4' />
{t('Back to API Keys')}
</Button>
</div>

{/* Header card */}
<div className='bg-card mb-6 flex items-start gap-4 rounded-xl border p-5'>
<div className='bg-muted text-muted-foreground flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border'>
<Icon className='h-6 w-6' />
</div>
<div className='min-w-0 flex-1'>
<div className='flex items-center gap-2'>
<h1 className='text-xl font-semibold sm:text-2xl'>
{tutorial.label}
</h1>
{tutorial.recommended && (
<span className='bg-foreground/10 text-foreground rounded-full px-2 py-0.5 text-[10px] font-medium'>
{t('Recommended')}
</span>
)}
</div>
<p className='text-muted-foreground mt-1 text-sm'>
{t(tutorial.descriptionKey)}
</p>
<div className='mt-3 flex flex-wrap gap-2'>
<Button size='sm' render={<Link to='/playground' />}>
<PlayCircle className='mr-1.5 h-4 w-4' />
{t('Try in Playground')}
</Button>
</div>
</div>
</div>

{/* Connection values — top-of-page copy-block for the most common need */}
<div className='bg-muted/30 mb-6 grid gap-3 rounded-xl border p-4 sm:grid-cols-3'>
<CopyField label={t('Base URL')} value={baseUrl} />
<CopyField label={t('Model')} value={modelName} />
<div className='text-muted-foreground flex flex-col justify-center text-xs leading-snug'>
{t(
'Use these values in the steps below. Your API Key was shown when you created it on the Keys page.'
)}
</div>
</div>

{/* Markdown body */}
<Markdown>{content}</Markdown>
</div>
)
}

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 (
<div className='min-w-0 space-y-1'>
<p className='text-muted-foreground text-[11px] font-medium'>{label}</p>
<div className='bg-background flex items-center gap-2 rounded-md border px-2.5 py-1.5'>
<code className='flex-1 truncate font-mono text-xs' title={value}>
{value}
</code>
<Button
type='button'
size='sm'
variant='ghost'
className={cn('h-6 px-1.5')}
onClick={handleCopy}
>
{copied ? (
<Check className='h-3 w-3' />
) : (
<Copy className='h-3 w-3' />
)}
</Button>
</div>
</div>
)
}
78 changes: 78 additions & 0 deletions web/default/src/features/onboarding/tutorials/chatbox.ts
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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\` 试试。
`,
}),
}
93 changes: 93 additions & 0 deletions web/default/src/features/onboarding/tutorials/cherry-studio.ts
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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) 充值。

> 截图教程整理中。卡住了可以来社区问。
`,
}),
}
Loading
Loading