fixed ai call end to end with saas multimandate

This commit is contained in:
ValueOn AG 2026-01-26 23:26:34 +01:00
parent 28af4cb068
commit f41e6d0088
30 changed files with 371 additions and 233 deletions

View file

@ -24,6 +24,7 @@ import {
FaThList, FaThList,
FaTh, FaTh,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import { import {
useAccessRules, useAccessRules,
type AccessRule, type AccessRule,
@ -529,6 +530,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
mandateId, mandateId,
featureCode, featureCode,
}) => { }) => {
const { showError } = useToast();
const { const {
rules, rules,
loading, loading,
@ -580,9 +582,8 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
}, [updateRuleLocally]); }, [updateRuleLocally]);
const handleDelete = useCallback((ruleId: string) => { const handleDelete = useCallback((ruleId: string) => {
if (window.confirm('Möchten Sie diese Regel wirklich löschen?')) { // Direct delete - rules are local until saved
removeRuleLocally(ruleId); removeRuleLocally(ruleId);
}
}, [removeRuleLocally]); }, [removeRuleLocally]);
const handleAdd = useCallback((ruleData: AccessRuleCreate) => { const handleAdd = useCallback((ruleData: AccessRuleCreate) => {
@ -607,16 +608,15 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
setHasChanges(false); setHasChanges(false);
onSave?.(); onSave?.();
} else { } else {
alert(result.error || 'Fehler beim Speichern'); showError('Fehler', result.error || 'Fehler beim Speichern');
} }
}; };
const handleReset = () => { const handleReset = () => {
if (window.confirm('Alle Änderungen verwerfen?')) { // Direct reset - user clicked the reset button intentionally
fetchRules().then(fetchedRules => { fetchRules().then(fetchedRules => {
setOriginalRules(fetchedRules); setOriginalRules(fetchedRules);
}); });
}
}; };
const handleJsonApply = (newRules: AccessRule[]) => { const handleJsonApply = (newRules: AccessRule[]) => {

View file

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { IoIosDownload } from 'react-icons/io';
import { useLanguage } from '../../../../providers/language/LanguageContext';
import styles from '../ActionButton.module.css';
export interface DownloadActionButtonProps<T = any> {
row: T;
onDownload: (row: T) => Promise<void> | void;
disabled?: boolean | { disabled: boolean; message?: string };
loading?: boolean;
className?: string;
title?: string;
hookData?: any;
idField?: string;
loadingStateName?: string;
}
export function DownloadActionButton<T = any>({
row,
onDownload,
disabled = false,
loading = false,
className = '',
title,
hookData,
idField = 'id',
loadingStateName = 'downloadingFiles'
}: DownloadActionButtonProps<T>) {
const { t } = useLanguage();
const [internalLoading, setInternalLoading] = useState(false);
// Extract disabled state and tooltip message
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation();
if (!isDisabled && !loading && !internalLoading) {
setInternalLoading(true);
try {
if (onDownload) {
await onDownload(row);
}
} finally {
setInternalLoading(false);
}
}
};
const buttonTitle = title || t('files.action.download', 'Download');
// Use hookData loading state if available
const loadingState = hookData?.[loadingStateName];
const actualIsLoading = loadingState?.has((row as any)[idField]) || loading || internalLoading;
// Determine the final button title (tooltip)
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
return (
<button
onClick={handleClick}
className={`${styles.actionButton} ${styles.download} ${actualIsLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
title={finalTitle}
disabled={isDisabled || actualIsLoading}
>
<span className={styles.actionIcon}>
{actualIsLoading ? '⏳' : <IoIosDownload />}
</span>
</button>
);
}
export default DownloadActionButton;

View file

@ -0,0 +1,2 @@
export { DownloadActionButton, type DownloadActionButtonProps } from './DownloadActionButton';
export { DownloadActionButton as default } from './DownloadActionButton';

View file

@ -4,6 +4,7 @@ export { DeleteActionButton } from './DeleteActionButton';
export { ViewActionButton } from './ViewActionButton'; export { ViewActionButton } from './ViewActionButton';
export { CopyActionButton } from './CopyActionButton'; export { CopyActionButton } from './CopyActionButton';
export { RemoveActionButton } from './RemoveActionButton'; export { RemoveActionButton } from './RemoveActionButton';
export { DownloadActionButton } from './DownloadActionButton';
// Generic Custom Action Button (for entity-specific actions) // Generic Custom Action Button (for entity-specific actions)
export { CustomActionButton } from './CustomActionButton'; export { CustomActionButton } from './CustomActionButton';
@ -14,4 +15,5 @@ export type { DeleteActionButtonProps } from './DeleteActionButton';
export type { ViewActionButtonProps } from './ViewActionButton'; export type { ViewActionButtonProps } from './ViewActionButton';
export type { CopyActionButtonProps } from './CopyActionButton'; export type { CopyActionButtonProps } from './CopyActionButton';
export type { RemoveActionButtonProps } from './RemoveActionButton'; export type { RemoveActionButtonProps } from './RemoveActionButton';
export type { DownloadActionButtonProps } from './DownloadActionButton';
export type { CustomActionButtonProps } from './CustomActionButton'; export type { CustomActionButtonProps } from './CustomActionButton';

View file

@ -853,7 +853,23 @@ export function FormGeneratorForm<T extends Record<string, any>>({
const minHeight = minRows * 1.5 * 16; const minHeight = minRows * 1.5 * 16;
const maxHeight = maxRows * 1.5 * 16; const maxHeight = maxRows * 1.5 * 16;
const currentValue = value || ''; // Handle object/array values by converting to JSON string for display
const isObjectValue = typeof value === 'object' && value !== null;
// Check if string value looks like JSON (for fields that were originally objects but temporarily invalid)
const looksLikeJson = typeof value === 'string' && value.trim().match(/^[\[{]/);
const isJsonField = isObjectValue || looksLikeJson;
let currentValue = '';
if (isObjectValue) {
try {
currentValue = JSON.stringify(value, null, 2);
} catch {
currentValue = String(value);
}
} else {
currentValue = value || '';
}
const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content'); const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content');
const textareaClassName = isContentField const textareaClassName = isContentField
? `${styles.fieldTextarea} ${styles.contentTextarea} ${hasError ? styles.fieldError : ''}` ? `${styles.fieldTextarea} ${styles.contentTextarea} ${hasError ? styles.fieldError : ''}`
@ -865,7 +881,20 @@ export function FormGeneratorForm<T extends Record<string, any>>({
name={attr.name} name={attr.name}
value={currentValue} value={currentValue}
onChange={(e) => { onChange={(e) => {
handleFieldChange(attr.name, e.target.value); const newTextValue = e.target.value;
const trimmed = newTextValue.trim();
// Try to parse as JSON if it looks like JSON (starts with { or [)
if (trimmed.match(/^[\[{]/)) {
try {
const parsed = JSON.parse(newTextValue);
handleFieldChange(attr.name, parsed);
} catch {
// If parsing fails, store as string (user is still typing)
handleFieldChange(attr.name, newTextValue);
}
} else {
handleFieldChange(attr.name, newTextValue);
}
const textarea = e.target; const textarea = e.target;
textarea.style.height = 'auto'; textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight; const scrollHeight = textarea.scrollHeight;
@ -879,6 +908,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
onBlur={() => handleFieldFocus(attr.name, false)} onBlur={() => handleFieldFocus(attr.name, false)}
className={textareaClassName} className={textareaClassName}
rows={minRows} rows={minRows}
style={isJsonField ? { fontFamily: 'monospace', fontSize: '0.85em' } : undefined}
ref={(textarea) => { ref={(textarea) => {
if (textarea) { if (textarea) {
textarea.style.setProperty('min-height', `${minHeight}px`, 'important'); textarea.style.setProperty('min-height', `${minHeight}px`, 'important');
@ -888,7 +918,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
} }
}} }}
/> />
<label className={getLabelClass(attr.name, value)}> <label className={getLabelClass(attr.name, currentValue)}>
{attr.label} {attr.label}
{attr.required && <span className={styles.required}>*</span>} {attr.required && <span className={styles.required}>*</span>}
</label> </label>

View file

@ -369,7 +369,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
} }
} else { } else {
console.warn('No delete handler found in hookData or props'); console.warn('No delete handler found in hookData or props');
alert('No delete handler configured');
return; return;
} }
} else if (onDeleteMultiple) { } else if (onDeleteMultiple) {
@ -388,7 +387,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
} }
} else { } else {
console.warn('No delete handler provided'); console.warn('No delete handler provided');
alert('No delete handler configured');
return; return;
} }
@ -398,7 +396,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
console.log('Delete completed, selection cleared'); console.log('Delete completed, selection cleared');
} catch (error) { } catch (error) {
console.error('Delete failed:', error); console.error('Delete failed:', error);
alert(`Delete failed: ${error}`);
} finally { } finally {
setIsDeleting(false); setIsDeleting(false);
} }

View file

@ -135,14 +135,13 @@ function SpeechSettings({ onDataUpdate }: SpeechSettingsProps) {
}; };
const handleReset = () => { const handleReset = () => {
if (window.confirm(t('speech.settings.reset_confirm'))) { // Direct reset - user clicked the reset button intentionally
localStorage.removeItem('speechSignUpData'); localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp'); localStorage.removeItem('speechSignUpTimestamp');
window.dispatchEvent(new CustomEvent('speechSignUpChanged')); window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
setFormData(null); setFormData(null);
setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') }); setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') });
setTimeout(() => setSaveMessage(null), 3000); setTimeout(() => setSaveMessage(null), 3000);
}
}; };
if (isLoading) { if (isLoading) {

View file

@ -14,9 +14,11 @@ export interface ChatMessageProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>; onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>; onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>; onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>; deletingFiles?: Set<string>;
previewingFiles?: Set<string>; previewingFiles?: Set<string>;
removingFiles?: Set<string>; removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string; workflowId?: string;
} }
@ -30,9 +32,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
onFileDelete, onFileDelete,
onFileRemove, onFileRemove,
onFileView, onFileView,
onFileDownload,
deletingFiles, deletingFiles,
previewingFiles, previewingFiles,
removingFiles, removingFiles,
downloadingFiles,
workflowId workflowId
}) => { }) => {
const isUser = message.role?.toLowerCase() === 'user'; const isUser = message.role?.toLowerCase() === 'user';
@ -131,9 +135,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
onFileDelete={onFileDelete} onFileDelete={onFileDelete}
onFileRemove={onFileRemove} onFileRemove={onFileRemove}
onFileView={onFileView} onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles} deletingFiles={deletingFiles}
previewingFiles={previewingFiles} previewingFiles={previewingFiles}
removingFiles={removingFiles} removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId} workflowId={workflowId}
/> />
)} )}

View file

@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { MessageDocument, Message } from '../MessagesTypes'; import { MessageDocument, Message } from '../MessagesTypes';
import { formatFileSize } from '../MessageUtils'; import { formatFileSize } from '../MessageUtils';
import { ViewActionButton, DeleteActionButton, RemoveActionButton } from '../../../FormGenerator/ActionButtons'; import { ViewActionButton, DeleteActionButton, RemoveActionButton, DownloadActionButton } from '../../../FormGenerator/ActionButtons';
import { WorkflowFile } from '../../../../hooks/usePlayground'; import { WorkflowFile } from '../../../../hooks/usePlayground';
import styles from '../Messages.module.css'; import styles from '../Messages.module.css';
@ -12,9 +12,11 @@ export interface DocumentItemProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>; onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>; onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>; onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>; deletingFiles?: Set<string>;
previewingFiles?: Set<string>; previewingFiles?: Set<string>;
removingFiles?: Set<string>; removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string; workflowId?: string;
} }
@ -28,9 +30,11 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
onFileDelete, onFileDelete,
onFileRemove, onFileRemove,
onFileView, onFileView,
onFileDownload,
deletingFiles = new Set(), deletingFiles = new Set(),
previewingFiles = new Set(), previewingFiles = new Set(),
removingFiles = new Set(), removingFiles = new Set(),
downloadingFiles = new Set(),
workflowId: _workflowId workflowId: _workflowId
}) => { }) => {
// Convert MessageDocument to WorkflowFile format for compatibility with action buttons // Convert MessageDocument to WorkflowFile format for compatibility with action buttons
@ -47,6 +51,7 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
const isDeleting = deletingFiles.has(document.fileId); const isDeleting = deletingFiles.has(document.fileId);
const isPreviewing = previewingFiles.has(document.fileId); const isPreviewing = previewingFiles.has(document.fileId);
const isRemoving = removingFiles.has(document.fileId); const isRemoving = removingFiles.has(document.fileId);
const isDownloading = downloadingFiles.has(document.fileId);
// Create hookData object for action buttons // Create hookData object for action buttons
const hookData = useMemo(() => ({ const hookData = useMemo(() => ({
@ -65,8 +70,9 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
}, },
deletingItems: deletingFiles, deletingItems: deletingFiles,
previewingFiles: previewingFiles, previewingFiles: previewingFiles,
removingItems: removingFiles removingItems: removingFiles,
}), [onFileDelete, workflowFile, deletingFiles, previewingFiles, removingFiles]); downloadingFiles: downloadingFiles
}), [onFileDelete, workflowFile, deletingFiles, previewingFiles, removingFiles, downloadingFiles]);
const handleView = async () => { const handleView = async () => {
if (onFileView) { if (onFileView) {
@ -74,6 +80,12 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
} }
}; };
const handleDownload = async () => {
if (onFileDownload) {
await onFileDownload(workflowFile);
}
};
const handleRemove = async () => { const handleRemove = async () => {
if (onFileRemove) { if (onFileRemove) {
await onFileRemove(workflowFile); await onFileRemove(workflowFile);
@ -89,7 +101,7 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
{formatFileSize(document.fileSize)} {document.mimeType} {formatFileSize(document.fileSize)} {document.mimeType}
</div> </div>
</div> </div>
{(onFileView || onFileDelete || onFileRemove) && ( {(onFileView || onFileDownload || onFileDelete || onFileRemove) && (
<div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}> <div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>
{onFileView && ( {onFileView && (
<ViewActionButton <ViewActionButton
@ -103,6 +115,17 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
typeField="mimeType" typeField="mimeType"
/> />
)} )}
{onFileDownload && (
<DownloadActionButton
row={workflowFile}
onDownload={handleDownload}
disabled={isDeleting || isRemoving}
loading={isDownloading}
hookData={hookData}
idField="fileId"
loadingStateName="downloadingFiles"
/>
)}
{onFileRemove && ( {onFileRemove && (
<RemoveActionButton <RemoveActionButton
row={workflowFile} row={workflowFile}

View file

@ -19,9 +19,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete, onFileDelete,
onFileRemove, onFileRemove,
onFileView, onFileView,
onFileDownload,
deletingFiles, deletingFiles,
previewingFiles, previewingFiles,
removingFiles, removingFiles,
downloadingFiles,
workflowId workflowId
}) => { }) => {
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
@ -61,9 +63,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete={onFileDelete} onFileDelete={onFileDelete}
onFileRemove={onFileRemove} onFileRemove={onFileRemove}
onFileView={onFileView} onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles} deletingFiles={deletingFiles}
previewingFiles={previewingFiles} previewingFiles={previewingFiles}
removingFiles={removingFiles} removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId} workflowId={workflowId}
/> />
); );
@ -79,9 +83,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete={onFileDelete} onFileDelete={onFileDelete}
onFileRemove={onFileRemove} onFileRemove={onFileRemove}
onFileView={onFileView} onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles} deletingFiles={deletingFiles}
previewingFiles={previewingFiles} previewingFiles={previewingFiles}
removingFiles={removingFiles} removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId} workflowId={workflowId}
/> />
); );

View file

@ -110,9 +110,11 @@ export interface MessagesProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>; onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>; onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>; onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>; deletingFiles?: Set<string>;
previewingFiles?: Set<string>; previewingFiles?: Set<string>;
removingFiles?: Set<string>; removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string; workflowId?: string;
} }

View file

@ -8,9 +8,12 @@ interface FileContextType {
refetch: () => Promise<void>; refetch: () => Promise<void>;
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>; handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>; handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob; isJsonContent?: boolean; decodedContent?: string; error?: string }>;
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
uploadingFile: boolean; uploadingFile: boolean;
deletingFiles: Set<string>; deletingFiles: Set<string>;
previewingFiles: Set<string>; previewingFiles: Set<string>;
downloadingFiles: Set<string>;
} }
const FileContext = createContext<FileContextType | undefined>(undefined); const FileContext = createContext<FileContextType | undefined>(undefined);
@ -20,9 +23,12 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const { const {
handleFileUpload: hookHandleFileUpload, handleFileUpload: hookHandleFileUpload,
handleFileDelete: hookHandleFileDelete, handleFileDelete: hookHandleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile, uploadingFile,
deletingFiles, deletingFiles,
previewingFiles previewingFiles,
downloadingFiles
} = useFileOperations(); } = useFileOperations();
// Centralized file upload that updates the shared state // Centralized file upload that updates the shared state
@ -77,9 +83,12 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
refetch, refetch,
handleFileUpload, handleFileUpload,
handleFileDelete, handleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile, uploadingFile,
deletingFiles, deletingFiles,
previewingFiles previewingFiles,
downloadingFiles
}} }}
> >
{children} {children}

View file

@ -83,7 +83,7 @@ export function useDashboardInputForm() {
useEffect(() => { useEffect(() => {
const checkPermissions = async () => { const checkPermissions = async () => {
try { try {
const uiPerm = await canView('UI', 'playground'); const uiPerm = await canView('UI', 'ui.system.playground');
setPlaygroundUIPermission(uiPerm); setPlaygroundUIPermission(uiPerm);
if (uiPerm) { if (uiPerm) {
@ -477,6 +477,17 @@ export function useDashboardInputForm() {
} }
}, [workflowId, messages, fileContext, request]); }, [workflowId, messages, fileContext, request]);
// handleFileView is a no-op because ViewActionButton's ContentPreview handles the preview internally
const handleFileView = useCallback(async (_file: WorkflowFile) => {
// The ViewActionButton component handles the preview via ContentPreview
// No additional action needed here
}, []);
const handleFileDownload = useCallback(async (file: WorkflowFile) => {
if (!file.fileId) return;
await fileContext.handleFileDownload(file.fileId, file.fileName);
}, [fileContext]);
const onInputChange = useCallback((value: string) => { const onInputChange = useCallback((value: string) => {
setInputValue(value); setInputValue(value);
}, []); }, []);
@ -798,9 +809,12 @@ export function useDashboardInputForm() {
handleFileUpload, handleFileUpload,
handleFileDelete, handleFileDelete,
handleFileRemove, handleFileRemove,
handleFileView,
uploadingFile: fileContext.uploadingFile, uploadingFile: fileContext.uploadingFile,
deletingFiles: fileContext.deletingFiles, deletingFiles: fileContext.deletingFiles,
previewingFiles: fileContext.previewingFiles, previewingFiles: fileContext.previewingFiles,
downloadingFiles: fileContext.downloadingFiles,
handleFileDownload,
isFileAttachmentPopupOpen, isFileAttachmentPopupOpen,
setIsFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen,
allUserFiles: fileContext.files || [], allUserFiles: fileContext.files || [],

View file

@ -515,13 +515,22 @@
border: 1px solid var(--border-color, #e0e0e0); border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px; border-radius: 6px;
padding: 1rem; padding: 1rem;
width: 100%;
box-sizing: border-box;
overflow-x: auto;
} }
.logEntry { .logEntry {
display: flex; display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.5rem; gap: 0.5rem;
padding: 0.25rem 0; padding: 0.25rem 0;
border-bottom: 1px solid var(--border-color, #e0e0e0); border-bottom: 1px solid var(--border-color, #e0e0e0);
width: 100%;
min-width: 100%;
box-sizing: border-box;
} }
.logEntry:last-child { .logEntry:last-child {
@ -530,7 +539,8 @@
.logTime { .logTime {
color: var(--text-secondary); color: var(--text-secondary);
flex-shrink: 0; flex: 0 0 auto;
white-space: nowrap;
} }
.logStatus { .logStatus {
@ -538,13 +548,17 @@
} }
.logMessage { .logMessage {
flex: 1; flex: 1 1 auto;
word-break: break-word; min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
} }
.logProgress { .logProgress {
color: var(--text-secondary); color: var(--text-secondary);
flex-shrink: 0; flex: 0 0 auto;
white-space: nowrap;
} }
/* Logs history */ /* Logs history */

View file

@ -327,17 +327,15 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
} }
}; };
// Handle remove user // Handle remove user (confirmation handled by DeleteActionButton)
const handleRemoveUser = async (user: FeatureAccessUser) => { const handleRemoveUser = async (user: FeatureAccessUser) => {
if (!selectedMandateId || !selectedInstanceId) return; if (!selectedMandateId || !selectedInstanceId) return;
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich aus dieser Feature-Instanz entfernen?`)) { const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId);
const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId); if (result.success) {
if (result.success) { refreshUsers();
refreshUsers(); showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`); } else {
} else { showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
}
} }
}; };

View file

@ -15,6 +15,7 @@ import { FormGeneratorTable } from '../../components/FormGenerator/FormGenerator
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { AccessRulesEditor } from '../../components/AccessRules'; import { AccessRulesEditor } from '../../components/AccessRules';
import { FaPlus, FaSync, FaUserShield, FaCube, FaShieldAlt } from 'react-icons/fa'; import { FaPlus, FaSync, FaUserShield, FaCube, FaShieldAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api'; import api from '../../api';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
@ -39,6 +40,7 @@ interface FeatureRole {
} }
export const AdminFeatureRolesPage: React.FC = () => { export const AdminFeatureRolesPage: React.FC = () => {
const { showError } = useToast();
// State // State
const [features, setFeatures] = useState<Feature[]>([]); const [features, setFeatures] = useState<Feature[]>([]);
const [selectedFeatureCode, setSelectedFeatureCode] = useState<string>(''); const [selectedFeatureCode, setSelectedFeatureCode] = useState<string>('');
@ -197,7 +199,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles(); await fetchRoles();
} catch (err: any) { } catch (err: any) {
console.error('Error creating role:', err); console.error('Error creating role:', err);
alert(err.response?.data?.detail || 'Fehler beim Erstellen der Rolle'); showError('Fehler', err.response?.data?.detail || 'Fehler beim Erstellen der Rolle');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -216,22 +218,20 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles(); await fetchRoles();
} catch (err: any) { } catch (err: any) {
console.error('Error updating role:', err); console.error('Error updating role:', err);
alert(err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle'); showError('Fehler', err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
// Handle delete role // Handle delete role (confirmation handled by DeleteActionButton)
const handleDeleteRole = async (role: FeatureRole) => { const handleDeleteRole = async (role: FeatureRole) => {
if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel}" wirklich löschen?`)) { try {
try { await api.delete(`/api/rbac/roles/${role.id}`);
await api.delete(`/api/rbac/roles/${role.id}`); await fetchRoles();
await fetchRoles(); } catch (err: any) {
} catch (err: any) { console.error('Error deleting role:', err);
console.error('Error deleting role:', err); showError('Fehler', err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
alert(err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
}
} }
}; };

View file

@ -11,10 +11,12 @@ import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMan
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaEnvelopeOpenText, FaBuilding, FaCopy, FaLink } from 'react-icons/fa'; import { FaPlus, FaSync, FaEnvelopeOpenText, FaBuilding, FaCopy, FaLink } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api'; import api from '../../api';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
export const AdminInvitationsPage: React.FC = () => { export const AdminInvitationsPage: React.FC = () => {
const { showError } = useToast();
const { const {
invitations, invitations,
loading, loading,
@ -188,7 +190,7 @@ export const AdminInvitationsPage: React.FC = () => {
setShowUrlModal(result.data); setShowUrlModal(result.data);
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }); fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
} else { } else {
alert(result.error || 'Fehler beim Erstellen der Einladung'); showError('Fehler', result.error || 'Fehler beim Erstellen der Einladung');
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -201,7 +203,7 @@ export const AdminInvitationsPage: React.FC = () => {
if (!selectedMandateId) return false; if (!selectedMandateId) return false;
const result = await revokeInvitation(selectedMandateId, invitationId); const result = await revokeInvitation(selectedMandateId, invitationId);
if (!result.success) { if (!result.success) {
alert(result.error || 'Fehler beim Widerrufen der Einladung'); showError('Fehler', result.error || 'Fehler beim Widerrufen der Einladung');
} }
return result.success; return result.success;
}; };

View file

@ -19,10 +19,12 @@ import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUserShield, FaBuilding, FaGlobe } from 'react-icons/fa'; import { FaPlus, FaSync, FaUserShield, FaBuilding, FaGlobe } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api'; import api from '../../api';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
export const AdminMandateRolesPage: React.FC = () => { export const AdminMandateRolesPage: React.FC = () => {
const { showError, showWarning } = useToast();
const { const {
roles, roles,
loading, loading,
@ -209,11 +211,11 @@ export const AdminMandateRolesPage: React.FC = () => {
setShowCreateModal(false); setShowCreateModal(false);
await fetchRoles(selectedMandateId, { scopeFilter }); await fetchRoles(selectedMandateId, { scopeFilter });
} else { } else {
alert(result.error || 'Fehler beim Erstellen der Rolle'); showError('Fehler', result.error || 'Fehler beim Erstellen der Rolle');
} }
} catch (err: any) { } catch (err: any) {
console.error('Create role error:', err); console.error('Create role error:', err);
alert(err.message || 'Fehler beim Erstellen der Rolle'); showError('Fehler', err.message || 'Fehler beim Erstellen der Rolle');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -248,31 +250,29 @@ export const AdminMandateRolesPage: React.FC = () => {
await fetchRoles(selectedMandateId, { scopeFilter }); await fetchRoles(selectedMandateId, { scopeFilter });
} }
} else { } else {
alert(result.error || 'Fehler beim Aktualisieren der Rolle'); showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rolle');
} }
} catch (err: any) { } catch (err: any) {
console.error('Update role error:', err); console.error('Update role error:', err);
alert(err.message || 'Fehler beim Aktualisieren der Rolle'); showError('Fehler', err.message || 'Fehler beim Aktualisieren der Rolle');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
// Handle delete role // Handle delete role (confirmation handled by DeleteActionButton)
const handleDeleteRole = async (role: Role) => { const handleDeleteRole = async (role: Role) => {
if (role.isSystemRole) { if (role.isSystemRole) {
alert('System-Rollen können nicht gelöscht werden.'); showWarning('Nicht erlaubt', 'System-Rollen können nicht gelöscht werden.');
return; return;
} }
if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel}" wirklich löschen?`)) { const result = await deleteRole(role.id);
const result = await deleteRole(role.id); if (result.success) {
if (result.success) { // Refetch to update the list
// Refetch to update the list await fetchRoles(selectedMandateId, { scopeFilter });
await fetchRoles(selectedMandateId, { scopeFilter }); } else {
} else { showError('Fehler', result.error || 'Fehler beim Löschen der Rolle');
alert(result.error || 'Fehler beim Löschen der Rolle');
}
} }
}; };

