+
✎
- File Edit Proposal: {edit.fileName}
+ {pendingEdits.filter(e => e.status === 'pending').length} Aenderungsvorschlag(e)
-
- {edit.newContent?.slice(0, 800)}
- {(edit.newContent?.length || 0) > 800 && '\n...'}
-
-
+
+ {pendingEdits.filter(e => e.status === 'pending').map(edit => (
+
+
+ {edit.fileName}
+
+ ))}
+
+
+ {onOpenEditor && (
+
+ )}
- ))}
+ )}
{/* Agent progress */}
{isProcessing && agentProgress && (
diff --git a/src/pages/views/workspace/WorkspaceEditorPage.tsx b/src/pages/views/workspace/WorkspaceEditorPage.tsx
new file mode 100644
index 0000000..8cb7b5d
--- /dev/null
+++ b/src/pages/views/workspace/WorkspaceEditorPage.tsx
@@ -0,0 +1,278 @@
+/**
+ * WorkspaceEditorPage -- Diff editor for reviewing AI agent file edit proposals.
+ *
+ * Full-page layout with:
+ * - Header: back-to-dashboard, accept-all / reject-all
+ * - Tab bar: one tab per pending edit
+ * - Center: Monaco DiffEditor (original vs. modified)
+ * - Footer: status bar with counts and file metadata
+ */
+
+import React, { useMemo, useState, useEffect, useRef } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { DiffEditor } from '@monaco-editor/react';
+import type { editor as monacoEditor } from 'monaco-editor';
+import { useInstanceId } from '../../../hooks/useCurrentInstance';
+import { useWorkspaceEditor, type EditorFileEdit } from './useWorkspaceEditor';
+import { FaArrowLeft, FaCheck, FaTimes, FaCheckDouble, FaBan, FaSync } from 'react-icons/fa';
+
+function _getMonacoLanguage(fileName: string): string {
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
+ const langMap: Record
= {
+ js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
+ py: 'python', json: 'json', html: 'html', css: 'css', md: 'markdown',
+ xml: 'xml', yaml: 'yaml', yml: 'yaml', sh: 'shell', sql: 'sql',
+ txt: 'plaintext', csv: 'plaintext', log: 'plaintext',
+ };
+ return langMap[ext] || 'plaintext';
+}
+
+function _formatBytes(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+export const WorkspaceEditorPage: React.FC = () => {
+ const instanceId = useInstanceId() || '';
+ const navigate = useNavigate();
+ const { mandateId, featureCode, instanceId: routeInstanceId } = useParams<{
+ mandateId: string; featureCode: string; instanceId: string;
+ }>();
+ const editor = useWorkspaceEditor(instanceId);
+
+ const activeEdit = useMemo(
+ () => editor.edits.find(e => e.id === editor.activeEditId) || null,
+ [editor.edits, editor.activeEditId],
+ );
+
+ const pendingEdits = useMemo(
+ () => editor.edits.filter(e => e.status === 'pending'),
+ [editor.edits],
+ );
+
+ const _goBack = () => navigate(`/mandates/${mandateId}/${featureCode}/${routeInstanceId}/dashboard`);
+
+ if (!instanceId) {
+ return (
+
+ Keine Workspace-Instanz ausgewaehlt.
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ File Edit Review
+
+
+ {editor.pendingCount} pending
+
+
+
+
+
+
+
+
+
+ {/* Tab bar */}
+ {pendingEdits.length > 0 && (
+
+ {pendingEdits.map(edit => (
+ <_EditorTab
+ key={edit.id}
+ edit={edit}
+ isActive={edit.id === editor.activeEditId}
+ onClick={() => editor.setActiveEditId(edit.id)}
+ />
+ ))}
+
+ )}
+
+ {/* Main content */}
+
+ {editor.isLoading ? (
+
+ Lade Aenderungsvorschlaege...
+
+ ) : pendingEdits.length === 0 ? (
+
+ ✓
+ Keine offenen Aenderungsvorschlaege
+
+
+ ) : activeEdit ? (
+ <_SafeDiffEditor
+ key={activeEdit.id}
+ original={activeEdit.oldContent}
+ modified={activeEdit.newContent}
+ language={_getMonacoLanguage(activeEdit.fileName)}
+ />
+ ) : null}
+
+
+ {/* Footer / action bar for active edit */}
+ {activeEdit && activeEdit.status === 'pending' && (
+
+
+ {activeEdit.fileName}
+ Original: {_formatBytes(activeEdit.oldContent.length)}
+ Geaendert: {_formatBytes(activeEdit.newContent.length)}
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Safe DiffEditor wrapper -- prevents "TextModel got disposed" errors
+// by tracking the editor ref and skipping disposal when already torn down.
+// ---------------------------------------------------------------------------
+
+const _SafeDiffEditor: React.FC<{
+ original: string;
+ modified: string;
+ language: string;
+}> = ({ original, modified, language }) => {
+ const editorRef = useRef(null);
+ const [ready, setReady] = useState(false);
+
+ useEffect(() => {
+ setReady(true);
+ return () => {
+ if (editorRef.current) {
+ try {
+ editorRef.current.dispose();
+ } catch { /* already disposed */ }
+ editorRef.current = null;
+ }
+ };
+ }, []);
+
+ if (!ready) return null;
+
+ return (
+ { editorRef.current = diffEditor; }}
+ options={{
+ readOnly: true,
+ renderSideBySide: true,
+ minimap: { enabled: false },
+ fontSize: 13,
+ lineNumbers: 'on',
+ scrollBeyondLastLine: false,
+ wordWrap: 'on',
+ originalEditable: false,
+ }}
+ />
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Sub-components
+// ---------------------------------------------------------------------------
+
+const _EditorTab: React.FC<{
+ edit: EditorFileEdit;
+ isActive: boolean;
+ onClick: () => void;
+}> = ({ edit, isActive, onClick }) => (
+
+);
+
+// ---------------------------------------------------------------------------
+// Shared styles
+// ---------------------------------------------------------------------------
+
+const _btnStyle: React.CSSProperties = {
+ padding: '6px 8px', borderRadius: 4, border: '1px solid var(--border-color, #ddd)',
+ background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center',
+};
+
+const _actionBtnStyle: React.CSSProperties = {
+ padding: '5px 14px', borderRadius: 4, border: 'none',
+ cursor: 'pointer', fontSize: 12, fontWeight: 600,
+ display: 'flex', alignItems: 'center', gap: 6,
+};
+
+export default WorkspaceEditorPage;
diff --git a/src/pages/views/workspace/WorkspacePage.tsx b/src/pages/views/workspace/WorkspacePage.tsx
index e5a7b60..e0935a4 100644
--- a/src/pages/views/workspace/WorkspacePage.tsx
+++ b/src/pages/views/workspace/WorkspacePage.tsx
@@ -8,6 +8,7 @@
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import { useFileOperations } from '../../../hooks/useFiles';
import { useWorkspace } from './useWorkspace';
@@ -69,6 +70,10 @@ export const WorkspacePage: React.FC = ({ persistentInstance
const instanceId = persistentInstanceId || instance?.id || '';
const workspace = useWorkspace(instanceId);
const fileOps = useFileOperations();
+ const navigate = useNavigate();
+ const { mandateId, featureCode, instanceId: routeInstanceId } = useParams<{
+ mandateId: string; featureCode: string; instanceId: string;
+ }>();
const [leftCollapsed, setLeftCollapsed] = useState(false);
const [rightCollapsed, setRightCollapsed] = useState(false);
const _leftResize = _useResizable(280, 200, 450);
@@ -386,6 +391,7 @@ export const WorkspacePage: React.FC = ({ persistentInstance
pendingEdits={workspace.pendingEdits}
onAcceptEdit={workspace.acceptEdit}
onRejectEdit={workspace.rejectEdit}
+ onOpenEditor={() => navigate(`/mandates/${mandateId}/${featureCode}/${routeInstanceId}/editor`)}
/>
{
- if (event.item) {
- setPendingEdits(prev => [...prev, event.item]);
+ const data = event.item || event.data || {};
+ if (data.id) {
+ setPendingEdits(prev => [...prev, {
+ id: data.id,
+ fileId: data.fileId || '',
+ fileName: data.fileName || '',
+ mimeType: data.mimeType || '',
+ oldSize: data.oldSize || 0,
+ newSize: data.newSize || 0,
+ status: 'pending' as const,
+ }]);
}
},
onFileVersion: (event) => {
@@ -359,21 +372,38 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
const acceptEdit = useCallback(
(editId: string) => {
- const edit = pendingEdits.find(e => e.id === editId);
- if (!edit || !instanceId || !workflowId) return;
+ if (!instanceId) return;
setPendingEdits(prev =>
prev.map(e => (e.id === editId ? { ...e, status: 'accepted' as const } : e)),
);
- refreshFiles();
+ api.post(`/api/workspace/${instanceId}/edit/${editId}/accept`)
+ .then(() => refreshFiles())
+ .catch(err => {
+ console.error('Failed to accept edit:', err);
+ setPendingEdits(prev =>
+ prev.map(e => (e.id === editId ? { ...e, status: 'pending' as const } : e)),
+ );
+ });
},
- [pendingEdits, instanceId, workflowId, refreshFiles],
+ [instanceId, refreshFiles],
);
- const rejectEdit = useCallback((editId: string) => {
- setPendingEdits(prev =>
- prev.map(e => (e.id === editId ? { ...e, status: 'rejected' as const } : e)),
- );
- }, []);
+ const rejectEdit = useCallback(
+ (editId: string) => {
+ if (!instanceId) return;
+ setPendingEdits(prev =>
+ prev.map(e => (e.id === editId ? { ...e, status: 'rejected' as const } : e)),
+ );
+ api.post(`/api/workspace/${instanceId}/edit/${editId}/reject`)
+ .catch(err => {
+ console.error('Failed to reject edit:', err);
+ setPendingEdits(prev =>
+ prev.map(e => (e.id === editId ? { ...e, status: 'pending' as const } : e)),
+ );
+ });
+ },
+ [instanceId],
+ );
return {
messages,
diff --git a/src/pages/views/workspace/useWorkspaceEditor.ts b/src/pages/views/workspace/useWorkspaceEditor.ts
new file mode 100644
index 0000000..4ac834e
--- /dev/null
+++ b/src/pages/views/workspace/useWorkspaceEditor.ts
@@ -0,0 +1,127 @@
+/**
+ * useWorkspaceEditor Hook
+ *
+ * State management for the workspace editor page.
+ * Loads pending file edit proposals from the API,
+ * provides accept/reject actions, and tracks the active tab.
+ */
+
+import { useState, useCallback, useEffect } from 'react';
+import api from '../../../api';
+
+export interface EditorFileEdit {
+ id: string;
+ fileId: string;
+ fileName: string;
+ mimeType: string;
+ oldContent: string;
+ newContent: string;
+ status: 'pending' | 'accepted' | 'rejected';
+ workflowId: string;
+}
+
+interface UseWorkspaceEditorReturn {
+ edits: EditorFileEdit[];
+ activeEditId: string | null;
+ isLoading: boolean;
+ setActiveEditId: (id: string | null) => void;
+ acceptEdit: (editId: string) => Promise;
+ rejectEdit: (editId: string) => Promise;
+ acceptAll: () => Promise;
+ rejectAll: () => Promise;
+ refresh: () => void;
+ pendingCount: number;
+}
+
+export function useWorkspaceEditor(instanceId: string): UseWorkspaceEditorReturn {
+ const [edits, setEdits] = useState([]);
+ const [activeEditId, setActiveEditId] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const refresh = useCallback(() => {
+ if (!instanceId) return;
+ setIsLoading(true);
+ api.get(`/api/workspace/${instanceId}/pending-edits`)
+ .then(res => {
+ const loadedEdits: EditorFileEdit[] = (res.data.edits || []).map((e: any) => ({
+ id: e.id,
+ fileId: e.fileId || '',
+ fileName: e.fileName || '',
+ mimeType: e.mimeType || '',
+ oldContent: e.oldContent || '',
+ newContent: e.newContent || '',
+ status: e.status || 'pending',
+ workflowId: e.workflowId || '',
+ }));
+ setEdits(loadedEdits);
+ if (loadedEdits.length > 0 && !activeEditId) {
+ setActiveEditId(loadedEdits[0].id);
+ }
+ })
+ .catch(err => console.error('Failed to load pending edits:', err))
+ .finally(() => setIsLoading(false));
+ }, [instanceId, activeEditId]);
+
+ useEffect(() => {
+ refresh();
+ }, [instanceId]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const acceptEdit = useCallback(async (editId: string) => {
+ if (!instanceId) return;
+ setEdits(prev => prev.map(e => (e.id === editId ? { ...e, status: 'accepted' as const } : e)));
+ try {
+ await api.post(`/api/workspace/${instanceId}/edit/${editId}/accept`);
+ } catch (err) {
+ console.error('Failed to accept edit:', err);
+ setEdits(prev => prev.map(e => (e.id === editId ? { ...e, status: 'pending' as const } : e)));
+ }
+ }, [instanceId]);
+
+ const rejectEdit = useCallback(async (editId: string) => {
+ if (!instanceId) return;
+ setEdits(prev => prev.map(e => (e.id === editId ? { ...e, status: 'rejected' as const } : e)));
+ try {
+ await api.post(`/api/workspace/${instanceId}/edit/${editId}/reject`);
+ } catch (err) {
+ console.error('Failed to reject edit:', err);
+ setEdits(prev => prev.map(e => (e.id === editId ? { ...e, status: 'pending' as const } : e)));
+ }
+ }, [instanceId]);
+
+ const acceptAll = useCallback(async () => {
+ if (!instanceId) return;
+ setEdits(prev => prev.map(e => (e.status === 'pending' ? { ...e, status: 'accepted' as const } : e)));
+ try {
+ await api.post(`/api/workspace/${instanceId}/edit/accept-all`);
+ } catch (err) {
+ console.error('Failed to accept all edits:', err);
+ refresh();
+ }
+ }, [instanceId, refresh]);
+
+ const rejectAll = useCallback(async () => {
+ if (!instanceId) return;
+ setEdits(prev => prev.map(e => (e.status === 'pending' ? { ...e, status: 'rejected' as const } : e)));
+ try {
+ await api.post(`/api/workspace/${instanceId}/edit/reject-all`);
+ } catch (err) {
+ console.error('Failed to reject all edits:', err);
+ refresh();
+ }
+ }, [instanceId, refresh]);
+
+ const pendingCount = edits.filter(e => e.status === 'pending').length;
+
+ return {
+ edits,
+ activeEditId,
+ isLoading,
+ setActiveEditId,
+ acceptEdit,
+ rejectEdit,
+ acceptAll,
+ rejectAll,
+ refresh,
+ pendingCount,
+ };
+}
diff --git a/src/pages/workflows/PlaygroundPage.module.css b/src/pages/workflows/PlaygroundPage.module.css
deleted file mode 100644
index 32e8025..0000000
--- a/src/pages/workflows/PlaygroundPage.module.css
+++ /dev/null
@@ -1,593 +0,0 @@
-/**
- * PlaygroundPage Styles
- *
- * Resizable two-column layout for Chat Playground.
- * Uses existing Nyla CSS variables and design patterns.
- */
-
-/* Main container */
-.playgroundContainer {
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
- padding: 1rem;
- gap: 1rem;
-}
-
-/* Page header */
-.pageHeader {
- flex-shrink: 0;
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.headerLeft {
- display: flex;
- flex-direction: column;
- gap: 0.125rem;
-}
-
-.headerTitleRow {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.pageTitle {
- font-size: 1.5rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.headerStats {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- font-size: 0.75rem;
- color: var(--text-secondary);
- background-color: var(--bg-secondary);
- padding: 0.25rem 0.75rem;
- border-radius: 12px;
-}
-
-.headerStatItem {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- white-space: nowrap;
-}
-
-.pageSubtitle {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0.25rem 0 0 0;
-}
-
-.headerControls {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-/* Main content area with resizable columns */
-.mainContent {
- flex: 1;
- display: flex;
- flex-direction: row;
- overflow: hidden;
- min-height: 0;
-}
-
-/* Left panel - Chat/Messages */
-.leftPanel {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-width: 300px;
-}
-
-/* Resizable divider between panels */
-.resizeDivider {
- width: 8px;
- cursor: col-resize;
- background-color: transparent;
- position: relative;
- flex-shrink: 0;
- z-index: 10;
- transition: background-color 0.15s ease;
-}
-
-.resizeDivider:hover,
-.resizeDivider.dragging {
- background-color: var(--border-color);
-}
-
-.dividerHandle {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 4px;
- height: 40px;
- border-radius: 2px;
- background-color: var(--text-secondary);
- opacity: 0;
- transition: opacity 0.15s ease;
-}
-
-.resizeDivider:hover .dividerHandle,
-.resizeDivider.dragging .dividerHandle {
- opacity: 0.5;
-}
-
-/* Right panel - Dashboard */
-.rightPanel {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-width: 200px;
- background: var(--surface-color);
- border-left: 1px solid var(--border-color);
- border-radius: 0 8px 8px 0;
-}
-
-.panelHeader {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-secondary);
- flex-shrink: 0;
-}
-
-.panelTitle {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.panelContent {
- flex: 1;
- overflow-y: auto;
- padding: 1rem;
-}
-
-/* Content section */
-.contentSection {
- background: var(--surface-color);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- flex: 1;
-}
-
-.contentHeader {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-secondary);
- flex-shrink: 0;
-}
-
-.contentArea {
- flex: 1;
- overflow-y: auto;
- padding: 1rem;
-}
-
-/* Messages container */
-.messagesContainer {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-/* Empty state */
-.emptyState {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 3rem;
- color: var(--text-secondary);
- text-align: center;
-}
-
-.emptyIcon {
- font-size: 3rem;
- margin-bottom: 1rem;
- opacity: 0.5;
-}
-
-.emptyTitle {
- font-size: 1.125rem;
- font-weight: 500;
- color: var(--text-primary);
- margin: 0 0 0.5rem 0;
-}
-
-.emptyDescription {
- margin: 0;
- max-width: 400px;
-}
-
-/* Footer / Input area */
-.inputFooter {
- flex-shrink: 0;
- padding: 1rem;
- background: var(--surface-color);
- border: 1px solid var(--border-color);
- border-radius: 8px;
-}
-
-.inputRow {
- display: flex;
- gap: 0.75rem;
- align-items: flex-start;
-}
-
-.selectors {
- display: flex;
- gap: 0.5rem;
- flex-shrink: 0;
-}
-
-.inputWrapper {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.textareaWrapper {
- position: relative;
-}
-
-.inputTextarea {
- width: 100%;
- min-height: 80px;
- max-height: 200px;
- padding: 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 0.875rem;
- resize: vertical;
- transition: border-color 0.2s;
-}
-
-.inputTextarea:focus {
- outline: none;
- border-color: var(--primary-color, #f25843);
-}
-
-.inputTextarea:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.inputControls {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.fileButtons {
- display: flex;
- gap: 0.5rem;
-}
-
-.actionButtons {
- display: flex;
- gap: 0.5rem;
-}
-
-/* Buttons */
-.iconButton {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--surface-color);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.iconButton:hover:not(:disabled) {
- background: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-.iconButton:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.primaryButton {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: var(--primary-color, #f25843);
- color: white;
- border: none;
- border-radius: 6px;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.primaryButton:hover:not(:disabled) {
- background: var(--primary-dark, #d94d3a);
-}
-
-.primaryButton:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.stopButton {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: var(--danger-color, #e53e3e);
- color: white;
- border: none;
- border-radius: 6px;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.stopButton:hover:not(:disabled) {
- background: #c53030;
-}
-
-.secondaryButton {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: var(--surface-color);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.secondaryButton:hover:not(:disabled) {
- background: var(--bg-secondary);
-}
-
-/* Select/Dropdown */
-.selector {
- min-width: 150px;
-}
-
-.selectDropdown {
- padding: 0.5rem 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--surface-color);
- color: var(--text-primary);
- font-size: 0.875rem;
- cursor: pointer;
- min-width: 150px;
-}
-
-.selectDropdown:focus {
- outline: none;
- border-color: var(--primary-color, #f25843);
-}
-
-/* Pending files */
-.pendingFiles {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- padding: 0.5rem 0;
-}
-
-.pendingFile {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- padding: 0.25rem 0.5rem;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 4px;
- font-size: 0.75rem;
-}
-
-.pendingFileName {
- max-width: 150px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.removeFileButton {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- padding: 0;
- border: none;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- border-radius: 50%;
-}
-
-.removeFileButton:hover {
- background: var(--danger-color, #e53e3e);
- color: white;
-}
-
-/* Dragging state - prevent text selection */
-.mainContent.dragging {
- user-select: none;
-}
-
-/* Responsive adjustments */
-@media (max-width: 768px) {
- .mainContent {
- flex-direction: column;
- }
-
- .leftPanel,
- .rightPanel {
- width: 100% !important;
- }
-
- .resizeDivider {
- display: none;
- }
-
- .rightPanel {
- border-left: none;
- border-top: 1px solid var(--border-color);
- border-radius: 0 0 8px 8px;
- max-height: 300px;
- }
-}
-
-/* Loading spinner */
-.loadingSpinner {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 2rem;
-}
-
-.spinner {
- width: 24px;
- height: 24px;
- border: 2px solid var(--border-color);
- border-top-color: var(--primary-color, #f25843);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-/* Drag & Drop Styles */
-.dragOver {
- position: relative;
-}
-
-.dragOverlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(var(--primary-rgb, 242, 88, 67), 0.1);
- border: 2px dashed var(--primary-color, #f25843);
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 100;
- pointer-events: none;
-}
-
-.dragOverlayContent {
- text-align: center;
- color: var(--primary-color, #f25843);
- font-size: 1rem;
- font-weight: 500;
-}
-
-.dragOverFooter {
- border-color: var(--primary-color, #f25843);
- background: rgba(var(--primary-rgb, 242, 88, 67), 0.05);
-}
-
-/* Prompts Row */
-.promptsRow {
- display: flex;
- align-items: center;
- padding-bottom: 0.75rem;
- border-bottom: 1px solid var(--border-color);
- margin-bottom: 0.75rem;
-}
-
-.promptsSelect {
- display: flex;
- align-items: center;
- flex: 1;
- max-width: 400px;
-}
-
-.promptDropdown {
- flex: 1;
- padding: 0.5rem 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--surface-color);
- color: var(--text-primary);
- font-size: 0.875rem;
- cursor: pointer;
-}
-
-.promptDropdown:focus {
- outline: none;
- border-color: var(--primary-color, #f25843);
-}
-
-.promptDropdown:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* Voice Recording Button */
-.iconButton.recording {
- background: var(--danger-color, #e53e3e);
- border-color: var(--danger-color, #e53e3e);
- color: white;
- animation: pulse 1.5s infinite;
-}
-
-.iconButton.recording:hover {
- background: #c53030;
- border-color: #c53030;
- color: white;
-}
-
-@keyframes pulse {
- 0%, 100% {
- box-shadow: 0 0 0 0 rgba(229, 62, 62, 0.4);
- }
- 50% {
- box-shadow: 0 0 0 8px rgba(229, 62, 62, 0);
- }
-}
diff --git a/src/pages/workflows/PlaygroundPage.tsx b/src/pages/workflows/PlaygroundPage.tsx
deleted file mode 100644
index 4b5371a..0000000
--- a/src/pages/workflows/PlaygroundPage.tsx
+++ /dev/null
@@ -1,811 +0,0 @@
-/**
- * PlaygroundPage (Chat Playground)
- *
- * Global page for workflow execution and chat interaction.
- * Features a resizable two-column layout with chat on the left and dashboard on the right.
- * Includes: Drag & Drop file upload, Prompts selection, Voice input
- */
-
-import React, { useRef, useState, useEffect, useCallback } from 'react';
-import { useSearchParams } from 'react-router-dom';
-import { useDashboardInputForm } from '../../hooks/usePlayground';
-import { useResizablePanels } from '../../hooks/useResizablePanels';
-import { usePrompts } from '../../hooks/usePrompts';
-import { useCurrentInstance } from '../../hooks/useCurrentInstance';
-import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
-import { useToast } from '../../contexts/ToastContext';
-import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
-import { ProviderMultiSelect } from '../../components/ProviderSelector';
-import type { Message } from '../../components/UiComponents/Messages/MessagesTypes';
-import api from '../../api';
-import styles from './PlaygroundPage.module.css';
-
-export const PlaygroundPage: React.FC = () => {
- // Read workflowId from URL query parameters
- const [searchParams] = useSearchParams();
- const urlWorkflowId = searchParams.get('workflowId');
-
- // Get feature instance context
- const { instance } = useCurrentInstance();
- const instanceId = instance?.id || '';
-
- // Main hook for input form and data
- const hookData = useDashboardInputForm(instanceId);
- const {
- inputValue,
- onInputChange,
- isRunning,
- isStopping,
- handleSubmit,
- handleStop,
- isSubmitting,
- workflowStatus,
- messages,
- dashboardTree,
- onToggleOperationExpanded,
- onToggleRoundExpanded,
- currentRound,
- workflowId,
- onWorkflowSelect,
- workflowItems,
- pendingFiles,
- handleFileRemove,
- handleFileDelete,
- handleFileView,
- handleFileDownload,
- latestStats,
- playgroundUIPermission,
- deletingFiles,
- previewingFiles,
- downloadingFiles,
- handleMessageDelete,
- deletingMessages,
- selectedProviders,
- onProvidersChange,
- } = hookData;
-
- const { prompts, refetch: refetchPrompts } = usePrompts();
- const { showError, showSuccess } = useToast();
-
- // Resizable panels hook
- const {
- leftWidth,
- isDragging,
- handleMouseDown,
- containerRef,
- } = useResizablePanels({
- storageKey: 'playground-panel-width',
- defaultLeftWidth: 70,
- minLeftWidth: 40,
- maxLeftWidth: 85,
- });
-
- // File input ref for hidden file input
- const fileInputRef = useRef(null);
-
- // Drag & Drop state
- const [isDragOver, setIsDragOver] = useState(false);
- const dragCounterRef = useRef(0);
-
- // Voice recording state
- const [isRecording, setIsRecording] = useState(false);
- const [mediaRecorder, setMediaRecorder] = useState(null);
-
- // Voice language selection (defaults to user profile language)
- const { voiceLanguage, setVoiceLanguage } = useVoiceLanguage();
-
- // Prompts dropdown state
- const [selectedPromptId, setSelectedPromptId] = useState('');
-
- // Load prompts on mount
- useEffect(() => {
- refetchPrompts();
- }, []);
-
- // Load workflow from URL parameter
- const urlWorkflowLoadedRef = useRef(false);
-
- // Debug: Log URL parameter status
- useEffect(() => {
- console.log('🔍 PlaygroundPage URL debug:', {
- urlWorkflowId,
- currentWorkflowId: workflowId,
- hasOnWorkflowSelect: !!onWorkflowSelect,
- alreadyLoaded: urlWorkflowLoadedRef.current,
- fullUrl: window.location.href
- });
- }, [urlWorkflowId, workflowId, onWorkflowSelect]);
-
- useEffect(() => {
- // Only load once on mount, and only if we have a URL workflowId
- if (urlWorkflowId && !urlWorkflowLoadedRef.current && onWorkflowSelect) {
- urlWorkflowLoadedRef.current = true;
- console.log('🔗 Loading workflow from URL:', urlWorkflowId);
- // Small delay to ensure hooks are initialized
- setTimeout(() => {
- onWorkflowSelect({ id: urlWorkflowId, label: '', value: urlWorkflowId });
- }, 100);
- }
- }, [urlWorkflowId, onWorkflowSelect]);
-
- // Handle prompt selection
- const handlePromptSelect = (promptId: string) => {
- setSelectedPromptId(promptId);
- if (promptId) {
- const prompt = prompts?.find((p: any) => p.id === promptId);
- if (prompt && prompt.content) {
- // Append prompt content to input
- const currentText = inputValue || '';
- const newText = currentText ? `${currentText}\n\n${prompt.content}` : prompt.content;
- onInputChange(newText);
- }
- }
- };
-
- // Drag & Drop handlers
- const handleDragEnter = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current++;
- if (e.dataTransfer.types.includes('Files')) {
- setIsDragOver(true);
- }
- }, []);
-
- const handleDragLeave = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current--;
- if (dragCounterRef.current === 0) {
- setIsDragOver(false);
- }
- }, []);
-
- const handleDragOver = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- }, []);
-
- const handleDrop = useCallback(async (e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current = 0;
- setIsDragOver(false);
-
- const files = e.dataTransfer.files;
- if (files.length > 0 && hookData.handleFileUpload) {
- for (const file of Array.from(files)) {
- await hookData.handleFileUpload(file);
- }
- }
- }, [hookData.handleFileUpload]);
-
- // Voice recording handlers
- const startRecording = async () => {
- try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
-
- // Find supported MIME type
- const mimeTypes = [
- 'audio/webm;codecs=opus',
- 'audio/webm',
- 'audio/ogg;codecs=opus',
- 'audio/mp4',
- ];
- let mimeType = '';
- for (const type of mimeTypes) {
- if (MediaRecorder.isTypeSupported(type)) {
- mimeType = type;
- break;
- }
- }
-
- const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
- const chunks: Blob[] = [];
-
- recorder.ondataavailable = (e) => {
- if (e.data.size > 0) {
- chunks.push(e.data);
- }
- };
-
- recorder.onstop = async () => {
- // Stop all tracks
- stream.getTracks().forEach(track => track.stop());
-
- // Process recording
- if (chunks.length > 0) {
- const audioBlob = new Blob(chunks, { type: mimeType || 'audio/webm' });
- await processVoiceRecording(audioBlob);
- }
- };
-
- recorder.start();
- setMediaRecorder(recorder);
- setIsRecording(true);
- } catch (error: any) {
- console.error('Error starting recording:', error);
- showError('Mikrofonzugriff verweigert', 'Bitte erlauben Sie den Mikrofonzugriff in Ihren Browser-Einstellungen.');
- }
- };
-
- const stopRecording = () => {
- if (mediaRecorder && mediaRecorder.state === 'recording') {
- mediaRecorder.stop();
- setIsRecording(false);
- setMediaRecorder(null);
- }
- };
-
- const processVoiceRecording = async (audioBlob: Blob) => {
- try {
- // Create FormData for speech-to-text API
- const formData = new FormData();
- formData.append('audioFile', audioBlob, 'voice_recording.webm');
- formData.append('language', voiceLanguage);
-
- // Call speech-to-text API (Google Cloud)
- const response = await api.post('/voice-google/speech-to-text', formData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- });
-
- if (response.data?.success && response.data?.text) {
- const transcribedText = response.data.text.trim();
- // Append transcribed text to input
- const currentText = inputValue || '';
- const newText = currentText ? `${currentText} ${transcribedText}` : transcribedText;
- onInputChange(newText);
- showSuccess('Transkription erfolgreich', 'Text wurde hinzugefügt.');
- } else {
- showError('Transkription fehlgeschlagen', response.data?.error || 'Unbekannter Fehler');
- }
- } catch (error: any) {
- console.error('Error processing voice recording:', error);
- showError('Transkription fehlgeschlagen', error.message || 'Fehler bei der Sprachverarbeitung');
- }
- };
-
- const handleVoiceClick = () => {
- if (isRecording) {
- stopRecording();
- } else {
- startRecording();
- }
- };
-
- // Simple wrapper for workflow selection
- const handleWorkflowChange = (id: string | null) => {
- if (!id) {
- onWorkflowSelect(null);
- } else {
- const item = workflowItems?.find((w: any) => w.id === id);
- if (item) {
- onWorkflowSelect(item);
- }
- }
- };
-
- // Handle file upload click
- const handleFileClick = () => {
- fileInputRef.current?.click();
- };
-
- // Handle file change
- const handleFileChange = async (e: React.ChangeEvent) => {
- const files = e.target.files;
- if (files && hookData.handleFileUpload) {
- for (const file of Array.from(files)) {
- await hookData.handleFileUpload(file);
- }
- }
- // Reset input
- if (fileInputRef.current) {
- fileInputRef.current.value = '';
- }
- };
-
- // Render messages using the Messages component with document support
- const renderMessages = () => {
- if (!messages || messages.length === 0) {
- return (
-
-
-
Keine Nachrichten
-
- Starten Sie einen neuen Workflow oder wählen Sie einen bestehenden aus.
-
-
- );
- }
-
- return (
-
- );
- };
-
- // Render dashboard tree with rounds
- const renderDashboard = () => {
- // Check if we have rounds data
- const hasRounds = dashboardTree && dashboardTree.rounds && dashboardTree.rounds.size > 0;
- const hasOperations = dashboardTree && dashboardTree.rootOperations.length > 0;
-
- if (!hasRounds && !hasOperations) {
- return (
-
-
-
- Keine aktiven Operationen
-
-
- );
- }
-
- const renderOperation = (operationId: string, depth: number = 0, roundOperations?: Map) => {
- const operation = roundOperations?.get(operationId) || dashboardTree.operations.get(operationId);
- if (!operation) return null;
-
- const childOps = Array.from(dashboardTree.operations.entries())
- .filter(([_, op]) => op.parentId === operationId)
- .map(([id]) => id);
-
- return (
-
-
onToggleOperationExpanded(operationId)}
- style={{
- display: 'flex',
- alignItems: 'center',
- gap: '0.5rem',
- cursor: childOps.length > 0 ? 'pointer' : 'default',
- }}
- >
- {childOps.length > 0 && (
-
- ▶
-
- )}
-
- {operation.operationName || operationId.slice(0, 20)}
-
- {operation.latestProgress !== null && operation.latestProgress < 1 && (
-
- {Math.round(operation.latestProgress * 100)}%
-
- )}
- {operation.latestStatus && (
-
- {operation.latestStatus}
-
- )}
-
- {operation.expanded && childOps.length > 0 && (
-
- {childOps.map(childId => renderOperation(childId, depth + 1, roundOperations))}
-
- )}
-
- );
- };
-
- // If we have rounds, render them
- if (hasRounds) {
- const sortedRounds = Array.from(dashboardTree.rounds.entries()).sort((a, b) => a[0] - b[0]);
-
- return (
-
- {sortedRounds.map(([roundNumber, round]) => (
-
- {/* Round Header */}
-
onToggleRoundExpanded(roundNumber)}
- style={{
- display: 'flex',
- alignItems: 'center',
- gap: '0.5rem',
- padding: '0.5rem 0.75rem',
- background: roundNumber === currentRound
- ? 'var(--primary-bg, #eff6ff)'
- : 'var(--bg-secondary)',
- borderRadius: '6px',
- cursor: 'pointer',
- marginBottom: round.expanded ? '0.5rem' : '0',
- }}
- >
-
- ▶
-
-
- Runde {roundNumber}
-
- {round.isCompleted && (
-
- abgeschlossen
-
- )}
- {roundNumber === currentRound && !round.isCompleted && (
-
- aktiv
-
- )}
-
- {/* Round Operations */}
- {round.expanded && (
-
- {round.rootOperations.map(opId => renderOperation(opId, 0, round.operations))}
-
- )}
-
- ))}
-
- );
- }
-
- // Fallback: render without rounds (for backward compatibility)
- return (
-
- {dashboardTree.rootOperations.map(opId => renderOperation(opId))}
-
- );
- };
-
- // Permission check - also show while loading
- if (playgroundUIPermission === false) {
- return (
-
-
-
Kein Zugriff
-
- Sie haben keine Berechtigung für den Chat Playground.
-
-
-
- );
- }
-
- // Show loading state while permission is being checked (undefined)
- if (playgroundUIPermission === undefined) {
- return (
-
- );
- }
-
- return (
-
- {/* Hidden file input */}
-
-
- {/* Page Header */}
-
-
- {/* Main Content - Resizable Two-Column Layout with Drag & Drop */}
-
- {/* Drag overlay */}
- {isDragOver && (
-
-
-
-
Dateien hier ablegen
-
-
- )}
- {/* Left Panel - Chat Messages */}
-
-
-
-
-
- Nachrichten
-
-
-
- {renderMessages()}
-
-
-
-
- {/* Resize Divider */}
-
-
- {/* Right Panel - Dashboard */}
-
-
-
-
- Dashboard
-
-
-
- {renderDashboard()}
-
-
-
-
- {/* Input Footer */}
-
- {/* Prompts Selection Row */}
-
-
-
-
-
-
-
- {/* Pending files */}
- {pendingFiles && pendingFiles.length > 0 && (
-
- {pendingFiles.map((file: any) => (
-
-
- {file.fileName}
-
-
- ))}
-
- )}
-
- {/* Input row */}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Stop button - only visible when running */}
- {isRunning && (
-
- )}
- {/* Send button - always visible with dynamic text */}
-
-
-
-
-
-
-
- );
-};
-
-export default PlaygroundPage;
diff --git a/src/pages/workflows/WorkflowPages.module.css b/src/pages/workflows/WorkflowPages.module.css
deleted file mode 100644
index a667dc5..0000000
--- a/src/pages/workflows/WorkflowPages.module.css
+++ /dev/null
@@ -1,298 +0,0 @@
-/* WorkflowPages.module.css - Shared styles for workflow pages */
-
-.page {
- padding: 2rem;
- max-width: 1400px;
- margin: 0 auto;
-}
-
-.header {
- margin-bottom: 2rem;
-}
-
-.header h1 {
- font-size: 1.75rem;
- font-weight: 600;
- color: var(--color-text-primary, #1a1a2e);
- margin: 0 0 0.5rem 0;
-}
-
-.subtitle {
- color: var(--color-text-secondary, #6b7280);
- margin: 0;
-}
-
-.content {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.section {
- background: var(--color-surface, #ffffff);
- border: 1px solid var(--color-border, #e5e7eb);
- border-radius: 8px;
- padding: 1.5rem;
-}
-
-.section h2 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--color-text-primary, #1a1a2e);
- margin: 0 0 1rem 0;
-}
-
-/* Loading, Error, Empty states */
-.loading,
-.error,
-.empty {
- text-align: center;
- padding: 3rem;
- color: var(--color-text-secondary, #6b7280);
-}
-
-.error {
- color: var(--color-error, #dc2626);
-}
-
-/* Table styles */
-.tableContainer {
- overflow-x: auto;
-}
-
-.table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.875rem;
-}
-
-.table th,
-.table td {
- text-align: left;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--color-border, #e5e7eb);
-}
-
-.table th {
- font-weight: 600;
- color: var(--color-text-secondary, #6b7280);
- background: var(--color-surface-secondary, #f9fafb);
-}
-
-.table tbody tr:hover {
- background: var(--color-surface-hover, #f3f4f6);
-}
-
-/* Badge styles */
-.badge {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- font-size: 0.75rem;
- font-weight: 500;
- background: var(--color-surface-secondary, #f3f4f6);
- color: var(--color-text-secondary, #6b7280);
-}
-
-.badge.running,
-.badge.active {
- background: var(--color-info-bg, #dbeafe);
- color: var(--color-info, #2563eb);
-}
-
-.badge.completed {
- background: var(--color-success-bg, #dcfce7);
- color: var(--color-success, #16a34a);
-}
-
-.badge.error,
-.badge.failed {
- background: var(--color-error-bg, #fee2e2);
- color: var(--color-error, #dc2626);
-}
-
-.badge.stopped,
-.badge.pending {
- background: var(--color-warning-bg, #fef3c7);
- color: var(--color-warning, #d97706);
-}
-
-/* Button styles */
-.actions {
- display: flex;
- gap: 0.5rem;
-}
-
-.deleteButton,
-.executeButton,
-.submitButton,
-.stopButton,
-.toggleButton {
- padding: 0.375rem 0.75rem;
- border-radius: 4px;
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- border: 1px solid transparent;
- transition: all 0.2s;
-}
-
-.deleteButton {
- background: var(--color-error-bg, #fee2e2);
- color: var(--color-error, #dc2626);
- border-color: var(--color-error, #dc2626);
-}
-
-.deleteButton:hover:not(:disabled) {
- background: var(--color-error, #dc2626);
- color: white;
-}
-
-.executeButton {
- background: var(--color-info-bg, #dbeafe);
- color: var(--color-info, #2563eb);
- border-color: var(--color-info, #2563eb);
-}
-
-.executeButton:hover:not(:disabled) {
- background: var(--color-info, #2563eb);
- color: white;
-}
-
-.submitButton {
- background: var(--color-primary, #4f46e5);
- color: white;
-}
-
-.submitButton:hover:not(:disabled) {
- background: var(--color-primary-dark, #4338ca);
-}
-
-.stopButton {
- background: var(--color-error, #dc2626);
- color: white;
-}
-
-.stopButton:hover:not(:disabled) {
- background: var(--color-error-dark, #b91c1c);
-}
-
-.toggleButton {
- background: var(--color-surface-secondary, #f3f4f6);
- color: var(--color-text-secondary, #6b7280);
- border-color: var(--color-border, #e5e7eb);
-}
-
-.toggleButton.active {
- background: var(--color-success-bg, #dcfce7);
- color: var(--color-success, #16a34a);
- border-color: var(--color-success, #16a34a);
-}
-
-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* Form styles */
-.select {
- width: 100%;
- max-width: 400px;
- padding: 0.5rem 0.75rem;
- border: 1px solid var(--color-border, #e5e7eb);
- border-radius: 6px;
- background: var(--color-surface, #ffffff);
- font-size: 0.875rem;
-}
-
-.inputForm {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.textarea {
- width: 100%;
- padding: 0.75rem;
- border: 1px solid var(--color-border, #e5e7eb);
- border-radius: 6px;
- font-size: 0.875rem;
- resize: vertical;
- min-height: 100px;
-}
-
-.textarea:focus {
- outline: none;
- border-color: var(--color-primary, #4f46e5);
- box-shadow: 0 0 0 3px var(--color-primary-light, rgba(79, 70, 229, 0.1));
-}
-
-.buttonGroup {
- display: flex;
- gap: 0.5rem;
- justify-content: flex-end;
-}
-
-/* Messages display */
-.messagesContainer {
- max-height: 400px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.message {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
- padding: 0.75rem;
- background: var(--color-surface-secondary, #f9fafb);
- border-radius: 6px;
-}
-
-.messageRole {
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--color-text-secondary, #6b7280);
- text-transform: uppercase;
-}
-
-.messageContent {
- color: var(--color-text-primary, #1a1a2e);
- white-space: pre-wrap;
-}
-
-.emptyMessage {
- text-align: center;
- color: var(--color-text-secondary, #6b7280);
- padding: 2rem;
-}
-
-/* Log display */
-.logContainer {
- max-height: 200px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- font-family: monospace;
- font-size: 0.8rem;
-}
-
-.logEntry {
- display: flex;
- gap: 0.5rem;
- padding: 0.5rem;
- background: var(--color-surface-secondary, #f9fafb);
- border-radius: 4px;
-}
-
-.logStatus {
- font-weight: 600;
- color: var(--color-info, #2563eb);
-}
-
-.logMessage {
- color: var(--color-text-primary, #1a1a2e);
-}
diff --git a/src/pages/workflows/WorkflowsPage.tsx b/src/pages/workflows/WorkflowsPage.tsx
deleted file mode 100644
index e38d710..0000000
--- a/src/pages/workflows/WorkflowsPage.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * WorkflowsPage
- *
- * Page for viewing and managing workflows using FormGeneratorTable.
- * Follows the pattern established in AdminUsersPage.
- */
-
-import React, { useState, useMemo, useEffect } from 'react';
-import { useUserWorkflows, useWorkflowOperations, getWorkflowApiBaseUrl } from '../../hooks/useWorkflows';
-import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
-import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
-import { FaSync, FaList, FaPlay } from 'react-icons/fa';
-import { useNavigate } from 'react-router-dom';
-import { useCurrentInstance } from '../../hooks/useCurrentInstance';
-import styles from '../admin/Admin.module.css';
-
-interface Workflow {
- id: string;
- name?: string;
- status: string;
- workflowMode?: string;
- [key: string]: any;
-}
-
-export const WorkflowsPage: React.FC = () => {
- const navigate = useNavigate();
- const { instanceId, featureCode } = useCurrentInstance();
- const workflowOptions = instanceId && featureCode ? { instanceId, featureCode } : undefined;
- const apiBaseUrl = getWorkflowApiBaseUrl(instanceId, featureCode);
- const apiEndpoint = apiBaseUrl ? `${apiBaseUrl}/workflows` : '';
-
- // Data hook - pass instance context when in feature route
- const {
- data: workflows,
- attributes,
- permissions,
- pagination,
- loading,
- error,
- refetch,
- fetchWorkflowById,
- updateOptimistically,
- } = useUserWorkflows(workflowOptions);
-
- // Operations hook - pass instance context when in feature route
- const {
- handleWorkflowDelete,
- handleWorkflowDeleteMultiple,
- handleWorkflowUpdate,
- handleInlineUpdate,
- deletingWorkflows,
- } = useWorkflowOperations(workflowOptions);
-
- const [editingWorkflow, setEditingWorkflow] = useState(null);
-
- // Initial fetch on mount
- useEffect(() => {
- refetch();
- }, []);
-
- // Generate columns from attributes
- const columns = useMemo(() => {
- return (attributes || []).map(attr => ({
- key: attr.name,
- label: attr.label || attr.name,
- type: attr.type as any,
- sortable: attr.sortable !== false,
- filterable: attr.filterable !== false,
- searchable: attr.searchable !== false,
- width: attr.width || 150,
- minWidth: attr.minWidth || 100,
- maxWidth: attr.maxWidth || 400,
- fkSource: (attr as any).fkSource,
- fkDisplayField: (attr as any).fkDisplayField,
- }));
- }, [attributes]);
-
- // Check permissions
- const canUpdate = permissions?.update !== 'n';
- const canDelete = permissions?.delete !== 'n';
-
- // Handle edit click - fetch full workflow data
- const handleEditClick = async (workflow: Workflow) => {
- const fullWorkflow = await fetchWorkflowById(workflow.id);
- if (fullWorkflow) {
- setEditingWorkflow(fullWorkflow as Workflow);
- }
- };
-
- // Handle continue workflow - navigate to playground within same feature instance
- // Uses relative navigation since WorkflowsPage is rendered under same instance route as playground
- const handleContinueWorkflow = (workflow: Workflow) => {
- // Navigate relatively to playground (sibling route under same instance)
- navigate(`../playground?workflowId=${workflow.id}`);
- };
-
- // Handle edit submit
- const handleEditSubmit = async (data: Partial) => {
- if (!editingWorkflow) return;
- const result = await handleWorkflowUpdate(editingWorkflow.id, data);
- if (result.success) {
- setEditingWorkflow(null);
- refetch();
- }
- };
-
- // Handle delete single workflow (confirmation handled by DeleteActionButton)
- const handleDelete = async (workflow: Workflow) => {
- const success = await handleWorkflowDelete(workflow.id);
- if (success) {
- refetch();
- }
- };
-
- // Handle delete multiple workflows (confirmation handled by FormGenerator)
- const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
- const ids = workflowsToDelete.map(w => w.id);
- const success = await handleWorkflowDeleteMultiple(ids);
- if (success) {
- refetch();
- }
- };
-
- // Form attributes for edit modal - filter out non-editable fields
- const formAttributes = useMemo(() => {
- const excludedFields = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt'];
- return (attributes || [])
- .filter(attr => !excludedFields.includes(attr.name));
- }, [attributes]);
-
- if (error) {
- return (
-
-
-
⚠️
-
Fehler beim Laden der Workflows: {error}
-
-
-
- );
- }
-
- return (
-
-
-
-
Workflows
-
Übersicht aller Workflows
-
-
-
-
-
-
-
- {loading && (!workflows || workflows.length === 0) ? (
-
- ) : !workflows || workflows.length === 0 ? (
-
-
-
Keine Workflows vorhanden
-
- Starten Sie einen neuen Workflow im Chat Playground.
-
-
- ) : (
-
deletingWorkflows.has(row.id),
- }] : []),
- ]}
- customActions={[
- {
- id: 'continue',
- icon: ,
- onClick: handleContinueWorkflow,
- title: 'Workflow fortsetzen',
- }
- ]}
- onDelete={handleDelete}
- onDeleteMultiple={handleDeleteMultiple}
- hookData={{
- refetch,
- permissions,
- pagination,
- handleDelete: handleWorkflowDelete,
- handleInlineUpdate,
- updateOptimistically,
- }}
- emptyMessage="Keine Workflows gefunden"
- />
- )}
-
-
- {/* Edit Modal */}
- {editingWorkflow && (
-
setEditingWorkflow(null)}>
-
e.stopPropagation()}>
-
-
Workflow bearbeiten
-
-
-
- {formAttributes.length === 0 ? (
-
- ) : (
-
setEditingWorkflow(null)}
- submitButtonText="Speichern"
- cancelButtonText="Abbrechen"
- />
- )}
-
-
-
- )}
-
- );
-};
-
-export default WorkflowsPage;
diff --git a/src/pages/workflows/index.ts b/src/pages/workflows/index.ts
deleted file mode 100644
index 43bc009..0000000
--- a/src/pages/workflows/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { PlaygroundPage } from './PlaygroundPage';
-export { WorkflowsPage } from './WorkflowsPage';
-
diff --git a/src/types/mandate.ts b/src/types/mandate.ts
index a9128cb..9b38cb9 100644
--- a/src/types/mandate.ts
+++ b/src/types/mandate.ts
@@ -241,24 +241,6 @@ export const FEATURE_REGISTRY: Record = {
{ code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true },
]
},
- chatplayground: {
- code: 'chatplayground',
- label: { de: 'Chat Playground', en: 'Chat Playground' },
- icon: 'message',
- views: [
- { code: 'playground', label: { de: 'Playground', en: 'Playground' }, path: 'playground' },
- { code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
- ]
- },
- codeeditor: {
- code: 'codeeditor',
- label: { de: 'Code Editor', en: 'Code Editor' },
- icon: 'description',
- views: [
- { code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' },
- { code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
- ]
- },
teamsbot: {
code: 'teamsbot',
label: { de: 'Teams Bot', en: 'Teams Bot' },
@@ -307,6 +289,7 @@ export const FEATURE_REGISTRY: Record = {
icon: 'psychology',
views: [
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' },
+ { code: 'editor', label: { de: 'Editor', en: 'Editor', fr: 'Editeur' }, path: 'editor' },
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Parametres' }, path: 'settings' },
]
},
diff --git a/src/utils/sseClient.ts b/src/utils/sseClient.ts
index b3dfc01..8223918 100644
--- a/src/utils/sseClient.ts
+++ b/src/utils/sseClient.ts
@@ -2,8 +2,7 @@
* Shared SSE Client Utility
*
* Generic fetch-based SSE streaming for POST requests with JSON body.
- * Extracted from useCodeEditor.ts and chatbotApi.ts to provide a single
- * reusable SSE implementation across all workspace features.
+ * Reusable SSE implementation across all workspace features.
*/
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './csrfUtils';
@@ -19,6 +18,8 @@ export interface SseEventHandlers {
onStatus?: (event: SseEvent) => void;
onFileEditProposal?: (event: SseEvent) => void;
onFileVersion?: (event: SseEvent) => void;
+ onFileEditRejected?: (event: SseEvent) => void;
+ onFileUpdated?: (event: SseEvent) => void;
onToolCall?: (event: SseEvent) => void;
onToolResult?: (event: SseEvent) => void;
onAgentProgress?: (event: SseEvent) => void;
@@ -50,6 +51,10 @@ const _EVENT_ROUTER: Record = {
fileEditProposal: 'onFileEditProposal',
file_version: 'onFileVersion',
fileVersion: 'onFileVersion',
+ file_edit_rejected: 'onFileEditRejected',
+ fileEditRejected: 'onFileEditRejected',
+ file_updated: 'onFileUpdated',
+ fileUpdated: 'onFileUpdated',
toolCall: 'onToolCall',
toolResult: 'onToolResult',
agent_progress: 'onAgentProgress',