- Reveal model selection and save actions only after fetching models - Move the API save success toast to the bottom-right corner - Auto-dismiss the success notification after a short delay - Remove icons from the model fetch and save buttons
723 lines
27 KiB
TypeScript
723 lines
27 KiB
TypeScript
import {
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import {
|
|
AlertCircle,
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
Check,
|
|
CheckCircle2,
|
|
LoaderCircle,
|
|
} 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";
|
|
|
|
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 OAuthProviderId = "codex" | "claude" | "antigravity" | "kimi" | "xai";
|
|
|
|
type OAuthProviderInfo = {
|
|
id: OAuthProviderId;
|
|
name: string;
|
|
icon: string;
|
|
description: string;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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 },
|
|
];
|
|
|
|
const isDeepSeekRecord = (record: Record<string, unknown>) => {
|
|
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({
|
|
onExit,
|
|
}: {
|
|
onExit?: () => void;
|
|
}) {
|
|
const { t } = useI18n();
|
|
|
|
const [activeStep, setActiveStep] = useState<SetupStep>(1);
|
|
const [authMethod, setAuthMethod] = useState<AuthMethod>("oauth");
|
|
|
|
const [loadingSources, setLoadingSources] = useState(true);
|
|
const [authFiles, setAuthFiles] = useState<Record<string, unknown>[]>([]);
|
|
const [apiCounts, setApiCounts] = useState<Record<ApiSection, number>>({
|
|
"openai-compatibility": 0,
|
|
deepseek: 0,
|
|
claude: 0,
|
|
gemini: 0,
|
|
codex: 0,
|
|
});
|
|
const [availableModels, setAvailableModels] = useState<ModelOption[]>([]);
|
|
|
|
const [oauthLoggingIn, setOauthLoggingIn] = useState<OAuthProviderId | null>(null);
|
|
const [oauthNotice, setOauthNotice] = useState<{
|
|
tone: "success" | "error" | "info";
|
|
message: string;
|
|
} | null>(null);
|
|
const oauthPollTimer = useRef<number | null>(null);
|
|
|
|
const [selectedApiSection, setSelectedApiSection] = useState<ApiSection>("openai-compatibility");
|
|
const [apiBaseUrl, setApiBaseUrl] = useState("");
|
|
const [apiKey, setApiKey] = useState("");
|
|
const [apiRemark, setApiRemark] = useState("");
|
|
const [apiTesting, setApiTesting] = useState(false);
|
|
const [apiTestedModels, setApiTestedModels] = useState<ModelOption[]>([]);
|
|
const [apiSelectedModels, setApiSelectedModels] = useState<ModelOption[]>([]);
|
|
const [apiTestError, setApiTestError] = useState("");
|
|
const [apiSaving, setApiSaving] = useState(false);
|
|
const [apiSaveNotice, setApiSaveNotice] = useState<string | null>(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<ApiSection, number> = {
|
|
"openai-compatibility": 0,
|
|
deepseek: 0,
|
|
claude: 0,
|
|
gemini: 0,
|
|
codex: 0,
|
|
};
|
|
const allFetchedModels: ModelOption[] = [];
|
|
|
|
const configPayload = await managementApi.get("/config");
|
|
const recordsBySection: Record<ApiManagementSection, Record<string, unknown>[]> = {
|
|
"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]);
|
|
|
|
useEffect(() => {
|
|
if (!apiSaveNotice) return undefined;
|
|
const timer = window.setTimeout(() => {
|
|
setApiSaveNotice(null);
|
|
}, 4500);
|
|
return () => window.clearTimeout(timer);
|
|
}, [apiSaveNotice]);
|
|
|
|
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([]);
|
|
setApiTestError("");
|
|
setApiSaveNotice(null);
|
|
};
|
|
|
|
const handleTestApi = async () => {
|
|
if (!apiBaseUrl.trim()) {
|
|
setApiTestError("请先填写 API URL");
|
|
return;
|
|
}
|
|
setApiTesting(true);
|
|
setApiTestError("");
|
|
setApiTestedModels([]);
|
|
|
|
const opt = apiSectionOptions.find((o) => o.id === selectedApiSection);
|
|
const providerType = opt ? opt.provider : "openai";
|
|
|
|
try {
|
|
const models = await fetchModels(
|
|
providerType,
|
|
apiBaseUrl.trim(),
|
|
apiKey.trim(),
|
|
undefined,
|
|
{},
|
|
10000,
|
|
);
|
|
if (models.length > 0) {
|
|
setApiTestedModels(models);
|
|
setApiSelectedModels(models);
|
|
} else {
|
|
setApiTestError("未获取到任何模型,请检查 API URL 和 API 密钥");
|
|
}
|
|
} catch (err) {
|
|
setApiTestError(String(err));
|
|
} finally {
|
|
setApiTesting(false);
|
|
}
|
|
};
|
|
|
|
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 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 setupStepStatus = t("easyMode.steps.current", {
|
|
current: activeStep,
|
|
total: 2,
|
|
});
|
|
|
|
return (
|
|
<section className="page simple-mode-page">
|
|
<nav className="simple-mode-step-status" aria-label={t("easyMode.steps.label")}>
|
|
<div className="simple-mode-step-status-heading">
|
|
<span>{t("easyMode.steps.label")}</span>
|
|
<strong>{setupStepStatus}</strong>
|
|
</div>
|
|
<div className="simple-mode-step-status-track">
|
|
<div
|
|
className={`simple-mode-step-status-item${activeStep === 1 ? " active" : " complete"}`}
|
|
aria-current={activeStep === 1 ? "step" : undefined}
|
|
>
|
|
<span className="simple-mode-step-status-number">
|
|
{activeStep > 1 ? <Check size={14} aria-hidden="true" /> : "1"}
|
|
</span>
|
|
<span className="simple-mode-step-status-copy">
|
|
<strong>{t("easyMode.overview.providerTitle")}</strong>
|
|
<small>{t("easyMode.steps.step1Description")}</small>
|
|
</span>
|
|
</div>
|
|
<span className={`simple-mode-step-status-connector${activeStep > 1 ? " complete" : ""}`} aria-hidden="true" />
|
|
<div
|
|
className={`simple-mode-step-status-item${activeStep === 2 ? " active" : ""}`}
|
|
aria-current={activeStep === 2 ? "step" : undefined}
|
|
>
|
|
<span className="simple-mode-step-status-number">2</span>
|
|
<span className="simple-mode-step-status-copy">
|
|
<strong>{t("easyMode.overview.agentTitle")}</strong>
|
|
<small>{t("easyMode.steps.step2Description")}</small>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* 第一步:接入模型 */}
|
|
{activeStep === 1 ? (
|
|
<section className="panel simple-mode-task">
|
|
<div className="simple-mode-task-heading">
|
|
<div>
|
|
<h2>{t("easyMode.overview.providerTitle")}</h2>
|
|
<p>{t("easyMode.setup.providerDescription")}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="simple-mode-choice-grid">
|
|
<button
|
|
type="button"
|
|
className={`simple-mode-choice${authMethod === "oauth" ? " selected" : ""}`}
|
|
aria-pressed={authMethod === "oauth"}
|
|
onClick={() => setAuthMethod("oauth")}
|
|
>
|
|
<div className="simple-mode-choice-heading">
|
|
<div className="simple-mode-choice-title">
|
|
<strong>{t("easyMode.oauth.title")}</strong>
|
|
{totalLoggedInOAuth > 0 ? (
|
|
<span className="state-pill success" style={{ fontSize: "12px" }}>
|
|
已登录 {totalLoggedInOAuth} 个账号
|
|
</span>
|
|
) : (
|
|
<span className="state-pill neutral" style={{ fontSize: "12px" }}>推荐</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<span>{t("easyMode.oauth.description")}</span>
|
|
<small>支持 Codex、Claude、Antigravity、Kimi、xAI</small>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className={`simple-mode-choice${authMethod === "api" ? " selected" : ""}`}
|
|
aria-pressed={authMethod === "api"}
|
|
onClick={() => setAuthMethod("api")}
|
|
>
|
|
<div className="simple-mode-choice-heading">
|
|
<div className="simple-mode-choice-title">
|
|
<strong>{t("easyMode.api.title")}</strong>
|
|
{totalApiProviders > 0 ? (
|
|
<span className="state-pill success" style={{ fontSize: "12px" }}>
|
|
已接入 {totalApiProviders} 个平台
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<span>{t("easyMode.api.description")}</span>
|
|
<small>支持 OpenAI、Anthropic、Codex、Gemini、DeepSeek</small>
|
|
</button>
|
|
</div>
|
|
|
|
{authMethod === "oauth" ? (
|
|
<div className="simple-mode-embedded-box">
|
|
{oauthNotice ? (
|
|
<div
|
|
className={`config-toast ${oauthNotice.tone}`}
|
|
>
|
|
{oauthNotice.tone === "success" ? <CheckCircle2 size={16} /> : <AlertCircle size={16} />}
|
|
<span>{oauthNotice.message}</span>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="simple-mode-provider-grid">
|
|
{oauthProviders.map((provider) => {
|
|
const loggedIn = isOAuthLoggedIn(provider.id);
|
|
const isLogging = oauthLoggingIn === provider.id;
|
|
|
|
return (
|
|
<article
|
|
key={provider.id}
|
|
className={`client-api-card simple-mode-provider-card${loggedIn ? " connected" : ""}`}
|
|
>
|
|
<div className="simple-mode-provider-card-head">
|
|
<span className="client-api-logo simple-mode-provider-logo">
|
|
<img src={provider.icon} alt="" />
|
|
</span>
|
|
<div className="simple-mode-provider-copy">
|
|
<strong>{provider.name}</strong>
|
|
<span>
|
|
{provider.description}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="simple-mode-provider-card-foot">
|
|
{loggedIn ? (
|
|
<span className="state-pill success" style={{ fontSize: "12px" }}>
|
|
<Check size={13} style={{ marginRight: 4 }} />
|
|
{t("easyMode.oauth.loggedIn")}
|
|
</span>
|
|
) : (
|
|
<span className="state-pill neutral" style={{ fontSize: "12px" }}>未授权</span>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
className={loggedIn ? "secondary-button" : "primary-button"}
|
|
disabled={isLogging}
|
|
onClick={() => void handleStartOAuth(provider.id)}
|
|
>
|
|
{isLogging ? (
|
|
<>
|
|
<LoaderCircle size={14} className="spin" style={{ marginRight: 6 }} />
|
|
{t("easyMode.oauth.loggingIn")}
|
|
</>
|
|
) : loggedIn ? (
|
|
t("easyMode.oauth.relogin")
|
|
) : (
|
|
"开始登录"
|
|
)}
|
|
</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{authMethod === "api" ? (
|
|
<div className="simple-mode-embedded-box">
|
|
{apiSaveNotice ? (
|
|
<div className="config-toast success">
|
|
<CheckCircle2 size={16} />
|
|
<span>{apiSaveNotice}</span>
|
|
</div>
|
|
) : null}
|
|
|
|
{apiTestError ? (
|
|
<div
|
|
className="config-toast error"
|
|
style={{ position: "static", transform: "none", margin: 0 }}
|
|
>
|
|
<AlertCircle size={16} />
|
|
<span>{apiTestError}</span>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="simple-mode-api-form">
|
|
<div className="simple-mode-api-platforms">
|
|
{apiSectionOptions.map((opt) => (
|
|
<button
|
|
type="button"
|
|
key={opt.id}
|
|
className={`secondary-button simple-mode-api-platform${selectedApiSection === opt.id ? " active" : ""}`}
|
|
onClick={() => handleApiSectionChange(opt.id)}
|
|
>
|
|
<img className="simple-mode-api-platform-icon" src={opt.icon} alt="" />
|
|
{opt.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="simple-mode-field">
|
|
<label>
|
|
{t("easyMode.api.name")}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className="text-input"
|
|
value={apiRemark}
|
|
onChange={(e) => setApiRemark(e.target.value)}
|
|
placeholder="例如:主力 DeepSeek V3、中转 API"
|
|
/>
|
|
</div>
|
|
|
|
<div className="simple-mode-api-fields">
|
|
<div className="simple-mode-field">
|
|
<label>
|
|
{t("easyMode.api.baseUrl")}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className="text-input"
|
|
value={apiBaseUrl}
|
|
onChange={(e) => setApiBaseUrl(e.target.value)}
|
|
placeholder="https://..."
|
|
/>
|
|
</div>
|
|
<div className="simple-mode-field">
|
|
<label>
|
|
{t("easyMode.api.apiKey")}
|
|
</label>
|
|
<input
|
|
type="password"
|
|
className="text-input"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
placeholder="sk-..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="simple-mode-api-model-card">
|
|
{apiTestedModels.length === 0 ? (
|
|
<div className="simple-mode-api-model-fetch">
|
|
<button
|
|
type="button"
|
|
className="secondary-button simple-mode-api-fetch-button"
|
|
disabled={apiTesting || !apiBaseUrl.trim()}
|
|
onClick={() => void handleTestApi()}
|
|
>
|
|
{apiTesting ? t("easyMode.api.fetchingModels") : t("easyMode.api.fetchModels")}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="simple-mode-api-model-heading">
|
|
<strong>{t("easyMode.api.modelListTitle")}</strong>
|
|
</div>
|
|
|
|
<div className="simple-mode-api-model-selection">
|
|
<div className="simple-mode-api-model-selection-heading">
|
|
<span>{t("easyMode.api.modelListHint")}</span>
|
|
</div>
|
|
<div className="simple-mode-api-model-options">
|
|
{apiTestedModels.map((model) => {
|
|
const key = model.name.trim().toLowerCase();
|
|
const selected = apiSelectedModels.some(
|
|
(item) => item.name.trim().toLowerCase() === key,
|
|
);
|
|
return (
|
|
<label
|
|
className={`simple-mode-api-model-option${selected ? " selected" : ""}`}
|
|
key={model.name}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={() => handleToggleApiModel(model)}
|
|
/>
|
|
<span title={model.name}>{model.name}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="simple-mode-api-actions">
|
|
<button
|
|
type="button"
|
|
className="primary-button"
|
|
disabled={apiSaving || !apiBaseUrl.trim() || apiSelectedModels.length === 0}
|
|
onClick={() => void handleSaveApi()}
|
|
>
|
|
保存并接入
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="simple-mode-task-footer" style={{ marginTop: 10 }}>
|
|
<div className="simple-mode-selection">
|
|
{hasConnectedSource ? (
|
|
<span style={{ color: "var(--ui-accent-strong)", fontWeight: 600 }}>
|
|
{t("easyMode.status.connectedModels", { count: Math.max(availableModels.length, 1) })}
|
|
</span>
|
|
) : (
|
|
<span className="muted">{t("easyMode.status.noModelsYet")}</span>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="primary-button"
|
|
style={{ minHeight: 42, padding: "0 22px", fontSize: "15px" }}
|
|
disabled={!hasConnectedSource}
|
|
onClick={() => setActiveStep(2)}
|
|
>
|
|
{t("easyMode.navigation.next")}: 配置智能体
|
|
<ArrowRight size={16} style={{ marginLeft: 6 }} />
|
|
</button>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
{/* 第二步:接入智能体 */}
|
|
{activeStep === 2 ? (
|
|
<section className="panel simple-mode-task">
|
|
<AgentsPage embedded />
|
|
|
|
<div className="simple-mode-task-footer" style={{ marginTop: 14 }}>
|
|
<button
|
|
type="button"
|
|
className="secondary-button"
|
|
onClick={() => setActiveStep(1)}
|
|
>
|
|
<ArrowLeft size={16} style={{ marginRight: 6 }} />
|
|
{t("easyMode.navigation.back")}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="primary-button"
|
|
style={{ minHeight: 42, padding: "0 22px", fontSize: "15px" }}
|
|
onClick={() => onExit?.()}
|
|
>
|
|
<CheckCircle2 size={16} style={{ marginRight: 6 }} />
|
|
{t("easyMode.complete.enterApp")}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|