'use client'; import { useCallback, useMemo } from 'react'; import { useLocalStorage } from '@djangocfg/ui-core/hooks'; import { CHAT_SETTINGS_STORAGE_KEY, DEFAULT_CHAT_SETTINGS, type ChatAudioSettings, type ChatDockSettings, type ChatPageContextSettings, type ChatSettings, type ChatSpeechSettings, } from './types'; /** Options for {@link useChatSettings}. */ export interface UseChatSettingsOptions { /** * Override the storage key (per-product scoping). Defaults to the * shared {@link CHAT_SETTINGS_STORAGE_KEY}. */ storageKey?: string; /** * Override baseline defaults — merged over {@link DEFAULT_CHAT_SETTINGS} * at the slice level (per-product branding, host opt-ins, etc.). */ defaults?: Partial; } /** Everything {@link useChatSettings} exposes. */ export interface UseChatSettingsReturn { /** The full, typed settings object (each slice always present). */ settings: ChatSettings; /** * Top-level grouped patch — shallow-merges whole slices. Use this for * cross-slice updates; for a single slice prefer the typed helpers. */ patch: (partial: Partial) => void; /** Merge a subset of dock fields. */ updateDock: (partial: Partial) => void; /** Merge a subset of audio fields. */ updateAudio: (partial: Partial) => void; /** Merge a subset of speech fields. */ updateSpeech: (partial: Partial) => void; /** Merge a subset of page-context fields. */ updatePageContext: (partial: Partial) => void; /** Master audio mute shortcut. */ setAudioMuted: (muted: boolean) => void; /** Page-context opt-in shortcut. */ setPageContextLinked: (linked: boolean) => void; /** Reset every slice back to defaults. */ reset: () => void; } /** Shallow-merge `defaults` slices over the baseline. */ function resolveDefaults(defaults?: Partial): ChatSettings { if (!defaults) return DEFAULT_CHAT_SETTINGS; return { dock: { ...DEFAULT_CHAT_SETTINGS.dock, ...defaults.dock }, audio: { ...DEFAULT_CHAT_SETTINGS.audio, ...defaults.audio }, speech: { ...DEFAULT_CHAT_SETTINGS.speech, ...defaults.speech }, pageContext: { ...DEFAULT_CHAT_SETTINGS.pageContext, ...defaults.pageContext, }, }; } /** * The single entry point for chat-owned persisted settings. * * Every chat feature that wants to remember a preference goes through * here instead of calling `useLocalStorage` with its own key. Why: * * - One typed object, one storage key — "the chat's settings" is a single * thing that can be reasoned about, exported, or reset wholesale. * - Coalesced writes: `useLocalStorage` (Part 1) updates React state * synchronously but batches the actual `localStorage` write to one per * tick — so several features patching different slices in the same * render commit collapse into a single write, no thrash. * - Cross-instance sync: two `useChatSettings()` consumers (e.g. the * header audio toggle and the settings panel) stay in lockstep within * the same tab and across tabs. * * Each helper updates only its own slice, leaving sibling slices * untouched — a feature never has to know about the others. * * SSR-safe: returns defaults on the server, hydrates on mount. */ export function useChatSettings( opts: UseChatSettingsOptions = {}, ): UseChatSettingsReturn { const key = opts.storageKey ?? CHAT_SETTINGS_STORAGE_KEY; // Memoized so the resolved defaults keep a stable identity — passing a // fresh object to `useLocalStorage` each render is harmless but wasteful. const initial = useMemo( () => resolveDefaults(opts.defaults), [opts.defaults], ); const [settings, setSettings, , patch] = useLocalStorage( key, initial, ); // Slice updaters: read the current slice, shallow-merge the partial, // write back via the coalescing top-level `patch`. The inner // `useLocalStorage` `patch` already skips no-op writes. const updateDock = useCallback( (partial: Partial) => { patch({ dock: { ...settings.dock, ...partial } }); }, [patch, settings.dock], ); const updateAudio = useCallback( (partial: Partial) => { patch({ audio: { ...settings.audio, ...partial } }); }, [patch, settings.audio], ); const updateSpeech = useCallback( (partial: Partial) => { patch({ speech: { ...settings.speech, ...partial } }); }, [patch, settings.speech], ); const updatePageContext = useCallback( (partial: Partial) => { patch({ pageContext: { ...settings.pageContext, ...partial } }); }, [patch, settings.pageContext], ); const setAudioMuted = useCallback( (muted: boolean) => updateAudio({ muted }), [updateAudio], ); const setPageContextLinked = useCallback( (linked: boolean) => updatePageContext({ linked }), [updatePageContext], ); const reset = useCallback( () => setSettings(initial), [setSettings, initial], ); return { settings, patch, updateDock, updateAudio, updateSpeech, updatePageContext, setAudioMuted, setPageContextLinked, reset, }; }