refactor: replace useLocalStorage with useUiPreferences for better state management in AppContent

This commit is contained in:
Haileyesus
2026-02-06 11:22:48 +03:00
parent a259c39c05
commit 2f9d699107
2 changed files with 187 additions and 15 deletions

View 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,
};
}