This commit is contained in:
seb
2026-10-16 14:32:10 +02:00
parent a592cc30f6
commit 5c57097ad0
4 changed files with 136 additions and 5 deletions

View File

@@ -331,9 +331,13 @@ const actions = {
/** /**
* Stage an extracted text snippet (e.g. from an invoice or delivery document) as a pill. * Stage an extracted text snippet (e.g. from an invoice or delivery document) as a pill.
* @param {{ text: string, label?: string }} snippet * @param {{ text: string, label?: string, instruction?: string }} snippet
*/ */
insertExtractionSnippet({ text = '', label = 'parsed invoice' } = {}) { insertExtractionSnippet({
text = '',
label = 'parsed invoice',
instruction = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file",
} = {}) {
const trimmed = String(text || '').trim(); const trimmed = String(text || '').trim();
if (!trimmed) return; if (!trimmed) return;
chatOpenedThisSession = true; chatOpenedThisSession = true;
@@ -343,6 +347,7 @@ const actions = {
pendingExtraction: { pendingExtraction: {
text: trimmed, text: trimmed,
label: label || 'parsed invoice', label: label || 'parsed invoice',
instruction: instruction || "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file",
}, },
}); });
ensureCompanyEventsConnected(); ensureCompanyEventsConnected();

View File

