From b790005c85e9f25fd14a14ac01b79e7562f1a1ea Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 21 Dec 2025 01:04:35 -0500 Subject: [PATCH 1/2] fix(ui): add unsaved changes confirmation when switching profiles - Add onHasChangesUpdate callback to ProfileEditor - Track editor dirty state in ApiPage parent - Show confirmation dialog before discarding unsaved changes - Handle edge case for 'New' profile button Closes #163 --- ui/src/components/profiles/editor/index.tsx | 9 +++-- ui/src/components/profiles/editor/types.ts | 1 + ui/src/pages/api.tsx | 37 ++++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ui/src/components/profiles/editor/index.tsx b/ui/src/components/profiles/editor/index.tsx index 01fc8b5b..da5f98c2 100644 --- a/ui/src/components/profiles/editor/index.tsx +++ b/ui/src/components/profiles/editor/index.tsx @@ -4,7 +4,7 @@ */ /* eslint-disable react-refresh/only-export-components */ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/shared/confirm-dialog'; @@ -16,7 +16,7 @@ import { FriendlyUISection } from './friendly-ui-section'; import { RawEditorSection } from './raw-editor-section'; import type { ProfileEditorProps, Settings, SettingsResponse } from './types'; -export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) { +export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: ProfileEditorProps) { const [localEdits, setLocalEdits] = useState>({}); const [conflictDialog, setConflictDialog] = useState(false); const [rawJsonEdits, setRawJsonEdits] = useState(null); @@ -100,6 +100,11 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) { return Object.keys(localEdits).length > 0; }, [rawJsonEdits, localEdits, settings]); + // Notify parent of hasChanges state + useEffect(() => { + onHasChangesUpdate?.(computedHasChanges); + }, [computedHasChanges, onHasChangesUpdate]); + // Save mutation const saveMutation = useMutation({ mutationFn: async () => { diff --git a/ui/src/components/profiles/editor/types.ts b/ui/src/components/profiles/editor/types.ts index d7e36a13..950ae645 100644 --- a/ui/src/components/profiles/editor/types.ts +++ b/ui/src/components/profiles/editor/types.ts @@ -16,4 +16,5 @@ export interface SettingsResponse { export interface ProfileEditorProps { profileName: string; onDelete?: () => void; + onHasChangesUpdate?: (hasChanges: boolean) => void; } diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index 53671c79..dce6f8c7 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -37,6 +37,8 @@ export function ApiPage() { const [isCreateDialogOpen, setCreateDialogOpen] = useState(false); const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal'); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [editorHasChanges, setEditorHasChanges] = useState(false); + const [pendingSwitch, setPendingSwitch] = useState(null); // Prefetch OpenRouter models when page loads (lazy - won't block render) useOpenRouterModels(); @@ -71,7 +73,21 @@ export function ApiPage() { // Handle create success const handleCreateSuccess = (name: string) => { setCreateDialogOpen(false); - setSelectedProfile(name); + // Use the same unsaved changes check as profile selection + if (editorHasChanges && selectedProfile !== null) { + setPendingSwitch(name); + } else { + setSelectedProfile(name); + } + }; + + // Handle profile selection with unsaved changes check + const handleProfileSelect = (name: string) => { + if (editorHasChanges && selectedProfile !== name) { + setPendingSwitch(name); + } else { + setSelectedProfile(name); + } }; return ( @@ -168,9 +184,7 @@ export function ApiPage() { key={profile.name} profile={profile} isSelected={selectedProfile === profile.name} - onSelect={() => { - setSelectedProfile(profile.name); - }} + onSelect={() => handleProfileSelect(profile.name)} onDelete={() => setDeleteConfirm(profile.name)} /> ))} @@ -208,6 +222,7 @@ export function ApiPage() { setDeleteConfirm(selectedProfileData.name)} + onHasChangesUpdate={setEditorHasChanges} /> ) : ( deleteConfirm && handleDelete(deleteConfirm)} onCancel={() => setDeleteConfirm(null)} /> + + {/* Unsaved Changes Confirmation */} + { + setSelectedProfile(pendingSwitch); + setPendingSwitch(null); + }} + onCancel={() => setPendingSwitch(null)} + /> ); } From 86d992fce623a8378d5f53b1aff7b53d2f80e3c4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 21 Dec 2025 01:20:57 -0500 Subject: [PATCH 2/2] fix(ui): fix profile switching and improve UX - Add key prop to ProfileEditor to force remount on profile change - Reset editorHasChanges on discard confirmation - Add text-white to destructive dialog button - Change OpenRouter default model to claude-opus-4.5 --- src/api/services/provider-presets.ts | 2 +- ui/src/components/shared/confirm-dialog.tsx | 2 +- ui/src/lib/provider-presets.ts | 2 +- ui/src/pages/api.tsx | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/services/provider-presets.ts b/src/api/services/provider-presets.ts index d4d1d1a0..8688cf12 100644 --- a/src/api/services/provider-presets.ts +++ b/src/api/services/provider-presets.ts @@ -38,7 +38,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ description: '349+ models from OpenAI, Anthropic, Google, Meta', baseUrl: OPENROUTER_BASE_URL, defaultProfileName: 'openrouter', - defaultModel: 'anthropic/claude-sonnet-4', + defaultModel: 'anthropic/claude-opus-4.5', apiKeyPlaceholder: 'sk-or-...', apiKeyHint: 'Get your API key at openrouter.ai/keys', category: 'recommended', diff --git a/ui/src/components/shared/confirm-dialog.tsx b/ui/src/components/shared/confirm-dialog.tsx index 43d7f940..c680a278 100644 --- a/ui/src/components/shared/confirm-dialog.tsx +++ b/ui/src/components/shared/confirm-dialog.tsx @@ -39,7 +39,7 @@ export function ConfirmDialog({ Cancel {confirmText} diff --git a/ui/src/lib/provider-presets.ts b/ui/src/lib/provider-presets.ts index 6761640c..c91a0ffd 100644 --- a/ui/src/lib/provider-presets.ts +++ b/ui/src/lib/provider-presets.ts @@ -34,7 +34,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ badge: '349+ models', featured: true, icon: '/icons/openrouter.svg', - defaultModel: 'anthropic/claude-sonnet-4', + defaultModel: 'anthropic/claude-opus-4.5', requiresApiKey: true, apiKeyPlaceholder: 'sk-or-...', apiKeyHint: 'Get your API key at openrouter.ai/keys', diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index dce6f8c7..0152d04a 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -220,6 +220,7 @@ export function ApiPage() {
{selectedProfileData ? ( setDeleteConfirm(selectedProfileData.name)} onHasChangesUpdate={setEditorHasChanges} @@ -266,6 +267,7 @@ export function ApiPage() { confirmText="Discard & Switch" variant="destructive" onConfirm={() => { + setEditorHasChanges(false); setSelectedProfile(pendingSwitch); setPendingSwitch(null); }}