diff --git a/dashboard/components/Modal.js b/dashboard/components/Modal.js index 5547168..4a1bccd 100644 --- a/dashboard/components/Modal.js +++ b/dashboard/components/Modal.js @@ -1,4 +1,4 @@ -import { html, useState, useEffect, useRef } from '../lib/preact.js'; +import { html, useState, useEffect, useRef, useCallback } from '../lib/preact.js'; import { getStatusMeta, getUserMessageBg } from '../utils/status.js'; import { formatDuration, formatTime } from '../utils/formatting.js'; import { MessageBubble, filterDisplayMessages } from './MessageBubble.js'; @@ -7,28 +7,28 @@ export function Modal({ session, conversations, conversationLoading, onClose, on const [inputValue, setInputValue] = useState(''); const [sending, setSending] = useState(false); const [inputFocused, setInputFocused] = useState(false); + const [closing, setClosing] = useState(false); const inputRef = useRef(null); - - if (!session) return null; - - const conversation = conversations[session.session_id] || []; - const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0; - const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0; - const status = getStatusMeta(session.status); - const agent = session.agent === 'codex' ? 'codex' : 'claude'; - const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude'; - - // Track if user has scrolled away from bottom const wasAtBottomRef = useRef(true); const prevConversationLenRef = useRef(0); const chatContainerRef = useRef(null); - // Initialize scroll position to bottom on mount (no animation) + const conversation = session ? (conversations[session.session_id] || []) : []; + + // Reset state when session changes useEffect(() => { - if (chatContainerRef.current) { - chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; - } - }, []); + setClosing(false); + prevConversationLenRef.current = 0; + }, [session?.session_id]); + + // Animated close handler + const handleClose = useCallback(() => { + setClosing(true); + setTimeout(() => { + setClosing(false); + onClose(); + }, 200); + }, [onClose]); // Track scroll position useEffect(() => { @@ -62,39 +62,42 @@ export function Modal({ session, conversations, conversationLoading, onClose, on if (inputRef.current) { inputRef.current.focus(); } - }, [session]); + }, [session?.session_id]); // Lock body scroll when modal is open useEffect(() => { + if (!session) return; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; - }, []); + }, [!!session]); // Handle keyboard events useEffect(() => { + if (!session) return; const handleKeyDown = (e) => { - // Escape closes modal - if (e.key === 'Escape') { - onClose(); - } + if (e.key === 'Escape') handleClose(); }; - document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [onClose]); + }, [!!session, handleClose]); + + if (!session) return null; + + const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0; + const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0; + const status = getStatusMeta(session.status); + const agent = session.agent === 'codex' ? 'codex' : 'claude'; + const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude'; - // Handle input key events const handleInputKeyDown = (e) => { - // Enter sends message (unless Shift+Enter for newline) if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; - // Send message const handleSend = async () => { const text = inputValue.trim(); if (!text || sending) return; @@ -105,7 +108,6 @@ export function Modal({ session, conversations, conversationLoading, onClose, on await onSendMessage(session.session_id, text, true, optionCount); } setInputValue(''); - // Refresh conversation after sending if (onRefreshConversation) { await onRefreshConversation(session.session_id, session.project_dir, agent); } @@ -116,13 +118,15 @@ export function Modal({ session, conversations, conversationLoading, onClose, on } }; + const displayMessages = filterDisplayMessages(conversation); + return html`
e.target === e.currentTarget && onClose()} + class="fixed inset-0 z-50 flex items-center justify-center bg-[#02050d]/84 p-4 backdrop-blur-sm ${closing ? 'modal-backdrop-out' : 'modal-backdrop-in'}" + onClick=${(e) => e.target === e.currentTarget && handleClose()} >
e.stopPropagation()} > @@ -147,8 +151,8 @@ export function Modal({ session, conversations, conversationLoading, onClose, on