feat: add easy mode and improve OAuth, API, and agent setup flow

This commit is contained in:
lzt404 committed 2026-08-30 16:55:25 +08:00
1 parent 6fd6b53e7c
commit 6755b7c90e
8 files changed
+1429 -7

No files matched your search

+31 -4
View File
@@ -15,6 +15,7 @@ import {
PackageOpen,
ServerCog,
Settings,
Sparkles,
X,
} from 'lucide-react';
import appLogo from './assets/logo.jpg';
@@ -25,6 +26,7 @@ import { KernelPage } from './pages/Kernel';
import { VersionManagementPage } from './pages/VersionManagementPage';
import { OAuthManagementPage } from './pages/ManagementPages';
import { AgentsPage } from './pages/AgentsPage';
import { EasyModePage, type EasyModeDestination } from './pages/EasyModePage';
import { UsageRecordsPage } from './pages/UsageRecordsPage';
import { languageOptions, useI18n } from './i18n';
import { AppUpdateDialog, AppUpdateProvider, useAppUpdate } from './appUpdate';
@@ -35,6 +37,13 @@ import { detectInitialTheme, saveTheme, type AppTheme } from './theme';
const CONTACT_URL = 'https://qm.qq.com/q/3queDaIG';
const pages = [
{
id: 'easy',
labelKey: 'app.nav.easy',
icon: Sparkles,
// The easy page is rendered separately so it can receive navigation callbacks.
component: HomePage,
},
{
id: 'home',
labelKey: 'app.nav.home',
@@ -127,7 +136,6 @@ function AppContent() {
const ActivePage = activePage.component;
const selectedLanguage = languageOptions.find((option) => option.value === locale)
?? languageOptions[0];
useEffect(() => {
saveTheme(theme);
}, [theme]);
@@ -220,6 +228,10 @@ function AppContent() {
setActive(pageId);
};
const openPage = (pageId: EasyModeDestination) => {
setActive(pageId);
};
const openContact = async () => {
try {
await invoke('open_external_url', { url: CONTACT_URL });
@@ -275,7 +287,7 @@ function AppContent() {
</div>
<nav className="nav-section" aria-label={t('app.navigation')}>
{pages.map((page) => {
{pages.filter((page) => page.id !== 'easy').map((page) => {
const Icon = page.icon;
const locked = !canOpenAppPage(page.id, coreRunning);
const updateIndicator = page.id === 'versions'
@@ -285,7 +297,10 @@ function AppContent() {
<button
key={page.id}
type="button"
className={[page.id === active ? 'active' : '', locked ? 'locked' : '']
className={[
page.id === active ? 'active' : '',
locked ? 'locked' : '',
]
.filter(Boolean)
.join(' ')}
disabled={locked}
@@ -311,6 +326,14 @@ function AppContent() {
</nav>
<div className="sidebar-bottom">
<button
type="button"
className={`sidebar-easy-entry${active === 'easy' ? ' active' : ''}`}
aria-current={active === 'easy' ? 'page' : undefined}
onClick={() => select('easy')}
>
<span>{t('app.nav.easy')}</span>
</button>
<div
className="sidebar-theme-selector"
role="group"
@@ -399,7 +422,11 @@ function AppContent() {
<div className="workspace">
<main className="content">
{isAlwaysAvailablePage(activePage.id) || coreRunning ? (
<ActivePage />
activePage.id === 'easy' ? (
<EasyModePage onOpenPage={openPage} />
) : (
<ActivePage />
)
) : (
<CoreLockedPage />
)}
+52
View File
@@ -12,6 +12,7 @@ export const jaOverrides = {
'app.theme.switchToDark': 'ダークモードに切り替え',
'app.desktopConsole': 'デスクトップコンソール',
'app.navigation': 'メインナビゲーション',
'app.nav.easy': '簡易モード',
'app.nav.home': 'ホーム',
'app.nav.versions': 'バージョン管理',
'app.nav.kernel': 'コア',
@@ -34,6 +35,57 @@ export const jaOverrides = {
'app.close.exiting': '終了しています...',
'app.close.exit': 'アプリを終了',
'app.close.remember': 'この選択を記憶',
'easyMode.eyebrow': 'クイックスタート',
'easyMode.title': '3 ステップで最初のエージェントを接続',
'easyMode.description': '認証方法を選び、接続を完了してから、モデルを普段使うエージェントクライアントに接続します。',
'easyMode.reset': '最初からやり直す',
'easyMode.steps.label': '接続ウィザードの手順',
'easyMode.step': 'ステップ {number}',
'easyMode.step1.title': '認証方法を選択',
'easyMode.step1.description': '既存のアカウントでログインするか、サードパーティ API キーを直接入力します。',
'easyMode.step1.selectHint': 'まず方法を 1 つ選びます。選択後、対応する設定手順に進みます。',
'easyMode.step1.selectFirst': '先に OAuth または API 接続を選択してください',
'easyMode.step1.continue': '次へ:接続を完了',
'easyMode.step1.hint': '迷ったら、公式アカウントを使い API キーを管理したくない場合は OAuth を選んでください。',
'easyMode.step2.title': 'モデル接続を完了',
'easyMode.step2.description': 'ログインまたは API 接続を完了し、保存して次へ進みます。',
'easyMode.step2.openTitle': 'まず上の設定を完了してください',
'easyMode.step2.returnHint': 'ボタンで設定ページを開きます。完了後、左側の「簡易モード」をクリックしてここに戻り、完了を確認してください。',
'easyMode.step2.doneHint': '接続設定が終わったら次へ進みます',
'easyMode.step2.complete': '接続完了、次へ',
'easyMode.step2.next': '完了しました。エージェントを接続',
'easyMode.selectedPath': '選択中の方法',
'easyMode.changePath': '方法を変更',
'easyMode.oauth.title': 'OAuth ログイン',
'easyMode.oauth.description': 'API キーを手動でコピーせず、ブラウザーで認証します。',
'easyMode.oauth.step1': 'OAuth ページを開き、ログインするサービスを選びます。',
'easyMode.oauth.step2': '「ログインを開始」をクリックし、ブラウザーで認証します。',
'easyMode.oauth.step3': 'ソフトウェアに戻り、ログイン状態が完了したことを確認します。',
'easyMode.oauth.action': 'OAuth ログインを開く',
'easyMode.api.title': 'API 接続',
'easyMode.api.description': 'サードパーティまたはセルフホストサービスの Base URL と API キーを入力します。',
'easyMode.api.step1': 'API 接続を開き、右上の「追加」をクリックします。',
'easyMode.api.step2': '備考、Base URL、API キーを入力してモデルを取得します。',
'easyMode.api.step3': '接続を保存し、少なくとも 1 つのモデルが利用可能なことを確認します。',
'easyMode.api.action': 'API 接続を設定',
'easyMode.step3.title': 'エージェントを接続',
'easyMode.step3.description': '使用するクライアントを選び、モデル設定を適用して会話を始めます。',
'easyMode.step3.ready': '準備完了',
'easyMode.step3.cardTitle': 'CPA をローカルエージェントに接続',
'easyMode.step3.cardDescription': 'エージェントページがインストール済みのクライアントを検出し、CPA の接続設定を書き込みます。',
'easyMode.step3.step1': 'エージェント設定を開き、インストール済みのクライアントを選びます。',
'easyMode.step3.step2': 'モデルを選びます。Codex では OAuth 設定も利用できます。',
'easyMode.step3.step3': '「設定変更を適用」をクリックし、CLI またはアプリを起動します。',
'easyMode.step3.action': 'エージェント設定を開く',
'easyMode.step3.back': '前の手順に戻る',
'easyMode.step3.note': '設定後は対応する CLI またはアプリを起動して利用できます。',
'easyMode.core.title': '先に CPA コアを起動',
'easyMode.core.description': 'OAuth、API 接続、エージェント設定にはコアの実行が必要です。',
'easyMode.core.action': 'コア制御を開く',
'easyMode.core.ready': 'コア実行中',
'easyMode.core.waiting': 'コアを待機中',
'easyMode.core.blocked': '設定ページを開く前にコアを起動してください。',
'easyMode.footer': '設定後は左側の機能メニューから詳細を管理できます。',
'error.render.title': 'ページの描画中にエラーが発生しました',
'error.unknown': '不明なエラー',
'error.reload': 'ページを再読み込み',
+52
View File
@@ -12,6 +12,7 @@ export const en: Record<MessageKey, string> = {
'app.theme.switchToDark': 'Switch to dark mode',
'app.desktopConsole': 'Desktop Console',
'app.navigation': 'Main navigation',
'app.nav.easy': 'Easy Mode',
'app.nav.home': 'Home',
'app.nav.versions': 'Version Management',
'app.nav.kernel': 'Core',
@@ -34,6 +35,57 @@ export const en: Record<MessageKey, string> = {
'app.close.exiting': 'Exiting...',
'app.close.exit': 'Exit App',
'app.close.remember': 'Remember this choice',
'easyMode.eyebrow': 'Quick start',
'easyMode.title': 'Connect your first agent in three steps',
'easyMode.description': 'Choose an authentication method, finish the connection, then link a model to your favorite agent client.',
'easyMode.reset': 'Start over',
'easyMode.steps.label': 'Connection wizard steps',
'easyMode.step': 'Step {number}',
'easyMode.step1.title': 'Choose authentication',
'easyMode.step1.description': 'Sign in to an existing account, or enter a third-party API key directly.',
'easyMode.step1.selectHint': 'Choose one method first. You will then enter the matching setup step.',
'easyMode.step1.selectFirst': 'Choose OAuth or API Access first',
'easyMode.step1.continue': 'Next: finish the connection',
'easyMode.step1.hint': 'Not sure? Choose OAuth when you have an official account and do not want to manage an API key.',
'easyMode.step2.title': 'Finish the model connection',
'easyMode.step2.description': 'Complete the sign-in or add an API connection, then save it to continue.',
'easyMode.step2.openTitle': 'Complete the setup above first',
'easyMode.step2.returnHint': 'Open the setup page with the button. When finished, click “Easy Mode” in the left sidebar to return here and confirm completion.',
'easyMode.step2.doneHint': 'Continue after the connection is configured',
'easyMode.step2.complete': 'Connection complete, next step',
'easyMode.step2.next': 'I am done, connect an agent',
'easyMode.selectedPath': 'Selected method',
'easyMode.changePath': 'Change method',
'easyMode.oauth.title': 'OAuth Sign-In',
'easyMode.oauth.description': 'Authorize in your browser without copying an API key manually.',
'easyMode.oauth.step1': 'Open OAuth and choose the service you want to sign in to.',
'easyMode.oauth.step2': 'Click “Start Sign-In” and finish authorization in the browser.',
'easyMode.oauth.step3': 'Return here and confirm the sign-in status is completed.',
'easyMode.oauth.action': 'Open OAuth Sign-In',
'easyMode.api.title': 'API Access',
'easyMode.api.description': 'Enter a Base URL and API key for a third-party or self-hosted service.',
'easyMode.api.step1': 'Open API Access and click “Add” in the upper-right.',
'easyMode.api.step2': 'Enter a remark, Base URL, and API key, then fetch models.',
'easyMode.api.step3': 'Save the connection and confirm that at least one model is available.',
'easyMode.api.action': 'Configure API Access',
'easyMode.step3.title': 'Connect an agent',
'easyMode.step3.description': 'Choose the client you use and apply one model configuration to start chatting.',
'easyMode.step3.ready': 'Ready',
'easyMode.step3.cardTitle': 'Connect CPA to a local agent client',
'easyMode.step3.cardDescription': 'The agent page detects installed clients and writes the CPA connection settings for you.',
'easyMode.step3.step1': 'Open Agent Configuration and choose an installed client.',
'easyMode.step3.step2': 'Choose a model; Codex can also use the OAuth configuration.',
'easyMode.step3.step3': 'Click “Apply Configuration Changes”, then launch the CLI or App.',
'easyMode.step3.action': 'Open Agent Configuration',
'easyMode.step3.back': 'Back to previous step',
'easyMode.step3.note': 'After configuration, launch the matching CLI or App to start.',
'easyMode.core.title': 'Start the CPA core first',
'easyMode.core.description': 'OAuth, API access, and agent configuration need the core to be running.',
'easyMode.core.action': 'Open Core Control',
'easyMode.core.ready': 'Core is running',
'easyMode.core.waiting': 'Waiting for core',
'easyMode.core.blocked': 'Start the core before opening a configuration page.',
'easyMode.footer': 'After setup, use the navigation on the left to manage more details.',
'error.render.title': 'Something went wrong while rendering this page',
'error.unknown': 'Unknown error',
'error.reload': 'Reload Page',
+52
View File
@@ -10,6 +10,7 @@ export const zhCN = {
'app.theme.switchToDark': '切换到夜间模式',
'app.desktopConsole': '桌面控制台',
'app.navigation': '主导航',
'app.nav.easy': '简易模式',
'app.nav.home': '首页',
'app.nav.versions': '版本管理',
'app.nav.kernel': '内核',
@@ -32,6 +33,57 @@ export const zhCN = {
'app.close.exiting': '正在退出...',
'app.close.exit': '退出程序',
'app.close.remember': '记住选项',
'easyMode.eyebrow': '新手引导',
'easyMode.title': '三步接入你的第一个智能体',
'easyMode.description': '先选择认证方式,再完成接入,最后把模型连接到常用的智能体客户端。',
'easyMode.reset': '重新开始',
'easyMode.steps.label': '接入向导步骤',
'easyMode.step': '第 {number} 步',
'easyMode.step1.title': '选择认证方式',
'easyMode.step1.description': '你可以登录已有账号,也可以直接填入第三方 API 密钥。',
'easyMode.step1.selectHint': '先选择一种方式。选定后,再进入对应的配置步骤。',
'easyMode.step1.selectFirst': '请先选择 OAuth 或 API 接入',
'easyMode.step1.continue': '下一步:完成接入',
'easyMode.step1.hint': '不知道选哪种?有官方账号并希望免填密钥时,优先选择 OAuth。',
'easyMode.step2.title': '完成模型接入',
'easyMode.step2.description': '按提示完成认证或新增 API 接入,保存后即可继续。',
'easyMode.step2.openTitle': '先完成上面的配置',
'easyMode.step2.returnHint': '点击按钮打开配置页面。完成后,点击左侧“简易模式”返回这里,再确认已完成。',
'easyMode.step2.doneHint': '完成配置后再继续下一步',
'easyMode.step2.complete': '已完成接入,下一步',
'easyMode.step2.next': '我已完成,连接智能体',
'easyMode.selectedPath': '当前选择',
'easyMode.changePath': '更换方式',
'easyMode.oauth.title': 'OAuth 登录',
'easyMode.oauth.description': '通过浏览器授权,不需要手动复制 API 密钥。',
'easyMode.oauth.step1': '打开 OAuth 页面,选择要登录的服务。',
'easyMode.oauth.step2': '点击“开始登录”,在浏览器中完成授权。',
'easyMode.oauth.step3': '回到软件,确认登录状态显示为已完成。',
'easyMode.oauth.action': '去 OAuth 登录',
'easyMode.api.title': 'API 接入',
'easyMode.api.description': '填入 Base URL 和 API 密钥,适合第三方或自建服务。',
'easyMode.api.step1': '打开 API 接入页面,点击右上角“新增”。',
'easyMode.api.step2': '填写备注、Base URL 和 API 密钥,然后获取模型。',
'easyMode.api.step3': '保存接入,并确认至少有一个模型可用。',
'easyMode.api.action': '去配置 API',
'easyMode.step3.title': '连接智能体',
'easyMode.step3.description': '选择你正在使用的客户端,应用一个模型配置即可开始对话。',
'easyMode.step3.ready': '准备就绪',
'easyMode.step3.cardTitle': '把 CPA 接入本地智能体客户端',
'easyMode.step3.cardDescription': '智能体页面会自动检测已安装的客户端,并为它写入 CPA 所需的连接配置。',
'easyMode.step3.step1': '打开智能体配置,选择一个已安装的客户端。',
'easyMode.step3.step2': '选择模型;Codex 还可以选择是否使用 OAuth 配置。',
'easyMode.step3.step3': '点击“应用配置修改”,然后启动 CLI 或 App。',
'easyMode.step3.action': '打开智能体配置',
'easyMode.step3.back': '返回上一步',
'easyMode.step3.note': '配置完成后,就可以从对应的 CLI 或 App 启动。',
'easyMode.core.title': '先启动 CPA 内核',
'easyMode.core.description': 'OAuth、API 接入和智能体配置都需要内核运行。',
'easyMode.core.action': '前往内核控制',
'easyMode.core.ready': '内核运行中',
'easyMode.core.waiting': '等待内核',
'easyMode.core.blocked': '启动内核后才能打开配置页面。',
'easyMode.footer': '完成接入后,可直接使用左侧功能入口管理更多细节。',
'error.render.title': '页面渲染出现异常',
'error.unknown': '未知错误',
'error.reload': '重新加载页面',
+1 -1
View File
@@ -1,4 +1,4 @@
const alwaysAvailablePages = new Set(['home', 'versions', 'config', 'usage-records']);
const alwaysAvailablePages = new Set(['easy', 'home', 'versions', 'config', 'usage-records']);
export function isAlwaysAvailablePage(pageId: string) {
return alwaysAvailablePages.has(pageId);
+279
View File
@@ -0,0 +1,279 @@
import { useEffect, useState } from 'react';
import { useCoreRuntime } from '../coreRuntime';
import { useI18n } from '../i18n';
export type EasyModeDestination = 'home' | 'oauth' | 'api' | 'agents';
type AuthMethod = 'oauth' | 'api';
type EasyModeStep = 1 | 2 | 3;
type SavedEasyModeState = {
authMethod: AuthMethod | null;
step: EasyModeStep;
};
const EASY_MODE_STATE_KEY = 'easy-cli-proxy-api.easy-mode.state';
const defaultState: SavedEasyModeState = {
authMethod: null,
step: 1,
};
function readSavedState(): SavedEasyModeState {
try {
const raw = window.localStorage.getItem(EASY_MODE_STATE_KEY);
if (!raw) return defaultState;
const parsed = JSON.parse(raw) as Partial<SavedEasyModeState>;
const authMethod = parsed.authMethod === 'oauth' || parsed.authMethod === 'api'
? parsed.authMethod
: null;
const step = parsed.step === 2 || parsed.step === 3 ? parsed.step : 1;
return { authMethod, step: authMethod ? step : 1 };
} catch {
return defaultState;
}
}
function saveState(state: SavedEasyModeState) {
try {
window.localStorage.setItem(EASY_MODE_STATE_KEY, JSON.stringify(state));
} catch {
// Keep the wizard usable when persistent storage is unavailable.
}
}
export function EasyModePage({
onOpenPage,
}: {
onOpenPage: (page: EasyModeDestination) => void;
}) {
const { t } = useI18n();
const { status: coreStatus } = useCoreRuntime();
const [wizardState, setWizardState] = useState<SavedEasyModeState>(readSavedState);
const coreRunning = Boolean(coreStatus?.running);
useEffect(() => {
saveState(wizardState);
}, [wizardState]);
const chooseAuthMethod = (authMethod: AuthMethod) => {
setWizardState((current) => ({ ...current, authMethod, step: 1 }));
};
const goToStep = (step: EasyModeStep) => {
if (!wizardState.authMethod && step !== 1) return;
setWizardState((current) => ({ ...current, step }));
};
const resetWizard = () => setWizardState(defaultState);
const openAuthPage = () => {
if (!coreRunning || !wizardState.authMethod) return;
onOpenPage(wizardState.authMethod);
};
const authTitle = wizardState.authMethod === 'oauth'
? t('easyMode.oauth.title')
: t('easyMode.api.title');
const authAction = wizardState.authMethod === 'oauth'
? t('easyMode.oauth.action')
: t('easyMode.api.action');
const authSteps = wizardState.authMethod === 'oauth'
? [t('easyMode.oauth.step1'), t('easyMode.oauth.step2'), t('easyMode.oauth.step3')]
: [t('easyMode.api.step1'), t('easyMode.api.step2'), t('easyMode.api.step3')];
const stepTitles = [
t('easyMode.step1.title'),
t('easyMode.step2.title'),
t('easyMode.step3.title'),
];
return (
<section className="page simple-mode-page">
<header className="simple-mode-header">
<div>
<span className="simple-mode-eyebrow">{t('easyMode.eyebrow')}</span>
<h1>{t('easyMode.title')}</h1>
<p>{t('easyMode.description')}</p>
</div>
<button type="button" className="secondary-button simple-mode-reset" onClick={resetWizard}>
{t('easyMode.reset')}
</button>
</header>
{!coreRunning ? (
<div className="simple-mode-core-alert" role="status">
<div>
<strong>{t('easyMode.core.title')}</strong>
<p>{t('easyMode.core.description')}</p>
</div>
<button type="button" className="secondary-button" onClick={() => onOpenPage('home')}>
{t('easyMode.core.action')}
</button>
</div>
) : null}
<div className="simple-mode-progress" role="list" aria-label={t('easyMode.steps.label')}>
{stepTitles.map((title, index) => {
const step = (index + 1) as EasyModeStep;
const active = wizardState.step === step;
const complete = wizardState.step > step;
const locked = !wizardState.authMethod && step > 1;
return (
<button
type="button"
role="listitem"
key={title}
className={`simple-mode-progress-item${active ? ' active' : ''}${complete ? ' complete' : ''}`}
aria-current={active ? 'step' : undefined}
disabled={locked}
onClick={() => goToStep(step)}
>
<span className="simple-mode-progress-number">{step}</span>
<span>
<small>{t('easyMode.step', { number: step })}</small>
<strong>{title}</strong>
</span>
</button>
);
})}
</div>
{wizardState.step === 1 ? (
<section className="panel simple-mode-task">
<div className="simple-mode-task-heading">
<span className="simple-mode-task-number">1</span>
<div>
<h2>{t('easyMode.step1.title')}</h2>
<p>{t('easyMode.step1.description')}</p>
</div>
</div>
<p className="simple-mode-instruction">{t('easyMode.step1.selectHint')}</p>
<div className="simple-mode-choice-grid">
<button
type="button"
className={`simple-mode-choice${wizardState.authMethod === 'oauth' ? ' selected' : ''}`}
aria-pressed={wizardState.authMethod === 'oauth'}
onClick={() => chooseAuthMethod('oauth')}
>
<strong>{t('easyMode.oauth.title')}</strong>
<span>{t('easyMode.oauth.description')}</span>
</button>
<button
type="button"
className={`simple-mode-choice${wizardState.authMethod === 'api' ? ' selected' : ''}`}
aria-pressed={wizardState.authMethod === 'api'}
onClick={() => chooseAuthMethod('api')}
>
<strong>{t('easyMode.api.title')}</strong>
<span>{t('easyMode.api.description')}</span>
</button>
</div>
<div className="simple-mode-task-footer">
<span className={wizardState.authMethod ? 'simple-mode-selection' : 'simple-mode-selection muted'}>
{wizardState.authMethod
? t('easyMode.selectedPath') + `:${authTitle}`
: t('easyMode.step1.selectFirst')}
</span>
<button
type="button"
className="primary-button"
disabled={!wizardState.authMethod}
onClick={() => goToStep(2)}
>
{t('easyMode.step1.continue')}
</button>
</div>
</section>
) : null}
{wizardState.step === 2 && wizardState.authMethod ? (
<section className="panel simple-mode-task">
<div className="simple-mode-task-heading">
<span className="simple-mode-task-number">2</span>
<div>
<h2>{t('easyMode.step2.title')}</h2>
<p>{t('easyMode.step2.description')}</p>
</div>
</div>
<div className="simple-mode-path-row">
<span>{t('easyMode.selectedPath')}</span>
<strong>{authTitle}</strong>
<button type="button" className="text-button" onClick={() => goToStep(1)}>
{t('easyMode.changePath')}
</button>
</div>
<div className="simple-mode-action-card">
<div>
<strong>{t('easyMode.step2.openTitle')}</strong>
<p>{t('easyMode.step2.returnHint')}</p>
</div>
<button type="button" className="primary-button" disabled={!coreRunning} onClick={openAuthPage}>
{authAction}
</button>
</div>
<ol className="simple-mode-checklist">
{authSteps.map((item, index) => (
<li key={item}>
<span>{index + 1}</span>
<p>{item}</p>
</li>
))}
</ol>
<div className="simple-mode-task-footer">
<span className="simple-mode-selection muted">{t('easyMode.step2.doneHint')}</span>
<button type="button" className="secondary-button" onClick={() => goToStep(3)}>
{t('easyMode.step2.complete')}
</button>
</div>
{!coreRunning ? <p className="simple-mode-warning">{t('easyMode.core.blocked')}</p> : null}
</section>
) : null}
{wizardState.step === 3 && wizardState.authMethod ? (
<section className="panel simple-mode-task">
<div className="simple-mode-task-heading">
<span className="simple-mode-task-number">3</span>
<div>
<h2>{t('easyMode.step3.title')}</h2>
<p>{t('easyMode.step3.description')}</p>
</div>
</div>
<div className="simple-mode-action-card simple-mode-agent-card">
<div>
<strong>{t('easyMode.step3.cardTitle')}</strong>
<p>{t('easyMode.step3.cardDescription')}</p>
</div>
<span className="simple-mode-ready-label">{t('easyMode.step3.ready')}</span>
</div>
<ol className="simple-mode-checklist">
<li><span>1</span><p>{t('easyMode.step3.step1')}</p></li>
<li><span>2</span><p>{t('easyMode.step3.step2')}</p></li>
<li><span>3</span><p>{t('easyMode.step3.step3')}</p></li>
</ol>
<div className="simple-mode-task-footer">
<span className="simple-mode-selection muted">{t('easyMode.step3.note')}</span>
<div className="simple-mode-actions">
<button type="button" className="primary-button" disabled={!coreRunning} onClick={() => onOpenPage('agents')}>
{t('easyMode.step3.action')}
</button>
<button type="button" className="secondary-button" onClick={() => goToStep(2)}>
{t('easyMode.step3.back')}
</button>
</div>
</div>
</section>
) : null}
<footer className="simple-mode-footer">{t('easyMode.footer')}</footer>
</section>
);
}
+958
View File
@@ -601,6 +601,40 @@ textarea:focus-visible {
border-top: 1px solid var(--theme-e3e1db);
}
.sidebar-easy-entry {
display: block;
width: 100%;
min-height: 40px;
padding: 0 10px;
border: 1px solid var(--theme-d8d4cc);
border-radius: 8px;
background: var(--theme-fffdf8);
color: var(--theme-4f4a43);
text-align: left;
transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
}
.sidebar-easy-entry:hover {
border-color: color-mix(in srgb, var(--ui-accent) 36%, var(--theme-d8d4cc));
background: var(--theme-e8e5dc);
color: var(--theme-2d2a26);
}
.sidebar-easy-entry.active {
border-color: color-mix(in srgb, var(--ui-accent) 42%, var(--theme-d8d4cc));
background: var(--ui-accent-soft);
color: var(--ui-accent-strong);
}
.sidebar-easy-entry span {
min-width: 0;
overflow: hidden;
font-size: var(--font-size-label);
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-theme-selector {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -9505,3 +9539,927 @@ body.table-col-resizing * {
background: var(--theme-fdf1ee);
color: var(--theme-a33b2b);
}
/* Easy mode onboarding */
.easy-mode-page {
display: grid;
gap: 20px;
max-width: 980px;
margin-inline: auto;
}
.easy-mode-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
}
.easy-mode-hero-copy {
min-width: 0;
}
.easy-mode-eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--ui-accent-strong);
font-size: var(--font-size-label);
font-weight: 800;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.easy-mode-hero h1 {
margin: 8px 0 7px;
color: var(--theme-2d2a26);
font-size: clamp(26px, 3vw, 36px);
line-height: 1.12;
}
.easy-mode-hero p {
max-width: 660px;
margin: 0;
color: var(--theme-5f5a53);
font-size: var(--font-size-body);
line-height: var(--line-height-body);
}
.easy-mode-reset {
flex: none;
min-height: 38px;
}
.easy-mode-core-alert {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 13px;
padding: 14px 16px;
border: 1px solid var(--theme-dfbf91);
border-radius: 11px;
background: var(--theme-fff7e9);
color: var(--theme-5f5a53);
}
.easy-mode-alert-icon {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 10px;
background: var(--theme-e4d0af);
color: var(--theme-805a24);
}
.easy-mode-core-alert strong {
display: block;
color: var(--theme-2d2a26);
}
.easy-mode-core-alert p {
margin: 3px 0 0;
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.easy-mode-core-alert .secondary-button {
min-height: 36px;
white-space: nowrap;
}
.easy-mode-stepper {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.easy-mode-stepper-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 10px;
min-width: 0;
min-height: 64px;
padding: 10px 12px;
border: 1px solid var(--theme-e3e1db);
border-radius: 10px;
background: var(--theme-fffdf8);
color: var(--theme-5f5a53);
text-align: left;
}
.easy-mode-stepper-item:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--ui-accent) 42%, var(--theme-e3e1db));
background: var(--theme-f8f5ee);
}
.easy-mode-stepper-item:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.easy-mode-stepper-item.active {
border-color: color-mix(in srgb, var(--ui-accent) 38%, var(--theme-e3e1db));
background: var(--ui-accent-soft);
color: var(--ui-accent-strong);
box-shadow: 0 5px 14px var(--ui-accent-shadow);
}
.easy-mode-stepper-number {
display: grid;
width: 30px;
height: 30px;
place-items: center;
border: 1px solid var(--theme-d5d2cb);
border-radius: 999px;
background: var(--theme-faf9f5);
color: var(--theme-716b61);
font-weight: 800;
}
.easy-mode-stepper-item.active .easy-mode-stepper-number,
.easy-mode-stepper-item.complete .easy-mode-stepper-number {
border-color: var(--ui-accent);
background: var(--ui-accent);
color: #ffffff;
}
.easy-mode-stepper-item small,
.easy-mode-stepper-item strong {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.easy-mode-stepper-item small {
margin-bottom: 2px;
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
}
.easy-mode-stepper-item strong {
color: var(--theme-2d2a26);
font-size: var(--font-size-body);
}
.easy-mode-panel {
display: grid;
gap: 20px;
padding: clamp(20px, 3vw, 30px);
}
.easy-mode-panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.easy-mode-panel-heading > div:first-child {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
.easy-mode-step-badge {
display: grid;
flex: none;
width: 32px;
height: 32px;
place-items: center;
border-radius: 9px;
background: var(--ui-accent);
color: #ffffff;
font-weight: 800;
}
.easy-mode-panel-heading h2 {
margin: 0;
}
.easy-mode-panel-heading p {
margin: 5px 0 0;
color: var(--theme-5f5a53);
line-height: var(--line-height-body);
}
.easy-mode-choice-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.easy-mode-choice-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 13px;
min-width: 0;
min-height: 118px;
padding: 18px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 11px;
background: var(--theme-faf9f5);
color: var(--theme-2d2a26);
text-align: left;
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease, box-shadow 140ms ease;
}
.easy-mode-choice-card:hover {
border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--theme-d5d2cb));
background: var(--theme-fffdf8);
transform: translateY(-1px);
}
.easy-mode-choice-card.selected {
border-color: var(--ui-accent);
background: var(--ui-accent-soft);
box-shadow: 0 7px 18px var(--ui-accent-shadow);
}
.easy-mode-choice-icon {
display: grid;
flex: none;
width: 43px;
height: 43px;
place-items: center;
border-radius: 11px;
}
.easy-mode-choice-icon.oauth {
background: var(--theme-dce2fb);
color: var(--theme-344ca1);
}
.easy-mode-choice-icon.api {
background: var(--theme-dce8df);
color: var(--theme-2f6b3a);
}
.easy-mode-choice-copy {
display: grid;
gap: 5px;
min-width: 0;
}
.easy-mode-choice-copy strong {
font-size: var(--font-size-section);
}
.easy-mode-choice-copy span {
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.easy-mode-choice-card > svg {
color: var(--theme-7a746c);
}
.easy-mode-hint,
.easy-mode-inline-warning,
.easy-mode-footer {
display: flex;
align-items: flex-start;
gap: 8px;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.easy-mode-hint {
padding: 11px 13px;
border-radius: 8px;
background: var(--theme-f1f8f1);
color: var(--theme-2f6b3a);
}
.easy-mode-hint svg,
.easy-mode-footer svg {
flex: none;
margin-top: 1px;
}
.easy-mode-selected-path {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--theme-e3e1db);
border-radius: 9px;
background: var(--theme-faf9f5);
}
.easy-mode-selected-path > div {
display: grid;
gap: 3px;
min-width: 0;
}
.easy-mode-selected-path small {
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
}
.easy-mode-selected-path strong {
color: var(--theme-2d2a26);
}
.text-button {
margin-left: auto;
padding: 4px 0;
border: 0;
background: transparent;
color: var(--ui-accent-strong);
font-size: var(--font-size-label);
font-weight: 700;
white-space: nowrap;
}
.text-button:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.easy-mode-checklist {
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.easy-mode-checklist li {
display: grid;
grid-template-columns: 27px minmax(0, 1fr);
align-items: start;
gap: 10px;
}
.easy-mode-checklist li > span {
display: grid;
width: 27px;
height: 27px;
place-items: center;
border: 1px solid var(--theme-d5d2cb);
border-radius: 999px;
background: var(--theme-faf9f5);
color: var(--ui-accent-strong);
font-size: var(--font-size-label);
font-weight: 800;
}
.easy-mode-checklist p {
margin: 3px 0 0;
color: var(--theme-2d2a26);
line-height: var(--line-height-body);
}
.easy-mode-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 2px;
}
.easy-mode-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 42px;
}
.easy-mode-inline-warning {
margin: -8px 0 0;
color: var(--theme-9f2d20);
}
.easy-mode-agent-layout {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 14px;
padding: 18px;
border: 1px solid var(--theme-c9d8ca);
border-radius: 10px;
background: var(--theme-f1f8f1);
}
.easy-mode-agent-icon {
display: grid;
width: 54px;
height: 54px;
place-items: center;
border-radius: 14px;
background: var(--theme-2f6b3a);
color: #ffffff;
}
.easy-mode-agent-layout strong {
color: var(--theme-2d2a26);
}
.easy-mode-agent-layout p {
margin: 4px 0 0;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.easy-mode-footer {
justify-content: center;
padding: 2px 0 8px;
color: var(--theme-7a746c);
}
@media (max-width: 680px) {
.easy-mode-hero {
flex-direction: column;
gap: 14px;
}
.easy-mode-reset {
width: 100%;
}
.easy-mode-core-alert {
grid-template-columns: auto minmax(0, 1fr);
}
.easy-mode-core-alert .secondary-button {
grid-column: 1 / -1;
width: 100%;
}
.easy-mode-stepper,
.easy-mode-choice-grid {
grid-template-columns: 1fr;
}
.easy-mode-stepper-item {
min-height: 56px;
}
.easy-mode-panel-heading {
flex-direction: column;
}
.easy-mode-selected-path {
align-items: flex-start;
flex-wrap: wrap;
}
.easy-mode-selected-path .text-button {
width: 100%;
margin-left: 0;
text-align: left;
}
.easy-mode-actions {
display: grid;
grid-template-columns: 1fr;
}
.easy-mode-actions button {
width: 100%;
}
}
/* Simplified onboarding flow */
.simple-mode-page {
display: grid;
gap: 18px;
max-width: 920px;
margin-inline: auto;
}
.simple-mode-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
}
.simple-mode-header > div {
min-width: 0;
}
.simple-mode-eyebrow {
color: var(--ui-accent-strong);
font-size: var(--font-size-label);
font-weight: 800;
}
.simple-mode-header h1 {
margin: 7px 0 6px;
color: var(--theme-2d2a26);
font-size: clamp(25px, 3vw, 34px);
line-height: 1.15;
}
.simple-mode-header p {
max-width: 680px;
margin: 0;
color: var(--theme-5f5a53);
line-height: var(--line-height-body);
}
.simple-mode-reset {
flex: none;
min-height: 38px;
}
.simple-mode-core-alert {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border: 1px solid var(--theme-dfbf91);
border-radius: 10px;
background: var(--theme-fff7e9);
color: var(--theme-5f5a53);
}
.simple-mode-core-alert > div {
min-width: 0;
}
.simple-mode-core-alert strong {
display: block;
color: var(--theme-2d2a26);
}
.simple-mode-core-alert p {
margin: 3px 0 0;
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.simple-mode-core-alert .secondary-button {
flex: none;
min-height: 36px;
white-space: nowrap;
}
.simple-mode-progress {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.simple-mode-progress-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 9px;
min-width: 0;
min-height: 60px;
padding: 9px 11px;
border: 1px solid var(--theme-e3e1db);
border-radius: 9px;
background: var(--theme-fffdf8);
color: var(--theme-5f5a53);
text-align: left;
}
.simple-mode-progress-item:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--ui-accent) 38%, var(--theme-e3e1db));
background: var(--theme-f8f5ee);
}
.simple-mode-progress-item:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.simple-mode-progress-item.active {
border-color: color-mix(in srgb, var(--ui-accent) 40%, var(--theme-e3e1db));
background: var(--ui-accent-soft);
box-shadow: 0 4px 12px var(--ui-accent-shadow);
}
.simple-mode-progress-number,
.simple-mode-task-number {
display: grid;
place-items: center;
border-radius: 999px;
font-weight: 800;
}
.simple-mode-progress-number {
width: 28px;
height: 28px;
border: 1px solid var(--theme-d5d2cb);
background: var(--theme-faf9f5);
color: var(--theme-716b61);
}
.simple-mode-progress-item.active .simple-mode-progress-number,
.simple-mode-progress-item.complete .simple-mode-progress-number {
border-color: var(--ui-accent);
background: var(--ui-accent);
color: #ffffff;
}
.simple-mode-progress-item small,
.simple-mode-progress-item strong {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.simple-mode-progress-item small {
margin-bottom: 2px;
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
}
.simple-mode-progress-item strong {
color: var(--theme-2d2a26);
font-size: var(--font-size-body);
}
.simple-mode-task {
display: grid;
gap: 18px;
padding: clamp(20px, 3vw, 28px);
}
.simple-mode-task-heading {
display: flex;
align-items: flex-start;
gap: 11px;
}
.simple-mode-task-number {
flex: none;
width: 31px;
height: 31px;
border-radius: 8px;
background: var(--ui-accent);
color: #ffffff;
}
.simple-mode-task-heading h2 {
margin: 0;
color: var(--theme-2d2a26);
font-size: var(--font-size-panel-title);
}
.simple-mode-task-heading p {
margin: 4px 0 0;
color: var(--theme-5f5a53);
line-height: var(--line-height-body);
}
.simple-mode-instruction {
margin: -3px 0 0;
color: var(--theme-2d2a26);
font-weight: 650;
}
.simple-mode-choice-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.simple-mode-choice {
display: grid;
align-content: start;
gap: 7px;
min-width: 0;
min-height: 104px;
padding: 16px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 10px;
background: var(--theme-faf9f5);
color: var(--theme-2d2a26);
text-align: left;
transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.simple-mode-choice:hover {
border-color: color-mix(in srgb, var(--ui-accent) 42%, var(--theme-d5d2cb));
background: var(--theme-fffdf8);
}
.simple-mode-choice.selected {
border-color: var(--ui-accent);
background: var(--ui-accent-soft);
box-shadow: 0 5px 14px var(--ui-accent-shadow);
}
.simple-mode-choice strong {
font-size: var(--font-size-section);
}
.simple-mode-choice span {
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.simple-mode-task-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding-top: 2px;
border-top: 1px solid var(--theme-e3e1db);
}
.simple-mode-selection {
min-width: 0;
color: var(--theme-2d2a26);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.simple-mode-selection.muted {
color: var(--theme-7a746c);
}
.simple-mode-task-footer > .primary-button,
.simple-mode-task-footer > .secondary-button {
flex: none;
min-height: 40px;
}
.simple-mode-path-row {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-faf9f5);
color: var(--theme-7a746c);
font-size: var(--font-size-label);
}
.simple-mode-path-row strong {
color: var(--theme-2d2a26);
}
.simple-mode-path-row .text-button {
margin-left: auto;
}
.simple-mode-action-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
border: 1px solid var(--theme-c9d6e2);
border-radius: 10px;
background: var(--theme-f1f6fa);
}
.simple-mode-action-card > div {
min-width: 0;
}
.simple-mode-action-card strong {
color: var(--theme-2d2a26);
}
.simple-mode-action-card p {
margin: 4px 0 0;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
}
.simple-mode-action-card .primary-button {
flex: none;
min-height: 40px;
}
.simple-mode-agent-card {
border-color: var(--theme-c9d8ca);
background: var(--theme-f1f8f1);
}
.simple-mode-ready-label {
flex: none;
color: var(--theme-2f6b3a);
font-size: var(--font-size-label);
font-weight: 800;
}
.simple-mode-checklist {
display: grid;
gap: 11px;
margin: 0;
padding: 0;
list-style: none;
}
.simple-mode-checklist li {
display: grid;
grid-template-columns: 25px minmax(0, 1fr);
align-items: start;
gap: 10px;
}
.simple-mode-checklist li > span {
display: grid;
width: 25px;
height: 25px;
place-items: center;
border: 1px solid var(--theme-d5d2cb);
border-radius: 999px;
background: var(--theme-faf9f5);
color: var(--ui-accent-strong);
font-size: var(--font-size-label);
font-weight: 800;
}
.simple-mode-checklist p {
margin: 3px 0 0;
color: var(--theme-2d2a26);
line-height: var(--line-height-body);
}
.simple-mode-warning {
margin: -6px 0 0;
color: var(--theme-9f2d20);
font-size: var(--font-size-label);
}
.simple-mode-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.simple-mode-footer {
padding: 0 0 8px;
color: var(--theme-7a746c);
font-size: var(--font-size-label);
line-height: var(--line-height-body);
text-align: center;
}
@media (max-width: 680px) {
.simple-mode-header {
flex-direction: column;
gap: 13px;
}
.simple-mode-reset {
width: 100%;
}
.simple-mode-core-alert,
.simple-mode-action-card,
.simple-mode-task-footer {
align-items: stretch;
flex-direction: column;
}
.simple-mode-core-alert .secondary-button,
.simple-mode-action-card .primary-button,
.simple-mode-task-footer > .primary-button,
.simple-mode-task-footer > .secondary-button {
width: 100%;
}
.simple-mode-progress,
.simple-mode-choice-grid {
grid-template-columns: 1fr;
}
.simple-mode-progress-item {
min-height: 54px;
}
.simple-mode-path-row {
flex-wrap: wrap;
}
.simple-mode-path-row .text-button {
width: 100%;
margin-left: 0;
text-align: left;
}
.simple-mode-actions {
display: grid;
grid-template-columns: 1fr;
}
.simple-mode-actions button {
width: 100%;
}
}
+4 -2
View File
@@ -3,12 +3,14 @@ import { appUpdateIndicatorState } from '../src/appUpdateModel';
import { canOpenAppPage, isAlwaysAvailablePage } from '../src/navigation';
import { oauthSubpages } from '../src/oauthNavigation';
describe('首页、配置与版本管理导航', () => {
test('内核停止时首页、配置和版本管理始终可进入', () => {
describe('简易模式、首页、配置与版本管理导航', () => {
test('内核停止时简易模式、首页、配置和版本管理始终可进入', () => {
expect(isAlwaysAvailablePage('easy')).toBe(true);
expect(isAlwaysAvailablePage('home')).toBe(true);
expect(isAlwaysAvailablePage('versions')).toBe(true);
expect(isAlwaysAvailablePage('config')).toBe(true);
expect(isAlwaysAvailablePage('usage-records')).toBe(true);
expect(canOpenAppPage('easy', false)).toBe(true);
expect(canOpenAppPage('home', false)).toBe(true);
expect(canOpenAppPage('versions', false)).toBe(true);
expect(canOpenAppPage('config', false)).toBe(true);