mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-22 16:47:37 +00:00
385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
import React, { useCallback, useEffect, useRef } from 'react';
|
|
import QuickSettingsPanel from '../../QuickSettingsPanel';
|
|
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
|
import { useTranslation } from 'react-i18next';
|
|
import ChatMessagesPane from './subcomponents/ChatMessagesPane';
|
|
import ChatComposer from './subcomponents/ChatComposer';
|
|
import type { ChatInterfaceProps } from '../types/types';
|
|
import { useChatProviderState } from '../hooks/useChatProviderState';
|
|
import { useChatSessionState } from '../hooks/useChatSessionState';
|
|
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
|
import { useChatComposerState } from '../hooks/useChatComposerState';
|
|
import type { Provider } from '../types/types';
|
|
|
|
type PendingViewSession = {
|
|
sessionId: string | null;
|
|
startedAt: number;
|
|
};
|
|
|
|
function ChatInterface({
|
|
selectedProject,
|
|
selectedSession,
|
|
ws,
|
|
sendMessage,
|
|
latestMessage,
|
|
onFileOpen,
|
|
onInputFocusChange,
|
|
onSessionActive,
|
|
onSessionInactive,
|
|
onSessionProcessing,
|
|
onSessionNotProcessing,
|
|
processingSessions,
|
|
onReplaceTemporarySession,
|
|
onNavigateToSession,
|
|
onShowSettings,
|
|
autoExpandTools,
|
|
showRawParameters,
|
|
showThinking,
|
|
autoScrollToBottom,
|
|
sendByCtrlEnter,
|
|
externalMessageUpdate,
|
|
onShowAllTasks,
|
|
}: ChatInterfaceProps) {
|
|
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
|
const { t } = useTranslation('chat');
|
|
|
|
const streamBufferRef = useRef('');
|
|
const streamTimerRef = useRef<number | null>(null);
|
|
const pendingViewSessionRef = useRef<PendingViewSession | null>(null);
|
|
|
|
const resetStreamingState = useCallback(() => {
|
|
if (streamTimerRef.current) {
|
|
clearTimeout(streamTimerRef.current);
|
|
streamTimerRef.current = null;
|
|
}
|
|
streamBufferRef.current = '';
|
|
}, []);
|
|
|
|
const {
|
|
provider,
|
|
setProvider,
|
|
cursorModel,
|
|
setCursorModel,
|
|
claudeModel,
|
|
setClaudeModel,
|
|
codexModel,
|
|
setCodexModel,
|
|
permissionMode,
|
|
pendingPermissionRequests,
|
|
setPendingPermissionRequests,
|
|
cyclePermissionMode,
|
|
} = useChatProviderState({
|
|
selectedSession,
|
|
});
|
|
|
|
const {
|
|
chatMessages,
|
|
setChatMessages,
|
|
isLoading,
|
|
setIsLoading,
|
|
currentSessionId,
|
|
setCurrentSessionId,
|
|
sessionMessages,
|
|
setSessionMessages,
|
|
isLoadingSessionMessages,
|
|
isLoadingMoreMessages,
|
|
hasMoreMessages,
|
|
totalMessages,
|
|
isSystemSessionChange,
|
|
setIsSystemSessionChange,
|
|
canAbortSession,
|
|
setCanAbortSession,
|
|
isUserScrolledUp,
|
|
setIsUserScrolledUp,
|
|
tokenBudget,
|
|
setTokenBudget,
|
|
visibleMessageCount,
|
|
visibleMessages,
|
|
loadEarlierMessages,
|
|
claudeStatus,
|
|
setClaudeStatus,
|
|
createDiff,
|
|
scrollContainerRef,
|
|
scrollToBottom,
|
|
handleScroll,
|
|
} = useChatSessionState({
|
|
selectedProject,
|
|
selectedSession,
|
|
ws,
|
|
sendMessage,
|
|
autoScrollToBottom,
|
|
externalMessageUpdate,
|
|
processingSessions,
|
|
resetStreamingState,
|
|
pendingViewSessionRef,
|
|
});
|
|
|
|
const {
|
|
input,
|
|
setInput,
|
|
textareaRef,
|
|
inputHighlightRef,
|
|
isTextareaExpanded,
|
|
thinkingMode,
|
|
setThinkingMode,
|
|
slashCommandsCount,
|
|
filteredCommands,
|
|
frequentCommands,
|
|
commandQuery,
|
|
showCommandMenu,
|
|
selectedCommandIndex,
|
|
resetCommandMenuState,
|
|
handleCommandSelect,
|
|
handleToggleCommandMenu,
|
|
showFileDropdown,
|
|
filteredFiles,
|
|
selectedFileIndex,
|
|
renderInputWithMentions,
|
|
selectFile,
|
|
attachedImages,
|
|
setAttachedImages,
|
|
uploadingImages,
|
|
imageErrors,
|
|
getRootProps,
|
|
getInputProps,
|
|
isDragActive,
|
|
openImagePicker,
|
|
handleSubmit,
|
|
handleInputChange,
|
|
handleKeyDown,
|
|
handlePaste,
|
|
handleTextareaClick,
|
|
handleTextareaInput,
|
|
syncInputOverlayScroll,
|
|
handleClearInput,
|
|
handleAbortSession,
|
|
handleTranscript,
|
|
handlePermissionDecision,
|
|
handleGrantToolPermission,
|
|
handleInputFocusChange,
|
|
} = useChatComposerState({
|
|
selectedProject,
|
|
selectedSession,
|
|
currentSessionId,
|
|
provider,
|
|
permissionMode,
|
|
cyclePermissionMode,
|
|
cursorModel,
|
|
claudeModel,
|
|
codexModel,
|
|
isLoading,
|
|
canAbortSession,
|
|
tokenBudget,
|
|
sendMessage,
|
|
sendByCtrlEnter,
|
|
onSessionActive,
|
|
onInputFocusChange,
|
|
onFileOpen,
|
|
onShowSettings,
|
|
pendingViewSessionRef,
|
|
scrollToBottom,
|
|
setChatMessages,
|
|
setSessionMessages,
|
|
setIsLoading,
|
|
setCanAbortSession,
|
|
setClaudeStatus,
|
|
setIsUserScrolledUp,
|
|
setPendingPermissionRequests,
|
|
});
|
|
|
|
useChatRealtimeHandlers({
|
|
latestMessage,
|
|
provider,
|
|
selectedProject,
|
|
selectedSession,
|
|
currentSessionId,
|
|
setCurrentSessionId,
|
|
setChatMessages,
|
|
setIsLoading,
|
|
setCanAbortSession,
|
|
setClaudeStatus,
|
|
setTokenBudget,
|
|
setIsSystemSessionChange,
|
|
setPendingPermissionRequests,
|
|
pendingViewSessionRef,
|
|
streamBufferRef,
|
|
streamTimerRef,
|
|
onSessionInactive,
|
|
onSessionProcessing,
|
|
onSessionNotProcessing,
|
|
onReplaceTemporarySession,
|
|
onNavigateToSession,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!isLoading || !canAbortSession) {
|
|
return;
|
|
}
|
|
|
|
const handleGlobalEscape = (event: KeyboardEvent) => {
|
|
if (event.key !== 'Escape' || event.repeat || event.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
handleAbortSession();
|
|
};
|
|
|
|
document.addEventListener('keydown', handleGlobalEscape, { capture: true });
|
|
return () => {
|
|
document.removeEventListener('keydown', handleGlobalEscape, { capture: true });
|
|
};
|
|
}, [canAbortSession, handleAbortSession, isLoading]);
|
|
|
|
useEffect(() => {
|
|
const processingSessionId = selectedSession?.id || currentSessionId;
|
|
if (processingSessionId && isLoading && onSessionProcessing) {
|
|
onSessionProcessing(processingSessionId);
|
|
}
|
|
}, [currentSessionId, isLoading, onSessionProcessing, selectedSession?.id]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
resetStreamingState();
|
|
};
|
|
}, [resetStreamingState]);
|
|
|
|
if (!selectedProject) {
|
|
const selectedProviderLabel =
|
|
provider === 'cursor'
|
|
? t('messageTypes.cursor')
|
|
: provider === 'codex'
|
|
? t('messageTypes.codex')
|
|
: t('messageTypes.claude');
|
|
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center text-muted-foreground">
|
|
<p className="text-sm">
|
|
{t('projectSelection.startChatWithProvider', {
|
|
provider: selectedProviderLabel,
|
|
defaultValue: 'Select a project to start chatting with {{provider}}',
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="h-full flex flex-col">
|
|
<ChatMessagesPane
|
|
scrollContainerRef={scrollContainerRef}
|
|
onWheel={handleScroll}
|
|
onTouchMove={handleScroll}
|
|
isLoadingSessionMessages={isLoadingSessionMessages}
|
|
chatMessages={chatMessages}
|
|
selectedSession={selectedSession}
|
|
currentSessionId={currentSessionId}
|
|
provider={provider}
|
|
setProvider={(nextProvider) => setProvider(nextProvider as Provider)}
|
|
textareaRef={textareaRef}
|
|
claudeModel={claudeModel}
|
|
setClaudeModel={setClaudeModel}
|
|
cursorModel={cursorModel}
|
|
setCursorModel={setCursorModel}
|
|
codexModel={codexModel}
|
|
setCodexModel={setCodexModel}
|
|
tasksEnabled={tasksEnabled}
|
|
isTaskMasterInstalled={isTaskMasterInstalled}
|
|
onShowAllTasks={onShowAllTasks}
|
|
setInput={setInput}
|
|
isLoadingMoreMessages={isLoadingMoreMessages}
|
|
hasMoreMessages={hasMoreMessages}
|
|
totalMessages={totalMessages}
|
|
sessionMessagesCount={sessionMessages.length}
|
|
visibleMessageCount={visibleMessageCount}
|
|
visibleMessages={visibleMessages}
|
|
loadEarlierMessages={loadEarlierMessages}
|
|
createDiff={createDiff}
|
|
onFileOpen={onFileOpen}
|
|
onShowSettings={onShowSettings}
|
|
onGrantToolPermission={handleGrantToolPermission}
|
|
autoExpandTools={autoExpandTools}
|
|
showRawParameters={showRawParameters}
|
|
showThinking={showThinking}
|
|
selectedProject={selectedProject}
|
|
isLoading={isLoading}
|
|
/>
|
|
|
|
<ChatComposer
|
|
pendingPermissionRequests={pendingPermissionRequests}
|
|
handlePermissionDecision={handlePermissionDecision}
|
|
handleGrantToolPermission={handleGrantToolPermission}
|
|
claudeStatus={claudeStatus}
|
|
isLoading={isLoading}
|
|
onAbortSession={handleAbortSession}
|
|
provider={provider}
|
|
permissionMode={permissionMode}
|
|
onModeSwitch={cyclePermissionMode}
|
|
thinkingMode={thinkingMode}
|
|
setThinkingMode={setThinkingMode}
|
|
tokenBudget={tokenBudget}
|
|
slashCommandsCount={slashCommandsCount}
|
|
onToggleCommandMenu={handleToggleCommandMenu}
|
|
hasInput={Boolean(input.trim())}
|
|
onClearInput={handleClearInput}
|
|
isUserScrolledUp={isUserScrolledUp}
|
|
hasMessages={chatMessages.length > 0}
|
|
onScrollToBottom={scrollToBottom}
|
|
onSubmit={handleSubmit}
|
|
isDragActive={isDragActive}
|
|
attachedImages={attachedImages}
|
|
onRemoveImage={(index) =>
|
|
setAttachedImages((previous) =>
|
|
previous.filter((_, currentIndex) => currentIndex !== index),
|
|
)
|
|
}
|
|
uploadingImages={uploadingImages}
|
|
imageErrors={imageErrors}
|
|
showFileDropdown={showFileDropdown}
|
|
filteredFiles={filteredFiles}
|
|
selectedFileIndex={selectedFileIndex}
|
|
onSelectFile={selectFile}
|
|
filteredCommands={filteredCommands}
|
|
selectedCommandIndex={selectedCommandIndex}
|
|
onCommandSelect={handleCommandSelect}
|
|
onCloseCommandMenu={resetCommandMenuState}
|
|
isCommandMenuOpen={showCommandMenu}
|
|
frequentCommands={commandQuery ? [] : frequentCommands}
|
|
getRootProps={getRootProps as (...args: unknown[]) => Record<string, unknown>}
|
|
getInputProps={getInputProps as (...args: unknown[]) => Record<string, unknown>}
|
|
openImagePicker={openImagePicker}
|
|
inputHighlightRef={inputHighlightRef}
|
|
renderInputWithMentions={renderInputWithMentions}
|
|
textareaRef={textareaRef}
|
|
input={input}
|
|
onInputChange={handleInputChange}
|
|
onTextareaClick={handleTextareaClick}
|
|
onTextareaKeyDown={handleKeyDown}
|
|
onTextareaPaste={handlePaste}
|
|
onTextareaScrollSync={syncInputOverlayScroll}
|
|
onTextareaInput={handleTextareaInput}
|
|
onInputFocusChange={handleInputFocusChange}
|
|
placeholder={t('input.placeholder', {
|
|
provider:
|
|
provider === 'cursor'
|
|
? t('messageTypes.cursor')
|
|
: provider === 'codex'
|
|
? t('messageTypes.codex')
|
|
: t('messageTypes.claude'),
|
|
})}
|
|
isTextareaExpanded={isTextareaExpanded}
|
|
sendByCtrlEnter={sendByCtrlEnter}
|
|
onTranscript={handleTranscript}
|
|
/>
|
|
</div>
|
|
|
|
<QuickSettingsPanel />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default React.memo(ChatInterface);
|