View file

@ -73,11 +73,9 @@ export const AdminMandatesPage: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteMandate = async (mandate: Mandate) => { const handleDeleteMandate = async (mandate: Mandate) => {
if (window.confirm(`Möchten Sie den Mandanten "${mandate.name || mandate.id}" wirklich löschen?`)) { await handleDelete(mandate.id);
await handleDelete(mandate.id);
}
}; };
if (error) { if (error) {

View file

@ -10,10 +10,12 @@ import { useUserMandates, type MandateUser, type Mandate, type Role, type Pagina
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUsers, FaBuilding } from 'react-icons/fa'; import { FaPlus, FaSync, FaUsers, FaBuilding } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api'; import api from '../../api';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
export const AdminUserMandatesPage: React.FC = () => { export const AdminUserMandatesPage: React.FC = () => {
const { showError } = useToast();
const { const {
users, users,
loading, loading,
@ -206,7 +208,7 @@ export const AdminUserMandatesPage: React.FC = () => {
setShowAddModal(false); setShowAddModal(false);
fetchMandateUsers(selectedMandateId); fetchMandateUsers(selectedMandateId);
} else { } else {
alert(result.error || 'Fehler beim Hinzufügen des Benutzers'); showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -223,21 +225,19 @@ export const AdminUserMandatesPage: React.FC = () => {
setEditingUser(null); setEditingUser(null);
fetchMandateUsers(selectedMandateId); fetchMandateUsers(selectedMandateId);
} else { } else {
alert(result.error || 'Fehler beim Aktualisieren der Rollen'); showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rollen');
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
// Handle remove user // Handle remove user (confirmation handled by DeleteActionButton)
const handleRemoveUser = async (user: MandateUser) => { const handleRemoveUser = async (user: MandateUser) => {
if (!selectedMandateId) return; if (!selectedMandateId) return;
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich aus diesem Mandanten entfernen?`)) { const result = await removeUserFromMandate(selectedMandateId, user.userId);
const result = await removeUserFromMandate(selectedMandateId, user.userId); if (!result.success) {
if (!result.success) { showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
alert(result.error || 'Fehler beim Entfernen des Benutzers');
}
} }
}; };

View file

@ -96,13 +96,11 @@ export const AdminUsersPage: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteUser = async (user: User) => { const handleDeleteUser = async (user: User) => {
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich löschen?`)) { const success = await deleteUser(user.id);
const success = await deleteUser(user.id); if (success) {
if (success) { refetch(); // Refresh the list
refetch(); // Refresh the list
}
} }
}; };

View file

@ -93,22 +93,20 @@ export const ConnectionsPage: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDelete = async (connection: Connection) => { const handleDelete = async (connection: Connection) => {
if (window.confirm(`Möchten Sie die Verbindung "${connection.name || connection.email || connection.id}" wirklich löschen?`)) { setDeletingConnections(prev => new Set(prev).add(connection.id));
setDeletingConnections(prev => new Set(prev).add(connection.id)); try {
try { await deleteConnection(connection.id);
await deleteConnection(connection.id); refetch();
refetch(); } catch (error) {
} catch (error) { console.error('Error deleting connection:', error);
console.error('Error deleting connection:', error); } finally {
} finally { setDeletingConnections(prev => {
setDeletingConnections(prev => { const newSet = new Set(prev);
const newSet = new Set(prev); newSet.delete(connection.id);
newSet.delete(connection.id); return newSet;
return newSet; });
});
}
} }
}; };

View file

@ -100,25 +100,20 @@ export const FilesPage: React.FC = () => {
} }
}; };
// Handle delete single file // Handle delete single file (confirmation handled by DeleteActionButton)
const handleDelete = async (file: UserFile) => { const handleDelete = async (file: UserFile) => {
if (window.confirm(`Möchten Sie die Datei "${file.fileName}" wirklich löschen?`)) { const success = await handleFileDelete(file.id);
const success = await handleFileDelete(file.id); if (success) {
if (success) { refetch();
refetch();
}
} }
}; };
// Handle delete multiple files // Handle delete multiple files (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => { const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
const count = filesToDelete.length; const ids = filesToDelete.map(f => f.id);
if (window.confirm(`Möchten Sie ${count} Datei(en) wirklich löschen?`)) { const success = await handleFileDeleteMultiple(ids);
const ids = filesToDelete.map(f => f.id); if (success) {
const success = await handleFileDeleteMultiple(ids); refetch();
if (success) {
refetch();
}
} }
}; };

View file

@ -109,13 +109,11 @@ export const PromptsPage: React.FC = () => {
} }
}; };
// Handle delete single prompt // Handle delete single prompt (confirmation handled by DeleteActionButton)
const handleDelete = async (prompt: Prompt) => { const handleDelete = async (prompt: Prompt) => {
if (window.confirm(`Möchten Sie den Prompt "${prompt.name}" wirklich löschen?`)) { const success = await handlePromptDelete(prompt.id);
const success = await handlePromptDelete(prompt.id); if (success) {
if (success) { refetch();
refetch();
}
} }
}; };

