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:
1 parent
a4a8b4ef90
commit
d93d8a38e4
4 files changed
+121
-142
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user