import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Plus, RefreshCw, RotateCcw, Square, X } from 'lucide-react'; import type { Components } from 'react-markdown'; import type { ChangeEventHandler, KeyboardEventHandler, RefObject } from 'react'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import nanobotLogo from '../../../assets/nanobot-logo.png'; import type { ChatMessage } from '../../../types/bot'; import { normalizeAssistantMessageText } from '../messageParser'; import { normalizeDashboardAttachmentPath } from '../shared/workspaceMarkdown'; 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; 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; 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; showInterruptSubmitAction: boolean; onSubmitAction: () => Promise | void; } 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, 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, showInterruptSubmitAction, onSubmitAction, }: DashboardChatPanelProps) { return (
{conversation.length === 0 ? (
{labels.noConversation}
) : ( )} {isThinking ? (
Nanobot
{labels.thinking}
) : 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} style={{ display: 'none' }} />
{chatDatePickerOpen ? (
) : null}