This commit is contained in:
seb
2026-10-16 14:17:22 +02:00
parent 764d6f850a
commit a592cc30f6
8 changed files with 477 additions and 64 deletions

View File

@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 248 API endpoints across all modules. Overview of all 250 API endpoints across all modules.
### Notation ### Notation
@@ -919,6 +919,9 @@ _none_
#### List virtual filesystem files — `GET /vfs` #### List virtual filesystem files — `GET /vfs`
_none_ _none_
- Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }` - Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }`
#### 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` #### Respond to a frontend draft request — `POST /vfs/drafts/response`
- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }` - Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
@@ -928,6 +931,9 @@ _none_
#### Rename or move a virtual filesystem file or directory — `POST /vfs/rename` #### Rename or move a virtual filesystem file or directory — `POST /vfs/rename`
- Params: `body: { oldPath: str, newPath: str }` - Params: `body: { oldPath: str, newPath: str }`
- Returns: `200 { success: bool, oldPath: str, newPath: str }` - Returns: `200 { success: bool, oldPath: str, newPath: str }`
#### Restore system default skills — `POST /vfs/skills/restore`
_none_
- Returns: `200 { success: bool, restored: str[], count: int }`
#### Upload files to virtual filesystem — `POST /vfs/upload` #### Upload files to virtual filesystem — `POST /vfs/upload`
- Params: `form: { files: str[], targetDir?: str }` - Params: `form: { files: str[], targetDir?: str }`
- Returns: `200 { success: bool, uploaded: { name: str, size: int, mime: str }[] }` - Returns: `200 { success: bool, uploaded: { name: str, size: int, mime: str }[] }`

View File

@@ -181,4 +181,32 @@ export const vfs = {
} }
}, },
// #endregion // #endregion
/**
* POST /api/vfs/skills/restore
* Restores all system default .SKILL files into the company's VFS (overwrites existing files).
* Requires no request body. Emits vfs SSE event.
* → 200 { success: true, restored: string[], count: number }
*/
// #region POST /api/vfs/skills/restore — restoreSkills
restoreSkills() {
return http('/api/vfs/skills/restore', {
method: 'POST',
});
},
// #endregion
/**
* POST /api/vfs/applets/restore
* Restores all system default .applet.js example applets into the company's VFS (overwrites existing files).
* Requires no request body. Emits vfs SSE event.
* → 200 { success: true, restored: string[], count: number }
*/
// #region POST /api/vfs/applets/restore — restoreApplets
restoreApplets() {
return http('/api/vfs/applets/restore', {
method: 'POST',
});
},
// #endregion
}; };

View File

@@ -331,20 +331,18 @@ 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, size?: number, label?: string }} snippet * @param {{ text: string, label?: string }} snippet
*/ */
insertExtractionSnippet({ text = '', size, label } = {}) { insertExtractionSnippet({ text = '', label = 'parsed invoice' } = {}) {
const trimmed = String(text || '').trim(); const trimmed = String(text || '').trim();
if (!trimmed) return; if (!trimmed) return;
const finalSize = Number(size) || new Blob([trimmed]).size;
chatOpenedThisSession = true; chatOpenedThisSession = true;
chatAutomat.setState({ chatAutomat.setState({
isOpen: true, isOpen: true,
sseEstablished: true, sseEstablished: true,
pendingExtraction: { pendingExtraction: {
text: trimmed, text: trimmed,
size: finalSize, label: label || 'parsed invoice',
label,
}, },
}); });
ensureCompanyEventsConnected(); ensureCompanyEventsConnected();
@@ -356,26 +354,17 @@ const actions = {
/** /**
* Open the chat drawer and append text / prompt to the chat composer. * Open the chat drawer and append text / prompt to the chat composer.
* If the prompt is an extracted text snippet (e.g. `Text aus „...“:`),
* automatically converts it into a staged extraction pill.
* @param {string} prompt * @param {string} prompt
*/ */
insertPrompt(prompt = '') { insertPrompt(prompt = '') {
chatOpenedThisSession = true; const text = String(prompt || '').trim();
const trimmedPrompt = String(prompt || '').trim(); if (!text) return;
const extractionMatch = trimmedPrompt.match(/^(?:Text for Data Extraction|Text aus „[^“]+“):\s*```(?:\w+)?\n([\s\S]*?)```$/);
if (extractionMatch) {
const content = (extractionMatch[1] || '').trim();
if (content) {
chatAutomat.actions.insertExtractionSnippet({ text: content });
return;
}
}
chatOpenedThisSession = true;
chatAutomat.setState((prev) => ({ chatAutomat.setState((prev) => ({
isOpen: true, isOpen: true,
sseEstablished: true, sseEstablished: true,
pendingInput: prev.pendingInput ? `${prev.pendingInput}\n\n${prompt}` : prompt, pendingInput: prev.pendingInput ? `${prev.pendingInput}\n\n${text}` : text,
})); }));
ensureCompanyEventsConnected(); ensureCompanyEventsConnected();
if (!chatAutomat.state.selectedConversationId && !chatAutomat.state.draftConversation) { if (!chatAutomat.state.selectedConversationId && !chatAutomat.state.draftConversation) {

View File

@@ -15,6 +15,7 @@ import SendRoundedIcon from '@mui/icons-material/SendRounded';
import AddRoundedIcon from '@mui/icons-material/AddRounded'; import AddRoundedIcon from '@mui/icons-material/AddRounded';
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded'; import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
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';
@@ -115,6 +116,15 @@ export default class ChatDrawer extends PureComponent {
isInputFocused: false, isInputFocused: false,
stagedAttachments: [], stagedAttachments: [],
stagedReferences: [], stagedReferences: [],
stagedExtractions: s.pendingExtraction
? [
{
id: `ext-${Date.now()}`,
text: s.pendingExtraction.text,
label: s.pendingExtraction.label || 'parsed invoice',
},
]
: [],
isProcessingFiles: false, isProcessingFiles: false,
isDraggingFiles: false, isDraggingFiles: false,
attachmentError: null, attachmentError: null,
@@ -178,6 +188,13 @@ export default class ChatDrawer extends PureComponent {
} }
}); });
} }
if (chatAutomat.state.pendingExtraction != null) {
queueMicrotask(() => {
if (chatAutomat.state.pendingExtraction != null) {
chatAutomat.setState({ pendingExtraction: null });
}
});
}
this.unsub = chatAutomat.subscribe(this, (state) => { this.unsub = chatAutomat.subscribe(this, (state) => {
const wasOpen = this.state.isOpen; const wasOpen = this.state.isOpen;
const willOpen = state.isOpen; const willOpen = state.isOpen;
@@ -211,6 +228,21 @@ export default class ChatDrawer extends PureComponent {
} else if (!wasOpen && willOpen) { } else if (!wasOpen && willOpen) {
this.focusInput(0); this.focusInput(0);
} }
if (state.pendingExtraction != null) {
const ext = state.pendingExtraction;
const newExt = {
id: `ext-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
text: ext.text,
label: ext.label || 'parsed invoice',
};
patch.stagedExtractions = [...(this.state.stagedExtractions || []), newExt];
queueMicrotask(() => {
if (chatAutomat.state.pendingExtraction != null) {
chatAutomat.setState({ pendingExtraction: null });
}
});
this.focusInput(0);
}
return patch; return patch;
}); });
@@ -309,6 +341,13 @@ export default class ChatDrawer extends PureComponent {
this.focusInput(50); this.focusInput(50);
}; };
handleRemoveExtraction = (id) => {
this.setState((prev) => ({
stagedExtractions: (prev.stagedExtractions || []).filter((e) => e.id !== id),
}));
this.focusInput(50);
};
handleVfsResizerStart = (e) => { handleVfsResizerStart = (e) => {
if (e.button !== undefined && e.button !== 0) return; if (e.button !== undefined && e.button !== 0) return;
e.preventDefault(); e.preventDefault();
@@ -483,9 +522,10 @@ export default class ChatDrawer extends PureComponent {
const rawText = this.state.inputText.trim(); const rawText = this.state.inputText.trim();
const attachments = this.state.stagedAttachments || []; const attachments = this.state.stagedAttachments || [];
const references = this.state.stagedReferences || []; const references = this.state.stagedReferences || [];
const extractions = this.state.stagedExtractions || [];
if ( if (
(!rawText && attachments.length === 0 && references.length === 0) || (!rawText && attachments.length === 0 && references.length === 0 && extractions.length === 0) ||
this.state.conversationState === 'busy' || this.state.conversationState === 'busy' ||
this.state.isSending this.state.isSending
) { ) {
@@ -493,6 +533,13 @@ export default class ChatDrawer extends PureComponent {
} }
let text = rawText; let text = rawText;
if (extractions.length > 0) {
const extMarkdown = extractions
.map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``)
.join('\n\n');
text = text ? `${extMarkdown}\n\n${text}` : extMarkdown;
}
if (references.length > 0) { if (references.length > 0) {
const refMarkdown = references const refMarkdown = references
.map((ref) => { .map((ref) => {
@@ -522,6 +569,7 @@ export default class ChatDrawer extends PureComponent {
inputText: '', inputText: '',
stagedAttachments: [], stagedAttachments: [],
stagedReferences: [], stagedReferences: [],
stagedExtractions: [],
attachmentError: null, attachmentError: null,
}); });
@@ -1265,6 +1313,7 @@ export default class ChatDrawer extends PureComponent {
sendError, sendError,
stagedAttachments, stagedAttachments,
stagedReferences, stagedReferences,
stagedExtractions = [],
isProcessingFiles, isProcessingFiles,
isDraggingFiles, isDraggingFiles,
attachmentError, attachmentError,
@@ -1277,7 +1326,10 @@ export default class ChatDrawer extends PureComponent {
const canSend = const canSend =
!isWorking && !isWorking &&
canChat && canChat &&
(Boolean(inputText.trim()) || stagedAttachments.length > 0 || stagedReferences.length > 0); (Boolean(inputText.trim()) ||
stagedAttachments.length > 0 ||
stagedReferences.length > 0 ||
stagedExtractions.length > 0);
return ( return (
<Box <Box
@@ -1449,7 +1501,7 @@ export default class ChatDrawer extends PureComponent {
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
> >
{stagedReferences.length > 0 && ( {(stagedReferences.length > 0 || (stagedExtractions && stagedExtractions.length > 0)) && (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -1546,6 +1598,64 @@ export default class ChatDrawer extends PureComponent {
</IconButton> </IconButton>
</Box> </Box>
))} ))}
{stagedExtractions && stagedExtractions.map((ext) => (
<Box
key={ext.id}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.6,
py: 0.2,
pl: 0.7,
pr: 0.4,
borderRadius: 1.5,
bgcolor: (theme) =>
theme.palette.mode === 'dark'
? 'rgba(255, 255, 255, 0.08)'
: 'rgba(0, 0, 0, 0.05)',
border: '1px solid',
borderColor: (theme) =>
theme.palette.mode === 'dark'
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(0, 0, 0, 0.1)',
color: 'text.primary',
fontSize: '0.75rem',
lineHeight: 1.2,
userSelect: 'none',
}}
>
<DescriptionOutlinedIcon sx={{ fontSize: 14, color: 'primary.main' }} />
<Typography
component="span"
sx={{
fontSize: '0.74rem',
fontWeight: 600,
letterSpacing: '-0.2px',
}}
>
{ext.label || 'parsed invoice'}
</Typography>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
this.handleRemoveExtraction(ext.id);
}}
aria-label="Entfernen"
sx={{
p: 0.2,
ml: 0.1,
color: 'text.secondary',
'&:hover': {
color: 'error.main',
bgcolor: 'action.hover',
},
}}
>
<CloseRoundedIcon sx={{ fontSize: 13 }} />
</IconButton>
</Box>
))}
</Box> </Box>
)} )}

View File

@@ -1,7 +1,6 @@
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined'; import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
@@ -9,7 +8,6 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles'; import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
import SkeleHashImage from '../settings/SkeleHashImage'; import SkeleHashImage from '../settings/SkeleHashImage';
import { enqueueSnackbar } from 'notistack';
import { generateTextThumbnail } from '../../utils/textThumbnail'; import { generateTextThumbnail } from '../../utils/textThumbnail';
import chatAutomat from '../../automats/chatAutomat'; import chatAutomat from '../../automats/chatAutomat';
import { import {
@@ -107,17 +105,12 @@ export class Attachment extends PureComponent {
} }
const trimmed = (textContent || '').trim(); const trimmed = (textContent || '').trim();
if (!trimmed) { if (!trimmed) return;
enqueueSnackbar('Datei enthält keinen Text zum Erwähnen im Chat.', { variant: 'warning' });
return;
}
const size = attachment.size || new Blob([trimmed]).size;
chatAutomat.actions.insertExtractionSnippet({ chatAutomat.actions.insertExtractionSnippet({
text: trimmed, text: trimmed,
size, label: 'parsed invoice',
}); });
enqueueSnackbar('Text im Chat erwähnt', { variant: 'success' });
}; };
handleDelete = (e) => { handleDelete = (e) => {
@@ -290,7 +283,6 @@ export class Attachment extends PureComponent {
disabled={disabled} disabled={disabled}
onClick={this.handleRescan} onClick={this.handleRescan}
aria-label="Zu Text" aria-label="Zu Text"
title="Zu Text"
sx={{ sx={{
bgcolor: SCRIM_ACTION, bgcolor: SCRIM_ACTION,
color: WHITE, color: WHITE,
@@ -311,7 +303,6 @@ export class Attachment extends PureComponent {
disabled={disabled} disabled={disabled}
onClick={this.handleMentionInChat} onClick={this.handleMentionInChat}
aria-label="Im Chat erwähnen" aria-label="Im Chat erwähnen"
title="Im Chat erwähnen"
sx={{ sx={{
bgcolor: SCRIM_ACTION, bgcolor: SCRIM_ACTION,
color: WHITE, color: WHITE,
@@ -331,7 +322,6 @@ export class Attachment extends PureComponent {
disabled={disabled} disabled={disabled}
onClick={this.handleDelete} onClick={this.handleDelete}
aria-label="Löschen" aria-label="Löschen"
title="Löschen"
sx={{ sx={{
bgcolor: SCRIM_ACTION, bgcolor: SCRIM_ACTION,
color: WHITE, color: WHITE,

View File

@@ -21,19 +21,28 @@ import TableRow from '@mui/material/TableRow';
import Tabs from '@mui/material/Tabs'; import Tabs from '@mui/material/Tabs';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import BackupOutlinedIcon from '@mui/icons-material/BackupOutlined'; import BackupOutlinedIcon from '@mui/icons-material/BackupOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined';
import CloseIcon from '@mui/icons-material/Close';
import CloudQueueOutlinedIcon from '@mui/icons-material/CloudQueueOutlined'; import CloudQueueOutlinedIcon from '@mui/icons-material/CloudQueueOutlined';
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import PsychologyOutlinedIcon from '@mui/icons-material/PsychologyOutlined';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import StorageIcon from '@mui/icons-material/Storage'; import StorageIcon from '@mui/icons-material/Storage';
import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { vfs } from '../../api';
import backupAutomat from '../../automats/backupAutomat'; import backupAutomat from '../../automats/backupAutomat';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { import {
@@ -92,11 +101,18 @@ export default class BackupSettingsPanel extends Component {
vfsLoadingByBackup: vfsLoadingByBackup || {}, vfsLoadingByBackup: vfsLoadingByBackup || {},
tierTab: 'hourly', // 'hourly' | 'daily' | 'monthly' tierTab: 'hourly', // 'hourly' | 'daily' | 'monthly'
expandedItemName: null, expandedItemName: null,
restoringSkills: false,
restoringApplets: false,
confirmRestore: null, // null | 'skills' | 'applets'
lastRestoreResult: null, // null | { type: 'skills' | 'applets', count: number, restored: string[] }
}; };
this.handleRefresh = this.handleRefresh.bind(this); this.handleRefresh = this.handleRefresh.bind(this);
this.handleTierChange = this.handleTierChange.bind(this); this.handleTierChange = this.handleTierChange.bind(this);
this.handleToggleExpand = this.handleToggleExpand.bind(this); this.handleToggleExpand = this.handleToggleExpand.bind(this);
this.handleOpenConfirm = this.handleOpenConfirm.bind(this);
this.handleCloseConfirm = this.handleCloseConfirm.bind(this);
this.handleExecuteRestore = this.handleExecuteRestore.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -151,6 +167,65 @@ export default class BackupSettingsPanel extends Component {
})); }));
} }
handleOpenConfirm(type) {
this.setState({ confirmRestore: type });
}
handleCloseConfirm() {
if (this.state.restoringSkills || this.state.restoringApplets) return;
this.setState({ confirmRestore: null });
}
// #region handleExecuteRestore
// MARK: calls POST /api/vfs/skills/restore or /api/vfs/applets/restore
async handleExecuteRestore() {
const { confirmRestore } = this.state;
if (!confirmRestore) return;
if (confirmRestore === 'skills') {
this.setState({ restoringSkills: true });
try {
const res = await vfs.restoreSkills();
enqueueSnackbar(`${res.count} Standard-Skills (.SKILL) erfolgreich wiederhergestellt.`, {
variant: 'success',
});
this.setState({
confirmRestore: null,
restoringSkills: false,
lastRestoreResult: {
type: 'skills',
count: res.count,
restored: res.restored || [],
},
});
} catch (err) {
this.setState({ restoringSkills: false });
notifyError(err);
}
} else if (confirmRestore === 'applets') {
this.setState({ restoringApplets: true });
try {
const res = await vfs.restoreApplets();
enqueueSnackbar(`${res.count} Beispiel-Applets (.applet.js) erfolgreich wiederhergestellt.`, {
variant: 'success',
});
this.setState({
confirmRestore: null,
restoringApplets: false,
lastRestoreResult: {
type: 'applets',
count: res.count,
restored: res.restored || [],
},
});
} catch (err) {
this.setState({ restoringApplets: false });
notifyError(err);
}
}
}
// #endregion
render() { render() {
const { const {
status, status,
@@ -161,6 +236,10 @@ export default class BackupSettingsPanel extends Component {
vfsLoadingByBackup, vfsLoadingByBackup,
tierTab, tierTab,
expandedItemName, expandedItemName,
restoringSkills,
restoringApplets,
confirmRestore,
lastRestoreResult,
} = this.state; } = this.state;
if (status === 'loading' && !backupStatus) { if (status === 'loading' && !backupStatus) {
@@ -224,7 +303,7 @@ export default class BackupSettingsPanel extends Component {
size="small" size="small"
startIcon={<RefreshIcon />} startIcon={<RefreshIcon />}
onClick={this.handleRefresh} onClick={this.handleRefresh}
disabled={isRunning} disabled={status === 'loading'}
> >
Aktualisieren Aktualisieren
</Button> </Button>
@@ -246,19 +325,11 @@ export default class BackupSettingsPanel extends Component {
> >
<Chip <Chip
size="small" size="small"
icon={backupStatus?.enabled ? <CheckCircleOutlineIcon /> : <WarningAmberIcon />} icon={backupStatus?.enabled && isRunning ? <CheckCircleOutlineIcon /> : <WarningAmberIcon />}
label={backupStatus?.enabled ? 'Dienst: Aktiv' : 'Dienst: Deaktiviert'} label={backupStatus?.enabled && isRunning ? 'Dienst: Aktiv' : 'Dienst: Deaktiviert'}
color={backupStatus?.enabled ? 'success' : 'default'} color={backupStatus?.enabled && isRunning ? 'success' : 'default'}
variant="outlined" variant="outlined"
/> />
{isRunning && (
<Chip
size="small"
icon={<CircularProgress size={12} color="inherit" />}
label="Backup wird ausgeführt..."
color="primary"
/>
)}
<Chip <Chip
size="small" size="small"
icon={<StorageIcon />} icon={<StorageIcon />}
@@ -431,6 +502,231 @@ export default class BackupSettingsPanel extends Component {
</TableContainer> </TableContainer>
)} )}
</Box> </Box>
{/* System Defaults Restore Section (VFS Skills & Applets) */}
<Box sx={{ border: `1px solid ${BORDER}`, borderRadius: 2, bgcolor: WHITE, overflow: 'hidden' }}>
<Box sx={{ borderBottom: `1px solid ${BORDER}`, bgcolor: SURFACE_RAISED, px: 2, py: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SettingsBackupRestoreIcon sx={{ color: PRIMARY, fontSize: 22 }} />
<Typography variant="subtitle2" fontWeight={700} sx={{ color: DARK_4 }}>
System-Standarddateien wiederherstellen (VFS)
</Typography>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
Setzt systemweite Vorlagen im virtuellen Dateisystem (VFS) des Unternehmens auf den Auslieferungszustand zurück. Bereits vorhandene gleichnamige Dateien werden dabei überschrieben.
</Typography>
</Box>
<Box sx={{ p: 2 }}>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
gap: 2,
}}
>
{/* Skills Restore Card */}
<Box
sx={{
p: 2,
borderRadius: 2,
border: `1px solid ${BORDER}`,
bgcolor: SURFACE_BASE,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 1.5,
}}
>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
bgcolor: 'rgba(15, 118, 110, 0.1)',
color: PRIMARY,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PsychologyOutlinedIcon fontSize="small" />
</Box>
<Typography variant="subtitle2" fontWeight={700} sx={{ color: DARK_4 }}>
Standard-Skills
</Typography>
</Box>
<Chip size="small" label=".SKILL" variant="outlined" sx={{ height: 20, fontSize: '0.75rem', fontWeight: 600 }} />
</Box>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.825rem', lineHeight: 1.4 }}>
Stellt alle systemweiten Standard-.SKILL-Dateien für KI-Assistenten und Workflows im VFS wieder her.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pt: 1, borderTop: `1px solid ${BORDER}` }}>
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic', fontSize: '0.75rem' }}>
Überschreibt existierende Dateien
</Typography>
<Button
size="small"
variant="contained"
startIcon={restoringSkills ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
onClick={() => this.handleOpenConfirm('skills')}
disabled={restoringSkills}
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
>
Skills wiederherstellen
</Button>
</Box>
</Box>
{/* Applets Restore Card */}
<Box
sx={{
p: 2,
borderRadius: 2,
border: `1px solid ${BORDER}`,
bgcolor: SURFACE_BASE,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 1.5,
}}
>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
bgcolor: 'rgba(15, 118, 110, 0.1)',
color: PRIMARY,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<WidgetsOutlinedIcon fontSize="small" />
</Box>
<Typography variant="subtitle2" fontWeight={700} sx={{ color: DARK_4 }}>
Beispiel-Applets
</Typography>
</Box>
<Chip size="small" label=".applet.js" variant="outlined" sx={{ height: 20, fontSize: '0.75rem', fontWeight: 600 }} />
</Box>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.825rem', lineHeight: 1.4 }}>
Stellt alle systemweiten Standard-.applet.js-Beispiel-Applets für Workflows und Auswertungen im VFS wieder her.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pt: 1, borderTop: `1px solid ${BORDER}` }}>
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic', fontSize: '0.75rem' }}>
Überschreibt existierende Dateien
</Typography>
<Button
size="small"
variant="contained"
startIcon={restoringApplets ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
onClick={() => this.handleOpenConfirm('applets')}
disabled={restoringApplets}
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
>
Applets wiederherstellen
</Button>
</Box>
</Box>
</Box>
{/* Last restore results notification / chip display */}
{lastRestoreResult && (
<Box sx={{ mt: 2, p: 1.5, borderRadius: 1.5, bgcolor: 'rgba(15, 118, 110, 0.08)', border: '1px solid rgba(15, 118, 110, 0.2)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="caption" fontWeight={600} sx={{ color: PRIMARY }}>
Erfolgreich wiederhergestellt ({lastRestoreResult.count} {lastRestoreResult.type === 'skills' ? 'Skills' : 'Applets'}):
</Typography>
<IconButton
size="small"
onClick={() => this.setState({ lastRestoreResult: null })}
sx={{ p: 0.25, color: 'text.secondary' }}
>
<CloseIcon fontSize="small" sx={{ fontSize: 16 }} />
</IconButton>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
{lastRestoreResult.restored.map((name) => (
<Chip
key={name}
size="small"
label={name}
variant="outlined"
sx={{
height: 22,
fontSize: '0.75rem',
fontFamily: 'monospace',
bgcolor: WHITE,
}}
/>
))}
</Box>
</Box>
)}
</Box>
</Box>
{/* Confirmation Dialog for System Defaults Restore */}
<Dialog
open={Boolean(confirmRestore)}
onClose={this.handleCloseConfirm}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<WarningAmberIcon color="warning" />
{confirmRestore === 'skills'
? 'Standard-Skills wiederherstellen?'
: 'Beispiel-Applets wiederherstellen?'}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: DARK_2 }}>
{confirmRestore === 'skills'
? 'Möchten Sie alle systemweiten Standard-.SKILL-Dateien im VFS des aktuellen Unternehmens wiederherstellen? Bestehende .SKILL-Dateien werden dabei überschrieben.'
: 'Möchten Sie alle systemweiten Standard-.applet.js-Beispiel-Applets im VFS des aktuellen Unternehmens wiederherstellen? Bestehende .applet.js-Dateien werden dabei überschrieben.'}
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button
onClick={this.handleCloseConfirm}
disabled={restoringSkills || restoringApplets}
variant="outlined"
sx={{ textTransform: 'none' }}
>
Abbrechen
</Button>
<Button
onClick={this.handleExecuteRestore}
disabled={restoringSkills || restoringApplets}
variant="contained"
color="primary"
startIcon={
(confirmRestore === 'skills' ? restoringSkills : restoringApplets) ? (
<CircularProgress size={14} color="inherit" />
) : (
<SettingsBackupRestoreIcon />
)
}
sx={{ textTransform: 'none' }}
>
{(confirmRestore === 'skills' ? restoringSkills : restoringApplets)
? 'Wiederherstellen...'
: 'Jetzt wiederherstellen'}
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
); );
} }

View File

@@ -659,20 +659,17 @@ class LieferungNeuPage extends PureComponent {
const existingTxt = this.state.attachments?.find( const existingTxt = this.state.attachments?.find(
(a) => a.parentPdfId === attachment.id || a.name === txtName, (a) => a.parentPdfId === attachment.id || a.name === txtName,
); );
let targetTxt = existingTxt;
if (existingTxt) { if (existingTxt) {
const updated = { this.automat.actions.updateAttachment({
id: existingTxt.id, id: existingTxt.id,
file: txtFile, file: txtFile,
size: txtFile.size, size: txtFile.size,
text, text,
thumbnail: txtThumbnail || existingTxt.thumbnail, thumbnail: txtThumbnail || existingTxt.thumbnail,
isDirty: true, isDirty: true,
}; });
this.automat.actions.updateAttachment(updated);
targetTxt = { ...existingTxt, ...updated };
} else { } else {
targetTxt = { this.automat.actions.addAttachment({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
file: txtFile, file: txtFile,
name: txtFile.name, name: txtFile.name,
@@ -682,14 +679,10 @@ class LieferungNeuPage extends PureComponent {
text, text,
parentPdfId: attachment.id, parentPdfId: attachment.id,
uploadedAt: new Date().toISOString(), uploadedAt: new Date().toISOString(),
}; });
this.automat.actions.addAttachment(targetTxt);
} }
this.handleAttachmentClick(targetTxt);
enqueueSnackbar('Text aus PDF extrahiert und geöffnet', { variant: 'success' });
} catch (txtErr) { } catch (txtErr) {
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr); console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
enqueueSnackbar('Fehler beim Extrahieren von Text aus der PDF', { variant: 'error' });
} }
} }
} }
@@ -715,17 +708,12 @@ class LieferungNeuPage extends PureComponent {
} }
const trimmed = (text || '').trim(); const trimmed = (text || '').trim();
if (!trimmed) { if (!trimmed) return;
enqueueSnackbar('Datei enthält keinen Text zum Erwähnen im Chat.', { variant: 'warning' });
return;
}
const size = attachment.size || new Blob([trimmed]).size;
chatAutomat.actions.insertExtractionSnippet({ chatAutomat.actions.insertExtractionSnippet({
text: trimmed, text: trimmed,
size, label: 'parsed invoice',
}); });
enqueueSnackbar('Text im Chat erwähnt', { variant: 'success' });
} }
handlePdfDelete(attachment) { handlePdfDelete(attachment) {

View File

@@ -505,6 +505,12 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/vfs/upload' && method === 'POST') { if (p === '/api/vfs/upload' && method === 'POST') {
return json(res, 200, { files: [] }); return json(res, 200, { files: [] });
} }
if (p === '/api/vfs/skills/restore' && method === 'POST') {
return json(res, 200, { success: true, restored: ['system.SKILL'], count: 1 });
}
if (p === '/api/vfs/applets/restore' && method === 'POST') {
return json(res, 200, { success: true, restored: ['example.applet.js'], count: 1 });
}
if (p.startsWith('/api/vfs/') && method === 'PUT') { if (p.startsWith('/api/vfs/') && method === 'PUT') {
return json(res, 200, { ok: true }); return json(res, 200, { ok: true });
} }