mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
perf(ai-providers): lighten dirty checks for edits
This commit is contained in:
@@ -13,6 +13,7 @@ import { ampcodeApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import type { AmpcodeConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { areStringArraysEqual } from '@/utils/compare';
|
||||
import {
|
||||
buildAmpcodeFormState,
|
||||
entriesToAmpcodeMappings,
|
||||
@@ -38,20 +39,52 @@ const normalizeMappingEntries = (entries: Array<{ name: string; alias: string }>
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const normalizeUpstreamApiKeyEntries = (form: AmpcodeFormState) =>
|
||||
entriesToAmpcodeUpstreamApiKeys(form.upstreamApiKeyEntries).map((entry) => ({
|
||||
upstreamApiKey: entry.upstreamApiKey,
|
||||
apiKeys: entry.apiKeys,
|
||||
}));
|
||||
type AmpcodeFormBaseline = {
|
||||
upstreamUrl: string;
|
||||
upstreamApiKey: string;
|
||||
forceModelMappings: boolean;
|
||||
upstreamApiKeys: ReturnType<typeof entriesToAmpcodeUpstreamApiKeys>;
|
||||
modelMappings: ReturnType<typeof normalizeMappingEntries>;
|
||||
};
|
||||
|
||||
const buildAmpcodeSignature = (form: AmpcodeFormState) =>
|
||||
JSON.stringify({
|
||||
upstreamUrl: String(form.upstreamUrl ?? '').trim(),
|
||||
upstreamApiKey: String(form.upstreamApiKey ?? '').trim(),
|
||||
forceModelMappings: Boolean(form.forceModelMappings),
|
||||
upstreamApiKeys: normalizeUpstreamApiKeyEntries(form),
|
||||
modelMappings: normalizeMappingEntries(form.mappingEntries),
|
||||
});
|
||||
const buildAmpcodeBaseline = (form: AmpcodeFormState): AmpcodeFormBaseline => ({
|
||||
upstreamUrl: String(form.upstreamUrl ?? '').trim(),
|
||||
upstreamApiKey: String(form.upstreamApiKey ?? '').trim(),
|
||||
forceModelMappings: Boolean(form.forceModelMappings),
|
||||
upstreamApiKeys: entriesToAmpcodeUpstreamApiKeys(form.upstreamApiKeyEntries),
|
||||
modelMappings: normalizeMappingEntries(form.mappingEntries),
|
||||
});
|
||||
|
||||
const areUpstreamApiKeysEqual = (
|
||||
a: readonly { upstreamApiKey: string; apiKeys: readonly string[] }[],
|
||||
b: readonly { upstreamApiKey: string; apiKeys: readonly string[] }[]
|
||||
) => {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
const left = a[i];
|
||||
const right = b[i];
|
||||
if (!left || !right) return false;
|
||||
if (left.upstreamApiKey !== right.upstreamApiKey) return false;
|
||||
if (!areStringArraysEqual(left.apiKeys, right.apiKeys)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const areModelMappingsEqual = (
|
||||
a: readonly { from: string; to: string }[],
|
||||
b: readonly { from: string; to: string }[]
|
||||
) => {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
const left = a[i];
|
||||
const right = b[i];
|
||||
if (!left || !right) return false;
|
||||
if (left.from !== right.from || left.to !== right.to) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export function AiProvidersAmpcodeEditPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -72,9 +105,7 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
const [upstreamApiKeysDirty, setUpstreamApiKeysDirty] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [baselineSignature, setBaselineSignature] = useState(() =>
|
||||
buildAmpcodeSignature(buildAmpcodeFormState(null))
|
||||
);
|
||||
const [baseline, setBaseline] = useState(() => buildAmpcodeBaseline(buildAmpcodeFormState(null)));
|
||||
const initializedRef = useRef(false);
|
||||
const mountedRef = useRef(false);
|
||||
|
||||
@@ -119,7 +150,7 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
setError('');
|
||||
const initialForm = buildAmpcodeFormState(useConfigStore.getState().config?.ampcode ?? null);
|
||||
setForm(initialForm);
|
||||
setBaselineSignature(buildAmpcodeSignature(initialForm));
|
||||
setBaseline(buildAmpcodeBaseline(initialForm));
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -131,7 +162,7 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
clearCache('ampcode');
|
||||
const nextForm = buildAmpcodeFormState(ampcode);
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildAmpcodeSignature(nextForm));
|
||||
setBaseline(buildAmpcodeBaseline(nextForm));
|
||||
} catch (err: unknown) {
|
||||
if (!mountedRef.current) return;
|
||||
setError(getErrorMessage(err) || t('notification.refresh_failed'));
|
||||
@@ -143,8 +174,28 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
})();
|
||||
}, [clearCache, t, updateConfigValue]);
|
||||
|
||||
const currentSignature = useMemo(() => buildAmpcodeSignature(form), [form]);
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const normalizedUpstreamApiKeys = useMemo(
|
||||
() => entriesToAmpcodeUpstreamApiKeys(form.upstreamApiKeyEntries),
|
||||
[form.upstreamApiKeyEntries]
|
||||
);
|
||||
const normalizedModelMappings = useMemo(
|
||||
() => normalizeMappingEntries(form.mappingEntries),
|
||||
[form.mappingEntries]
|
||||
);
|
||||
const isUpstreamApiKeysDirty = useMemo(
|
||||
() => !areUpstreamApiKeysEqual(baseline.upstreamApiKeys, normalizedUpstreamApiKeys),
|
||||
[baseline.upstreamApiKeys, normalizedUpstreamApiKeys]
|
||||
);
|
||||
const isModelMappingsDirtyNormalized = useMemo(
|
||||
() => !areModelMappingsEqual(baseline.modelMappings, normalizedModelMappings),
|
||||
[baseline.modelMappings, normalizedModelMappings]
|
||||
);
|
||||
const isDirty =
|
||||
baseline.upstreamUrl !== form.upstreamUrl.trim() ||
|
||||
baseline.upstreamApiKey !== form.upstreamApiKey.trim() ||
|
||||
baseline.forceModelMappings !== Boolean(form.forceModelMappings) ||
|
||||
isUpstreamApiKeysDirty ||
|
||||
isModelMappingsDirtyNormalized;
|
||||
const canGuard = !loading && !saving;
|
||||
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
@@ -263,7 +314,7 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
clearCache('ampcode');
|
||||
showNotification(t('notification.ampcode_updated'), 'success');
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildAmpcodeSignature(form));
|
||||
setBaseline(buildAmpcodeBaseline(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = getErrorMessage(err);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { modelsApi, providersApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers';
|
||||
import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare';
|
||||
import { entriesToModels, modelsToEntries } from '@/components/ui/modelInputListUtils';
|
||||
import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils';
|
||||
import type { ProviderFormState } from '@/components/providers';
|
||||
@@ -63,21 +64,30 @@ const normalizeModelEntries = (entries: Array<{ name: string; alias: string }>)
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const buildCodexSignature = (form: ProviderFormState) =>
|
||||
JSON.stringify({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority)
|
||||
? Math.trunc(form.priority)
|
||||
: null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
websockets: Boolean(form.websockets),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
type CodexFormBaseline = {
|
||||
apiKey: string;
|
||||
priority: number | null;
|
||||
prefix: string;
|
||||
baseUrl: string;
|
||||
websockets: boolean;
|
||||
proxyUrl: string;
|
||||
headers: ReturnType<typeof normalizeHeaderEntries>;
|
||||
models: ReturnType<typeof normalizeModelEntries>;
|
||||
excludedModels: string[];
|
||||
};
|
||||
|
||||
const buildCodexBaseline = (form: ProviderFormState): CodexFormBaseline => ({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
websockets: Boolean(form.websockets),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
|
||||
export function AiProvidersCodexEditPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -98,9 +108,7 @@ export function AiProvidersCodexEditPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [form, setForm] = useState<ProviderFormState>(() => buildEmptyForm());
|
||||
const [baselineSignature, setBaselineSignature] = useState(() =>
|
||||
buildCodexSignature(buildEmptyForm())
|
||||
);
|
||||
const [baseline, setBaseline] = useState(() => buildCodexBaseline(buildEmptyForm()));
|
||||
|
||||
const [modelDiscoveryOpen, setModelDiscoveryOpen] = useState(false);
|
||||
const [modelDiscoveryEndpoint, setModelDiscoveryEndpoint] = useState('');
|
||||
@@ -186,16 +194,50 @@ export function AiProvidersCodexEditPage() {
|
||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||
};
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildCodexSignature(nextForm));
|
||||
setBaseline(buildCodexBaseline(nextForm));
|
||||
return;
|
||||
}
|
||||
const nextForm = buildEmptyForm();
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildCodexSignature(nextForm));
|
||||
setBaseline(buildCodexBaseline(nextForm));
|
||||
}, [initialData, loading]);
|
||||
|
||||
const currentSignature = useMemo(() => buildCodexSignature(form), [form]);
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const normalizedHeaders = useMemo(() => normalizeHeaderEntries(form.headers), [form.headers]);
|
||||
const normalizedModels = useMemo(
|
||||
() => normalizeModelEntries(form.modelEntries),
|
||||
[form.modelEntries]
|
||||
);
|
||||
const normalizedExcludedModels = useMemo(
|
||||
() => parseExcludedModels(form.excludedText ?? ''),
|
||||
[form.excludedText]
|
||||
);
|
||||
const normalizedPriority = useMemo(() => {
|
||||
return form.priority !== undefined && Number.isFinite(form.priority)
|
||||
? Math.trunc(form.priority)
|
||||
: null;
|
||||
}, [form.priority]);
|
||||
const isHeadersDirty = useMemo(
|
||||
() => !areKeyValueEntriesEqual(baseline.headers, normalizedHeaders),
|
||||
[baseline.headers, normalizedHeaders]
|
||||
);
|
||||
const isModelsDirty = useMemo(
|
||||
() => !areModelEntriesEqual(baseline.models, normalizedModels),
|
||||
[baseline.models, normalizedModels]
|
||||
);
|
||||
const isExcludedModelsDirty = useMemo(
|
||||
() => !areStringArraysEqual(baseline.excludedModels, normalizedExcludedModels),
|
||||
[baseline.excludedModels, normalizedExcludedModels]
|
||||
);
|
||||
const isDirty =
|
||||
baseline.apiKey !== form.apiKey.trim() ||
|
||||
baseline.priority !== normalizedPriority ||
|
||||
baseline.prefix !== String(form.prefix ?? '').trim() ||
|
||||
baseline.baseUrl !== String(form.baseUrl ?? '').trim() ||
|
||||
baseline.websockets !== Boolean(form.websockets) ||
|
||||
baseline.proxyUrl !== String(form.proxyUrl ?? '').trim() ||
|
||||
isHeadersDirty ||
|
||||
isModelsDirty ||
|
||||
isExcludedModelsDirty;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
@@ -430,7 +472,7 @@ export function AiProvidersCodexEditPage() {
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildCodexSignature(form));
|
||||
setBaseline(buildCodexBaseline(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { modelsApi, providersApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import type { GeminiKeyConfig } from '@/types';
|
||||
import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers';
|
||||
import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare';
|
||||
import type { ModelInfo } from '@/utils/models';
|
||||
import { entriesToModels, modelsToEntries } from '@/components/ui/modelInputListUtils';
|
||||
import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils';
|
||||
@@ -60,20 +61,28 @@ const normalizeModelEntries = (entries: Array<{ name: string; alias: string }>)
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const buildGeminiSignature = (form: GeminiFormState) =>
|
||||
JSON.stringify({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority)
|
||||
? Math.trunc(form.priority)
|
||||
: null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
type GeminiFormBaseline = {
|
||||
apiKey: string;
|
||||
priority: number | null;
|
||||
prefix: string;
|
||||
baseUrl: string;
|
||||
proxyUrl: string;
|
||||
headers: ReturnType<typeof normalizeHeaderEntries>;
|
||||
models: ReturnType<typeof normalizeModelEntries>;
|
||||
excludedModels: string[];
|
||||
};
|
||||
|
||||
const buildGeminiBaseline = (form: GeminiFormState): GeminiFormBaseline => ({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
|
||||
export function AiProvidersGeminiEditPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -94,9 +103,7 @@ export function AiProvidersGeminiEditPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [form, setForm] = useState<GeminiFormState>(() => buildEmptyForm());
|
||||
const [baselineSignature, setBaselineSignature] = useState(() =>
|
||||
buildGeminiSignature(buildEmptyForm())
|
||||
);
|
||||
const [baseline, setBaseline] = useState(() => buildGeminiBaseline(buildEmptyForm()));
|
||||
|
||||
const [modelDiscoveryOpen, setModelDiscoveryOpen] = useState(false);
|
||||
const [modelDiscoveryEndpoint, setModelDiscoveryEndpoint] = useState('');
|
||||
@@ -185,12 +192,12 @@ export function AiProvidersGeminiEditPage() {
|
||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||
};
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildGeminiSignature(nextForm));
|
||||
setBaseline(buildGeminiBaseline(nextForm));
|
||||
return;
|
||||
}
|
||||
const nextForm = buildEmptyForm();
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildGeminiSignature(nextForm));
|
||||
setBaseline(buildGeminiBaseline(nextForm));
|
||||
}, [initialData, loading]);
|
||||
|
||||
const canSave = !disableControls && !saving && !loading && !invalidIndexParam && !invalidIndex;
|
||||
@@ -378,8 +385,41 @@ export function AiProvidersGeminiEditPage() {
|
||||
setModelDiscoveryOpen(false);
|
||||
};
|
||||
|
||||
const currentSignature = useMemo(() => buildGeminiSignature(form), [form]);
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const normalizedHeaders = useMemo(() => normalizeHeaderEntries(form.headers), [form.headers]);
|
||||
const normalizedModels = useMemo(
|
||||
() => normalizeModelEntries(form.modelEntries),
|
||||
[form.modelEntries]
|
||||
);
|
||||
const normalizedExcludedModels = useMemo(
|
||||
() => parseExcludedModels(form.excludedText ?? ''),
|
||||
[form.excludedText]
|
||||
);
|
||||
const normalizedPriority = useMemo(() => {
|
||||
return form.priority !== undefined && Number.isFinite(form.priority)
|
||||
? Math.trunc(form.priority)
|
||||
: null;
|
||||
}, [form.priority]);
|
||||
const isHeadersDirty = useMemo(
|
||||
() => !areKeyValueEntriesEqual(baseline.headers, normalizedHeaders),
|
||||
[baseline.headers, normalizedHeaders]
|
||||
);
|
||||
const isModelsDirty = useMemo(
|
||||
() => !areModelEntriesEqual(baseline.models, normalizedModels),
|
||||
[baseline.models, normalizedModels]
|
||||
);
|
||||
const isExcludedModelsDirty = useMemo(
|
||||
() => !areStringArraysEqual(baseline.excludedModels, normalizedExcludedModels),
|
||||
[baseline.excludedModels, normalizedExcludedModels]
|
||||
);
|
||||
const isDirty =
|
||||
baseline.apiKey !== form.apiKey.trim() ||
|
||||
baseline.priority !== normalizedPriority ||
|
||||
baseline.prefix !== String(form.prefix ?? '').trim() ||
|
||||
baseline.baseUrl !== String(form.baseUrl ?? '').trim() ||
|
||||
baseline.proxyUrl !== String(form.proxyUrl ?? '').trim() ||
|
||||
isHeadersDirty ||
|
||||
isModelsDirty ||
|
||||
isExcludedModelsDirty;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
@@ -432,7 +472,7 @@ export function AiProvidersGeminiEditPage() {
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildGeminiSignature(form));
|
||||
setBaseline(buildGeminiBaseline(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils';
|
||||
import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers';
|
||||
import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare';
|
||||
import type { VertexFormState } from '@/components/providers';
|
||||
import layoutStyles from './AiProvidersEditLayout.module.scss';
|
||||
|
||||
@@ -47,18 +48,28 @@ const normalizeModelEntries = (entries: Array<{ name: string; alias: string }>)
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const buildVertexSignature = (form: VertexFormState) =>
|
||||
JSON.stringify({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
type VertexFormBaseline = {
|
||||
apiKey: string;
|
||||
priority: number | null;
|
||||
prefix: string;
|
||||
baseUrl: string;
|
||||
proxyUrl: string;
|
||||
headers: ReturnType<typeof normalizeHeaderEntries>;
|
||||
models: ReturnType<typeof normalizeModelEntries>;
|
||||
excludedModels: string[];
|
||||
};
|
||||
|
||||
const buildVertexBaseline = (form: VertexFormState): VertexFormBaseline => ({
|
||||
apiKey: String(form.apiKey ?? '').trim(),
|
||||
priority:
|
||||
form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null,
|
||||
prefix: String(form.prefix ?? '').trim(),
|
||||
baseUrl: String(form.baseUrl ?? '').trim(),
|
||||
proxyUrl: String(form.proxyUrl ?? '').trim(),
|
||||
headers: normalizeHeaderEntries(form.headers),
|
||||
models: normalizeModelEntries(form.modelEntries),
|
||||
excludedModels: parseExcludedModels(form.excludedText ?? ''),
|
||||
});
|
||||
|
||||
export function AiProvidersVertexEditPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -79,7 +90,7 @@ export function AiProvidersVertexEditPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [form, setForm] = useState<VertexFormState>(() => buildEmptyForm());
|
||||
const [baselineSignature, setBaselineSignature] = useState(() => buildVertexSignature(buildEmptyForm()));
|
||||
const [baseline, setBaseline] = useState(() => buildVertexBaseline(buildEmptyForm()));
|
||||
|
||||
const hasIndexParam = typeof params.index === 'string';
|
||||
const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]);
|
||||
@@ -160,18 +171,51 @@ export function AiProvidersVertexEditPage() {
|
||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||
};
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildVertexSignature(nextForm));
|
||||
setBaseline(buildVertexBaseline(nextForm));
|
||||
return;
|
||||
}
|
||||
const nextForm = buildEmptyForm();
|
||||
setForm(nextForm);
|
||||
setBaselineSignature(buildVertexSignature(nextForm));
|
||||
setBaseline(buildVertexBaseline(nextForm));
|
||||
}, [initialData, loading]);
|
||||
|
||||
const canSave = !disableControls && !saving && !loading && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
const currentSignature = useMemo(() => buildVertexSignature(form), [form]);
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const normalizedHeaders = useMemo(() => normalizeHeaderEntries(form.headers), [form.headers]);
|
||||
const normalizedModels = useMemo(
|
||||
() => normalizeModelEntries(form.modelEntries),
|
||||
[form.modelEntries]
|
||||
);
|
||||
const normalizedExcludedModels = useMemo(
|
||||
() => parseExcludedModels(form.excludedText ?? ''),
|
||||
[form.excludedText]
|
||||
);
|
||||
const normalizedPriority = useMemo(() => {
|
||||
return form.priority !== undefined && Number.isFinite(form.priority)
|
||||
? Math.trunc(form.priority)
|
||||
: null;
|
||||
}, [form.priority]);
|
||||
const isHeadersDirty = useMemo(
|
||||
() => !areKeyValueEntriesEqual(baseline.headers, normalizedHeaders),
|
||||
[baseline.headers, normalizedHeaders]
|
||||
);
|
||||
const isModelsDirty = useMemo(
|
||||
() => !areModelEntriesEqual(baseline.models, normalizedModels),
|
||||
[baseline.models, normalizedModels]
|
||||
);
|
||||
const isExcludedModelsDirty = useMemo(
|
||||
() => !areStringArraysEqual(baseline.excludedModels, normalizedExcludedModels),
|
||||
[baseline.excludedModels, normalizedExcludedModels]
|
||||
);
|
||||
const isDirty =
|
||||
baseline.apiKey !== form.apiKey.trim() ||
|
||||
baseline.priority !== normalizedPriority ||
|
||||
baseline.prefix !== String(form.prefix ?? '').trim() ||
|
||||
baseline.baseUrl !== String(form.baseUrl ?? '').trim() ||
|
||||
baseline.proxyUrl !== String(form.proxyUrl ?? '').trim() ||
|
||||
isHeadersDirty ||
|
||||
isModelsDirty ||
|
||||
isExcludedModelsDirty;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
@@ -230,7 +274,7 @@ export function AiProvidersVertexEditPage() {
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildVertexSignature(form));
|
||||
setBaseline(buildVertexBaseline(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
export function areStringArraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areKeyValueEntriesEqual(
|
||||
a: readonly { key: string; value: string }[],
|
||||
b: readonly { key: string; value: string }[]
|
||||
): boolean {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
const left = a[i];
|
||||
const right = b[i];
|
||||
if (!left || !right) return false;
|
||||
if (left.key !== right.key || left.value !== right.value) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areModelEntriesEqual(
|
||||
a: readonly { name: string; alias: string }[],
|
||||
b: readonly { name: string; alias: string }[]
|
||||
): boolean {
|
||||
if (a === b) return true;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
const left = a[i];
|
||||
const right = b[i];
|
||||
if (!left || !right) return false;
|
||||
if (left.name !== right.name || left.alias !== right.alias) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user