mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
fix(ai-providers): prompt on unsaved edits and preserve editor drafts
This commit is contained in:
+29
-18
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Outlet, RouterProvider, createHashRouter } from 'react-router-dom';
|
||||
import { LoginPage } from '@/pages/LoginPage';
|
||||
import { NotificationContainer } from '@/components/common/NotificationContainer';
|
||||
import { ConfirmationModal } from '@/components/common/ConfirmationModal';
|
||||
@@ -7,6 +7,33 @@ import { MainLayout } from '@/components/layout/MainLayout';
|
||||
import { ProtectedRoute } from '@/router/ProtectedRoute';
|
||||
import { useLanguageStore, useThemeStore } from '@/stores';
|
||||
|
||||
function RootShell() {
|
||||
return (
|
||||
<>
|
||||
<NotificationContainer />
|
||||
<ConfirmationModal />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootShell />,
|
||||
children: [
|
||||
{ path: '/login', element: <LoginPage /> },
|
||||
{
|
||||
path: '/*',
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
const initializeTheme = useThemeStore((state) => state.initializeTheme);
|
||||
const language = useLanguageStore((state) => state.language);
|
||||
@@ -26,23 +53,7 @@ function App() {
|
||||
document.documentElement.lang = language;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<NotificationContainer />
|
||||
<ConfirmationModal />
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { BlockerFunction } from 'react-router';
|
||||
import { useBlocker } from 'react-router';
|
||||
import { useBlocker, useLocation } from 'react-router';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
|
||||
type ConfirmationVariant = 'danger' | 'primary' | 'secondary';
|
||||
@@ -23,9 +23,18 @@ export function useUnsavedChangesGuard(options: UseUnsavedChangesGuardOptions) {
|
||||
const { enabled = true, shouldBlock, dialog } = options;
|
||||
const { showConfirmation } = useNotificationStore();
|
||||
const lastBlockedRef = useRef<string>('');
|
||||
const allowNextNavigationRef = useRef(false);
|
||||
const location = useLocation();
|
||||
|
||||
const allowNextNavigation = useCallback(() => {
|
||||
allowNextNavigationRef.current = true;
|
||||
}, []);
|
||||
|
||||
const shouldBlockFunction = useCallback<BlockerFunction>(
|
||||
(args) => {
|
||||
if (allowNextNavigationRef.current) {
|
||||
return false;
|
||||
}
|
||||
if (!enabled) return false;
|
||||
return typeof shouldBlock === 'function' ? shouldBlock(args) : shouldBlock;
|
||||
},
|
||||
@@ -34,6 +43,11 @@ export function useUnsavedChangesGuard(options: UseUnsavedChangesGuardOptions) {
|
||||
|
||||
const blocker = useBlocker(shouldBlockFunction);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowNextNavigationRef.current) return;
|
||||
allowNextNavigationRef.current = false;
|
||||
}, [location.key]);
|
||||
|
||||
const blockedKey = useMemo(() => {
|
||||
if (blocker.state !== 'blocked' || !blocker.location) return '';
|
||||
return `${blocker.location.pathname}${blocker.location.search}${blocker.location.hash}`;
|
||||
@@ -60,5 +74,6 @@ export function useUnsavedChangesGuard(options: UseUnsavedChangesGuardOptions) {
|
||||
onCancel: () => blocker.reset(),
|
||||
});
|
||||
}, [blockedKey, blocker, dialog, showConfirmation]);
|
||||
}
|
||||
|
||||
return { allowNextNavigation };
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const canGuard = !loading && !saving;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ currentLocation, nextLocation }) =>
|
||||
isDirty && currentLocation.pathname !== nextLocation.pathname,
|
||||
@@ -234,6 +234,8 @@ export function AiProvidersAmpcodeEditPage() {
|
||||
updateConfigValue('ampcode', next);
|
||||
clearCache('ampcode');
|
||||
showNotification(t('notification.ampcode_updated'), 'success');
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildAmpcodeSignature(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = getErrorMessage(err);
|
||||
|
||||
@@ -139,8 +139,6 @@ export function AiProvidersClaudeEditLayout() {
|
||||
const [configs, setConfigs] = useState<ProviderKeyConfig[]>(() => config?.claudeApiKeys ?? []);
|
||||
const [loading, setLoading] = useState(() => !isCacheValid('claude-api-key'));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [baselineDraftKey, setBaselineDraftKey] = useState<string>('');
|
||||
const [baselineSignature, setBaselineSignature] = useState<string>('');
|
||||
|
||||
const draftKey = useMemo(() => {
|
||||
if (invalidIndexParam) return `claude:invalid:${params.index ?? 'unknown'}`;
|
||||
@@ -149,9 +147,10 @@ export function AiProvidersClaudeEditLayout() {
|
||||
}, [editIndex, invalidIndexParam, params.index]);
|
||||
|
||||
const draft = useClaudeEditDraftStore((state) => state.drafts[draftKey]);
|
||||
const ensureDraft = useClaudeEditDraftStore((state) => state.ensureDraft);
|
||||
const acquireDraft = useClaudeEditDraftStore((state) => state.acquireDraft);
|
||||
const releaseDraft = useClaudeEditDraftStore((state) => state.releaseDraft);
|
||||
const initDraft = useClaudeEditDraftStore((state) => state.initDraft);
|
||||
const clearDraft = useClaudeEditDraftStore((state) => state.clearDraft);
|
||||
const setDraftBaselineSignature = useClaudeEditDraftStore((state) => state.setDraftBaselineSignature);
|
||||
const setDraftForm = useClaudeEditDraftStore((state) => state.setDraftForm);
|
||||
const setDraftTestModel = useClaudeEditDraftStore((state) => state.setDraftTestModel);
|
||||
const setDraftTestStatus = useClaudeEditDraftStore((state) => state.setDraftTestStatus);
|
||||
@@ -203,8 +202,9 @@ export function AiProvidersClaudeEditLayout() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
ensureDraft(draftKey);
|
||||
}, [draftKey, ensureDraft]);
|
||||
acquireDraft(draftKey);
|
||||
return () => releaseDraft(draftKey);
|
||||
}, [acquireDraft, draftKey, releaseDraft]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
const state = location.state as LocationState;
|
||||
@@ -215,12 +215,6 @@ export function AiProvidersClaudeEditLayout() {
|
||||
navigate('/ai-providers', { replace: true });
|
||||
}, [location.state, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearDraft(draftKey);
|
||||
};
|
||||
}, [clearDraft, draftKey]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const hasValidCache = isCacheValid('claude-api-key');
|
||||
@@ -260,7 +254,9 @@ export function AiProvidersClaudeEditLayout() {
|
||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||
};
|
||||
const available = seededForm.modelEntries.map((entry) => entry.name.trim()).filter(Boolean);
|
||||
const baselineSignature = buildClaudeSignature(seededForm);
|
||||
initDraft(draftKey, {
|
||||
baselineSignature,
|
||||
form: seededForm,
|
||||
testModel: available[0] || '',
|
||||
testStatus: 'idle',
|
||||
@@ -269,8 +265,10 @@ export function AiProvidersClaudeEditLayout() {
|
||||
return;
|
||||
}
|
||||
|
||||
const emptyForm = buildEmptyForm();
|
||||
initDraft(draftKey, {
|
||||
form: buildEmptyForm(),
|
||||
baselineSignature: buildClaudeSignature(emptyForm),
|
||||
form: emptyForm,
|
||||
testModel: '',
|
||||
testStatus: 'idle',
|
||||
testMessage: '',
|
||||
@@ -279,15 +277,8 @@ export function AiProvidersClaudeEditLayout() {
|
||||
|
||||
const resolvedLoading = !draft?.initialized;
|
||||
const currentSignature = useMemo(() => buildClaudeSignature(form), [form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolvedLoading) return;
|
||||
if (baselineDraftKey === draftKey) return;
|
||||
setBaselineDraftKey(draftKey);
|
||||
setBaselineSignature(currentSignature);
|
||||
}, [baselineDraftKey, currentSignature, draftKey, resolvedLoading]);
|
||||
|
||||
const isDirty = baselineDraftKey === draftKey && baselineSignature !== currentSignature;
|
||||
const baselineSignature = draft?.baselineSignature ?? '';
|
||||
const isDirty = Boolean(draft?.initialized) && baselineSignature !== currentSignature;
|
||||
const editorRootPath = useMemo(() => {
|
||||
if (hasIndexParam) {
|
||||
return `/ai-providers/claude/${params.index ?? ''}`;
|
||||
@@ -296,7 +287,7 @@ export function AiProvidersClaudeEditLayout() {
|
||||
}, [hasIndexParam, params.index]);
|
||||
const canGuard = !resolvedLoading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ nextLocation }) => {
|
||||
const nextPath = nextLocation.pathname;
|
||||
@@ -405,6 +396,8 @@ export function AiProvidersClaudeEditLayout() {
|
||||
editIndex !== null ? t('notification.claude_config_updated') : t('notification.claude_config_added'),
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setDraftBaselineSignature(draftKey, buildClaudeSignature(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
showNotification(`${t('notification.update_failed')}: ${getErrorMessage(err)}`, 'error');
|
||||
@@ -412,8 +405,10 @@ export function AiProvidersClaudeEditLayout() {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [
|
||||
allowNextNavigation,
|
||||
clearCache,
|
||||
configs,
|
||||
draftKey,
|
||||
disableControls,
|
||||
editIndex,
|
||||
form,
|
||||
@@ -421,6 +416,7 @@ export function AiProvidersClaudeEditLayout() {
|
||||
invalidIndex,
|
||||
invalidIndexParam,
|
||||
resolvedLoading,
|
||||
setDraftBaselineSignature,
|
||||
saving,
|
||||
showNotification,
|
||||
t,
|
||||
|
||||
@@ -202,7 +202,7 @@ export function AiProvidersCodexEditPage() {
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ currentLocation, nextLocation }) =>
|
||||
isDirty && currentLocation.pathname !== nextLocation.pathname,
|
||||
@@ -381,6 +381,8 @@ export function AiProvidersCodexEditPage() {
|
||||
editIndex !== null ? t('notification.codex_config_updated') : t('notification.codex_config_added'),
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildCodexSignature(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
@@ -390,6 +392,7 @@ export function AiProvidersCodexEditPage() {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [
|
||||
allowNextNavigation,
|
||||
canSave,
|
||||
clearCache,
|
||||
configs,
|
||||
|
||||
@@ -335,7 +335,7 @@ export function AiProvidersGeminiEditPage() {
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ currentLocation, nextLocation }) =>
|
||||
isDirty && currentLocation.pathname !== nextLocation.pathname,
|
||||
@@ -382,6 +382,8 @@ export function AiProvidersGeminiEditPage() {
|
||||
editIndex !== null ? t('notification.gemini_key_updated') : t('notification.gemini_key_added'),
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildGeminiSignature(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
@@ -391,6 +393,7 @@ export function AiProvidersGeminiEditPage() {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [
|
||||
allowNextNavigation,
|
||||
canSave,
|
||||
clearCache,
|
||||
configs,
|
||||
|
||||
@@ -154,8 +154,6 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
() => !isCacheValid('openai-compatibility')
|
||||
);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [baselineDraftKey, setBaselineDraftKey] = useState<string>('');
|
||||
const [baselineSignature, setBaselineSignature] = useState<string>('');
|
||||
|
||||
const draftKey = useMemo(() => {
|
||||
if (invalidIndexParam) return `openai:invalid:${params.index ?? 'unknown'}`;
|
||||
@@ -164,9 +162,10 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
}, [editIndex, invalidIndexParam, params.index]);
|
||||
|
||||
const draft = useOpenAIEditDraftStore((state) => state.drafts[draftKey]);
|
||||
const ensureDraft = useOpenAIEditDraftStore((state) => state.ensureDraft);
|
||||
const acquireDraft = useOpenAIEditDraftStore((state) => state.acquireDraft);
|
||||
const releaseDraft = useOpenAIEditDraftStore((state) => state.releaseDraft);
|
||||
const initDraft = useOpenAIEditDraftStore((state) => state.initDraft);
|
||||
const clearDraft = useOpenAIEditDraftStore((state) => state.clearDraft);
|
||||
const setDraftBaselineSignature = useOpenAIEditDraftStore((state) => state.setDraftBaselineSignature);
|
||||
const setDraftForm = useOpenAIEditDraftStore((state) => state.setDraftForm);
|
||||
const setDraftTestModel = useOpenAIEditDraftStore((state) => state.setDraftTestModel);
|
||||
const setDraftTestStatus = useOpenAIEditDraftStore((state) => state.setDraftTestStatus);
|
||||
@@ -236,8 +235,9 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
ensureDraft(draftKey);
|
||||
}, [draftKey, ensureDraft]);
|
||||
acquireDraft(draftKey);
|
||||
return () => releaseDraft(draftKey);
|
||||
}, [acquireDraft, draftKey, releaseDraft]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
const state = location.state as LocationState;
|
||||
@@ -248,12 +248,6 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
navigate('/ai-providers', { replace: true });
|
||||
}, [location.state, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearDraft(draftKey);
|
||||
};
|
||||
}, [clearDraft, draftKey]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const hasValidCache = isCacheValid('openai-compatibility');
|
||||
@@ -305,7 +299,9 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
initialData.testModel && available.includes(initialData.testModel)
|
||||
? initialData.testModel
|
||||
: available[0] || '';
|
||||
const baselineSignature = buildOpenAISignature(seededForm, initialTestModel);
|
||||
initDraft(draftKey, {
|
||||
baselineSignature,
|
||||
form: seededForm,
|
||||
testModel: initialTestModel,
|
||||
testStatus: 'idle',
|
||||
@@ -313,8 +309,10 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
keyTestStatuses: [],
|
||||
});
|
||||
} else {
|
||||
const emptyForm = buildEmptyForm();
|
||||
initDraft(draftKey, {
|
||||
form: buildEmptyForm(),
|
||||
baselineSignature: buildOpenAISignature(emptyForm, ''),
|
||||
form: emptyForm,
|
||||
testModel: '',
|
||||
testStatus: 'idle',
|
||||
testMessage: '',
|
||||
@@ -376,6 +374,35 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
[setForm, showNotification, t]
|
||||
);
|
||||
|
||||
const resolvedLoading = !draft?.initialized;
|
||||
const currentSignature = useMemo(() => buildOpenAISignature(form, testModel), [form, testModel]);
|
||||
const baselineSignature = draft?.baselineSignature ?? '';
|
||||
const isDirty = Boolean(draft?.initialized) && baselineSignature !== currentSignature;
|
||||
const editorRootPath = useMemo(() => {
|
||||
if (hasIndexParam) {
|
||||
return `/ai-providers/openai/${params.index ?? ''}`;
|
||||
}
|
||||
return '/ai-providers/openai/new';
|
||||
}, [hasIndexParam, params.index]);
|
||||
const canGuard = !resolvedLoading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ nextLocation }) => {
|
||||
const nextPath = nextLocation.pathname;
|
||||
const isWithinRoot =
|
||||
nextPath === editorRootPath || nextPath.startsWith(`${editorRootPath}/`);
|
||||
return isDirty && !isWithinRoot;
|
||||
},
|
||||
dialog: {
|
||||
title: t('common.unsaved_changes_title'),
|
||||
message: t('common.unsaved_changes_message'),
|
||||
confirmText: t('common.leave'),
|
||||
cancelText: t('common.stay'),
|
||||
variant: 'danger',
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const name = form.name.trim();
|
||||
const baseUrl = form.baseUrl.trim();
|
||||
@@ -430,6 +457,8 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
: t('notification.openai_provider_added'),
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setDraftBaselineSignature(draftKey, buildOpenAISignature(form, testModel));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
showNotification(`${t('notification.update_failed')}: ${getErrorMessage(err)}`, 'error');
|
||||
@@ -437,52 +466,19 @@ export function AiProvidersOpenAIEditLayout() {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [
|
||||
allowNextNavigation,
|
||||
draftKey,
|
||||
editIndex,
|
||||
fetchConfig,
|
||||
form,
|
||||
handleBack,
|
||||
providers,
|
||||
testModel,
|
||||
setDraftBaselineSignature,
|
||||
showNotification,
|
||||
t,
|
||||
testModel,
|
||||
]);
|
||||
|
||||
const resolvedLoading = !draft?.initialized;
|
||||
const currentSignature = useMemo(() => buildOpenAISignature(form, testModel), [form, testModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolvedLoading) return;
|
||||
if (baselineDraftKey === draftKey) return;
|
||||
setBaselineDraftKey(draftKey);
|
||||
setBaselineSignature(currentSignature);
|
||||
}, [baselineDraftKey, currentSignature, draftKey, resolvedLoading]);
|
||||
|
||||
const isDirty = baselineDraftKey === draftKey && baselineSignature !== currentSignature;
|
||||
const editorRootPath = useMemo(() => {
|
||||
if (hasIndexParam) {
|
||||
return `/ai-providers/openai/${params.index ?? ''}`;
|
||||
}
|
||||
return '/ai-providers/openai/new';
|
||||
}, [hasIndexParam, params.index]);
|
||||
const canGuard = !resolvedLoading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ nextLocation }) => {
|
||||
const nextPath = nextLocation.pathname;
|
||||
const isWithinRoot =
|
||||
nextPath === editorRootPath || nextPath.startsWith(`${editorRootPath}/`);
|
||||
return isDirty && !isWithinRoot;
|
||||
},
|
||||
dialog: {
|
||||
title: t('common.unsaved_changes_title'),
|
||||
message: t('common.unsaved_changes_message'),
|
||||
confirmText: t('common.leave'),
|
||||
cancelText: t('common.stay'),
|
||||
variant: 'danger',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Outlet
|
||||
context={{
|
||||
|
||||
@@ -180,7 +180,7 @@ export function AiProvidersVertexEditPage() {
|
||||
const isDirty = baselineSignature !== currentSignature;
|
||||
const canGuard = !loading && !saving && !invalidIndexParam && !invalidIndex;
|
||||
|
||||
useUnsavedChangesGuard({
|
||||
const { allowNextNavigation } = useUnsavedChangesGuard({
|
||||
enabled: canGuard,
|
||||
shouldBlock: ({ currentLocation, nextLocation }) =>
|
||||
isDirty && currentLocation.pathname !== nextLocation.pathname,
|
||||
@@ -234,6 +234,8 @@ export function AiProvidersVertexEditPage() {
|
||||
editIndex !== null ? t('notification.vertex_config_updated') : t('notification.vertex_config_added'),
|
||||
'success'
|
||||
);
|
||||
allowNextNavigation();
|
||||
setBaselineSignature(buildVertexSignature(form));
|
||||
handleBack();
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
@@ -243,6 +245,7 @@ export function AiProvidersVertexEditPage() {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [
|
||||
allowNextNavigation,
|
||||
canSave,
|
||||
clearCache,
|
||||
configs,
|
||||
|
||||
@@ -16,6 +16,7 @@ export type ClaudeTestStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
type ClaudeEditDraft = {
|
||||
initialized: boolean;
|
||||
baselineSignature: string;
|
||||
form: ProviderFormState;
|
||||
testModel: string;
|
||||
testStatus: ClaudeTestStatus;
|
||||
@@ -24,11 +25,15 @@ type ClaudeEditDraft = {
|
||||
|
||||
interface ClaudeEditDraftState {
|
||||
drafts: Record<string, ClaudeEditDraft>;
|
||||
refCounts: Record<string, number>;
|
||||
acquireDraft: (key: string) => void;
|
||||
releaseDraft: (key: string) => void;
|
||||
ensureDraft: (key: string) => void;
|
||||
initDraft: (
|
||||
key: string,
|
||||
draft: Omit<ClaudeEditDraft, 'initialized'>
|
||||
) => void;
|
||||
setDraftBaselineSignature: (key: string, signature: string) => void;
|
||||
setDraftForm: (
|
||||
key: string,
|
||||
action: SetStateAction<ProviderFormState>
|
||||
@@ -59,6 +64,7 @@ const buildEmptyForm = (): ProviderFormState => ({
|
||||
|
||||
const buildEmptyDraft = (): ClaudeEditDraft => ({
|
||||
initialized: false,
|
||||
baselineSignature: '',
|
||||
form: buildEmptyForm(),
|
||||
testModel: '',
|
||||
testStatus: 'idle',
|
||||
@@ -67,6 +73,35 @@ const buildEmptyDraft = (): ClaudeEditDraft => ({
|
||||
|
||||
export const useClaudeEditDraftStore = create<ClaudeEditDraftState>((set, get) => ({
|
||||
drafts: {},
|
||||
refCounts: {},
|
||||
|
||||
acquireDraft: (key) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const existingDraft = state.drafts[key];
|
||||
const currentCount = state.refCounts[key] ?? 0;
|
||||
return {
|
||||
drafts: existingDraft ? state.drafts : { ...state.drafts, [key]: buildEmptyDraft() },
|
||||
refCounts: { ...state.refCounts, [key]: currentCount + 1 },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
releaseDraft: (key) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const currentCount = state.refCounts[key];
|
||||
if (!currentCount) return state;
|
||||
if (currentCount > 1) {
|
||||
return { refCounts: { ...state.refCounts, [key]: currentCount - 1 } };
|
||||
}
|
||||
const nextCounts = { ...state.refCounts };
|
||||
delete nextCounts[key];
|
||||
const nextDrafts = { ...state.drafts };
|
||||
delete nextDrafts[key];
|
||||
return { refCounts: nextCounts, drafts: nextDrafts };
|
||||
});
|
||||
},
|
||||
|
||||
ensureDraft: (key) => {
|
||||
if (!key) return;
|
||||
@@ -89,6 +124,19 @@ export const useClaudeEditDraftStore = create<ClaudeEditDraftState>((set, get) =
|
||||
}));
|
||||
},
|
||||
|
||||
setDraftBaselineSignature: (key, signature) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const existing = state.drafts[key] ?? buildEmptyDraft();
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[key]: { ...existing, initialized: true, baselineSignature: signature },
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setDraftForm: (key, action) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type KeyTestStatus = {
|
||||
|
||||
export type OpenAIEditDraft = {
|
||||
initialized: boolean;
|
||||
baselineSignature: string;
|
||||
form: OpenAIFormState;
|
||||
testModel: string;
|
||||
testStatus: OpenAITestStatus;
|
||||
@@ -31,8 +32,12 @@ export type OpenAIEditDraft = {
|
||||
|
||||
interface OpenAIEditDraftState {
|
||||
drafts: Record<string, OpenAIEditDraft>;
|
||||
refCounts: Record<string, number>;
|
||||
acquireDraft: (key: string) => void;
|
||||
releaseDraft: (key: string) => void;
|
||||
ensureDraft: (key: string) => void;
|
||||
initDraft: (key: string, draft: Omit<OpenAIEditDraft, 'initialized'>) => void;
|
||||
setDraftBaselineSignature: (key: string, signature: string) => void;
|
||||
setDraftForm: (key: string, action: SetStateAction<OpenAIFormState>) => void;
|
||||
setDraftTestModel: (key: string, action: SetStateAction<string>) => void;
|
||||
setDraftTestStatus: (key: string, action: SetStateAction<OpenAITestStatus>) => void;
|
||||
@@ -57,6 +62,7 @@ const buildEmptyForm = (): OpenAIFormState => ({
|
||||
|
||||
const buildEmptyDraft = (): OpenAIEditDraft => ({
|
||||
initialized: false,
|
||||
baselineSignature: '',
|
||||
form: buildEmptyForm(),
|
||||
testModel: '',
|
||||
testStatus: 'idle',
|
||||
@@ -66,6 +72,35 @@ const buildEmptyDraft = (): OpenAIEditDraft => ({
|
||||
|
||||
export const useOpenAIEditDraftStore = create<OpenAIEditDraftState>((set, get) => ({
|
||||
drafts: {},
|
||||
refCounts: {},
|
||||
|
||||
acquireDraft: (key) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const existingDraft = state.drafts[key];
|
||||
const currentCount = state.refCounts[key] ?? 0;
|
||||
return {
|
||||
drafts: existingDraft ? state.drafts : { ...state.drafts, [key]: buildEmptyDraft() },
|
||||
refCounts: { ...state.refCounts, [key]: currentCount + 1 },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
releaseDraft: (key) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const currentCount = state.refCounts[key];
|
||||
if (!currentCount) return state;
|
||||
if (currentCount > 1) {
|
||||
return { refCounts: { ...state.refCounts, [key]: currentCount - 1 } };
|
||||
}
|
||||
const nextCounts = { ...state.refCounts };
|
||||
delete nextCounts[key];
|
||||
const nextDrafts = { ...state.drafts };
|
||||
delete nextDrafts[key];
|
||||
return { refCounts: nextCounts, drafts: nextDrafts };
|
||||
});
|
||||
},
|
||||
|
||||
ensureDraft: (key) => {
|
||||
if (!key) return;
|
||||
@@ -88,6 +123,19 @@ export const useOpenAIEditDraftStore = create<OpenAIEditDraftState>((set, get) =
|
||||
}));
|
||||
},
|
||||
|
||||
setDraftBaselineSignature: (key, signature) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const existing = state.drafts[key] ?? buildEmptyDraft();
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[key]: { ...existing, initialized: true, baselineSignature: signature },
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setDraftForm: (key, action) => {
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
@@ -186,4 +234,3 @@ export const useOpenAIEditDraftStore = create<OpenAIEditDraftState>((set, get) =
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user