nexus/ui/src/context/ChatPanelContext.tsx
Nexus Dev ea0b12d87b feat(21-04): create ChatPanelContext, ChatInput, and ChatMessage components
- ChatPanelContext with localStorage persistence (nexus:chat-panel-open)
- ChatInput with Enter/Shift+Enter/Escape keyboard shortcuts and auto-resize
- ChatMessage renders user bubbles (bg-secondary) and assistant markdown via ChatMarkdownMessage
- ChatInput.test.tsx with 6 passing tests (keyboard shortcuts, max-height, submit state)
2026-04-02 15:08:50 +00:00

62 lines
1.6 KiB
TypeScript

import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
const STORAGE_KEY = "nexus:chat-panel-open";
interface ChatPanelContextValue {
chatOpen: boolean;
activeConversationId: string | null;
setChatOpen: (open: boolean) => void;
toggleChat: () => void;
setActiveConversationId: (id: string | null) => void;
}
const ChatPanelContext = createContext<ChatPanelContextValue | null>(null);
function readPreference(): boolean {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw === "true";
} catch {
return false;
}
}
function writePreference(open: boolean) {
try {
localStorage.setItem(STORAGE_KEY, String(open));
} catch {
/* ignore */
}
}
export function ChatPanelProvider({ children }: { children: ReactNode }) {
const [chatOpen, setChatOpenState] = useState(readPreference);
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
const setChatOpen = useCallback((open: boolean) => {
setChatOpenState(open);
writePreference(open);
}, []);
const toggleChat = useCallback(() => {
setChatOpenState((prev) => {
const next = !prev;
writePreference(next);
return next;
});
}, []);
return (
<ChatPanelContext.Provider
value={{ chatOpen, activeConversationId, setChatOpen, toggleChat, setActiveConversationId }}
>
{children}
</ChatPanelContext.Provider>
);
}
export function useChatPanel() {
const ctx = useContext(ChatPanelContext);
if (!ctx) throw new Error("useChatPanel must be used within ChatPanelProvider");
return ctx;
}