@@ -20,7 +20,12 @@ import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import Badge from '@mui/material/Badge'; import Badge from '@mui/material/Badge';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded'; import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
import TuneRoundedIcon from '@mui/icons-material/TuneRounded'; import TuneRoundedIcon from '@mui/icons-material/TuneRounded';
import ElderlyRoundedIcon from '@mui/icons-material/ElderlyRounded'; import ElderlyRoundedIcon from '@mui/icons-material/ElderlyRounded';
import DirectionsWalkRoundedIcon from '@mui/icons-material/DirectionsWalkRounded'; import DirectionsWalkRoundedIcon from '@mui/icons-material/DirectionsWalkRounded';
@@ -47,6 +52,7 @@ import {
} from '../../colors'; } from '../../colors';
const DEFAULT_DRAWER_WIDTH = 460; const DEFAULT_DRAWER_WIDTH = 460;
const EXTRACTION_MODEL_INSTRUCTION = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file";
/** Sort by updated_at desc; subagents (started_by set) directly below their parent. */ /** Sort by updated_at desc; subagents (started_by set) directly below their parent. */
function sortConversations(list) { function sortConversations(list) {
@@ -122,9 +128,11 @@ export default class ChatDrawer extends PureComponent {
id: `ext-${Date.now()}`, id: `ext-${Date.now()}`,
text: s.pendingExtraction.text, text: s.pendingExtraction.text,
label: s.pendingExtraction.label || 'parsed invoice', label: s.pendingExtraction.label || 'parsed invoice',
instruction: s.pendingExtraction.instruction || EXTRACTION_MODEL_INSTRUCTION,
}, },
] ]
: [], : [],
previewExtraction: null,
isProcessingFiles: false, isProcessingFiles: false,
isDraggingFiles: false, isDraggingFiles: false,
attachmentError: null, attachmentError: null,
@@ -234,6 +242,7 @@ export default class ChatDrawer extends PureComponent {
id: `ext-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, id: `ext-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
text: ext.text, text: ext.text,
label: ext.label || 'parsed invoice', label: ext.label || 'parsed invoice',
instruction: ext.instruction || EXTRACTION_MODEL_INSTRUCTION,
}; };
patch.stagedExtractions = [...(this.state.stagedExtractions || []), newExt]; patch.stagedExtractions = [...(this.state.stagedExtractions || []), newExt];
queueMicrotask(() => { queueMicrotask(() => {
@@ -348,6 +357,22 @@ export default class ChatDrawer extends PureComponent {
this.focusInput(50); this.focusInput(50);
}; };
handlePreviewExtraction = (ext) => {
console.log('[ChatDrawer] Clicked extraction pill:', ext);
this.setState({ previewExtraction: ext });
};
handleCloseExtractionPreview = () => {
this.setState({ previewExtraction: null });
};
handleCopyExtractionText = () => {
const text = this.state.previewExtraction?.text;
if (text && navigator?.clipboard?.writeText) {
navigator.clipboard.writeText(text);
}
};
handleVfsResizerStart = (e) => { handleVfsResizerStart = (e) => {
if (e.button !== undefined && e.button !== 0) return; if (e.button !== undefined && e.button !== 0) return;
e.preventDefault(); e.preventDefault();
@@ -534,10 +559,22 @@ export default class ChatDrawer extends PureComponent {
let text = rawText; let text = rawText;
if (extractions.length > 0) { if (extractions.length > 0) {
const instructions = extractions
.map((e) => e.instruction)
.filter(Boolean);
const uniqueInstructions = Array.from(new Set(instructions));
const instructionText =
uniqueInstructions.join('\n\n') || EXTRACTION_MODEL_INSTRUCTION;
const extMarkdown = extractions const extMarkdown = extractions
.map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``) .map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``)
.join('\n\n'); .join('\n\n');
text = text ? `${extMarkdown}\n\n${text}` : extMarkdown;
if (text) {
text = `${instructionText}\n\n${text}\n\n${extMarkdown}`;
} else {
text = `${instructionText}\n\n${extMarkdown}`;
}
} }
if (references.length > 0) { if (references.length > 0) {
@@ -1601,6 +1638,10 @@ export default class ChatDrawer extends PureComponent {
{stagedExtractions && stagedExtractions.map((ext) => ( {stagedExtractions && stagedExtractions.map((ext) => (
<Box <Box
key={ext.id} key={ext.id}
onClick={(e) => {
e.stopPropagation();
this.handlePreviewExtraction(ext);
}}
sx={{ sx={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@@ -1622,6 +1663,15 @@ export default class ChatDrawer extends PureComponent {
fontSize: '0.75rem', fontSize: '0.75rem',
lineHeight: 1.2, lineHeight: 1.2,
userSelect: 'none', userSelect: 'none',
cursor: 'pointer',
transition: 'all 0.15s ease',
'&:hover': {
bgcolor: (theme) =>
theme.palette.mode === 'dark'
? 'rgba(255, 255, 255, 0.14)'
: 'rgba(0, 0, 0, 0.09)',
borderColor: 'primary.light',
},
}} }}
> >
<DescriptionOutlinedIcon sx={{ fontSize: 14, color: 'primary.main' }} /> <DescriptionOutlinedIcon sx={{ fontSize: 14, color: 'primary.main' }} />
@@ -1921,6 +1971,71 @@ export default class ChatDrawer extends PureComponent {
{this.renderFooter()} {this.renderFooter()}
</Box> </Box>
</Drawer> </Drawer>
{/* Extraction Preview Dialog */}
<Dialog
open={Boolean(this.state.previewExtraction)}
onClose={this.handleCloseExtractionPreview}
maxWidth="md"
fullWidth
slotProps={{
paper: {
sx: {
borderRadius: 2.5,
bgcolor: 'background.paper',
backgroundImage: 'none',
},
},
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DescriptionOutlinedIcon sx={{ color: 'primary.main', fontSize: 22 }} />
<Typography variant="h6" component="span" sx={{ fontSize: '1.05rem', fontWeight: 600 }}>
{this.state.previewExtraction?.label || 'parsed invoice'}
</Typography>
</Box>
<IconButton size="small" onClick={this.handleCloseExtractionPreview} aria-label="Schließen">
<CloseRoundedIcon sx={{ fontSize: 20 }} />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ p: 2, bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.02)' }}>
<Box
component="pre"
sx={{
m: 0,
p: 1.5,
borderRadius: 1.5,
fontFamily: 'monospace',
fontSize: '0.8rem',
lineHeight: 1.5,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
overflow: 'auto',
maxHeight: '60vh',
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
}}
>
{this.state.previewExtraction?.text || ''}
</Box>
</DialogContent>
<DialogActions sx={{ px: 2.5, py: 1.5 }}>
<Button
startIcon={<ContentCopyRoundedIcon sx={{ fontSize: 18 }} />}
onClick={this.handleCopyExtractionText}
color="primary"
variant="outlined"
size="small"
>
Kopieren
</Button>
<Button onClick={this.handleCloseExtractionPreview} color="inherit" size="small">
Schließen
</Button>
</DialogActions>
</Dialog>
</> </>
); );
} }

View File

@@ -69,6 +69,7 @@ export class Attachment extends PureComponent {
handleMentionInChat = async (e) => { handleMentionInChat = async (e) => {
e?.stopPropagation?.(); e?.stopPropagation?.();
const { attachment, onMentionInChat } = this.props; const { attachment, onMentionInChat } = this.props;
console.log('[Attachment] Mention in chat clicked', attachment);
if (!attachment) return; if (!attachment) return;
if (onMentionInChat) { if (onMentionInChat) {
@@ -105,11 +106,15 @@ export class Attachment extends PureComponent {
} }
const trimmed = (textContent || '').trim(); const trimmed = (textContent || '').trim();
if (!trimmed) return; if (!trimmed) {
console.warn('[Attachment] No text found to mention in chat', attachment);
return;
}
chatAutomat.actions.insertExtractionSnippet({ chatAutomat.actions.insertExtractionSnippet({
text: trimmed, text: trimmed,
label: 'parsed invoice', label: 'parsed invoice',
instruction: "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file",
}); });
}; };

View File

@@ -688,6 +688,7 @@ class LieferungNeuPage extends PureComponent {
} }
async handleMentionInChat(attachment) { async handleMentionInChat(attachment) {
console.log('[neu.jsx] handleMentionInChat called', attachment);
if (!attachment) return; if (!attachment) return;
let text = attachment.text; let text = attachment.text;
if (!text && (!attachment.file || attachment.file.size === 0)) { if (!text && (!attachment.file || attachment.file.size === 0)) {
@@ -708,11 +709,16 @@ class LieferungNeuPage extends PureComponent {
} }
const trimmed = (text || '').trim(); const trimmed = (text || '').trim();
if (!trimmed) return; if (!trimmed) {
console.warn('[neu.jsx] handleMentionInChat: text is empty for attachment', attachment);
return;
}
console.log('[neu.jsx] Inserting extraction snippet to chat, length:', trimmed.length);
chatAutomat.actions.insertExtractionSnippet({ chatAutomat.actions.insertExtractionSnippet({
text: trimmed, text: trimmed,
label: 'parsed invoice', label: 'parsed invoice',
instruction: "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file",
}); });
} }