feat: add easy mode and improve OAuth, API, and agent setup flow
This commit is contained in:
1 parent
6fd6b53e7c
commit
6755b7c90e
8 files changed
+1429
-7
No files matched your search
+31
-4
@@ -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 />
|
||||
)}
|
||||
|
||||
@@ -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': 'ページを再読み込み',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user