diff --git a/src/App.tsx b/src/App.tsx
index 548cfcc5..c2938e3d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 (
+ <>
+
+
+
+ >
+ );
+}
+
+const router = createHashRouter([
+ {
+ element: ,
+ children: [
+ { path: '/login', element: },
+ {
+ path: '/*',
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+]);
+
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 (
-
-
-
-
- } />
-
-
-
- }
- />
-
-
- );
+ return ;
}
export default App;
diff --git a/src/hooks/useUnsavedChangesGuard.ts b/src/hooks/useUnsavedChangesGuard.ts
index 4eddb866..14e2cf2a 100644
--- a/src/hooks/useUnsavedChangesGuard.ts
+++ b/src/hooks/useUnsavedChangesGuard.ts
@@ -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('');
+ const allowNextNavigationRef = useRef(false);
+ const location = useLocation();
+
+ const allowNextNavigation = useCallback(() => {
+ allowNextNavigationRef.current = true;
+ }, []);
const shouldBlockFunction = useCallback(
(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 };
+}
diff --git a/src/pages/AiProvidersAmpcodeEditPage.tsx b/src/pages/AiProvidersAmpcodeEditPage.tsx
index a416e8a6..fc98a271 100644
--- a/src/pages/AiProvidersAmpcodeEditPage.tsx
+++ b/src/pages/AiProvidersAmpcodeEditPage.tsx
@@ -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);
diff --git a/src/pages/AiProvidersClaudeEditLayout.tsx b/src/pages/AiProvidersClaudeEditLayout.tsx
index cc9296c9..1f811866 100644
--- a/src/pages/AiProvidersClaudeEditLayout.tsx
+++ b/src/pages/AiProvidersClaudeEditLayout.tsx
@@ -139,8 +139,6 @@ export function AiProvidersClaudeEditLayout() {
const [configs, setConfigs] = useState(() => config?.claudeApiKeys ?? []);
const [loading, setLoading] = useState(() => !isCacheValid('claude-api-key'));
const [saving, setSaving] = useState(false);
- const [baselineDraftKey, setBaselineDraftKey] = useState('');
- const [baselineSignature, setBaselineSignature] = useState('');
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,
diff --git a/src/pages/AiProvidersCodexEditPage.tsx b/src/pages/AiProvidersCodexEditPage.tsx
index 7d98bbbf..6691fd25 100644
--- a/src/pages/AiProvidersCodexEditPage.tsx
+++ b/src/pages/AiProvidersCodexEditPage.tsx
@@ -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,
diff --git a/src/pages/AiProvidersGeminiEditPage.tsx b/src/pages/AiProvidersGeminiEditPage.tsx
index 3b77294c..84fe6e9a 100644
--- a/src/pages/AiProvidersGeminiEditPage.tsx
+++ b/src/pages/AiProvidersGeminiEditPage.tsx
@@ -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,
diff --git a/src/pages/AiProvidersOpenAIEditLayout.tsx b/src/pages/AiProvidersOpenAIEditLayout.tsx
index aaa46b1e..2875aaa7 100644
--- a/src/pages/AiProvidersOpenAIEditLayout.tsx
+++ b/src/pages/AiProvidersOpenAIEditLayout.tsx
@@ -154,8 +154,6 @@ export function AiProvidersOpenAIEditLayout() {
() => !isCacheValid('openai-compatibility')
);
const [saving, setSaving] = useState(false);
- const [baselineDraftKey, setBaselineDraftKey] = useState('');
- const [baselineSignature, setBaselineSignature] = useState('');
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 (
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,
diff --git a/src/stores/useClaudeEditDraftStore.ts b/src/stores/useClaudeEditDraftStore.ts
index b284d2b2..d7ab7aa1 100644
--- a/src/stores/useClaudeEditDraftStore.ts
+++ b/src/stores/useClaudeEditDraftStore.ts
@@ -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;
+ refCounts: Record;
+ acquireDraft: (key: string) => void;
+ releaseDraft: (key: string) => void;
ensureDraft: (key: string) => void;
initDraft: (
key: string,
draft: Omit
) => void;
+ setDraftBaselineSignature: (key: string, signature: string) => void;
setDraftForm: (
key: string,
action: SetStateAction
@@ -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((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((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) => {
diff --git a/src/stores/useOpenAIEditDraftStore.ts b/src/stores/useOpenAIEditDraftStore.ts
index 4c8566c0..cff52a0a 100644
--- a/src/stores/useOpenAIEditDraftStore.ts
+++ b/src/stores/useOpenAIEditDraftStore.ts
@@ -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;
+ refCounts: Record;
+ acquireDraft: (key: string) => void;
+ releaseDraft: (key: string) => void;
ensureDraft: (key: string) => void;
initDraft: (key: string, draft: Omit) => void;
+ setDraftBaselineSignature: (key: string, signature: string) => void;
setDraftForm: (key: string, action: SetStateAction) => void;
setDraftTestModel: (key: string, action: SetStateAction) => void;
setDraftTestStatus: (key: string, action: SetStateAction) => 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((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((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((set, get) =
});
},
}));
-