View file

@ -11,10 +11,12 @@ import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaFileAlt, FaDownload } from 'react-icons/fa'; import { FaSync, FaFileAlt, FaDownload } from 'react-icons/fa';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api'; import api from '../../../api';
import styles from '../../admin/Admin.module.css'; import styles from '../../admin/Admin.module.css';
export const TrusteeDocumentsView: React.FC = () => { export const TrusteeDocumentsView: React.FC = () => {
const { showError } = useToast();
const instanceId = useInstanceId(); const instanceId = useInstanceId();
// Entity hook // Entity hook
@ -103,14 +105,12 @@ export const TrusteeDocumentsView: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteDoc = async (doc: TrusteeDocument) => { const handleDeleteDoc = async (doc: TrusteeDocument) => {
if (window.confirm(`Dokument "${doc.documentName}" wirklich löschen?`)) { removeOptimistically(doc.id);
removeOptimistically(doc.id); const success = await handleDelete(doc.id);
const success = await handleDelete(doc.id); if (!success) {
if (!success) { refetch(); // Revert on error
refetch(); // Revert on error
}
} }
}; };
@ -136,7 +136,7 @@ export const TrusteeDocumentsView: React.FC = () => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (err) { } catch (err) {
console.error('Download error:', err); console.error('Download error:', err);
alert('Fehler beim Herunterladen des Dokuments.'); showError('Fehler', 'Fehler beim Herunterladen des Dokuments.');
} finally { } finally {
setDownloadingId(null); setDownloadingId(null);
} }

View file

@ -117,14 +117,12 @@ export const TrusteePositionDocumentsView: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteLink = async (link: TrusteePositionDocument) => { const handleDeleteLink = async (link: TrusteePositionDocument) => {
if (window.confirm('Verknüpfung wirklich entfernen?')) { removeOptimistically(link.id);
removeOptimistically(link.id); const success = await handleDelete(link.id);
const success = await handleDelete(link.id); if (!success) {
if (!success) { refetch(); // Revert on error
refetch(); // Revert on error
}
} }
}; };

View file

@ -50,19 +50,24 @@ export const TrusteePositionsView: React.FC = () => {
} }
}, [instanceId]); }, [instanceId]);
// Hidden columns (not shown in table view, but available in form)
const hiddenColumns = ['desc'];
// Generate columns from attributes // Generate columns from attributes
const columns = useMemo(() => { const columns = useMemo(() => {
return (attributes || []).map(attr => ({ return (attributes || [])
key: attr.name, .filter(attr => !hiddenColumns.includes(attr.name))
label: attr.label || attr.name, .map(attr => ({
type: attr.type as any, key: attr.name,
sortable: attr.sortable !== false, label: attr.label || attr.name,
filterable: attr.filterable !== false, type: attr.type as any,
searchable: attr.searchable !== false, sortable: attr.sortable !== false,
width: attr.width || 150, filterable: attr.filterable !== false,
minWidth: attr.minWidth || 100, searchable: attr.searchable !== false,
maxWidth: attr.maxWidth || 400, width: attr.width || 150,
})); minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
}, [attributes]); }, [attributes]);
// Check permissions // Check permissions
@ -108,14 +113,12 @@ export const TrusteePositionsView: React.FC = () => {
} }
}; };
// Handle delete // Handle delete (confirmation handled by DeleteActionButton)
const handleDeletePos = async (pos: TrusteePosition) => { const handleDeletePos = async (pos: TrusteePosition) => {
if (window.confirm(`Position "${pos.desc || pos.id}" wirklich löschen?`)) { removeOptimistically(pos.id);
removeOptimistically(pos.id); const success = await handleDelete(pos.id);
const success = await handleDelete(pos.id); if (!success) {
if (!success) { refetch(); // Revert on error
refetch(); // Revert on error
}
} }
}; };

