u
This commit is contained in:
@@ -16,10 +16,17 @@ import AddRoundedIcon from '@mui/icons-material/AddRounded';
|
||||
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||
import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
|
||||
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||
|
||||
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
||||
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
||||
import ChatFlowList from './ChatFlowList';
|
||||
import AskUserPrompt from './AskUserPrompt';
|
||||
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
|
||||
import { ChatStagedAttachmentsTray } from './ChatAttachmentItems';
|
||||
import ChatAttachmentPreviewModal from './ChatAttachmentPreviewModal';
|
||||
import {
|
||||
BLUE_GLOW,
|
||||
SHADOW_DRAWER,
|
||||
@@ -50,10 +57,16 @@ export default class ChatDrawer extends PureComponent {
|
||||
sendError: s.sendError,
|
||||
activeTurns: s.activeTurns || [],
|
||||
inputText: '',
|
||||
stagedAttachments: [],
|
||||
isProcessingFiles: false,
|
||||
isDraggingFiles: false,
|
||||
attachmentError: null,
|
||||
previewAttachment: null,
|
||||
drawerWidth: settings?.chatDrawerWidth || DEFAULT_DRAWER_WIDTH,
|
||||
isResizing: false,
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
this.fileInputRef = React.createRef();
|
||||
this.focusTimeout = null;
|
||||
}
|
||||
|
||||
@@ -204,17 +217,122 @@ export default class ChatDrawer extends PureComponent {
|
||||
handleSend = async (e) => {
|
||||
e?.preventDefault();
|
||||
const text = this.state.inputText.trim();
|
||||
if (!text || this.state.conversationState === 'busy' || this.state.isSending) {
|
||||
const attachments = this.state.stagedAttachments || [];
|
||||
if ((!text && attachments.length === 0) || this.state.conversationState === 'busy' || this.state.isSending) {
|
||||
return;
|
||||
}
|
||||
this.setState({ inputText: '' });
|
||||
this.setState({ inputText: '', stagedAttachments: [], attachmentError: null });
|
||||
try {
|
||||
await chatAutomat.actions.sendMessage(text);
|
||||
await chatAutomat.actions.sendMessage(text, attachments);
|
||||
} catch {
|
||||
// Error is tracked in chatAutomat state (sendError)
|
||||
}
|
||||
};
|
||||
|
||||
handleOpenFileDialog = () => {
|
||||
this.fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
handleFileSelect = (e) => {
|
||||
const files = e.target?.files;
|
||||
if (files && files.length > 0) {
|
||||
void this.handleFiles(Array.from(files));
|
||||
}
|
||||
if (e.target) {
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
handleFiles = async (fileList) => {
|
||||
if (!fileList || fileList.length === 0) return;
|
||||
this.setState({ isProcessingFiles: true, attachmentError: null });
|
||||
|
||||
const newAttachments = [];
|
||||
const errors = [];
|
||||
|
||||
for (const file of fileList) {
|
||||
try {
|
||||
const att = await readFileAttachment(file);
|
||||
newAttachments.push(att);
|
||||
} catch (err) {
|
||||
errors.push(err.message || `Datei "${file.name}" konnte nicht verarbeitet werden.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState((prev) => ({
|
||||
stagedAttachments: [...prev.stagedAttachments, ...newAttachments],
|
||||
isProcessingFiles: false,
|
||||
attachmentError: errors.length > 0 ? errors.join(' ') : null,
|
||||
}));
|
||||
this.focusInput(50);
|
||||
};
|
||||
|
||||
handleRemoveAttachment = (idOrIdx) => {
|
||||
this.setState((prev) => ({
|
||||
stagedAttachments: prev.stagedAttachments.filter((a, idx) => (a.id ? a.id !== idOrIdx : idx !== idOrIdx)),
|
||||
}));
|
||||
};
|
||||
|
||||
handlePreviewAttachment = (attachment) => {
|
||||
this.setState({ previewAttachment: attachment });
|
||||
};
|
||||
|
||||
handleClosePreview = () => {
|
||||
this.setState({ previewAttachment: null });
|
||||
};
|
||||
|
||||
handlePaste = (e) => {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items || items.length === 0) return;
|
||||
|
||||
const files = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
void this.handleFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.setState({ isDraggingFiles: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.state.isDraggingFiles && e.dataTransfer?.types?.includes('Files')) {
|
||||
this.setState({ isDraggingFiles: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.currentTarget.contains(e.relatedTarget)) return;
|
||||
this.setState({ isDraggingFiles: false });
|
||||
};
|
||||
|
||||
handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({ isDraggingFiles: false });
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
void this.handleFiles(Array.from(files));
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -449,26 +567,37 @@ export default class ChatDrawer extends PureComponent {
|
||||
draftConversation,
|
||||
inputText,
|
||||
sendError,
|
||||
stagedAttachments,
|
||||
isProcessingFiles,
|
||||
isDraggingFiles,
|
||||
attachmentError,
|
||||
previewAttachment,
|
||||
} = this.state;
|
||||
|
||||
const pendingAsk = this.getPendingAskUser();
|
||||
const isBusy = conversationState === 'busy';
|
||||
const isWorking = isBusy || isSending;
|
||||
const canChat = (Boolean(selectedConversationId) || draftConversation) && !pendingAsk;
|
||||
const canSend = !isWorking && canChat && Boolean(inputText.trim());
|
||||
const canSend = !isWorking && canChat && (Boolean(inputText.trim()) || stagedAttachments.length > 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={this.handleSend}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
onDrop={this.handleDrop}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderTop: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
borderColor: isDraggingFiles ? 'primary.main' : 'divider',
|
||||
bgcolor: isDraggingFiles ? 'action.hover' : 'background.paper',
|
||||
transition: 'border-color 0.15s, background-color 0.15s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{sendError && (
|
||||
@@ -495,7 +624,105 @@ export default class ChatDrawer extends PureComponent {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{attachmentError && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: 'warning.light',
|
||||
color: 'warning.contrastText',
|
||||
fontSize: '0.75rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">{attachmentError}</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.setState({ attachmentError: null })}
|
||||
sx={{ color: 'inherit', p: 0.2 }}
|
||||
>
|
||||
<CloseRoundedIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Staged attachments tray */}
|
||||
<ChatStagedAttachmentsTray
|
||||
attachments={stagedAttachments}
|
||||
onRemove={this.handleRemoveAttachment}
|
||||
onPreview={this.handlePreviewAttachment}
|
||||
/>
|
||||
|
||||
{isProcessingFiles && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5, px: 1 }}>
|
||||
<CircularProgress size={14} thickness={5} />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.72rem' }}>
|
||||
Dateien werden eingelesen…
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isDraggingFiles && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
border: '2px dashed',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: 2,
|
||||
textAlign: 'center',
|
||||
bgcolor: 'action.selected',
|
||||
color: 'primary.main',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
Dateien hier ablegen (Bilder, PDFs, CSV, Text, XML, HTML)
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1 }}>
|
||||
{/* Attachment Icon Button */}
|
||||
<Tooltip title="Dateien anhängen (Bilder, PDFs, CSV, Text, XML, HTML)">
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={this.handleOpenFileDialog}
|
||||
disabled={!canChat || isWorking}
|
||||
aria-label="Dateien anhängen"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2.5,
|
||||
color: stagedAttachments.length > 0 ? 'primary.main' : 'text.secondary',
|
||||
bgcolor: stagedAttachments.length > 0 ? 'action.selected' : 'transparent',
|
||||
border: '1px solid',
|
||||
borderColor: stagedAttachments.length > 0 ? 'primary.main' : 'transparent',
|
||||
flexShrink: 0,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge badgeContent={stagedAttachments.length} color="primary">
|
||||
<AttachFileRoundedIcon sx={{ fontSize: 22 }} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* Hidden File Input */}
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={ACCEPT_STRING}
|
||||
onChange={this.handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
inputRef={this.inputRef}
|
||||
multiline
|
||||
@@ -504,11 +731,14 @@ export default class ChatDrawer extends PureComponent {
|
||||
value={inputText}
|
||||
onChange={(e) => this.setState({ inputText: e.target.value })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onPaste={this.handlePaste}
|
||||
disabled={!canChat}
|
||||
placeholder={
|
||||
pendingAsk
|
||||
? 'Bitte Frage oben beantworten…'
|
||||
: undefined
|
||||
: stagedAttachments.length > 0
|
||||
? 'Nachricht hinzufügen oder direkt absenden…'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -527,6 +757,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2.5,
|
||||
flexShrink: 0,
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.dark',
|
||||
},
|
||||
@@ -544,6 +775,13 @@ export default class ChatDrawer extends PureComponent {
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Modal for previewing staged attachments */}
|
||||
<ChatAttachmentPreviewModal
|
||||
open={Boolean(previewAttachment)}
|
||||
attachment={previewAttachment}
|
||||
onClose={this.handleClosePreview}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user