From 52411ac45ddab981f5611725fec4d89e82bea52a Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 16 Oct 2026 15:38:39 +0200 Subject: [PATCH] u --- .agents/skills/backend/SKILL.md | 5 +- src/components/chat/ChatAttachmentItems.jsx | 218 ++++++++++++++++++ src/components/chat/ChatDrawer.jsx | 46 +++- src/components/chat/ChatFlowList.jsx | 31 ++- .../settings/BackupSettingsPanel.jsx | 46 +--- 5 files changed, 298 insertions(+), 48 deletions(-) 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'} + + + + ({ + zIndex: theme.zIndex.modal + 50, + })} + slotProps={{ + paper: { + sx: { + borderRadius: 2.5, + bgcolor: 'background.paper', + backgroundImage: 'none', + }, + }, + }} + > + + + + + {label || 'parsed invoice'} + + + + + + + theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.02)' }}> + + {text} + + + + + + + + + ); + } +} + /** * 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. - - - - - + {/* Header Title */} + + + + Datensicherung & Backups + + + Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher. + {/* Status Badges Bar */}