mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-02 10:33:00 +08:00
refactor: replace useLocalStorage with useUiPreferences for better state management in AppContent
This commit is contained in:
26
src/App.jsx
26
src/App.jsx
@@ -32,7 +32,7 @@ import { TaskMasterProvider } from './contexts/TaskMasterContext';
|
|||||||
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
||||||
import { WebSocketProvider, useWebSocket } from './contexts/WebSocketContext';
|
import { WebSocketProvider, useWebSocket } from './contexts/WebSocketContext';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
import useLocalStorage from './hooks/useLocalStorage';
|
import { useUiPreferences } from './hooks/useUiPreferences';
|
||||||
import { api, authenticatedFetch } from './utils/api';
|
import { api, authenticatedFetch } from './utils/api';
|
||||||
import { I18nextProvider, useTranslation } from 'react-i18next';
|
import { I18nextProvider, useTranslation } from 'react-i18next';
|
||||||
import i18n from './i18n/config.js';
|
import i18n from './i18n/config.js';
|
||||||
@@ -60,12 +60,8 @@ function AppContent() {
|
|||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
|
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
|
||||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||||
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
|
const { preferences, setPreference } = useUiPreferences();
|
||||||
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
|
const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, sidebarVisible } = preferences;
|
||||||
const [showThinking, setShowThinking] = useLocalStorage('showThinking', true);
|
|
||||||
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
|
|
||||||
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
|
|
||||||
const [sidebarVisible, setSidebarVisible] = useLocalStorage('sidebarVisible', true);
|
|
||||||
// Session Protection System: Track sessions with active conversations to prevent
|
// Session Protection System: Track sessions with active conversations to prevent
|
||||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||||
// a message, the session is marked as "active" and project updates are paused
|
// a message, the session is marked as "active" and project updates are paused
|
||||||
@@ -600,9 +596,9 @@ function AppContent() {
|
|||||||
onShowSettings={() => setShowSettings(true)}
|
onShowSettings={() => setShowSettings(true)}
|
||||||
isPWA={isPWA}
|
isPWA={isPWA}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onToggleSidebar={() => setSidebarVisible(false)}
|
onToggleSidebar={() => setPreference('sidebarVisible', false)}
|
||||||
isCollapsed={!sidebarVisible}
|
isCollapsed={!sidebarVisible}
|
||||||
onExpandSidebar={() => setSidebarVisible(true)}
|
onExpandSidebar={() => setPreference('sidebarVisible', true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -647,7 +643,7 @@ function AppContent() {
|
|||||||
onShowSettings={() => setShowSettings(true)}
|
onShowSettings={() => setShowSettings(true)}
|
||||||
isPWA={isPWA}
|
isPWA={isPWA}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onToggleSidebar={() => setSidebarVisible(false)}
|
onToggleSidebar={() => setPreference('sidebarVisible', false)}
|
||||||
isCollapsed={false}
|
isCollapsed={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -700,15 +696,15 @@ function AppContent() {
|
|||||||
isOpen={showQuickSettings}
|
isOpen={showQuickSettings}
|
||||||
onToggle={setShowQuickSettings}
|
onToggle={setShowQuickSettings}
|
||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
onAutoExpandChange={setAutoExpandTools}
|
onAutoExpandChange={(value) => setPreference('autoExpandTools', value)}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
onShowRawParametersChange={setShowRawParameters}
|
onShowRawParametersChange={(value) => setPreference('showRawParameters', value)}
|
||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
onShowThinkingChange={setShowThinking}
|
onShowThinkingChange={(value) => setPreference('showThinking', value)}
|
||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
onAutoScrollChange={setAutoScrollToBottom}
|
onAutoScrollChange={(value) => setPreference('autoScrollToBottom', value)}
|
||||||
sendByCtrlEnter={sendByCtrlEnter}
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
onSendByCtrlEnterChange={setSendByCtrlEnter}
|
onSendByCtrlEnterChange={(value) => setPreference('sendByCtrlEnter', value)}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
176
src/hooks/useUiPreferences.ts
Normal file
176
src/hooks/useUiPreferences.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
type UiPreferences = {
|
||||||
|
autoExpandTools: boolean;
|
||||||
|
showRawParameters: boolean;
|
||||||
|
showThinking: boolean;
|
||||||
|
autoScrollToBottom: boolean;
|
||||||
|
sendByCtrlEnter: boolean;
|
||||||
|
sidebarVisible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UiPreferenceKey = keyof UiPreferences;
|
||||||
|
|
||||||
|
type SetPreferenceAction = {
|
||||||
|
type: 'set';
|
||||||
|
key: UiPreferenceKey;
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetManyPreferencesAction = {
|
||||||
|
type: 'set_many';
|
||||||
|
value?: Partial<Record<UiPreferenceKey, unknown>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResetPreferencesAction = {
|
||||||
|
type: 'reset';
|
||||||
|
value?: Partial<UiPreferences>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UiPreferencesAction =
|
||||||
|
| SetPreferenceAction
|
||||||
|
| SetManyPreferencesAction
|
||||||
|
| ResetPreferencesAction;
|
||||||
|
|
||||||
|
const DEFAULTS: UiPreferences = {
|
||||||
|
autoExpandTools: false,
|
||||||
|
showRawParameters: false,
|
||||||
|
showThinking: true,
|
||||||
|
autoScrollToBottom: true,
|
||||||
|
sendByCtrlEnter: false,
|
||||||
|
sidebarVisible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PREFERENCE_KEYS = Object.keys(DEFAULTS) as UiPreferenceKey[];
|
||||||
|
const VALID_KEYS = new Set<UiPreferenceKey>(PREFERENCE_KEYS); // prevents unknown keys from being written
|
||||||
|
|
||||||
|
const parseBoolean = (value: unknown, fallback: boolean): boolean => {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (value === 'true') return true;
|
||||||
|
if (value === 'false') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readLegacyPreference = (key: UiPreferenceKey, fallback: boolean): boolean => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (raw === null) return fallback;
|
||||||
|
|
||||||
|
// Supports values written by both JSON.stringify and plain strings.
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return parseBoolean(parsed, fallback);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readInitialPreferences = (storageKey: string): UiPreferences => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return DEFAULTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
const parsedRecord = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
|
return PREFERENCE_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = parseBoolean(parsedRecord[key], DEFAULTS[key]);
|
||||||
|
return acc;
|
||||||
|
}, { ...DEFAULTS });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to legacy keys when unified key is missing or invalid.
|
||||||
|
}
|
||||||
|
|
||||||
|
return PREFERENCE_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = readLegacyPreference(key, DEFAULTS[key]);
|
||||||
|
return acc;
|
||||||
|
}, { ...DEFAULTS });
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state: UiPreferences, action: UiPreferencesAction): UiPreferences {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'set': {
|
||||||
|
const { key, value } = action;
|
||||||
|
if (!VALID_KEYS.has(key)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValue = parseBoolean(value, state[key]);
|
||||||
|
if (state[key] === nextValue) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, [key]: nextValue };
|
||||||
|
}
|
||||||
|
case 'set_many': {
|
||||||
|
const updates = action.value || {};
|
||||||
|
let changed = false;
|
||||||
|
const nextState = { ...state };
|
||||||
|
|
||||||
|
for (const key of PREFERENCE_KEYS) {
|
||||||
|
if (!(key in updates)) continue;
|
||||||
|
|
||||||
|
const value = updates[key];
|
||||||
|
const nextValue = parseBoolean(value, state[key]);
|
||||||
|
if (nextState[key] !== nextValue) {
|
||||||
|
nextState[key] = nextValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? nextState : state;
|
||||||
|
}
|
||||||
|
case 'reset':
|
||||||
|
return { ...DEFAULTS, ...(action.value || {}) };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUiPreferences(storageKey = 'uiPreferences') {
|
||||||
|
const [state, dispatch] = useReducer(
|
||||||
|
reducer,
|
||||||
|
storageKey,
|
||||||
|
readInitialPreferences
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(state));
|
||||||
|
}, [state, storageKey]);
|
||||||
|
|
||||||
|
const setPreference = (key: UiPreferenceKey, value: unknown) => {
|
||||||
|
dispatch({ type: 'set', key, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPreferences = (value: Partial<Record<UiPreferenceKey, unknown>>) => {
|
||||||
|
dispatch({ type: 'set_many', value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPreferences = (value?: Partial<UiPreferences>) => {
|
||||||
|
dispatch({ type: 'reset', value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
preferences: state,
|
||||||
|
setPreference,
|
||||||
|
setPreferences,
|
||||||
|
resetPreferences,
|
||||||
|
dispatch,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user