From dd008d64ba427aadb8db92245f5dcdf756cc1783 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 31 Aug 2026 15:55:25 +0800 Subject: [PATCH] feat(easy-mode): improve API onboarding and agent configuration flow - Fix management API 404 errors when saving API providers - Simplify the model list and support custom model IDs - Adjust API field order and default provider selection - Reuse the full agent configuration page and restore agent detection --- src/App.tsx | 3 +- src/i18n/locales/en.ts | 39 ++ src/i18n/locales/zh-CN.ts | 43 +- src/pages/AgentsPage.tsx | 8 +- src/pages/EasyModePage.tsx | 832 +++++++++++++++++++++++------ src/styles.css | 1012 ++++++++++++++++++++++++++++++++++++ 6 files changed, 1777 insertions(+), 160 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e721b0c..73e259f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,7 +41,6 @@ const pages = [ id: 'easy', labelKey: 'app.nav.easy', icon: Sparkles, - // The easy page is rendered separately so it can receive navigation callbacks. component: HomePage, }, { @@ -419,7 +418,7 @@ function AppContent() {
{isAlwaysAvailablePage(activePage.id) || coreRunning ? ( activePage.id === 'easy' ? ( - + select('home')} /> ) : ( ) diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 02f102a..584970f 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -61,6 +61,45 @@ export const en: Record = { 'easyMode.guide.step3Description': 'Choose a model from “Available models”, select an installed agent client, and apply the configuration.', 'easyMode.navigation.back': 'Previous', 'easyMode.navigation.next': 'Next', + 'easyMode.status.ready': 'Ready', + 'easyMode.status.connectedModels': '{count} available models detected', + 'easyMode.status.noModelsYet': 'No models connected yet. Please sign in or add an API key first.', + 'easyMode.oauth.loggedIn': 'Signed In', + 'easyMode.oauth.relogin': 'Re-login', + 'easyMode.oauth.loggingIn': 'Waiting for browser authorization...', + 'easyMode.api.quickAdd': 'Quick Add API', + 'easyMode.api.testAndSave': 'Test & Save', + 'easyMode.api.testing': 'Testing connection...', + 'easyMode.api.fetchModels': 'Fetch model list', + 'easyMode.api.fetchingModels': 'Fetching model list...', + 'easyMode.api.customModelId': 'Custom Model ID', + 'easyMode.api.customModelPlaceholder': 'For example: gpt-5.6-sol or deepseek-v4-pro', + 'easyMode.api.addModelId': 'Add Model ID', + 'easyMode.api.credentialsRequired': 'Enter an API URL and API key first', + 'easyMode.api.modelRequired': 'Fetch and select a model, or add a Model ID manually', + 'easyMode.api.testSuccess': 'Connection successful! Found {count} models', + 'easyMode.api.testFailed': 'Connection test failed', + 'easyMode.api.platform': 'Platform', + 'easyMode.api.baseUrl': 'API Base URL', + 'easyMode.api.apiKey': 'API Key', + 'easyMode.api.name': 'Remark Name (Optional)', + 'easyMode.api.viewExisting': 'View all connected providers ({count})', + 'easyMode.api.backToQuick': 'Back to Quick Add', + 'easyMode.api.savedSuccess': 'API provider saved successfully!', + 'easyMode.scenario.title': 'Choose how to use', + 'easyMode.scenario.agentTab': 'Connect AI Agent (Recommended)', + 'easyMode.scenario.apiTab': 'Copy Local API Endpoints', + 'easyMode.agent.installedBadge': 'Installed on this machine', + 'easyMode.agent.notDetected': 'Not detected', + 'easyMode.agent.chooseModel': 'Select Model', + 'easyMode.agent.applyConfig': '⚡ Apply Configuration', + 'easyMode.agent.applySuccess': 'Configuration applied successfully!', + 'easyMode.agent.launchClient': '🚀 Launch Client / Open Terminal', + 'easyMode.agent.launching': 'Launching...', + 'easyMode.complete.title': 'Setup Complete!', + 'easyMode.complete.enterApp': 'Enter Dashboard', + 'easyMode.apiCopy.desc': 'You can also use the local proxy URL in any OpenAI / Claude / Gemini compatible tool.', + 'easyMode.apiCopy.copied': 'Copied to clipboard', 'error.render.title': 'Something went wrong while rendering this page', 'error.unknown': 'Unknown error', 'error.reload': 'Reload Page', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index cdc97b9..dcc9ae2 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -37,7 +37,7 @@ export const zhCN = { 'easyMode.overview.coreTitle': '使用指导', 'easyMode.overview.providerTitle': '接入模型', 'easyMode.overview.agentTitle': '接入智能体', - 'easyMode.setup.providerDescription': '可以通过 OAuth 登录自己的账号并使用账号额度,也可以通过 API 接入任意平台。', + 'easyMode.setup.providerDescription': '可选择两种方式接入\n1.OAuth登录自己的订阅账号,使用自己账号的额度\n2.API URL的方式接入', 'easyMode.oauth.title': 'OAuth 登录', 'easyMode.oauth.description': '通过浏览器授权登录自己账号,并使用账号的额度。', 'easyMode.oauth.nextHint': '账号登录完成后,点击页面上方的“下一步”继续。', @@ -52,13 +52,52 @@ export const zhCN = { 'easyMode.guide.title': '使用指导', 'easyMode.guide.description': '先确定连接方式,完成模型连接后,再从“可用模型”列表中选择智能体要使用的模型。', 'easyMode.guide.step1Title': '选择连接方式', - 'easyMode.guide.step1Description': '可以通过 OAuth 登录自己的账号并使用账号额度,也可以通过 API 接入任意平台。', + 'easyMode.guide.step1Description': '可选择两种方式接入\n1.OAuth登录自己的订阅账号,使用自己账号的额度\n2.API URL的方式接入', 'easyMode.guide.step2Title': '连接模型', 'easyMode.guide.step2Description': '这里可以通过 OAuth 完成账号授权,也可以填写第三方平台提供的 Base URL 和 API 密钥。保存后,模型会出现在“可用模型”列表中。', 'easyMode.guide.step3Title': '选择模型并接入智能体', 'easyMode.guide.step3Description': '在“可用模型”列表中选定模型,再选择已安装的智能体客户端并应用配置。', 'easyMode.navigation.back': '上一步', 'easyMode.navigation.next': '下一步', + 'easyMode.status.ready': '已就绪', + 'easyMode.status.connectedModels': '已检测到 {count} 个可用模型', + 'easyMode.status.noModelsYet': '尚未连接模型,请先登录账号或添加 API', + 'easyMode.oauth.loggedIn': '已登录', + 'easyMode.oauth.relogin': '重新登录', + 'easyMode.oauth.loggingIn': '等待浏览器授权...', + 'easyMode.api.quickAdd': '快速接入 API', + 'easyMode.api.testAndSave': '测试并保存', + 'easyMode.api.testing': '正在测试连接...', + 'easyMode.api.fetchModels': '获取模型列表', + 'easyMode.api.fetchingModels': '正在获取模型列表...', + 'easyMode.api.customModelId': '自定义 Model ID', + 'easyMode.api.customModelPlaceholder': '例如:gpt-5.6-sol 或 deepseek-v4-pro', + 'easyMode.api.addModelId': '添加 Model ID', + 'easyMode.api.credentialsRequired': '请先填写 API URL 和 API 密钥', + 'easyMode.api.modelRequired': '请先获取模型列表并选择模型,或手动添加 Model ID', + 'easyMode.api.testSuccess': '连接成功!发现 {count} 个模型', + 'easyMode.api.testFailed': '连接测试失败', + 'easyMode.api.platform': '平台类型', + 'easyMode.api.baseUrl': 'API Base URL', + 'easyMode.api.apiKey': 'API 密钥 (API Key)', + 'easyMode.api.name': '备注名称 (选填)', + 'easyMode.api.viewExisting': '查看全部已接入列表 ({count})', + 'easyMode.api.backToQuick': '返回快速接入', + 'easyMode.api.savedSuccess': 'API 接入已保存!', + 'easyMode.scenario.title': '选择你的使用方式', + 'easyMode.scenario.agentTab': '接入 AI 智能体客户端 (推荐)', + 'easyMode.scenario.apiTab': '复制本地 API 接口地址', + 'easyMode.agent.installedBadge': '本机已安装', + 'easyMode.agent.notDetected': '未检测到', + 'easyMode.agent.chooseModel': '选择使用的模型', + 'easyMode.agent.applyConfig': '⚡ 一键应用配置', + 'easyMode.agent.applySuccess': '配置已成功应用!', + 'easyMode.agent.launchClient': '🚀 启动客户端 / 打开终端', + 'easyMode.agent.launching': '正在启动...', + 'easyMode.complete.title': '配置就绪!', + 'easyMode.complete.enterApp': '进入完整控制台', + 'easyMode.apiCopy.desc': '你也可以直接将本地代理地址填入任何支持 OpenAI / Claude / Gemini 协议的工具中。', + 'easyMode.apiCopy.copied': '已复制到剪贴板', 'error.render.title': '页面渲染出现异常', 'error.unknown': '未知错误', 'error.reload': '重新加载页面', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index f169daf..ea90405 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -629,7 +629,11 @@ function AgentModelPicker({ ); } -export function AgentsPage() { +type AgentsPageProps = { + embedded?: boolean; +}; + +export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { const { t } = useI18n(); const [selected, setSelected] = useState(readSelectedAgentClient); const [activeSubpage, setActiveSubpage] = useState(DEFAULT_AGENT_SUBPAGE); @@ -1442,7 +1446,7 @@ export function AgentsPage() { ) : oauthLoginRequiredAction ? t(`agents.oauthLoginRequired.${oauthLoginRequiredAction}Description`) : ''; return ( -
+
Agent Clients diff --git a/src/pages/EasyModePage.tsx b/src/pages/EasyModePage.tsx index d5281d7..0c2cd4b 100644 --- a/src/pages/EasyModePage.tsx +++ b/src/pages/EasyModePage.tsx @@ -1,198 +1,722 @@ -import { useState } from 'react'; -import { useI18n } from '../i18n'; -import { ApiAccessPage } from './ApiAccessPage'; -import { AgentsPage } from './AgentsPage'; -import { OAuthLoginPage } from './ManagementPages'; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { + AlertCircle, + ArrowLeft, + ArrowRight, + Check, + CheckCircle2, + LoaderCircle, + Plus, + RefreshCw, +} from "lucide-react"; +import { useI18n } from "../i18n"; +import { + isRecord, + managementApi, + readBoolean, + readString, + responseList, +} from "../services/managementApi"; +import { + fetchModels, + normalizeBaseUrl, + type ModelOption, + type ModelProvider, +} from "../services/modelService"; +import { AgentsPage } from "./AgentsPage"; -type AuthMethod = 'oauth' | 'api'; +import codexIcon from "../assets/icons/codex.svg"; +import claudeIcon from "../assets/icons/claude.svg"; +import antigravityIcon from "../assets/icons/antigravity.svg"; +import kimiIcon from "../assets/icons/kimi-light.svg"; +import grokIcon from "../assets/icons/grok.svg"; +import openaiIcon from "../assets/icons/openai-light.svg"; +import deepseekIcon from "../assets/icons/deepseek.svg"; +import geminiIcon from "../assets/icons/gemini.svg"; + +type AuthMethod = "oauth" | "api"; type SetupStep = 1 | 2; -type SetupView = 1 | 'oauth' | 'api' | 'agents'; -const EASY_MODE_STATE_KEY = 'easy-cli-proxy-api.easy-mode.state'; +type OAuthProviderId = "codex" | "claude" | "antigravity" | "kimi" | "xai"; -type SavedEasyModeState = { - authMethod: AuthMethod; - providerReturnView: 1 | AuthMethod | null; +type OAuthProviderInfo = { + id: OAuthProviderId; + name: string; + icon: string; + description: string; }; -const defaultEasyModeState: SavedEasyModeState = { - authMethod: 'oauth', - providerReturnView: null, +const oauthProviders: OAuthProviderInfo[] = [ + { id: "codex", name: "Codex OAuth", icon: codexIcon, description: "OpenAI / ChatGPT 账号授权登录" }, + { id: "claude", name: "Claude OAuth", icon: claudeIcon, description: "Anthropic / Claude 账号授权登录" }, + { id: "antigravity", name: "Antigravity OAuth", icon: antigravityIcon, description: "Antigravity 账号授权登录" }, + { id: "kimi", name: "Kimi OAuth", icon: kimiIcon, description: "Moonshot / Kimi 账号授权登录" }, + { id: "xai", name: "xAI OAuth", icon: grokIcon, description: "xAI / Grok 账号授权登录" }, +]; + +type ApiSection = "openai-compatibility" | "deepseek" | "claude" | "gemini" | "codex"; +type ApiManagementSection = "openai-compatibility" | "claude-api-key" | "codex-api-key" | "gemini-api-key"; + +type ApiSectionOption = { + id: ApiSection; + managementSection: ApiManagementSection; + name: string; + provider: ModelProvider; + defaultBaseUrl: string; + icon: string; }; -function readSavedState(): SavedEasyModeState { - try { - const raw = window.localStorage.getItem(EASY_MODE_STATE_KEY); - if (!raw) return defaultEasyModeState; - const parsed = JSON.parse(raw) as { - authMethod?: unknown; - providerReturnView?: unknown; - }; - return { - authMethod: parsed.authMethod === 'api' ? 'api' : 'oauth', - providerReturnView: parsed.providerReturnView === 1 - || parsed.providerReturnView === 'oauth' - || parsed.providerReturnView === 'api' - ? parsed.providerReturnView - : null, - }; - } catch { - return defaultEasyModeState; - } -} +const apiSectionOptions: ApiSectionOption[] = [ + { id: "openai-compatibility", managementSection: "openai-compatibility", name: "OpenAI 格式", provider: "openai", defaultBaseUrl: "", icon: openaiIcon }, + { id: "claude", managementSection: "claude-api-key", name: "Anthropic 格式", provider: "claude", defaultBaseUrl: "", icon: claudeIcon }, + { id: "codex", managementSection: "codex-api-key", name: "Codex API", provider: "codex", defaultBaseUrl: "", icon: codexIcon }, + { id: "gemini", managementSection: "gemini-api-key", name: "Gemini 格式", provider: "gemini", defaultBaseUrl: "", icon: geminiIcon }, + { id: "deepseek", managementSection: "openai-compatibility", name: "DeepSeek", provider: "openai", defaultBaseUrl: "https://api.deepseek.com", icon: deepseekIcon }, +]; -function saveState(state: SavedEasyModeState) { - try { - window.localStorage.setItem(EASY_MODE_STATE_KEY, JSON.stringify(state)); - } catch { - // Keep the selection in memory when persistent storage is unavailable. - } -} +const isDeepSeekRecord = (record: Record) => { + const name = readString(record, "name").trim().toLowerCase(); + const baseUrl = readString(record, "base-url", "baseUrl").trim().toLowerCase(); + return name.includes("deepseek") || /^https?:\/\/api\.deepseek\.com(?:\/|$)/i.test(baseUrl); +}; -export function EasyModePage() { +export function EasyModePage({ + onExit, +}: { + onExit?: () => void; +}) { const { t } = useI18n(); - const initialState = readSavedState(); - const [authMethod, setAuthMethod] = useState(initialState.authMethod); - const [providerReturnView, setProviderReturnView] = useState( - initialState.providerReturnView, - ); - const [view, setView] = useState(1); - const chooseAuthMethod = (method: AuthMethod) => { - setAuthMethod(method); - saveState({ authMethod: method, providerReturnView }); + const [activeStep, setActiveStep] = useState(1); + const [authMethod, setAuthMethod] = useState("oauth"); + + const [loadingSources, setLoadingSources] = useState(true); + const [authFiles, setAuthFiles] = useState[]>([]); + const [apiCounts, setApiCounts] = useState>({ + "openai-compatibility": 0, + deepseek: 0, + claude: 0, + gemini: 0, + codex: 0, + }); + const [availableModels, setAvailableModels] = useState([]); + + const [oauthLoggingIn, setOauthLoggingIn] = useState(null); + const [oauthNotice, setOauthNotice] = useState<{ + tone: "success" | "error" | "info"; + message: string; + } | null>(null); + const oauthPollTimer = useRef(null); + + const [selectedApiSection, setSelectedApiSection] = useState("openai-compatibility"); + const [apiBaseUrl, setApiBaseUrl] = useState(""); + const [apiKey, setApiKey] = useState(""); + const [apiRemark, setApiRemark] = useState(""); + const [apiTesting, setApiTesting] = useState(false); + const [apiTestedModels, setApiTestedModels] = useState([]); + const [apiSelectedModels, setApiSelectedModels] = useState([]); + const [apiCustomModelId, setApiCustomModelId] = useState(""); + const [apiTestError, setApiTestError] = useState(""); + const [apiSaving, setApiSaving] = useState(false); + const [apiSaveNotice, setApiSaveNotice] = useState(null); + + const refreshSourceStatus = useCallback(async () => { + setLoadingSources(true); + try { + const authFilesPayload = await managementApi.get("/auth-files"); + const files = responseList(authFilesPayload, "files"); + setAuthFiles(files); + + const counts: Record = { + "openai-compatibility": 0, + deepseek: 0, + claude: 0, + gemini: 0, + codex: 0, + }; + const allFetchedModels: ModelOption[] = []; + + const configPayload = await managementApi.get("/config"); + const recordsBySection: Record[]> = { + "openai-compatibility": responseList(configPayload, "openai-compatibility"), + "claude-api-key": responseList(configPayload, "claude-api-key"), + "codex-api-key": responseList(configPayload, "codex-api-key"), + "gemini-api-key": responseList(configPayload, "gemini-api-key"), + }; + + for (const section of apiSectionOptions) { + const sourceList = recordsBySection[section.managementSection]; + const list = section.id === "deepseek" + ? sourceList.filter(isDeepSeekRecord) + : section.id === "openai-compatibility" + ? sourceList.filter((record) => !isDeepSeekRecord(record)) + : sourceList; + counts[section.id] = list.length; + list.forEach((item) => { + if (Array.isArray(item.models)) { + item.models.forEach((m) => { + const name = typeof m === "string" ? m : readString(m, "name", "id"); + if (name && !allFetchedModels.some((x) => x.name === name)) { + allFetchedModels.push({ name }); + } + }); + } + }); + } + setApiCounts(counts); + + setAvailableModels(allFetchedModels); + } catch (e) { + console.warn("Failed to refresh source status", e); + } finally { + setLoadingSources(false); + } + }, []); + + useEffect(() => { + void refreshSourceStatus(); + return () => { + if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current); + }; + }, []); + + useEffect(() => { + if (!oauthNotice) return undefined; + const timer = window.setTimeout(() => { + setOauthNotice(null); + }, 4500); + return () => window.clearTimeout(timer); + }, [oauthNotice]); + + const handleStartOAuth = async (provider: OAuthProviderId) => { + if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current); + setOauthLoggingIn(provider); + setOauthNotice(null); + + try { + const result = await invoke<{ + url?: string; + state?: string; + opened?: boolean; + openError?: string; + }>("start_oauth_login", { + provider, + browser: "default", + }); + + if (!result.state) { + setOauthNotice({ + tone: "error", + message: "获取授权状态失败,请重试", + }); + setOauthLoggingIn(null); + return; + } + + const stateKey = result.state; + oauthPollTimer.current = window.setInterval(async () => { + try { + const pollRes = await invoke<{ status: string; error?: string }>( + "get_oauth_status", + { state: stateKey }, + ); + const status = (pollRes.status || "").toLowerCase(); + if (status === "ok") { + if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current); + setOauthLoggingIn(null); + setOauthNotice({ + tone: "success", + message: "登录成功!已成功授权并获取账号额度。", + }); + void refreshSourceStatus(); + } else if (status === "error") { + if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current); + setOauthLoggingIn(null); + setOauthNotice({ + tone: "error", + message: pollRes.error ? `授权失败: ${pollRes.error}` : "授权失败,请重试", + }); + } + } catch {} + }, 1500); + } catch (err) { + setOauthLoggingIn(null); + setOauthNotice({ + tone: "error", + message: String(err), + }); + } + }; + const handleApiSectionChange = (sec: ApiSection) => { + setSelectedApiSection(sec); + const opt = apiSectionOptions.find((o) => o.id === sec); + if (opt) setApiBaseUrl(opt.defaultBaseUrl); + setApiTestedModels([]); + setApiSelectedModels([]); + setApiCustomModelId(""); + setApiTestError(""); + setApiSaveNotice(null); }; - const openSelectedProvider = () => { - setView(authMethod); - }; + const handleTestApi = async () => { + if (!apiBaseUrl.trim()) { + setApiTestError("请先填写 API URL"); + return; + } + setApiTesting(true); + setApiTestError(""); + setApiTestedModels([]); - const leaveProviderForAgents = () => { - const currentProviderView: 1 | AuthMethod = view === 1 || view === 'oauth' || view === 'api' - ? view - : providerReturnView ?? 1; - setProviderReturnView(currentProviderView); - saveState({ authMethod, providerReturnView: currentProviderView }); - setView('agents'); - }; + const opt = apiSectionOptions.find((o) => o.id === selectedApiSection); + const providerType = opt ? opt.provider : "openai"; - const previousView: SetupView | null = view === 'oauth' || view === 'api' - ? 1 - : view === 'agents' - ? providerReturnView ?? 1 - : null; - const continueSetup = () => { - if (view === 1) setView(authMethod); - else if (view === 'oauth' || view === 'api') leaveProviderForAgents(); - }; - - const openProgressStep = (step: SetupStep) => { - if (step === 1) { - setView(view === 'agents' ? providerReturnView ?? 1 : view === 'oauth' || view === 'api' ? view : 1); - } else { - leaveProviderForAgents(); + try { + const models = await fetchModels( + providerType, + apiBaseUrl.trim(), + apiKey.trim(), + undefined, + {}, + 10000, + ); + if (models.length > 0) { + setApiTestedModels(models); + setApiSelectedModels((current) => { + const fetchedKeys = new Set(models.map((model) => model.name.trim().toLowerCase())); + const customModels = current.filter((model) => !fetchedKeys.has(model.name.trim().toLowerCase())); + return [...models, ...customModels]; + }); + } else { + setApiTestError("未获取到任何模型,请检查 API URL 和 API 密钥"); + } + } catch (err) { + setApiTestError(String(err)); + } finally { + setApiTesting(false); } }; - const stepTitles = [ - t('easyMode.overview.providerTitle'), - t('easyMode.overview.agentTitle'), - ]; + const handleToggleApiModel = (model: ModelOption) => { + const key = model.name.trim().toLowerCase(); + if (!key) return; + setApiSelectedModels((current) => { + const selected = current.some((item) => item.name.trim().toLowerCase() === key); + return selected + ? current.filter((item) => item.name.trim().toLowerCase() !== key) + : [...current, model]; + }); + }; + + const handleAddCustomModel = () => { + const name = apiCustomModelId.trim(); + if (!name) return; + const key = name.toLowerCase(); + setApiSelectedModels((current) => { + if (current.some((model) => model.name.trim().toLowerCase() === key)) return current; + const fetchedModel = apiTestedModels.find((model) => model.name.trim().toLowerCase() === key); + return [...current, fetchedModel ?? { name }]; + }); + setApiCustomModelId(""); + setApiTestError(""); + }; + + const handleSaveApi = async () => { + if (!apiBaseUrl.trim()) { + setApiTestError("请先填写 API URL"); + return; + } + if (apiSelectedModels.length === 0) { + setApiTestError(t("easyMode.api.modelRequired")); + return; + } + setApiSaving(true); + setApiSaveNotice(null); + setApiTestError(""); + + try { + const selectedOption = apiSectionOptions.find((option) => option.id === selectedApiSection); + const managementSection = selectedOption?.managementSection ?? "openai-compatibility"; + const configPayload = await managementApi.get("/config"); + const list = responseList(configPayload, managementSection); + const models = apiSelectedModels.map((model) => ({ name: model.name.trim() })); + const newEntry = managementSection === "openai-compatibility" + ? { + name: apiRemark.trim() || `${selectedApiSection} (${list.length + 1})`, + "base-url": normalizeBaseUrl(apiBaseUrl.trim()), + "api-key-entries": [ + { "api-key": apiKey.trim() }, + ], + models, + } + : { + "api-key": apiKey.trim(), + "base-url": normalizeBaseUrl(apiBaseUrl.trim()), + models, + }; + + await managementApi.put(`/${managementSection}`, [...list, newEntry]); + setApiSaveNotice("API 接入已保存成功!"); + void refreshSourceStatus(); + } catch (err) { + setApiTestError(String(err)); + } finally { + setApiSaving(false); + } + }; + + const isOAuthLoggedIn = (providerId: OAuthProviderId) => { + const norm = providerId === "claude" ? "claude" : providerId === "codex" ? "codex" : providerId; + return authFiles.some((f) => { + const p = readString(f, "provider", "type").toLowerCase(); + return p.includes(norm) || (norm === "codex" && p.includes("openai")) || (norm === "claude" && p.includes("anthropic")); + }); + }; + + const totalLoggedInOAuth = oauthProviders.filter((p) => isOAuthLoggedIn(p.id)).length; + const totalApiProviders = Object.values(apiCounts).reduce((a, b) => a + b, 0); + const hasConnectedSource = totalLoggedInOAuth > 0 || totalApiProviders > 0; + const displayedApiModels = useMemo(() => { + const fetchedKeys = new Set(apiTestedModels.map((model) => model.name.trim().toLowerCase())); + return [ + ...apiTestedModels, + ...apiSelectedModels.filter((model) => !fetchedKeys.has(model.name.trim().toLowerCase())), + ]; + }, [apiSelectedModels, apiTestedModels]); return (
-
- {stepTitles.map((title, index) => { - const step = (index + 1) as SetupStep; - const active = step === 1 - ? view === 1 || view === 'oauth' || view === 'api' - : view === 'agents'; - return ( - - ); - })} -
- -
- {previousView !== null ? ( - - ) : null} - {view !== 'agents' ? ( - - ) : null} -
- - {view === 'oauth' || view === 'api' ? ( -
- {t(view === 'oauth' ? 'easyMode.oauth.nextHint' : 'easyMode.api.nextHint')} -
- ) : null} - - {view === 1 ? ( + {/* 第一步:接入模型 */} + {activeStep === 1 ? (
-

{t('easyMode.overview.providerTitle')}

-

{t('easyMode.setup.providerDescription')}

+

{t("easyMode.overview.providerTitle")}

+

{t("easyMode.setup.providerDescription")}

+
+ +
+ + {authMethod === "oauth" ? ( +
+ {oauthNotice ? ( +
+ {oauthNotice.tone === "success" ? : } + {oauthNotice.message} +
+ ) : null} + +
+ {oauthProviders.map((provider) => { + const loggedIn = isOAuthLoggedIn(provider.id); + const isLogging = oauthLoggingIn === provider.id; + + return ( +
+
+ + + +
+ {provider.name} + + {provider.description} + +
+
+ +
+ {loggedIn ? ( + + + {t("easyMode.oauth.loggedIn")} + + ) : ( + 未授权 + )} + + +
+
+ ); + })} +
+
+ ) : null} + + {authMethod === "api" ? ( +
+ {apiSaveNotice ? ( +
+ + {apiSaveNotice} +
+ ) : null} + + {apiTestError ? ( +
+ + {apiTestError} +
+ ) : null} + +
+
+ {apiSectionOptions.map((opt) => ( + + ))} +
+ +
+ + setApiRemark(e.target.value)} + placeholder="例如:主力 DeepSeek V3、中转 API" + /> +
+ +
+
+ + setApiBaseUrl(e.target.value)} + placeholder="https://..." + /> +
+
+ + setApiKey(e.target.value)} + placeholder="sk-..." + /> +
+
+ +
+
+ setApiCustomModelId(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddCustomModel(); + } + }} + placeholder={t("easyMode.api.customModelPlaceholder")} + aria-label={t("easyMode.api.customModelId")} + /> + + +
+ + {displayedApiModels.length > 0 ? ( +
+ {displayedApiModels.map((model) => { + const key = model.name.trim().toLowerCase(); + const selected = apiSelectedModels.some( + (item) => item.name.trim().toLowerCase() === key, + ); + return ( + + ); + })} +
+ ) : null} + +
+ +
+ +
+
+
+ ) : null} + +
+
+ {hasConnectedSource ? ( + + {t("easyMode.status.connectedModels", { count: Math.max(availableModels.length, 1) })} + + ) : ( + {t("easyMode.status.noModelsYet")} + )} +
+ +
) : null} - {view === 'oauth' || view === 'api' ? ( -
-
- {view === 'oauth' ? : } -
-
- ) : null} + {/* 第二步:接入智能体 */} + {activeStep === 2 ? ( +
+ - {view === 'agents' ? ( -
-
- +
+ + +
) : null} diff --git a/src/styles.css b/src/styles.css index f092d75..e92a730 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5019,6 +5019,10 @@ textarea:focus-visible { gap: 14px; } +.agents-page-embedded .agent-client-list { + min-height: 0; +} + .agent-header-actions { display: flex; min-width: 0; @@ -10143,6 +10147,7 @@ body.table-col-resizing * { margin: 4px 0 0; color: var(--theme-5f5a53); line-height: var(--line-height-body); + white-space: pre-line; } .simple-mode-instruction { @@ -10457,7 +10462,772 @@ body.table-col-resizing * { margin-left: auto; } +.simple-mode-progress { + position: relative; + gap: 12px; +} + +.simple-mode-progress-item { + position: relative; + min-height: 76px; + padding: 13px 16px; + border-radius: 13px; + background: color-mix(in srgb, var(--theme-fffdf8) 90%, var(--theme-faf9f5)); + transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease, transform 160ms ease; +} + +.simple-mode-progress-item:hover:not(:disabled) { + transform: translateY(-1px); +} + +.simple-mode-progress-item-head { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + min-width: 0; +} + +.simple-mode-step-badge { + display: grid; + width: 29px; + height: 29px; + place-items: center; + border: 1px solid var(--theme-d5d2cb); + border-radius: 999px; + background: var(--theme-faf9f5); + color: var(--theme-7a746c); + font-size: var(--font-size-label); + font-weight: 800; + transition: border-color 160ms ease, background 160ms ease, color 160ms ease; +} + +.simple-mode-progress-item.active .simple-mode-step-badge { + border-color: var(--ui-accent); + background: var(--ui-accent); + color: #ffffff; +} + +.simple-mode-progress-copy { + display: grid; + min-width: 0; + gap: 3px; +} + +.simple-mode-progress-copy strong { + min-width: 0; + overflow: hidden; + color: var(--theme-2d2a26); + font-size: var(--font-size-body); + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-progress-copy small { + overflow: hidden; + color: var(--theme-7a746c); + font-size: var(--font-size-caption); + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-task { + gap: 22px; + padding: clamp(22px, 3vw, 32px); + border-radius: 16px; + box-shadow: 0 14px 34px color-mix(in srgb, var(--ui-accent-shadow) 58%, transparent); +} + +.simple-mode-task-heading { + align-items: center; + gap: 12px; +} + +.simple-mode-heading-icon { + display: grid; + flex: none; + width: 38px; + height: 38px; + place-items: center; + border: 1px solid color-mix(in srgb, var(--ui-accent) 27%, var(--theme-e3e1db)); + border-radius: 11px; + background: var(--ui-accent-soft); + color: var(--ui-accent-strong); +} + +.simple-mode-task-heading > div { + min-width: 0; +} + +.simple-mode-task-heading h2 { + font-size: clamp(20px, 2.2vw, 24px); + line-height: 1.2; +} + +.simple-mode-task-heading p { + max-width: 700px; + margin-top: 6px; + font-size: var(--font-size-body); +} + +.simple-mode-choice-grid { + gap: 14px; +} + +.simple-mode-choice { + gap: 10px; + min-height: 132px; + padding: 18px; + border-radius: 13px; + background: var(--theme-fffdf8); + transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease, transform 160ms ease; +} + +.simple-mode-choice:hover { + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(45, 42, 38, 0.08); +} + +.simple-mode-choice-heading { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.simple-mode-choice-icon { + display: grid; + flex: none; + width: 32px; + height: 32px; + place-items: center; + border: 1px solid var(--theme-e3e1db); + border-radius: 9px; + background: var(--theme-faf9f5); + color: var(--theme-7a746c); +} + +.simple-mode-choice.selected .simple-mode-choice-icon { + border-color: color-mix(in srgb, var(--ui-accent) 30%, var(--theme-e3e1db)); + background: var(--theme-fffdf8); + color: var(--ui-accent-strong); +} + +.simple-mode-choice-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; + flex: 1; +} + +.simple-mode-choice-title strong { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-choice-title .state-pill { + flex: none; +} + +.simple-mode-choice > span:not(.simple-mode-choice-icon) { + padding-left: 42px; +} + +.simple-mode-choice > small { + padding-left: 42px; +} + +.simple-mode-embedded-box { + display: grid; + gap: 14px; + padding: 14px; + border: 1px solid var(--theme-e3e1db); + border-radius: 13px; + background: color-mix(in srgb, var(--theme-faf9f5) 82%, var(--theme-fffdf8)); +} + +.simple-mode-provider-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); + gap: 12px; +} + +.simple-mode-provider-card { + grid-template-rows: minmax(0, 1fr) auto; + gap: 16px; + min-height: 158px; + height: auto; + padding: 16px; + border-radius: 12px; + background: var(--theme-fffdf8); + transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease, transform 160ms ease; +} + +.simple-mode-provider-card:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--ui-accent) 30%, var(--theme-e3e1db)); + box-shadow: 0 8px 20px rgba(45, 42, 38, 0.07); +} + +.simple-mode-provider-card.connected { + border-color: var(--theme-b6d3bd); + background: var(--ui-accent-soft); +} + +.simple-mode-provider-card-head { + display: flex; + align-items: flex-start; + gap: 11px; + min-width: 0; +} + +.simple-mode-provider-logo { + width: 36px; + height: 36px; + flex: none; + border-radius: 10px; +} + +.simple-mode-provider-logo img { + width: 22px; + height: 22px; +} + +.simple-mode-provider-copy { + display: grid; + min-width: 0; + gap: 4px; +} + +.simple-mode-provider-copy strong { + overflow: hidden; + color: var(--theme-2d2a26); + font-size: 15px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-provider-copy span { + color: var(--theme-5f5a53); + font-size: var(--font-size-meta); + line-height: var(--line-height-body); +} + +.simple-mode-provider-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; +} + +.simple-mode-provider-card-foot .primary-button, +.simple-mode-provider-card-foot .secondary-button { + min-height: 34px; + padding-inline: 13px; + font-size: var(--font-size-label); +} + +.simple-mode-api-form { + display: grid; + gap: 18px; + padding: 20px; + border: 1px solid var(--theme-d8d4cc); + border-radius: 12px; + background: var(--theme-fffdf8); +} + +.simple-mode-api-model-card { + display: grid; + gap: 11px; + padding-top: 2px; +} + +.simple-mode-api-model-toolbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 7px; +} + +.simple-mode-api-model-toolbar .text-input { + box-sizing: border-box; + width: 100%; + min-height: 40px; + padding: 0 12px; + border: 1px solid var(--theme-d5d2cb); + border-radius: 8px; + background: var(--theme-faf9f5); + color: var(--theme-2d2a26); + font-size: var(--font-size-body); +} + +.simple-mode-api-model-toolbar .text-input:focus { + outline: none; + border-color: var(--ui-accent); + background: var(--theme-fffdf8); + box-shadow: 0 0 0 3px var(--ui-accent-shadow); +} + +.simple-mode-api-model-toolbar .secondary-button { + min-height: 40px; + gap: 7px; + padding-inline: 12px; + white-space: nowrap; +} + +.simple-mode-api-model-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 2px 18px; + max-height: 220px; + overflow-y: auto; + padding: 6px 0; + border-top: 1px solid var(--theme-e3e1db); + border-bottom: 1px solid var(--theme-e3e1db); +} + +.simple-mode-api-model-option { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + min-height: 33px; + padding: 5px 7px; + border-radius: 6px; + background: transparent; + color: var(--theme-2d2a26); + font-size: var(--font-size-label); + cursor: pointer; +} + +.simple-mode-api-model-option:hover, +.simple-mode-api-model-option.selected { + background: var(--theme-faf9f5); +} + +.simple-mode-api-model-option input { + flex: none; + width: 15px; + height: 15px; + margin: 0; + accent-color: var(--ui-accent); +} + +.simple-mode-api-model-option > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-api-platforms { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 4px; + border: 1px solid var(--theme-e3e1db); + border-radius: 10px; + background: var(--theme-faf9f5); +} + +.simple-mode-api-platform { + flex: 1 1 132px; + min-width: 0; + min-height: 38px; + gap: 7px; + padding-inline: 12px; + border-color: transparent; + background: transparent; + color: var(--theme-5f5a53); + font-size: var(--font-size-label); + font-weight: 700; +} + +.simple-mode-api-platform:hover:not(:disabled) { + border-color: transparent; + background: var(--theme-e8e5dc); +} + +.simple-mode-api-platform.active { + border-color: var(--ui-accent); + background: var(--theme-fffdf8); + color: var(--theme-2d2a26); + box-shadow: 0 2px 6px rgba(45, 42, 38, 0.08); +} + +.simple-mode-api-platform-icon { + width: 16px; + height: 16px; + object-fit: contain; +} + +.simple-mode-api-fields { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.9fr); + gap: 14px; +} + +.simple-mode-field { + display: grid; + gap: 8px; + min-width: 0; +} + +.simple-mode-field label { + color: var(--theme-2d2a26); + font-size: var(--font-size-label); + font-weight: 700; + letter-spacing: 0.01em; +} + +.simple-mode-field .text-input, +.simple-mode-field .select-input { + box-sizing: border-box; + width: 100%; + min-height: 44px; + padding: 0 13px; + border: 1px solid var(--theme-d5d2cb); + border-radius: 9px; + background: var(--theme-faf9f5); + color: var(--theme-2d2a26); + font-size: var(--font-size-body); + box-shadow: inset 0 1px 2px rgba(45, 42, 38, 0.04); + transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease; +} + +.simple-mode-field .text-input:hover:not(:disabled), +.simple-mode-field .select-input:hover:not(:disabled) { + border-color: var(--theme-9b958c); + background: var(--theme-fffdf8); +} + +.simple-mode-field .text-input:focus, +.simple-mode-field .select-input:focus { + outline: none; + border-color: var(--ui-accent); + background: var(--theme-fffdf8); + box-shadow: 0 0 0 3px var(--ui-accent-shadow); +} + +.simple-mode-field .text-input::placeholder { + color: var(--theme-9b958c); +} + +.simple-mode-api-actions, +.simple-mode-agent-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; +} + +.simple-mode-api-actions .primary-button, +.simple-mode-api-actions .secondary-button, +.simple-mode-agent-actions .primary-button, +.simple-mode-agent-actions .secondary-button { + gap: 7px; +} + +.simple-mode-api-result { + display: grid; + gap: 9px; + margin-top: 1px; + padding: 12px; + border: 1px solid var(--theme-b6d3bd); + border-radius: 9px; + background: var(--ui-accent-soft); +} + +.simple-mode-api-result-heading { + display: flex; + align-items: center; + gap: 7px; + color: var(--theme-2d2a26); + font-size: var(--font-size-label); + font-weight: 700; +} + +.simple-mode-model-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.simple-mode-model-tag { + padding: 3px 8px; + border: 1px solid var(--theme-d5d2cb); + border-radius: 5px; + background: var(--theme-fffdf8); + color: var(--theme-4f4a43); + font-size: var(--font-size-meta); +} + +.simple-mode-more-models { + align-self: center; + color: var(--theme-7a746c); + font-size: var(--font-size-meta); +} + +.simple-mode-agent-layout { + display: grid; + grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); + gap: 14px; + margin-top: 2px; +} + +.simple-mode-agent-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 460px; + padding: 8px; + overflow-y: auto; + border: 1px solid var(--theme-e3e1db); + border-radius: 12px; + background: var(--theme-faf9f5); +} + +.simple-mode-agent-option { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + padding: 10px 11px; + border: 1px solid transparent; + border-radius: 9px; + background: transparent; + color: var(--theme-2d2a26); + text-align: left; + transition: border-color 140ms ease, background 140ms ease; +} + +.simple-mode-agent-option:hover { + border-color: var(--theme-d5d2cb); + background: var(--theme-fffdf8); +} + +.simple-mode-agent-option.selected { + border-color: var(--ui-accent); + background: var(--ui-accent-soft); + box-shadow: 0 3px 10px var(--ui-accent-shadow); +} + +.simple-mode-agent-option-icon { + width: 23px; + height: 23px; + flex: none; + object-fit: contain; +} + +.simple-mode-agent-option-copy { + display: grid; + min-width: 0; + flex: 1; + gap: 3px; +} + +.simple-mode-agent-option-copy strong { + overflow: hidden; + color: var(--theme-2d2a26); + font-size: 14px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-agent-option-copy span { + overflow: hidden; + color: var(--theme-7a746c); + font-size: var(--font-size-caption); + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-agent-option-copy span.installed { + color: var(--ui-accent-strong); +} + +.simple-mode-agent-config { + display: grid; + align-content: start; + gap: 16px; + min-width: 0; + padding: 20px; + border: 1px solid var(--theme-e3e1db); + border-radius: 12px; + background: var(--theme-fffdf8); +} + +.simple-mode-agent-identity { + display: flex; + align-items: center; + gap: 13px; + min-width: 0; +} + +.simple-mode-agent-logo { + width: 44px; + height: 44px; + flex: none; + border-radius: 11px; +} + +.simple-mode-agent-logo img { + width: 28px; + height: 28px; +} + +.simple-mode-agent-identity-copy { + display: grid; + min-width: 0; + gap: 4px; +} + +.simple-mode-agent-identity-copy h3 { + margin: 0; + overflow: hidden; + color: var(--theme-2d2a26); + font-size: 18px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-agent-identity-copy span { + overflow: hidden; + color: var(--theme-5f5a53); + font-size: var(--font-size-label); + line-height: var(--line-height-body); + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mode-agent-detection { + padding: 10px 13px; + border: 1px solid var(--theme-e3e1db); + border-radius: 8px; + background: var(--theme-faf9f5); +} + +.simple-mode-agent-detection span { + display: block; + color: var(--theme-7a746c); + font-size: var(--font-size-meta); + line-height: var(--line-height-body); +} + +.simple-mode-agent-actions { + margin-top: 1px; +} + +.simple-mode-agent-actions .primary-button { + flex: 1; +} + +.simple-mode-agent-actions .primary-button, +.simple-mode-agent-actions .secondary-button { + min-height: 42px; + font-size: var(--font-size-label); +} + +.simple-mode-task-footer { + gap: 16px; + padding-top: 16px; +} + +.simple-mode-selection { + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 760px) { + .simple-mode-agent-layout { + grid-template-columns: 1fr; + } + + .simple-mode-agent-list { + max-height: 260px; + } +} + @media (max-width: 680px) { + .simple-mode-progress-item { + min-height: 62px; + } + + .simple-mode-progress-item-head { + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + } + + .simple-mode-progress-item-head > .state-pill { + grid-column: 2; + justify-self: start; + } + + .simple-mode-choice-title { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .simple-mode-provider-card-foot { + align-items: flex-start; + flex-direction: column; + } + + .simple-mode-provider-card-foot .primary-button, + .simple-mode-provider-card-foot .secondary-button { + width: 100%; + } + + .simple-mode-task { + gap: 18px; + padding: 18px; + border-radius: 13px; + } + + .simple-mode-task-heading { + align-items: flex-start; + } + + .simple-mode-heading-icon { + width: 34px; + height: 34px; + } + + .simple-mode-choice > span:not(.simple-mode-choice-icon), + .simple-mode-choice > small { + padding-left: 0; + } + + .simple-mode-api-form { + padding: 14px; + } + + .simple-mode-api-fields { + grid-template-columns: 1fr; + } + + .simple-mode-api-actions, + .simple-mode-agent-actions { + display: grid; + grid-template-columns: 1fr; + align-items: stretch; + } + + .simple-mode-api-actions button, + .simple-mode-agent-actions button { + width: 100%; + } + + .simple-mode-agent-config { + padding: 16px; + } + .simple-mode-core-alert, .simple-mode-action-card, .simple-mode-task-footer { @@ -10536,3 +11306,245 @@ body.table-col-resizing * { } } + +.simple-mode-page { + max-width: 900px; + gap: 14px; +} + +.simple-mode-progress { + gap: 8px; +} + +.simple-mode-progress-item, +.simple-mode-progress-item:hover:not(:disabled) { + min-height: 58px; + padding: 10px 12px; + border-radius: 9px; + background: var(--theme-fffdf8); + box-shadow: none; + transform: none; +} + +.simple-mode-progress-item.active { + border-color: color-mix(in srgb, var(--ui-accent) 32%, var(--theme-e3e1db)); + background: var(--ui-accent-soft); + box-shadow: none; +} + +.simple-mode-progress-item-head { + gap: 8px; +} + +.simple-mode-step-badge { + width: 26px; + height: 26px; +} + +.simple-mode-task { + gap: 16px; + padding: 20px; + border-radius: 10px; + box-shadow: none; +} + +.simple-mode-task-heading { + align-items: flex-start; + gap: 0; +} + +.simple-mode-task-heading h2 { + font-size: 21px; +} + +.simple-mode-task-heading p { + margin-top: 4px; + font-size: var(--font-size-label); +} + +.simple-mode-choice-grid { + gap: 8px; +} + +.simple-mode-choice, +.simple-mode-choice:hover { + min-height: 88px; + gap: 5px; + padding: 13px 14px; + border-radius: 8px; + box-shadow: none; + transform: none; +} + +.simple-mode-choice.selected { + box-shadow: none; +} + +.simple-mode-choice-heading { + gap: 0; +} + +.simple-mode-choice-title { + justify-content: flex-start; + gap: 8px; +} + +.simple-mode-choice > span:not(.simple-mode-choice-icon), +.simple-mode-choice > small { + padding-left: 0; +} + +.simple-mode-embedded-box { + gap: 10px; + padding: 0; + border: 0; + background: transparent; +} + +.simple-mode-provider-grid { + gap: 8px; +} + +.simple-mode-provider-card, +.simple-mode-provider-card:hover { + min-height: 0; + gap: 12px; + padding: 12px; + border-radius: 8px; + box-shadow: none; + transform: none; +} + +.simple-mode-provider-card-foot .primary-button, +.simple-mode-provider-card-foot .secondary-button { + min-height: 32px; +} + +.simple-mode-api-form { + gap: 12px; + padding: 14px; + border-radius: 8px; + box-shadow: none; +} + +.simple-mode-api-platforms { + gap: 6px; +} + +.simple-mode-api-platform { + min-height: 32px; + padding-inline: 10px; + font-weight: 600; +} + +.simple-mode-api-result { + border-radius: 8px; +} + +.simple-mode-agent-layout { + gap: 10px; +} + +.simple-mode-agent-list, +.simple-mode-agent-config { + border-radius: 8px; + box-shadow: none; +} + +.simple-mode-agent-list { + padding: 6px; +} + +.simple-mode-agent-config { + padding: 16px; +} + +.simple-mode-task-footer { + gap: 12px; + padding-top: 12px; +} + +@media (max-width: 760px) { + .simple-mode-agent-layout { + grid-template-columns: 1fr; + } + + .simple-mode-agent-list { + max-height: 260px; + } +} + +@media (max-width: 680px) { + .simple-mode-progress-item { + min-height: 62px; + } + + .simple-mode-progress-item-head { + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + } + + .simple-mode-progress-item-head > .state-pill { + grid-column: 2; + justify-self: start; + } + + .simple-mode-choice-title { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .simple-mode-provider-card-foot { + align-items: flex-start; + flex-direction: column; + } + + .simple-mode-provider-card-foot .primary-button, + .simple-mode-provider-card-foot .secondary-button { + width: 100%; + } + + .simple-mode-task { + gap: 18px; + padding: 18px; + } + + .simple-mode-api-form { + padding: 14px; + } + + .simple-mode-api-fields { + grid-template-columns: 1fr; + } + + .simple-mode-api-actions, + .simple-mode-agent-actions { + display: grid; + grid-template-columns: 1fr; + align-items: stretch; + } + + .simple-mode-api-actions button, + .simple-mode-agent-actions button { + width: 100%; + } + + .simple-mode-agent-config { + padding: 16px; + } +} + +@media (max-width: 680px) { + .simple-mode-api-model-toolbar { + grid-template-columns: 1fr; + } + + .simple-mode-api-model-toolbar .secondary-button { + width: 100%; + } + + .simple-mode-api-model-options { + grid-template-columns: 1fr; + } +}