fix(easy-mode): refine API onboarding actions and notifications

- 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
This commit is contained in:
lzt404 committed 2026-09-01 23:32:26 +08:00
1 parent a4a8b4ef90
commit d93d8a38e4
4 files changed
+121 -142

No files matched your search

+3 -4
View File
@@ -75,11 +75,10 @@ export const en: Record<MessageKey, string> = {
'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.modelListTitle': 'Select models',
'easyMode.api.modelListHint': 'Select the models to connect',
'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.modelRequired': 'Fetch and select a model',
'easyMode.api.testSuccess': 'Connection successful! Found {count} models',
'easyMode.api.testFailed': 'Connection test failed',
'easyMode.api.platform': 'Platform',
+3 -4
View File
@@ -73,11 +73,10 @@ export const zhCN = {
'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.modelListTitle': '选择模型',
'easyMode.api.modelListHint': '勾选需要接入的模型',
'easyMode.api.credentialsRequired': '请先填写 API URL 和 API 密钥',
'easyMode.api.modelRequired': '请先获取模型列表并选择模型,或手动添加 Model ID',
'easyMode.api.modelRequired': '请先获取模型列表并选择模型',
'easyMode.api.testSuccess': '连接成功!发现 {count} 个模型',
'easyMode.api.testFailed': '连接测试失败',
'easyMode.api.platform': '平台类型',
+63 -103
View File
@@ -1,7 +1,6 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
@@ -13,8 +12,6 @@ import {
Check,
CheckCircle2,
LoaderCircle,
Plus,
RefreshCw,
} from "lucide-react";
import { useI18n } from "../i18n";
import {
@@ -122,7 +119,6 @@ export function EasyModePage({
const [apiTesting, setApiTesting] = useState(false);
const [apiTestedModels, setApiTestedModels] = useState<ModelOption[]>([]);
const [apiSelectedModels, setApiSelectedModels] = useState<ModelOption[]>([]);
const [apiCustomModelId, setApiCustomModelId] = useState("");
const [apiTestError, setApiTestError] = useState("");
const [apiSaving, setApiSaving] = useState(false);
const [apiSaveNotice, setApiSaveNotice] = useState<string | null>(null);
@@ -195,6 +191,14 @@ export function EasyModePage({
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);
@@ -260,7 +264,6 @@ export function EasyModePage({
if (opt) setApiBaseUrl(opt.defaultBaseUrl);
setApiTestedModels([]);
setApiSelectedModels([]);
setApiCustomModelId("");
setApiTestError("");
setApiSaveNotice(null);
};
@@ -288,11 +291,7 @@ export function EasyModePage({
);
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];
});
setApiSelectedModels(models);
} else {
setApiTestError("未获取到任何模型,请检查 API URL 和 API 密钥");
}
@@ -314,19 +313,6 @@ export function EasyModePage({
});
};
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");
@@ -382,14 +368,6 @@ export function EasyModePage({
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]);
const setupStepStatus = t("easyMode.steps.current", {
current: activeStep,
total: 2,
@@ -554,10 +532,7 @@ export function EasyModePage({
{authMethod === "api" ? (
<div className="simple-mode-embedded-box">
{apiSaveNotice ? (
<div
className="config-toast success"
style={{ position: "static", transform: "none", margin: 0 }}
>
<div className="config-toast success">
<CheckCircle2 size={16} />
<span>{apiSaveNotice}</span>
</div>
@@ -629,77 +604,62 @@ export function EasyModePage({
</div>
<div className="simple-mode-api-model-card">
<div className="simple-mode-api-model-toolbar">
<input
type="text"
className="text-input"
value={apiCustomModelId}
onChange={(e) => setApiCustomModelId(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddCustomModel();
}
}}
placeholder={t("easyMode.api.customModelPlaceholder")}
aria-label={t("easyMode.api.customModelId")}
/>
<button
type="button"
className="secondary-button"
disabled={!apiCustomModelId.trim()}
onClick={handleAddCustomModel}
>
<Plus size={14} />
{t("easyMode.api.addModelId")}
</button>
<button
type="button"
className="secondary-button"
disabled={apiTesting || !apiBaseUrl.trim()}
onClick={() => void handleTestApi()}
>
{apiTesting ? <LoaderCircle size={14} className="spin" /> : <RefreshCw size={14} />}
{apiTesting ? t("easyMode.api.fetchingModels") : t("easyMode.api.fetchModels")}
</button>
</div>
{displayedApiModels.length > 0 ? (
<div className="simple-mode-api-model-options">
{displayedApiModels.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>
);
})}
{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>
) : null}
) : (
<>
<div className="simple-mode-api-model-heading">
<strong>{t("easyMode.api.modelListTitle")}</strong>
</div>
</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()}
>
{apiSaving ? <LoaderCircle size={14} className="spin" /> : <Plus size={14} />}
保存并接入
</button>
<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>
+52 -31
View File
@@ -10870,39 +10870,60 @@ body.table-col-resizing * {
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-fetch {
display: flex;
justify-content: flex-start;
}
.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;
.simple-mode-api-fetch-button {
min-height: 36px;
padding-inline: 14px;
white-space: nowrap;
}
.simple-mode-api-model-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
}
.simple-mode-api-model-heading strong {
color: var(--theme-2d2a26);
font-size: var(--font-size-label);
font-weight: 700;
}
.simple-mode-api-model-heading .secondary-button {
min-height: 36px;
flex: none;
padding-inline: 11px;
white-space: nowrap;
}
.simple-mode-api-model-selection {
display: grid;
gap: 8px;
}
.simple-mode-api-model-selection-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.simple-mode-api-model-selection-heading strong {
color: var(--theme-2d2a26);
font-size: var(--font-size-label);
}
.simple-mode-api-model-selection-heading span {
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
}
.simple-mode-api-model-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -11659,11 +11680,11 @@ body.table-col-resizing * {
}
@media (max-width: 680px) {
.simple-mode-api-model-toolbar {
grid-template-columns: 1fr;
.simple-mode-api-model-heading {
align-items: flex-start;
}
.simple-mode-api-model-toolbar .secondary-button {
.simple-mode-api-model-heading .secondary-button {
width: 100%;
}