"use client";
import { useEffect, useRef, useState } from "react";
import { ChatComposer } from "@/components/chat/chat-composer";
import { ChatEmptyState } from "@/components/chat/chat-empty-state";
import { ChatHeader } from "@/components/chat/chat-header";
import { ChatMessageList } from "@/components/chat/chat-message-list";
import { ChatSidebar } from "@/components/chat/chat-sidebar";
import { useChat } from "@/hooks/use-chat";
import { useModels } from "@/hooks/use-models";
import type { AuthIdentity } from "@/lib/auth/types";
type ChatAppProps = {
user: AuthIdentity;
onLogout: () => void;
};
export function ChatApp({ user, onLogout }: ChatAppProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const mobileDrawerRef = useRef<HTMLElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
const catalog = useModels(user.id, user.lastModelId, onLogout);
const chat = useChat(user.id, onLogout, user.lastModelId);
const { activeModelId, setModel } = chat;
const sidebarProps = {
activeConversationId: chat.activeConversation?.id ?? null,
conversations: chat.conversations,
onDeleteConversation: chat.deleteConversation,
onLogout,
onNewConversation: chat.newConversation,
onSelectConversation: chat.selectConversation,
user,
};
useEffect(() => {
if (
catalog.status !== "loading" &&
!catalog.models.some((model) => model.id === activeModelId)
) {
setModel(catalog.effectiveModelId);
}
}, [activeModelId, catalog.effectiveModelId, catalog.models, catalog.status, setModel]);
useEffect(() => {
if (!mobileMenuOpen) return;
const drawer = mobileDrawerRef.current;
const focusable = drawer?.querySelectorAll<HTMLElement>(
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
setMobileMenuOpen(false);
return;
}
if (event.key !== "Tab" || !focusable?.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
previousFocusRef.current?.focus();
};
}, [mobileMenuOpen]);
useEffect(() => {
if (typeof window.matchMedia !== "function") return;
const desktopQuery = window.matchMedia("(min-width: 768px)");
function closeMobileDrawer(event: MediaQueryListEvent) {
if (!event.matches) return;
previousFocusRef.current = null;
setMobileMenuOpen(false);
}
desktopQuery.addEventListener("change", closeMobileDrawer);
return () => desktopQuery.removeEventListener("change", closeMobileDrawer);
}, []);
return (
<main className="flex h-screen min-h-0 overflow-hidden bg-background">
<aside
className="hidden w-[272px] shrink-0 border-r border-sidebar-border md:block"
inert={mobileMenuOpen ? true : undefined}
>
<ChatSidebar {...sidebarProps} />
</aside>
{mobileMenuOpen ? (
<div className="fixed inset-0 z-40 md:hidden">
<button
aria-label="Закрыть меню"
className="chat-overlay absolute inset-0 bg-foreground/25"
onClick={() => setMobileMenuOpen(false)}
type="button"
/>
<aside
aria-label="История чатов"
aria-modal="true"
className="relative h-full w-[272px] max-w-[85vw] border-r border-sidebar-border shadow-xl"
ref={mobileDrawerRef}
role="dialog"
>
<ChatSidebar
{...sidebarProps}
onClose={() => setMobileMenuOpen(false)}
/>
</aside>
</div>
) : null}
<div
className="flex min-w-0 flex-1 flex-col"
inert={mobileMenuOpen ? true : undefined}
>
<ChatHeader
models={catalog.models}
modelId={chat.activeModelId}
onModelChange={chat.setModel}
onOpenMenu={() => {
previousFocusRef.current = document.activeElement as HTMLElement;
setMobileMenuOpen(true);
}}
quota={chat.quota}
/>
<div className="min-h-0 flex-1 overflow-y-auto">
{chat.activeConversation ? (
<ChatMessageList
canRetryLast={chat.canRetryLast}
conversation={chat.activeConversation}
isGenerating={chat.isGenerating}
onRetry={chat.retryLast}
/>
) : (
<ChatEmptyState firstName={user.firstName} onSuggestion={chat.sendMessage} />
)}
</div>
<ChatComposer
isGenerating={chat.isGenerating}
onSend={chat.sendMessage}
onStop={chat.stopGenerating}
quotaExhausted={
chat.quota.status === "ready" && chat.quota.snapshot.exhausted
}
/>
</div>
</main>
);
}