import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { addProvider, addModel, getProviderList, testConnection } from '@/services/model' import { getTranscriberConfig, updateTranscriberConfig } from '@/services/transcriber' import logo from '@/assets/icon.svg' // 桌面端首启 4 步引导。完成后写 localStorage('bilinote-onboarded') = '1',路由守卫不再拦。 // // 1. 后端连通性自检 // 2. LLM 供应商 + 模型(最简:只引导填一个 OpenAI-兼容供应商 + 一个 model 名) // 3. 转写引擎选择(推荐 Groq 在线,避开本地模型下载坑) // 4. (可选)Cookie 同步说明(仅当用户关注 B 站等需要登录态的平台时) const ONBOARD_KEY = 'bilinote-onboarded' export function isOnboarded(): boolean { return localStorage.getItem(ONBOARD_KEY) === '1' } function markOnboarded() { localStorage.setItem(ONBOARD_KEY, '1') } const Onboarding = () => { const navigate = useNavigate() const [step, setStep] = useState(1) const [error, setError] = useState('') // step 1 const [pinging, setPinging] = useState(false) const [backendOk, setBackendOk] = useState(null) // step 2 const [providerName, setProviderName] = useState('OpenAI') const [apiKey, setApiKey] = useState('') const [baseUrl, setBaseUrl] = useState('https://api.openai.com/v1') const [modelName, setModelName] = useState('gpt-4o-mini') const [providerId, setProviderId] = useState(null) const [savingProvider, setSavingProvider] = useState(false) // step 3 const [transcriberType, setTranscriberType] = useState('groq') const [savingTranscriber, setSavingTranscriber] = useState(false) function next() { setError('') setStep(s => s + 1) } function prev() { setError('') setStep(s => Math.max(1, s - 1)) } // step 1: ping 后端 useEffect(() => { if (step !== 1) return let cancelled = false ;(async () => { setPinging(true) try { await getProviderList() if (!cancelled) setBackendOk(true) } catch { if (!cancelled) setBackendOk(false) } finally { if (!cancelled) setPinging(false) } })() return () => { cancelled = true } }, [step]) async function saveProvider() { setError('') if (!apiKey.trim()) { setError('请填 API Key'); return } if (!baseUrl.trim()) { setError('请填 API 地址'); return } if (!providerName.trim()) { setError('请填供应商名'); return } if (!modelName.trim()) { setError('请填模型名'); return } setSavingProvider(true) try { // 复用桌面 web 端的 add_provider;type 必须是 'custom'(backend 强制) const res: any = await addProvider({ name: providerName.trim(), api_key: apiKey.trim(), base_url: baseUrl.trim(), type: 'custom', logo: 'custom', }) const newId = (res?.data ?? res) as string | undefined if (!newId) throw new Error('后端未返回 provider id') setProviderId(newId) // 加一个默认 model await addModel({ provider_id: newId, model_name: modelName.trim() }) // 测试连通 try { await testConnection({ id: newId }) } catch (e: any) { // 测试失败不阻断流程,让用户自己决定继续 console.warn('测试连接失败:', e?.message ?? e) } next() } catch (e: any) { setError(`保存失败:${e?.message ?? e}`) } finally { setSavingProvider(false) } } async function saveTranscriber() { setError('') setSavingTranscriber(true) try { // fast-whisper / mlx-whisper 需指定 model size;在线 (groq/bcut/kuaishou) 不用 const needsSize = transcriberType === 'fast-whisper' || transcriberType === 'mlx-whisper' await updateTranscriberConfig({ transcriber_type: transcriberType, ...(needsSize ? { whisper_model_size: 'tiny' } : {}), } as any) next() } catch (e: any) { setError(`保存失败:${e?.message ?? e}`) } finally { setSavingTranscriber(false) } } function finish() { markOnboarded() navigate('/', { replace: true }) } return (
logo

欢迎使用 BiliNote

几步配置后就可以开始把视频转笔记。

{/* Stepper */}
{[1, 2, 3, 4].map(s => (
= s ? 'border-blue-600 bg-blue-600 text-white' : 'border-gray-300 bg-white text-gray-400'}`} >{s}
{s < 4 &&
s ? 'bg-blue-600' : 'bg-gray-300'}`} />}
))}
{step === 1 && (

第 1 步 · 后端连通性

桌面端会自动启动 Python 后端进程。检查连通中…

{pinging &&
检测中…
} {backendOk === true &&
✓ 后端已就绪
} {backendOk === false && (
✗ 暂时连不上后端。可能正在初始化(首次启动会下载依赖),等 1-2 分钟再试。 右下角的「后端」状态点会持续监控。
)}
)} {step === 2 && (

第 2 步 · 模型供应商

填一个 OpenAI 兼容供应商:DeepSeek / Qwen / Claude / 自托管 / OpenAI 都行。

{error &&
{error}
}
)} {step === 3 && (

第 3 步 · 音频转写引擎

把视频音频转成文字。推荐在线引擎,避免本地下载 ~600MB 的模型。

{[ { value: 'groq', title: 'Groq(在线,推荐)', desc: '注册 https://groq.com/ 拿免费 key;速度快、英文语料佳。无需本地模型。' }, { value: 'bcut', title: '必剪(在线,免登)', desc: '免登,中文表现好;偶尔限流。' }, { value: 'kuaishou', title: '快手(在线,免登)', desc: '与必剪类似,备选。' }, { value: 'fast-whisper', title: 'Faster Whisper(本地)', desc: '完全离线但首次需下载 ~75MB(tiny)至 ~3GB(large-v3)的模型。CPU 慢。' }, ].map(opt => ( ))}
{error &&
{error}
}
)} {step === 4 && (

第 4 步 · Cookie 同步(可选)

想总结 B 站 / 抖音 / 快手 等需要登录态的平台时,需要把浏览器 cookie 复制到「下载配置」页。
YouTube 一般不需要 cookie。先跳过也没问题,到时再去配。

提示:插件版(BillNote_extension)支持一键 cookie 同步;桌面版需手动复制。
)}
) } export default Onboarding