Files
stack/apps/web/src/hooks/useChatOverlay.ts
Jason Woltje 0669c7cb77
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
feat(#42): Implement persistent Jarvis chat overlay
Add a persistent chat overlay accessible from any authenticated view.
The overlay wraps the existing Chat component and adds state management,
keyboard shortcuts, and responsive design.

Features:
- Three states: Closed (floating button), Open (full panel), Minimized (header)
- Keyboard shortcuts:
  - Cmd/Ctrl + K: Open chat (when closed)
  - Escape: Minimize chat (when open)
  - Cmd/Ctrl + Shift + J: Toggle chat panel
- State persistence via localStorage
- Responsive design (full-width mobile, sidebar desktop)
- PDA-friendly design with calm colors
- 32 comprehensive tests (14 hook tests + 18 component tests)

Files added:
- apps/web/src/hooks/useChatOverlay.ts
- apps/web/src/hooks/useChatOverlay.test.ts
- apps/web/src/components/chat/ChatOverlay.tsx
- apps/web/src/components/chat/ChatOverlay.test.tsx

Files modified:
- apps/web/src/components/chat/index.ts (added export)
- apps/web/src/app/(authenticated)/layout.tsx (integrated overlay)

All tests passing (490 tests, 50 test files)
All lint checks passing
Build succeeds

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 20:24:41 -06:00

110 lines
2.4 KiB
TypeScript

/**
* @file useChatOverlay.ts
* @description Hook for managing the global chat overlay state
*/
import { useState, useEffect, useCallback } from "react";
interface ChatOverlayState {
isOpen: boolean;
isMinimized: boolean;
}
interface UseChatOverlayResult extends ChatOverlayState {
open: () => void;
close: () => void;
minimize: () => void;
expand: () => void;
toggle: () => void;
toggleMinimize: () => void;
}
const STORAGE_KEY = "chatOverlayState";
const DEFAULT_STATE: ChatOverlayState = {
isOpen: false,
isMinimized: false,
};
/**
* Load state from localStorage
*/
function loadState(): ChatOverlayState {
if (typeof window === "undefined") {
return DEFAULT_STATE;
}
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored) as ChatOverlayState;
}
} catch (error) {
console.warn("Failed to load chat overlay state from localStorage:", error);
}
return DEFAULT_STATE;
}
/**
* Save state to localStorage
*/
function saveState(state: ChatOverlayState): void {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (error) {
console.warn("Failed to save chat overlay state to localStorage:", error);
}
}
/**
* Custom hook for managing chat overlay state
* Persists state to localStorage for consistency across page refreshes
*/
export function useChatOverlay(): UseChatOverlayResult {
const [state, setState] = useState<ChatOverlayState>(loadState);
// Persist state changes to localStorage
useEffect(() => {
saveState(state);
}, [state]);
const open = useCallback(() => {
setState({ isOpen: true, isMinimized: false });
}, []);
const close = useCallback(() => {
setState((prev) => ({ ...prev, isOpen: false }));
}, []);
const minimize = useCallback(() => {
setState((prev) => ({ ...prev, isMinimized: true }));
}, []);
const expand = useCallback(() => {
setState((prev) => ({ ...prev, isMinimized: false }));
}, []);
const toggle = useCallback(() => {
setState((prev) => ({ ...prev, isOpen: !prev.isOpen }));
}, []);
const toggleMinimize = useCallback(() => {
setState((prev) => ({ ...prev, isMinimized: !prev.isMinimized }));
}, []);
return {
...state,
open,
close,
minimize,
expand,
toggle,
toggleMinimize,
};
}