import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Pencil, Plus, RefreshCw, RotateCcw, Square, Trash2, X } from 'lucide-react'; import type { Components } from 'react-markdown'; import { memo, type ChangeEventHandler, type KeyboardEventHandler, type RefObject } from 'react'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import nanobotLogo from '../../../assets/nanobot-logo.png'; import type { ChatMessage } from '../../../types/bot'; import { normalizeAssistantMessageText, normalizeUserMessageText } from '../messageParser'; import { normalizeDashboardAttachmentPath } from '../shared/workspaceMarkdown'; import type { StagedSubmissionDraft } from '../types'; import { formatDateInputValue, workspaceFileAction } from '../utils'; import { DashboardConversationMessages } from './DashboardConversationMessages'; import './DashboardChatPanel.css'; interface DashboardChatPanelLabels { badReply: string; botDisabledHint: string; botStarting: string; botStopping: string; chatDisabled: string; close: string; controlCommandsHide: string; controlCommandsShow: string; copyPrompt: string; copyReply: string; deleteMessage: string; disabledPlaceholder: string; download: string; editPrompt: string; fileNotPreviewable: string; goodReply: string; inputPlaceholder: string; interrupt: string; noConversation: string; previewTitle: string; stagedSubmissionAttachmentCount: (count: number) => string; stagedSubmissionEmpty: string; stagedSubmissionRestore: string; stagedSubmissionRemove: string; quoteReply: string; quotedReplyLabel: string; send: string; thinking: string; uploadFile: string; uploadingFile: string; user: string; voiceStart: string; voiceStop: string; voiceTranscribing: string; you: string; } interface DashboardChatPanelProps { conversation: ChatMessage[]; isZh: boolean; labels: DashboardChatPanelLabels; chatScrollRef: RefObject; onChatScroll: () => void; expandedProgressByKey: Record; expandedUserByKey: Record; deletingMessageIdMap: Record; feedbackSavingByMessageId: Record; markdownComponents: Components; workspaceDownloadExtensionSet: ReadonlySet; onToggleProgressExpand: (key: string) => void; onToggleUserExpand: (key: string) => void; onEditUserPrompt: (text: string) => void; onCopyUserPrompt: (text: string) => Promise | void; onDeleteConversationMessage: (message: ChatMessage) => Promise | void; onOpenWorkspacePath: (path: string) => Promise | void; onSubmitAssistantFeedback: (message: ChatMessage, direction: 'up' | 'down') => Promise | void; onQuoteAssistantReply: (message: ChatMessage) => void; onCopyAssistantReply: (text: string) => Promise | void; isThinking: boolean; canChat: boolean; isChatEnabled: boolean; speechEnabled: boolean; selectedBotEnabled: boolean; selectedBotControlState?: 'starting' | 'stopping' | 'enabling' | 'disabling'; quotedReply: { text: string } | null; onClearQuotedReply: () => void; stagedSubmissions: StagedSubmissionDraft[]; onRestoreStagedSubmission: (stagedSubmissionId: string) => void; onRemoveStagedSubmission: (stagedSubmissionId: string) => void; pendingAttachments: string[]; onRemovePendingAttachment: (path: string) => void; attachmentUploadPercent: number | null; isUploadingAttachments: boolean; filePickerRef: RefObject; allowedAttachmentExtensions: string[]; onPickAttachments: ChangeEventHandler; controlCommandPanelOpen: boolean; controlCommandPanelRef: RefObject; onToggleControlCommandPanel: () => void; activeControlCommand: string; canSendControlCommand: boolean; isInterrupting: boolean; onSendControlCommand: (command: '/restart' | '/new') => Promise | void; onInterruptExecution: () => Promise | void; chatDateTriggerRef: RefObject; hasSelectedBot: boolean; chatDateJumping: boolean; onToggleChatDatePicker: () => void; chatDatePickerOpen: boolean; chatDatePanelPosition: { bottom: number; right: number } | null; chatDateValue: string; onChatDateValueChange: (value: string) => void; onCloseChatDatePicker: () => void; onJumpConversationToDate: () => Promise | void; command: string; onCommandChange: (value: string) => void; composerTextareaRef: RefObject; onComposerKeyDown: KeyboardEventHandler; isVoiceRecording: boolean; isVoiceTranscribing: boolean; isCompactMobile: boolean; voiceCountdown: number; onVoiceInput: () => Promise | void; onTriggerPickAttachments: () => Promise | void; submitActionMode: 'interrupt' | 'send' | 'stage'; onSubmitAction: () => Promise | void; } interface DashboardChatTranscriptProps { conversation: ChatMessage[]; isZh: boolean; labels: DashboardChatPanelLabels; chatScrollRef: RefObject; onChatScroll: () => void; expandedProgressByKey: Record; expandedUserByKey: Record; deletingMessageIdMap: Record; feedbackSavingByMessageId: Record; markdownComponents: Components; workspaceDownloadExtensionSet: ReadonlySet; onToggleProgressExpand: (key: string) => void; onToggleUserExpand: (key: string) => void; onEditUserPrompt: (text: string) => void; onCopyUserPrompt: (text: string) => Promise | void; onDeleteConversationMessage: (message: ChatMessage) => Promise | void; onOpenWorkspacePath: (path: string) => Promise | void; onSubmitAssistantFeedback: (message: ChatMessage, direction: 'up' | 'down') => Promise | void; onQuoteAssistantReply: (message: ChatMessage) => void; onCopyAssistantReply: (text: string) => Promise | void; isThinking: boolean; } const MemoizedChatTranscript = memo(function MemoizedChatTranscript({ conversation, isZh, labels, chatScrollRef, onChatScroll, expandedProgressByKey, expandedUserByKey, deletingMessageIdMap, feedbackSavingByMessageId, markdownComponents, workspaceDownloadExtensionSet, onToggleProgressExpand, onToggleUserExpand, onEditUserPrompt, onCopyUserPrompt, onDeleteConversationMessage, onOpenWorkspacePath, onSubmitAssistantFeedback, onQuoteAssistantReply, onCopyAssistantReply, isThinking, }: DashboardChatTranscriptProps) { return (
{conversation.length === 0 ? (
{labels.noConversation}
) : ( )} {isThinking ? (
Nanobot
{labels.thinking}
) : null}
); }, (prev, next) => ( prev.conversation === next.conversation && prev.isZh === next.isZh && prev.isThinking === next.isThinking && prev.chatScrollRef === next.chatScrollRef && prev.expandedProgressByKey === next.expandedProgressByKey && prev.expandedUserByKey === next.expandedUserByKey && prev.deletingMessageIdMap === next.deletingMessageIdMap && prev.feedbackSavingByMessageId === next.feedbackSavingByMessageId && prev.markdownComponents === next.markdownComponents && prev.workspaceDownloadExtensionSet === next.workspaceDownloadExtensionSet && prev.labels.badReply === next.labels.badReply && prev.labels.copyPrompt === next.labels.copyPrompt && prev.labels.copyReply === next.labels.copyReply && prev.labels.deleteMessage === next.labels.deleteMessage && prev.labels.download === next.labels.download && prev.labels.editPrompt === next.labels.editPrompt && prev.labels.fileNotPreviewable === next.labels.fileNotPreviewable && prev.labels.goodReply === next.labels.goodReply && prev.labels.noConversation === next.labels.noConversation && prev.labels.previewTitle === next.labels.previewTitle && prev.labels.quoteReply === next.labels.quoteReply && prev.labels.quotedReplyLabel === next.labels.quotedReplyLabel && prev.labels.thinking === next.labels.thinking && prev.labels.user === next.labels.user && prev.labels.you === next.labels.you )); export function DashboardChatPanel({ conversation, isZh, labels, chatScrollRef, onChatScroll, expandedProgressByKey, expandedUserByKey, deletingMessageIdMap, feedbackSavingByMessageId, markdownComponents, workspaceDownloadExtensionSet, onToggleProgressExpand, onToggleUserExpand, onEditUserPrompt, onCopyUserPrompt, onDeleteConversationMessage, onOpenWorkspacePath, onSubmitAssistantFeedback, onQuoteAssistantReply, onCopyAssistantReply, isThinking, canChat, isChatEnabled, speechEnabled, selectedBotEnabled, selectedBotControlState, quotedReply, onClearQuotedReply, stagedSubmissions, onRestoreStagedSubmission, onRemoveStagedSubmission, pendingAttachments, onRemovePendingAttachment, attachmentUploadPercent, isUploadingAttachments, filePickerRef, allowedAttachmentExtensions, onPickAttachments, controlCommandPanelOpen, controlCommandPanelRef, onToggleControlCommandPanel, activeControlCommand, canSendControlCommand, isInterrupting, onSendControlCommand, onInterruptExecution, chatDateTriggerRef, hasSelectedBot, chatDateJumping, onToggleChatDatePicker, chatDatePickerOpen, chatDatePanelPosition, chatDateValue, onChatDateValueChange, onCloseChatDatePicker, onJumpConversationToDate, command, onCommandChange, composerTextareaRef, onComposerKeyDown, isVoiceRecording, isVoiceTranscribing, isCompactMobile, voiceCountdown, onVoiceInput, onTriggerPickAttachments, submitActionMode, onSubmitAction, }: DashboardChatPanelProps) { const showInterruptSubmitAction = submitActionMode === 'interrupt'; const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply); return (
{stagedSubmissions.length > 0 ? (
{stagedSubmissions.map((stagedSubmission, index) => (
{index + 1}
{normalizeUserMessageText(stagedSubmission.command) || labels.stagedSubmissionEmpty}
{(stagedSubmission.quotedReply || stagedSubmission.attachments.length > 0) ? (
{stagedSubmission.quotedReply ? ( {labels.quotedReplyLabel} ) : null} {stagedSubmission.attachments.length > 0 ? ( {labels.stagedSubmissionAttachmentCount(stagedSubmission.attachments.length)} ) : null}
) : null}
))}
) : null} {(quotedReply || pendingAttachments.length > 0) ? (
{quotedReply ? (
{labels.quotedReplyLabel}
{normalizeAssistantMessageText(quotedReply.text)}
) : null} {pendingAttachments.length > 0 ? (
{pendingAttachments.map((path) => ( {(() => { const filePath = normalizeDashboardAttachmentPath(path); const fileAction = workspaceFileAction(filePath, workspaceDownloadExtensionSet); const filename = filePath.split('/').pop() || filePath; return ( { event.preventDefault(); event.stopPropagation(); void onOpenWorkspacePath(filePath); }} > {fileAction === 'download' ? ( ) : fileAction === 'preview' ? ( ) : ( )} {filename} ); })()} ))}
) : null}
) : null} {isUploadingAttachments ? (
{attachmentUploadPercent === null ? labels.uploadingFile : `${labels.uploadingFile} ${attachmentUploadPercent}%`}
) : null}
0 ? allowedAttachmentExtensions.join(',') : undefined} onChange={onPickAttachments} className="ops-hidden-file-input" />
{chatDatePickerOpen ? (
) : null}