View file

@ -124,7 +124,7 @@ export const AutomationsPage: React.FC = () => {
// Generate columns from attributes - exclude ID fields from display // Generate columns from attributes - exclude ID fields from display
const columns = useMemo(() => { const columns = useMemo(() => {
const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs']; const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs', 'placeholders'];
return (attributes || []) return (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name)) .filter(attr => !hiddenColumns.includes(attr.name))
@ -188,14 +188,12 @@ export const AutomationsPage: React.FC = () => {
} }
}; };
// Handle delete single automation // Handle delete single automation (confirmation handled by DeleteActionButton)
const handleDelete = async (automation: Automation) => { const handleDelete = async (automation: Automation) => {
if (window.confirm(`Möchten Sie die Automatisierung "${automation.label}" wirklich löschen?`)) { const success = await handleAutomationDelete(automation.id);
const success = await handleAutomationDelete(automation.id); if (success) {
if (success) { showSuccess('Automatisierung gelöscht');
showSuccess('Automatisierung gelöscht'); await refetch();
await refetch();
}
} }
}; };
@ -274,10 +272,16 @@ export const AutomationsPage: React.FC = () => {
const logs: WorkflowLog[] = response?.items || response || []; const logs: WorkflowLog[] = response?.items || response || [];
if (logs.length > 0) { if (logs.length > 0) {
setExecutionModal(prev => ({ setExecutionModal(prev => {
...prev, // Deduplicate logs by ID
logs: [...prev.logs, ...logs], const existingIds = new Set(prev.logs.map(l => l.id));
})); const newLogs = logs.filter(l => !existingIds.has(l.id));
return {
...prev,
logs: [...prev.logs, ...newLogs],
};
});
lastLogIdRef.current = logs[logs.length - 1].id; lastLogIdRef.current = logs[logs.length - 1].id;
} }
@ -377,7 +381,7 @@ export const AutomationsPage: React.FC = () => {
try { try {
await request({ await request({
url: `/api/workflows/${executionModal.workflowId}/stop`, url: `/api/chat/playground/${executionModal.workflowId}/stop`,
method: 'post', method: 'post',
}); });

View file

@ -9,12 +9,11 @@
import React, { useRef, useState, useEffect, useCallback } from 'react'; import React, { useRef, useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useDashboardInputForm } from '../../hooks/usePlayground'; import { useDashboardInputForm } from '../../hooks/usePlayground';
import { useUserWorkflows } from '../../hooks/useWorkflows';
import { useResizablePanels } from '../../hooks/useResizablePanels'; import { useResizablePanels } from '../../hooks/useResizablePanels';
import { usePrompts } from '../../hooks/usePrompts'; import { usePrompts } from '../../hooks/usePrompts';
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa'; import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
import { useVoiceLanguage, VoiceLanguageSelect } from '../../components/UiComponents'; import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
import api from '../../api'; import api from '../../api';
import styles from './PlaygroundPage.module.css'; import styles from './PlaygroundPage.module.css';
@ -44,11 +43,16 @@ export const PlaygroundPage: React.FC = () => {
workflowItems, workflowItems,
pendingFiles, pendingFiles,
handleFileRemove, handleFileRemove,
handleFileDelete,
handleFileView,
handleFileDownload,
latestStats, latestStats,
playgroundUIPermission, playgroundUIPermission,
deletingFiles,
previewingFiles,
downloadingFiles,
} = hookData; } = hookData;
const { data: workflows } = useUserWorkflows();
const { prompts, refetch: refetchPrompts } = usePrompts(); const { prompts, refetch: refetchPrompts } = usePrompts();
const { showError, showSuccess } = useToast(); const { showError, showSuccess } = useToast();
@ -310,14 +314,7 @@ export const PlaygroundPage: React.FC = () => {
} }
}; };
// Format timestamp for messages // Render messages using the Messages component with document support
const formatTime = (timestamp: number | undefined) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
};
// Render messages
const renderMessages = () => { const renderMessages = () => {
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
return ( return (
@ -332,43 +329,21 @@ export const PlaygroundPage: React.FC = () => {
} }
return ( return (
<div className={styles.messagesContainer}> <Messages
{messages.map((msg: any, index: number) => ( messages={messages}
<div variant="chat"
key={msg.id || index} showDocuments={true}
style={{ showMetadata={false}
padding: '0.75rem 1rem', onFileDelete={handleFileDelete}
borderRadius: '8px', onFileRemove={handleFileRemove}
background: msg.role === 'user' onFileView={handleFileView}
? 'var(--bg-secondary)' onFileDownload={handleFileDownload}
: 'var(--surface-color)', deletingFiles={deletingFiles}
border: '1px solid var(--border-color)', previewingFiles={previewingFiles}
marginLeft: msg.role === 'user' ? '2rem' : '0', downloadingFiles={downloadingFiles}
marginRight: msg.role === 'assistant' ? '2rem' : '0', workflowId={workflowId}
}} emptyMessage="Keine Nachrichten"
> />
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '0.5rem',
fontSize: '0.75rem',
color: 'var(--text-secondary)',
}}>
<span style={{ fontWeight: 500 }}>
{msg.role === 'user' ? 'Sie' : 'Assistent'}
</span>
<span>{formatTime(msg.publishedAt)}</span>
</div>
<div style={{
color: 'var(--text-primary)',
fontSize: '0.875rem',
whiteSpace: 'pre-wrap',
}}>
{msg.message || msg.content}
</div>
</div>
))}
</div>
); );
}; };
@ -615,9 +590,9 @@ export const PlaygroundPage: React.FC = () => {
disabled={isRunning} disabled={isRunning}
> >
<option value="">Neuer Workflow</option> <option value="">Neuer Workflow</option>
{(workflowItems || workflows)?.map((item: any) => ( {workflowItems?.map((item: any) => (
<option key={item.id} value={item.id}> <option key={item.id} value={item.id}>
{item.label || item.name || item.id} {item.label || item.id}
</option> </option>
))} ))}
</select> </select>

View file

@ -98,25 +98,20 @@ export const WorkflowsPage: React.FC = () => {
} }
}; };
// Handle delete single workflow // Handle delete single workflow (confirmation handled by DeleteActionButton)
const handleDelete = async (workflow: Workflow) => { const handleDelete = async (workflow: Workflow) => {
if (window.confirm(`Möchten Sie den Workflow "${workflow.name || workflow.id}" wirklich löschen?`)) { const success = await handleWorkflowDelete(workflow.id);
const success = await handleWorkflowDelete(workflow.id); if (success) {
if (success) { refetch();
refetch();
}
} }
}; };
// Handle delete multiple workflows // Handle delete multiple workflows (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => { const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
const count = workflowsToDelete.length; const ids = workflowsToDelete.map(w => w.id);
if (window.confirm(`Möchten Sie ${count} Workflow(s) wirklich löschen?`)) { const success = await handleWorkflowDeleteMultiple(ids);
const ids = workflowsToDelete.map(w => w.id); if (success) {
const success = await handleWorkflowDeleteMultiple(ids); refetch();
if (success) {
refetch();
}
} }
}; };