godcrm/components/AIChatPanel/AIChatPanel.tsx
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

502 lines
29 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** AIChatPanel v2 — Modular Composition (ADR-119) */
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { logger } from '@/shared/utils/logger';
import { useAIChat } from '../../context/AIChatContext';
import { useAuthStore } from '@/features/auth/store/authStore';
import { useCurrentSpace } from '@/features/spaces/store/spacesStore';
import { cn } from '@/shared/utils/cn';
import type { ContextSettings } from './types';
import type { AIChatPanelProps } from '../AIChatPanel.types';
import type { MentionUser } from '../MentionInput';
import type { ChatMessage } from '../../types';
import { useChatState } from './hooks/useChatState';
import { useChatActions } from './hooks/useChatActions';
import { useDataQueries } from './hooks/useDataQueries';
import { useChatMutations } from './hooks/useChatMutations';
import { useResizeHandlers } from './hooks/useResizeHandlers';
import { useSyncEffects } from './hooks/useSyncEffects';
import { useScrollManagement } from './hooks/useScrollManagement';
import { useEventHandlers } from './hooks/useEventHandlers';
import { useVoiceInput } from '../../hooks/useVoiceInput';
import { useConversationMessages } from '../../hooks/useConversationMessages';
import { CHAT_CONFIG } from '../../constants/chatConfig';
import { usePanelContentWiring } from './hooks/usePanelContentWiring';
import { ChatHeaderFull } from './components/ChatHeader/ChatHeaderFull';
import { MessagesArea } from './components/MessagesArea';
import { InputArea } from './components/InputArea';
import { TerminalPanel } from '@/features/terminal';
import { AgentEditModal } from '../AgentEditModal';
import { FilePreviewModal, detectFileType } from '@/features/files/components/FilePreviewModal';
import { ChevronDown, ChevronUp, Maximize2 } from 'lucide-react';
// Re-export for barrel pattern compatibility
export type { AIChatPanelProps };
// ─── Main Component ─────────────────────────────────────────────────
export function AIChatPanel({ className }: AIChatPanelProps) {
const ctx = useAIChat();
const {
isOpen, closeChat, currentAgent, agents, messages, isLoading, isLoadingAgents,
error, selectAgent, sendMessage, clearMessages, loadAgents,
conversations, currentConversationId, loadConversations, selectConversation,
createNewConversation, deleteConversation, isLoadingConversations,
spaceId: contextSpaceId, pendingTaskChat, clearPendingTaskChat,
rowFilter, clearRowFilter,
isAgentProcessing, processingAgentName, processingStartedAt,
setProcessingAgentName, dismissProcessing, resetProcessing, stopAgent,
historyAgentFilter, setHistoryAgentFilter, renameConversation,
} = ctx;
const { state: chatState, actions: chatActions } = useChatState();
const {
activePanel, chatMode, chatPartner, inputValue, attachments, previewFile, dragOver,
mentionedUsers, boundRows, messageBoundRows, chatParticipants, tasksSource, filesSource,
showFilePicker, attachTab, panelHeight, panelMode, isResizing, panelWidth, isResizingWidth,
sidebarWidth, isResizingSidebar, isMobile, mobileKeyboardHeight, processingElapsed,
markdownEnabled, agentMode, thinkingEnabled, settingsTab, localError, showTerminal,
terminalFocusSessionId, contactsSearch, agentsSearch, historySearch, filesSearch,
tasksSearch, userTypeFilter, showFavorites, favorites, showRowBinding, showBoundRowsBar,
showMessageRowPicker, expandedTaskChats, showAllContacts, chatOperatorId, chatModelId,
chatSystemPrompt, isSavingAgentSettings, sortOption, subAgents, editingAgentId,
defaultAgentId, isSavingDefaultAgent, favoriteAgents, showFavoriteAgents, expandedAgentId,
isVectorSearching, vectorSearchResults, agentChats, quickEmojis, isSavingEmojis,
messageReactions, voiceMode, showScrollToBottom, newMessageCount, agentWorking,
userConversationId,
} = chatState;
const {
setActivePanel, setChatMode, setChatPartner, setInputValue, setAttachments, setPreviewFile,
setDragOver, setMentionedUsers, setBoundRows, setMessageBoundRows, setChatParticipants,
setTasksSource, setFilesSource, setShowFilePicker, setAttachTab, setPanelHeight, setPanelMode,
setIsResizing, setPanelWidth, setIsResizingWidth, setSidebarWidth, setIsResizingSidebar,
setProcessingElapsed, setMarkdownEnabled, setAgentMode, setThinkingEnabled, setSettingsTab,
setLocalError, setShowTerminal, setTerminalFocusSessionId, setContactsSearch, setAgentsSearch,
setHistorySearch, setFilesSearch, setTasksSearch, setUserTypeFilter, setShowFavorites,
setFavorites, setShowRowBinding, setShowBoundRowsBar, setShowMessageRowPicker,
setExpandedTaskChats, setShowAllContacts, setChatOperatorId, setChatModelId, setChatSystemPrompt,
setIsSavingAgentSettings, setSortOption, setSubAgents, setEditingAgentId, setDefaultAgentId,
setIsSavingDefaultAgent, setFavoriteAgents, setShowFavoriteAgents, setExpandedAgentId,
setIsVectorSearching, setVectorSearchResults, setAgentChats, setQuickEmojis, setIsSavingEmojis,
setMessageReactions, setVoiceMode, setShowScrollToBottom, setNewMessageCount, setAgentWorking,
setUserConversationId,
} = chatActions;
const _chatActionHandlers = useChatActions();
const currentUser = useAuthStore((state) => state.user);
const isAdminOrOwner = currentUser?.role === 'admin' || currentUser?.role === 'owner';
const currentSpace = useCurrentSpace();
const effectiveSpaceId = contextSpaceId ?? currentSpace?.id;
const isWideMode = !isMobile && panelWidth >= 600;
const conversationMode = chatPartner?.type === 'agent' ? 'solo' as const : chatPartner?.type === 'group' ? 'group' as const : chatPartner?.type === 'user' ? 'solo' as const : null;
const [inboxSearch, setInboxSearch] = useState('');
const [inboxAgentFilter, setInboxAgentFilter] = useState<string>('');
const [inboxDateFrom, setInboxDateFrom] = useState<string>('');
const [inboxDateTo, setInboxDateTo] = useState<string>('');
const [showInboxFilters, setShowInboxFilters] = useState(false);
const [contextSettings, setContextSettings] = useState<ContextSettings | string | undefined | null>(
currentAgent ? (currentAgent as unknown as Record<string, unknown>).context_settings as ContextSettings | string | undefined : undefined
);
const [isSavingContextSettings, setIsSavingContextSettings] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const loadMoreSentinelRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const forceNewChatRef = useRef(false);
const dataQueries = useDataQueries({
activePanel, effectiveSpaceId, showAllContacts, tasksSource, tasksSearch,
filesSource, showFilePicker, chatOperatorId,
currentAgentOperatorId: currentAgent?.operator_id,
currentAgentProviderId: currentAgent?.provider_id,
contextSpaceId, inboxSearch, inboxAgentFilter, inboxDateFrom, inboxDateTo,
});
const {
users, isLoadingUsers, totalUnreadCount, refetchUnread,
inboxConversations, isLoadingInbox, refetchInbox,
taskRows, filteredTaskRows, isLoadingTasks, taskStatusDict, tasksTableColumns,
projectFiles, isLoadingFiles, operators, models,
usersForMentions, aiAgentsData, allTablesDataMain,
} = dataQueries;
const {
messages: userConversationMessages, conversation: userConversationData,
hasNextPage: hasOlderMessages, fetchNextPage: fetchOlderMessages,
isFetchingNextPage: isFetchingOlderMessages, refetch: refetchUserMessages,
markAsRead, pollingError: userPollingError, pollingStopped: userPollingStopped,
reconnect: userReconnect, fetchToolSteps: userFetchToolSteps,
} = useConversationMessages(userConversationId, {
pageSize: CHAT_CONFIG.MESSAGE_PAGE_SIZE,
enabled: !!userConversationId && (chatPartner?.type === 'user' || chatPartner?.type === 'group'),
adaptivePolling: true, chatActivityState: 'active',
currentUserId: currentUser?.id ? Number(currentUser.id) : undefined,
});
const {
messages: aiConversationMessages, conversation: aiConversationData,
fetchNextPage: fetchNextAIPage, hasNextPage: hasNextAIPage,
isFetchingNextPage: isFetchingNextAIPage,
pollingError: aiPollingError, pollingStopped: aiPollingStopped,
reconnect: aiReconnect,
isProcessing: aiBackendProcessing,
processingAgentName: aiBackendProcessingAgentName,
fetchToolSteps: aiFetchToolSteps,
} = useConversationMessages(currentConversationId, {
pageSize: CHAT_CONFIG.MESSAGE_PAGE_SIZE,
enabled: !!currentConversationId && chatPartner?.type === 'agent',
adaptivePolling: true,
chatActivityState: isAgentProcessing ? 'agent_processing' : 'idle',
currentUserId: currentUser?.id ? Number(currentUser.id) : undefined,
});
const activePollingError = chatPartner?.type === 'agent' ? aiPollingError : userPollingError;
const activePollingStopped = chatPartner?.type === 'agent' ? aiPollingStopped : userPollingStopped;
const activeReconnect = chatPartner?.type === 'agent' ? aiReconnect : userReconnect;
const mutations = useChatMutations({
currentAgent, chatOperatorId, chatModelId, chatSystemPrompt,
setIsSavingAgentSettings, setContextSettings,
setDefaultAgentId, setIsSavingDefaultAgent,
setQuickEmojis, setIsSavingEmojis,
setMessageReactions: setMessageReactions as any,
loadAgents, refetchUserMessages,
});
// ========== Resize ==========
const resize = useResizeHandlers({
panelHeight, panelWidth, sidebarWidth, panelMode,
setPanelHeight: setPanelHeight as any, setPanelMode: setPanelMode as any,
setIsResizing, setPanelWidth: setPanelWidth as any, setIsResizingWidth,
setSidebarWidth: setSidebarWidth as any, setIsResizingSidebar,
activePanel, setActivePanel,
});
// ========== Voice Input ==========
const {
isRecording, isProcessing: isTranscribing, error: voiceError,
duration: recordingDuration, startRecording, stopRecording, cancelRecording,
webSpeechAvailable,
} = useVoiceInput({
mode: voiceMode, language: 'ru-RU', spaceId: currentSpace?.id,
onResult: (text) => { setInputValue(prev => { const sep = prev.trim() ? ' ' : ''; return prev + sep + text; }); },
onError: (error) => { logger.error('[Voice Input] Error:', error); },
});
// ========== Display Messages ==========
const displayMessages = useMemo(() => {
if (chatPartner?.type === 'user' || chatPartner?.type === 'group') return (userConversationMessages || []) as ChatMessage[];
if (chatPartner?.type === 'agent') {
if (aiConversationMessages && aiConversationMessages.length > 0) return aiConversationMessages as ChatMessage[];
if (messages && messages.length > 0) return messages;
return [];
}
return messages;
}, [chatPartner?.type, userConversationMessages, aiConversationMessages, messages]);
// ========== Mention/Agent data ==========
const availableMentionUsers: MentionUser[] = useMemo(() => {
return (usersForMentions || []).map(user => ({
id: user.id, name: user.name, email: user.email, avatar: user.avatar_url,
type: user.managed_by_agent_table_id ? 'bot' as const : 'human' as const
}));
}, [usersForMentions]);
const availableSlashAgents: MentionUser[] = useMemo(() => {
return (aiAgentsData?.data?.agents || [])
.filter(agent => agent.status !== 'inactive' && agent.name)
.map(agent => ({ id: agent.id, name: agent.name, icon: agent.icon, email: agent.description, type: 'agent' as const }));
}, [aiAgentsData]);
const resolvedConvTitle = useMemo(() => {
const convId = currentConversationId || userConversationId;
if (!convId) return null;
const conv = conversations.find(c => c.id === convId);
if (conv?.title) return conv.title;
const inboxConv = inboxConversations?.find(c => c.id === convId);
if (inboxConv?.title) return inboxConv.title;
return null;
}, [currentConversationId, userConversationId, conversations, inboxConversations]);
const hasSlashCommand = useMemo(() => /(^|\s)\/[a-z][a-z0-9_-]*(\s|$)/i.test(inputValue), [inputValue]);
// ========== Sync Effects (delegated to hook) ==========
useSyncEffects({
currentAgent, setChatOperatorId, setChatModelId, setChatSystemPrompt,
aiBackendProcessing, isAgentProcessing, dismissProcessing,
aiBackendProcessingAgentName, setProcessingAgentName,
userConversationId, userConversationData, markAsRead, refetchUnread,
isOpen, agents, defaultAgentId, selectAgent, loadAgents, loadConversations,
isMobile, chatPartner: chatPartner as any, setChatPartner: setChatPartner as any,
setUserConversationId,
pendingTaskChat, clearPendingTaskChat, setChatMode, setBoundRows: setBoundRows as any,
setShowBoundRowsBar, setActivePanel: setActivePanel as any,
aiConversationData, setSubAgents, subAgents, currentConversationId: currentConversationId ?? null,
setChatParticipants: setChatParticipants as any,
userParticipantsCount: chatParticipants.length,
aiParticipantsCount: 0,
processingStartedAt, setProcessingElapsed,
});
// Extra effects not in useSyncEffects
useEffect(() => {
setContextSettings(currentAgent ? (currentAgent as unknown as Record<string, unknown>).context_settings as ContextSettings | string | undefined : undefined);
}, [currentAgent]);
useEffect(() => {
if (currentSpace?.settings && typeof currentSpace.settings === 'object') {
const s = currentSpace.settings as Record<string, unknown>;
setDefaultAgentId(s.default_agent_id ? Number(s.default_agent_id) : null);
setQuickEmojis(Array.isArray(s.quick_emojis) ? s.quick_emojis as string[] : ['👍','❤️','😂','🔥','💯','🙏','😍','😮']);
} else { setDefaultAgentId(null); setQuickEmojis(['👍','❤️','😂','🔥','💯','🙏','😍','😮']); }
}, [currentSpace?.id, currentSpace?.settings]);
useEffect(() => { if (isOpen && activePanel === 'none') setTimeout(() => inputRef.current?.focus(), 300); }, [isOpen, activePanel]);
// ========== Scroll Management (delegated to hook) ==========
useScrollManagement({
displayMessages, aiConversationMessages,
currentConversationId: currentConversationId ?? null,
userConversationId, chatPartnerId: chatPartner?.id,
chatPartnerType: chatPartner?.type,
hasOlderMessages, fetchOlderMessages,
hasNextAIPage, fetchNextAIPage,
isFetchingOlderMessages: !!isFetchingOlderMessages,
isFetchingNextAIPage: !!isFetchingNextAIPage,
isAgentProcessing,
messagesEndRef, messagesContainerRef, loadMoreSentinelRef,
setShowScrollToBottom, setNewMessageCount: setNewMessageCount as any,
setAgentWorking, fetchReactionsForMessages: mutations.fetchReactionsForMessages,
});
// ========== Event Handlers (delegated to hook) ==========
const events = useEventHandlers({
inputValue, setInputValue: setInputValue as any,
attachments, setAttachments: setAttachments as any,
mentionedUsers: mentionedUsers as any, setMentionedUsers: setMentionedUsers as any,
messageBoundRows: messageBoundRows as any, setMessageBoundRows: setMessageBoundRows as any,
setLocalError, setDragOver, chatPartner: chatPartner as any, currentAgent,
agentMode, thinkingEnabled, subAgents,
userConversationId, setUserConversationId,
currentSpaceId: currentSpace?.id, effectiveSpaceId,
availableMentionUsers, availableSlashAgents,
sendMessage, selectAgent,
sendUserMessageMutation: mutations.sendUserMessageMutation,
setChatMode, setChatPartner: setChatPartner as any, setChatParticipants: setChatParticipants as any,
setBoundRows: setBoundRows as any, setShowBoundRowsBar, setActivePanel: setActivePanel as any,
setVectorSearchResults,
});
// ========== Guard ==========
if (!isOpen) return null;
// ========== Panel content (delegated to wiring hook) ==========
const { renderPanelContent } = usePanelContentWiring({
activePanel, setActivePanel, contactsSearch, setContactsSearch,
showFavorites, setShowFavorites: setShowFavorites as any,
userTypeFilter, setUserTypeFilter, showAllContacts, setShowAllContacts: setShowAllContacts as any,
users, isLoadingUsers, chatParticipants: chatParticipants as any,
chatPartner: chatPartner as any, favorites,
setFavorites: setFavorites as any,
setUserConversationId, setChatPartner: setChatPartner as any,
setChatParticipants: setChatParticipants as any, setBoundRows: setBoundRows as any,
setShowBoundRowsBar, handleAgentSelect: events.handleAgentSelect,
selectConversation, createNewConversation, forceNewChatRef,
setChatMode, clearMessages,
agentsSearch, setAgentsSearch, agents, isLoadingAgents, currentAgent,
showFavoriteAgents, setShowFavoriteAgents, favoriteAgents, setFavoriteAgents,
isVectorSearching, vectorSearchResults, setVectorSearchResults,
setIsVectorSearching, currentSpaceId: currentSpace?.id,
createTablesMutation: mutations.createTablesMutation, currentSpace: currentSpace as any,
inboxConversations: inboxConversations as any, isLoadingInbox, totalUnreadCount, refetchInbox,
inboxSearch, setInboxSearch, inboxAgentFilter, setInboxAgentFilter,
inboxDateFrom, setInboxDateFrom, inboxDateTo, setInboxDateTo,
showInboxFilters, setShowInboxFilters,
markAsReadMutation: mutations.markAsReadMutation, selectAgent, renameConversation,
userConversationId, currentConversationId, allTablesDataMain: allTablesDataMain as any,
tasksSource, filteredTaskRows, isLoadingTasks, taskRows, taskStatusDict,
tasksTableColumns, tasksSearch, setTasksSearch, setTasksSource,
expandedTaskChats, setExpandedTaskChats, conversations: conversations as any, effectiveSpaceId,
settingsTab, setSettingsTab, chatOperatorId, setChatOperatorId,
chatModelId, setChatModelId, chatSystemPrompt, setChatSystemPrompt,
operators, models, isAdminOrOwner, isSavingAgentSettings,
saveAgentSettings: mutations.saveAgentSettings,
messages, contextSettings: contextSettings as any, setContextSettings,
saveContextSettings: mutations.saveContextSettings as any, isSavingContextSettings,
defaultAgentId, saveDefaultAgent: mutations.saveDefaultAgent, isSavingDefaultAgent,
quickEmojis, setQuickEmojis, saveQuickEmojis: mutations.saveQuickEmojis, isSavingEmojis,
voiceMode, setVoiceMode, voiceError: voiceError || null, webSpeechAvailable,
filesSource, setFilesSource,
});
// ========== JSX Render ==========
return (
<>
{isMobile && (
<div role="button" tabIndex={0} className="fixed top-14 left-0 right-0 bottom-0 bg-black/50 z-30"
onClick={closeChat} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') closeChat(); }} />
)}
<div className={cn('bg-[var(--bg-primary)] shadow-lg flex',
isMobile ? 'fixed top-14 left-0 right-0 z-40 flex-col' : 'relative z-30 flex-shrink-0 h-full border-l border-[var(--border-primary)]', className)}
style={isMobile ? { bottom: `${mobileKeyboardHeight > 0 ? mobileKeyboardHeight : 0}px` } : { width: panelWidth }}>
{/* Left resize handle */}
{!isMobile && (
<div className="w-1 cursor-ew-resize hover:bg-purple-500/50 transition-colors flex-shrink-0"
onMouseDown={resize.handleWidthResizeStart}
style={{ backgroundColor: isResizingWidth ? 'var(--color-purple-500)' : 'transparent' }} />
)}
<div className={cn('flex-1 flex min-w-0 min-h-0 overflow-hidden', isWideMode ? 'flex-row' : 'flex-col')}>
{/* Chat area */}
<div className="flex flex-col min-w-0 min-h-0 overflow-hidden relative flex-1">
<ChatHeaderFull
chatMode={chatMode} setChatMode={setChatMode}
activePanel={activePanel} togglePanel={resize.togglePanel}
agents={agents} users={users}
totalUnreadCount={totalUnreadCount} refetchInbox={refetchInbox}
createNewConversation={createNewConversation} closeChat={closeChat}
chatPartner={chatPartner as any} setChatPartner={setChatPartner as any}
chatParticipants={chatParticipants as any}
resolvedConvTitle={resolvedConvTitle}
currentConversationId={currentConversationId} userConversationId={userConversationId}
renameConversation={renameConversation} deleteConversation={deleteConversation}
conversationMode={conversationMode}
showRowBinding={showRowBinding} setShowRowBinding={setShowRowBinding as any}
boundRows={boundRows as any} setBoundRows={setBoundRows as any}
setShowBoundRowsBar={setShowBoundRowsBar}
effectiveSpaceId={effectiveSpaceId} tasksSource={tasksSource as any}
currentAgent={currentAgent as any} isWideMode={isWideMode}
contactsSearch={contactsSearch} setContactsSearch={setContactsSearch}
isMobile={isMobile}
/>
{/* Panel + Messages Area */}
<div className="flex-1 flex flex-col min-h-0 overflow-hidden relative" data-panel-container>
{/* Panel overlay */}
{!isWideMode && activePanel !== 'none' && (
<div className={cn(
"border-b border-[var(--border-secondary)] bg-[var(--bg-secondary)] overflow-hidden flex flex-col z-30",
(panelMode === 'collapsed' || (isMobile && panelMode === 'default')) ? "relative min-h-0" :
panelMode === 'expanded' ? "absolute left-0 right-0 top-0 bottom-[400px]" :
panelMode === 'fullscreen' ? "absolute inset-0" : "relative flex-1 min-h-0"
)} style={
(panelMode === 'collapsed' || (isMobile && panelMode === 'default')) ? { height: 'min(400px, 40vh)' } :
panelMode === 'default' && !isMobile ? { maxHeight: '50%' } : undefined
}>
<div className="flex-1 overflow-y-auto">{renderPanelContent()}</div>
<div className="flex-shrink-0 h-6 px-2 flex items-center justify-between border-t border-[var(--border-secondary)] bg-[var(--bg-tertiary)]">
<div className="text-[10px] text-[var(--text-tertiary)] flex items-center gap-3">
{activePanel === 'contacts' && <span>{users.length} контактов</span>}
{activePanel === 'ai-agents' && <span>{agents.length} агентов</span>}
{activePanel === 'tasks' && <span>{taskRows.length} задач</span>}
{activePanel === 'settings' && <span>Настройки чата</span>}
</div>
<button onClick={resize.togglePanelMode}
className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] rounded transition-colors">
{panelMode === 'fullscreen' ? <ChevronDown className="w-4 h-4" /> :
panelMode === 'collapsed' ? <ChevronUp className="w-4 h-4" /> :
<Maximize2 className="w-4 h-4" />}
</button>
</div>
</div>
)}
<MessagesArea
chatMode={chatMode} chatPartner={chatPartner as any}
displayMessages={displayMessages} markdownEnabled={markdownEnabled}
isAgentProcessing={isAgentProcessing}
processingAgentName={processingAgentName}
processingElapsed={processingElapsed} stopAgent={stopAgent}
messageReactions={messageReactions} quickEmojis={quickEmojis}
currentUserId={currentUser?.id ? Number(currentUser.id) : undefined}
currentUser={currentUser ? { name: currentUser.name, id: Number(currentUser.id) } : undefined}
onReact={mutations.handleReaction} onCopy={mutations.handleCopyMessage}
onForward={mutations.handleForwardMessage} onDelete={mutations.handleDeleteMessage}
onCheckboxClick={events.handleCheckboxClick}
onMentionClick={(token) => setInputValue(prev => prev ? `${prev} ${token} ` : `${token} `)}
onOpenTerminal={(sessionId) => { setShowTerminal(true); if (sessionId) setTerminalFocusSessionId(sessionId); }}
sendMessage={sendMessage} currentAgent={currentAgent}
messagesEndRef={messagesEndRef} messagesContainerRef={messagesContainerRef}
loadMoreSentinelRef={loadMoreSentinelRef}
dragOver={dragOver} setDragOver={setDragOver} onDrop={events.handleDrop}
isMobile={isMobile} setActivePanel={setActivePanel as any}
hasOlderMessages={!!hasOlderMessages} isFetchingOlderMessages={!!isFetchingOlderMessages}
hasNextAIPage={!!hasNextAIPage} isFetchingNextAIPage={!!isFetchingNextAIPage}
showScrollToBottom={showScrollToBottom} setShowScrollToBottom={setShowScrollToBottom}
newMessageCount={newMessageCount} setNewMessageCount={setNewMessageCount as any}
agentWorking={agentWorking} setAgentWorking={setAgentWorking}
activePollingError={activePollingError} activePollingStopped={activePollingStopped}
activeReconnect={activeReconnect}
error={error} localError={localError}
fetchToolSteps={chatMode === 'ai' ? aiFetchToolSteps : userFetchToolSteps}
/>
</div>
<InputArea
inputValue={inputValue} setInputValue={setInputValue as any}
attachments={attachments} setAttachments={setAttachments as any}
mentionedUsers={mentionedUsers as any} setMentionedUsers={setMentionedUsers as any}
messageBoundRows={messageBoundRows as any} setMessageBoundRows={setMessageBoundRows as any}
showFilePicker={showFilePicker} setShowFilePicker={setShowFilePicker as any}
attachTab={attachTab} setAttachTab={setAttachTab}
filesSource={filesSource} updateFilesSource={(config) => setFilesSource(config)}
projectFiles={projectFiles} isLoadingFiles={isLoadingFiles}
filesSearch={filesSearch} setFilesSearch={setFilesSearch}
effectiveSpaceId={effectiveSpaceId} tasksSource={tasksSource as any}
chatPartner={chatPartner as any} hasSlashCommand={hasSlashCommand}
thinkingEnabled={thinkingEnabled} setThinkingEnabled={setThinkingEnabled as any}
agentMode={agentMode} setAgentMode={setAgentMode as any}
markdownEnabled={markdownEnabled} setMarkdownEnabled={setMarkdownEnabled as any}
showTerminal={showTerminal} setShowTerminal={setShowTerminal as any}
isRecording={isRecording} isTranscribing={isTranscribing}
voiceError={voiceError} recordingDuration={recordingDuration}
startRecording={startRecording} stopRecording={stopRecording} cancelRecording={cancelRecording}
voiceMode={voiceMode}
isLoading={isLoading} isAgentProcessing={isAgentProcessing} stopAgent={stopAgent}
handleSubmit={events.handleSubmit} handleFileSelect={events.handleFileSelect}
fileInputRef={fileInputRef as any}
availableMentionUsers={availableMentionUsers} availableSlashAgents={availableSlashAgents}
panelMode={panelMode}
/>
{showTerminal && (
<div className="flex-shrink-0 border-t border-[var(--border-primary)]" style={{ height: 260 }}>
<TerminalPanel className="h-full rounded-none border-0" compact={false}
focusSessionId={terminalFocusSessionId} onCollapse={() => setShowTerminal(false)} />
</div>
)}
</div>
{/* Sidebar for wide mode */}
{isWideMode && activePanel !== 'none' && (
<>
<div role="separator" onMouseDown={resize.handleSidebarResizeStart}
className={cn("w-1 cursor-col-resize hover:bg-[var(--color-primary-500)]/30 transition-colors flex-shrink-0",
isResizingSidebar && "bg-[var(--color-primary-500)]/50")} />
<div className="border-l border-[var(--border-primary)] flex-shrink-0 overflow-hidden flex flex-col bg-[var(--bg-secondary)]"
style={{ width: sidebarWidth }}>
<div className="flex-1 overflow-y-auto">{renderPanelContent()}</div>
</div>
</>
)}
</div>
{editingAgentId && (
<AgentEditModal isOpen={!!editingAgentId} onClose={() => setEditingAgentId(null)}
agent={agents.find(a => a.id === editingAgentId) || null}
onSave={() => { loadAgents(); }} />
)}
{previewFile && (
<FilePreviewModal isOpen={!!previewFile} onClose={() => setPreviewFile(null)}
fileUrl={previewFile.url} fileName={previewFile.name} fileType={detectFileType(previewFile.url)} />
)}
</div>
</>
);
}