diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md
index 7e335b7..56bd702 100644
--- a/.agents/skills/backend/SKILL.md
+++ b/.agents/skills/backend/SKILL.md
@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All)
-Overview of all 250 API endpoints across all modules.
+Overview of all 249 API endpoints across all modules.
### Notation
@@ -922,9 +922,6 @@ _none_
#### Restore system default applets — `POST /vfs/applets/restore`
_none_
- Returns: `200 { success: bool, restored: str[], count: int }`
-#### Respond to a frontend draft request — `POST /vfs/drafts/response`
-- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }`
-- Returns: `200 { success: bool }`
#### Create directory in virtual filesystem — `POST /vfs/mkdir`
- Params: `body: { path: str }`
- Returns: `200 { success: bool, dir: str }`
diff --git a/src/components/chat/ChatAttachmentItems.jsx b/src/components/chat/ChatAttachmentItems.jsx
index c71a4a4..327cf2a 100644
--- a/src/components/chat/ChatAttachmentItems.jsx
+++ b/src/components/chat/ChatAttachmentItems.jsx
@@ -8,8 +8,14 @@ import React, { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
+import Button from '@mui/material/Button';
+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 CircularProgress from '@mui/material/CircularProgress';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
+import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
@@ -202,6 +208,218 @@ export function parseAttachmentTag(attrString) {
export { stripUserEnv };
+/**
+ * Extract all `[EXTRACTION ...]` blocks (or legacy model instruction code blocks)
+ * and return structured extraction objects along with clean non-invoice text.
+ * @param {string} rawText
+ * @returns {{ extractions: Array<{ label: string, text: string }>, cleanText: string }}
+ */
+export function parseExtractionsFromText(rawText) {
+ if (typeof rawText !== 'string' || !rawText) {
+ return { extractions: [], cleanText: '' };
+ }
+
+ const extractions = [];
+ let cleanText = rawText;
+
+ // 1. Explicit [EXTRACTION label="..."]...[/EXTRACTION] or ...
+ const tagRegex = /(?:\[EXTRACTION(?:\s+label="([^"]*)")?\]|)([\s\S]*?)(?:\[\/EXTRACTION\]|<\/extraction>)/gi;
+ let match;
+ while ((match = tagRegex.exec(rawText)) !== null) {
+ const label = match[1] || match[2] || 'parsed invoice';
+ let text = match[3] || '';
+ text = text.replace(/^```[a-z]*\s*\n?|\s*```$/gi, '').trim();
+ extractions.push({ label, text });
+ }
+
+ if (extractions.length > 0) {
+ cleanText = cleanText.replace(tagRegex, '').trim();
+ return { extractions, cleanText };
+ }
+
+ // 2. Legacy / fallback: message contains "use 'Process Incoming Product and Invoices' SKILL" + code block
+ const instructionPhrase = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file";
+ if (cleanText.includes(instructionPhrase)) {
+ const codeBlockRegex = /```[a-z]*\s*\n([\s\S]*?)```/g;
+ let cbMatch;
+ while ((cbMatch = codeBlockRegex.exec(cleanText)) !== null) {
+ const code = (cbMatch[1] || '').trim();
+ if (code.length > 20) {
+ extractions.push({ label: 'parsed invoice', text: code });
+ }
+ }
+ if (extractions.length > 0) {
+ cleanText = cleanText.replace(codeBlockRegex, '').trim();
+ return { extractions, cleanText };
+ }
+ }
+
+ // 3. Standalone code block (e.g. ```\n...invoice...```)
+ const soleCodeBlockMatch = cleanText.match(/^\s*```[a-z]*\s*\n([\s\S]{40,})```\s*$/);
+ if (soleCodeBlockMatch) {
+ const code = soleCodeBlockMatch[1].trim();
+ extractions.push({ label: 'parsed invoice', text: code });
+ cleanText = '';
+ return { extractions, cleanText };
+ }
+
+ return { extractions: [], cleanText };
+}
+
+/**
+ * Clickable pill representing an extracted invoice/data snippet inside a message bubble.
+ * Clicking opens a modal preview to inspect and copy the full text.
+ */
+export class ExtractionPill extends PureComponent {
+ state = {
+ openPreview: false,
+ };
+
+ handleClick = (e) => {
+ e?.preventDefault?.();
+ e?.stopPropagation?.();
+ this.setState({ openPreview: true });
+ };
+
+ handleClose = () => {
+ this.setState({ openPreview: false });
+ };
+
+ handleCopy = () => {
+ const { text } = this.props;
+ if (text && navigator?.clipboard?.writeText) {
+ navigator.clipboard.writeText(text);
+ enqueueSnackbar('Extrahierter Text kopiert', { variant: 'success' });
+ }
+ };
+
+ render() {
+ const { label = 'parsed invoice', text = '', isUser = false } = this.props;
+ const { openPreview } = this.state;
+
+ return (
+ <>
+ (theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)'),
+ color: isUser ? '#fff' : 'text.primary',
+ cursor: 'pointer',
+ verticalAlign: 'middle',
+ maxWidth: '100%',
+ outline: 'none',
+ fontFamily: 'inherit',
+ transition: 'all 0.15s ease',
+ '&:hover': {
+ bgcolor: isUser ? 'rgba(255, 255, 255, 0.3)' : 'action.hover',
+ borderColor: isUser ? '#fff' : 'primary.main',
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
+ },
+ }}
+ >
+
+
+ {label || 'parsed invoice'}
+
+
+
+
+ >
+ );
+ }
+}
+
/**
* Extract all `[ATTACHMENT ...]` markers and return clean text plus attachment metadata.
* Also strips tags so route context is never rendered in chat views.
diff --git a/src/components/chat/ChatDrawer.jsx b/src/components/chat/ChatDrawer.jsx
index 1e2f735..197e724 100644
--- a/src/components/chat/ChatDrawer.jsx
+++ b/src/components/chat/ChatDrawer.jsx
@@ -88,8 +88,36 @@ function formatUpdated(value) {
}
function conversationTitle(c) {
- const msg = (c.first_message || '').replace(/\s+/g, ' ').trim();
- return msg || '…';
+ let msg = (c.first_message || '').trim();
+ if (!msg) return '…';
+
+ const extMatch = msg.match(/\[EXTRACTION(?:\s+label="([^"]*)")?\]|/i);
+ const hasInvoiceSkill = msg.includes("Process Incoming Product and Invoices");
+
+ let stripped = msg
+ .replace(/\[EXTRACTION(?:\s+label="[^"]*")?\][\s\S]*?\[\/EXTRACTION\]/gi, '')
+ .replace(/[\s\S]*?<\/extraction>/gi, '')
+ .replace(/```[\s\S]*?```/g, '')
+ .replace(/use 'Process Incoming Product and Invoices' SKILL to fill drafts\/delivery vfs file/gi, '')
+ .replace(/[\s\S]*?<\/USERENV>/gi, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ if (extMatch || hasInvoiceSkill) {
+ const label = extMatch?.[1] || extMatch?.[2] || 'parsed invoice';
+ if (stripped) {
+ return `${label}: ${stripped}`;
+ }
+ return label;
+ }
+
+ let clean = msg
+ .replace(/[\s\S]*?<\/USERENV>/gi, '')
+ .replace(/```[\s\S]*?```/g, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ return clean || msg.slice(0, 40) || '…';
}
const MIN_DRAWER_WIDTH = 320;
@@ -566,14 +594,17 @@ export default class ChatDrawer extends PureComponent {
const instructionText =
uniqueInstructions.join('\n\n') || EXTRACTION_MODEL_INSTRUCTION;
- const extMarkdown = extractions
- .map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``)
+ const extBlocks = extractions
+ .map(
+ (ext) =>
+ `[EXTRACTION label="${ext.label || 'parsed invoice'}"]\n\`\`\`\n${ext.text.trim()}\n\`\`\`\n[/EXTRACTION]`
+ )
.join('\n\n');
if (text) {
- text = `${instructionText}\n\n${text}\n\n${extMarkdown}`;
+ text = `${instructionText}\n\n${text}\n\n${extBlocks}`;
} else {
- text = `${instructionText}\n\n${extMarkdown}`;
+ text = `${instructionText}\n\n${extBlocks}`;
}
}
@@ -1978,6 +2009,9 @@ export default class ChatDrawer extends PureComponent {
onClose={this.handleCloseExtractionPreview}
maxWidth="md"
fullWidth
+ sx={(theme) => ({
+ zIndex: theme.zIndex.modal + 50,
+ })}
slotProps={{
paper: {
sx: {
diff --git a/src/components/chat/ChatFlowList.jsx b/src/components/chat/ChatFlowList.jsx
index f1d013f..9d18c12 100644
--- a/src/components/chat/ChatFlowList.jsx
+++ b/src/components/chat/ChatFlowList.jsx
@@ -27,7 +27,13 @@ import CalculateIcon from '@mui/icons-material/Calculate';
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
import DirectionsIcon from '@mui/icons-material/Directions';
import HealingMarkdown from './HealingMarkdown';
-import { VfsAttachmentLink, parseAttachmentsFromText, stripUserEnv } from './ChatAttachmentItems';
+import {
+ VfsAttachmentLink,
+ ExtractionPill,
+ parseAttachmentsFromText,
+ parseExtractionsFromText,
+ stripUserEnv,
+} from './ChatAttachmentItems';
import {
CHROMA,
SHADOW_BUBBLE_ASSISTANT,
@@ -878,7 +884,8 @@ function MessageRow({ msg, showTime = true }) {
} catch { }
}
- const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textContent);
+ const { extractions, cleanText: textAfterExtractions } = parseExtractionsFromText(textContent);
+ const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textAfterExtractions);
const cleanText = stripUserEnv(rawCleanText);
if (msg.role === 'system') {
@@ -957,6 +964,26 @@ function MessageRow({ msg, showTime = true }) {
lineHeight: 1.5,
})}
>
+ {extractions.length > 0 && (
+ 0 ? 0.75 : 0,
+ }}
+ >
+ {extractions.map((ext, idx) => (
+
+ ))}
+
+ )}
+
{vfsAttachments.length > 0 && (
}
- onClick={this.handleRefresh}
+ onClick={() => void this.loadBackups()}
sx={{ alignSelf: 'flex-start' }}
>
Erneut versuchen
@@ -286,28 +273,15 @@ export default class BackupSettingsPanel extends Component {
return (
- {/* Header Title & Actions */}
-
-
-
-
- Datensicherung & Backups
-
-
- Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher.
-
-
-
- }
- onClick={this.handleRefresh}
- disabled={status === 'loading'}
- >
- Aktualisieren
-
-
+ {/* Header Title */}
+
+
+
+ Datensicherung & Backups
+
+
+ Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher.
+
{/* Status Badges Bar */}