e.stopPropagation()}>
Neue Automatisierung
- setShowCreateModal(false)}
- >
- ✕
+ setShowCreateModal(false)}>
+
@@ -299,11 +599,8 @@ export const AutomationsPage: React.FC = () => {
e.stopPropagation()}>
Automatisierung bearbeiten
- setEditingAutomation(null)}
- >
- ✕
+ setEditingAutomation(null)}>
+
@@ -327,6 +624,166 @@ export const AutomationsPage: React.FC = () => {
)}
+
+ {/* Template Selection Modal */}
+ {showTemplateModal && (
+
setShowTemplateModal(false)}>
+
e.stopPropagation()}>
+
+
Vorlage auswählen
+ setShowTemplateModal(false)}>
+
+
+
+
+
+ {templates.map((template, index) => (
+
+
+
+ {template.template?.overview || `Vorlage ${index + 1}`}
+
+
+
+ {template.template?.tasks?.[0]?.description ||
+ template.template?.tasks?.[0]?.objective ||
+ 'Keine Beschreibung'}
+
+
handleTemplateSelect(template)}
+ >
+ Verwenden
+
+
+ ))}
+
+
+
+ setShowTemplateModal(false)}>
+ Abbrechen
+
+
+
+
+ )}
+
+ {/* Execution Modal */}
+ {executionModal.visible && (
+
+
e.stopPropagation()} style={{ maxWidth: '700px' }}>
+
+
+ {getStatusIcon(executionModal.status)} Ausführung: {executionModal.automationLabel}
+
+
+
+
+
+
+
+
+ {executionModal.status === 'starting' && 'Wird gestartet...'}
+ {executionModal.status === 'running' && 'Läuft...'}
+ {executionModal.status === 'completed' && 'Abgeschlossen'}
+ {executionModal.status === 'stopped' && 'Gestoppt'}
+ {executionModal.status === 'error' && 'Fehler'}
+
+ {executionModal.workflowId && (
+
+ Workflow: {executionModal.workflowId}
+
+ )}
+
+
+ {executionModal.logs.map((log, index) => (
+
+ [{formatTime(log.timestamp)}]
+ {log.status && {log.status}: }
+ {log.message}
+ {log.progress !== undefined && log.progress !== null && log.progress < 1 && (
+ ({Math.round(log.progress * 100)}%)
+ )}
+
+ ))}
+
+
+
+ {executionModal.status === 'running' && (
+
+ Stoppen
+
+ )}
+
+ Schliessen
+
+
+
+
+ )}
+
+ {/* Logs History Modal */}
+ {logsModal.visible && logsModal.automation && (
+
setLogsModal({ visible: false, automation: null })}>
+
e.stopPropagation()} style={{ maxWidth: '700px' }}>
+
+
+ Ausführungsverlauf: {logsModal.automation.label}
+
+ setLogsModal({ visible: false, automation: null })}
+ >
+
+
+
+
+ {(!logsModal.automation.executionLogs || logsModal.automation.executionLogs.length === 0) ? (
+
+
Keine Ausführungen vorhanden
+
+ ) : (
+
+ {[...logsModal.automation.executionLogs].reverse().map((log, index) => (
+
+
+ {formatTimestamp(log.timestamp)}
+
+ {log.status || 'Unbekannt'}
+
+ {log.workflowId && (
+
+ Workflow: {log.workflowId}
+
+ )}
+
+ {log.messages && log.messages.length > 0 && (
+
+ {log.messages.map((msg, msgIndex) => (
+
{msg}
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ setLogsModal({ visible: false, automation: null })}
+ >
+ Schliessen
+
+
+
+
+ )}
);
};
diff --git a/src/pages/workflows/PlaygroundPage.module.css b/src/pages/workflows/PlaygroundPage.module.css
index 418c9ef..cbb271b 100644
--- a/src/pages/workflows/PlaygroundPage.module.css
+++ b/src/pages/workflows/PlaygroundPage.module.css
@@ -491,3 +491,96 @@
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
index 3fbe661..5c45d3b 100644
--- a/src/pages/workflows/PlaygroundPage.tsx
+++ b/src/pages/workflows/PlaygroundPage.tsx
@@ -3,27 +3,41 @@
*
* 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 } from 'react';
+import React, { useRef, useState, useEffect, useCallback } from 'react';
+import { useSearchParams } from 'react-router-dom';
import { useDashboardInputForm } from '../../hooks/usePlayground';
import { useUserWorkflows } from '../../hooks/useWorkflows';
import { useResizablePanels } from '../../hooks/useResizablePanels';
-import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus } from 'react-icons/fa';
+import { usePrompts } from '../../hooks/usePrompts';
+import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
+import { useToast } from '../../contexts/ToastContext';
+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');
+
// Main hook for input form and data
const hookData = useDashboardInputForm();
const {
inputValue,
onInputChange,
isRunning,
+ isStopping,
handleSubmit,
+ handleStop,
isSubmitting,
+ workflowStatus,
messages,
dashboardTree,
onToggleOperationExpanded,
+ onToggleRoundExpanded,
+ currentRound,
workflowId,
onWorkflowSelect,
workflowItems,
@@ -34,6 +48,8 @@ export const PlaygroundPage: React.FC = () => {
} = hookData;
const { data: workflows } = useUserWorkflows();
+ const { prompts, refetch: refetchPrompts } = usePrompts();
+ const { showError, showSuccess } = useToast();
// Resizable panels hook
const {
@@ -51,6 +67,214 @@ export const PlaygroundPage: React.FC = () => {
// 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);
+ const [audioChunks, setAudioChunks] = useState([]);
+
+ // 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]);
+
+ // Format bytes helper
+ const formatBytes = (bytes: number): string => {
+ if (!bytes || bytes < 0) return '0 B';
+ if (bytes < 1024) return `${bytes} B`;
+ const kbytes = bytes / 1024;
+ if (kbytes < 1000) return `${Math.round(kbytes)} kB`;
+ const mbytes = kbytes / 1024;
+ return `${Math.round(mbytes * 10) / 10} MB`;
+ };
+
+ // Format duration helper (for stats)
+ const formatDuration = (seconds: number): string => {
+ if (!seconds || seconds < 0) return '0s';
+ if (seconds < 60) return `${Math.round(seconds)}s`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.round(seconds % 60);
+ return `${minutes}m ${remainingSeconds}s`;
+ };
+
+ // 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);
+ setAudioChunks([]);
+ 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('file', audioBlob, 'voice_recording.webm');
+ formData.append('language', 'de-DE');
+
+ // Call speech-to-text API
+ const response = await api.post('/api/ai/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) {
@@ -144,9 +368,13 @@ export const PlaygroundPage: React.FC = () => {
);
};
- // Render dashboard tree
+ // Render dashboard tree with rounds
const renderDashboard = () => {
- if (!dashboardTree || dashboardTree.rootOperations.length === 0) {
+ // 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 (
@@ -157,8 +385,8 @@ export const PlaygroundPage: React.FC = () => {
);
}
- const renderOperation = (operationId: string, depth: number = 0) => {
- const operation = dashboardTree.operations.get(operationId);
+ 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())
@@ -202,23 +430,31 @@ export const PlaygroundPage: React.FC = () => {
}}>
{operation.operationName || operationId.slice(0, 20)}
+ {operation.latestProgress !== null && operation.latestProgress < 1 && (
+
+ {Math.round(operation.latestProgress * 100)}%
+
+ )}
{operation.latestStatus && (
@@ -228,13 +464,93 @@ export const PlaygroundPage: React.FC = () => {
{operation.expanded && childOps.length > 0 && (
- {childOps.map(childId => renderOperation(childId, depth + 1))}
+ {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))}
@@ -242,8 +558,11 @@ export const PlaygroundPage: React.FC = () => {
);
};
- // Permission check
- if (!playgroundUIPermission) {
+ // Debug: Log permission status
+ console.log('🔐 PlaygroundPage permission check:', { playgroundUIPermission });
+
+ // Permission check - also show while loading
+ if (playgroundUIPermission === false) {
return (
@@ -255,6 +574,17 @@ export const PlaygroundPage: React.FC = () => {
);
}
+
+ // Show loading state while permission is being checked (undefined)
+ if (playgroundUIPermission === undefined) {
+ return (
+
+ );
+ }
return (
@@ -290,11 +620,24 @@ export const PlaygroundPage: React.FC = () => {
- {/* Main Content - Resizable Two-Column Layout */}
+ {/* Main Content - Resizable Two-Column Layout with Drag & Drop */}
+ {/* Drag overlay */}
+ {isDragOver && (
+
+
+
+
Dateien hier ablegen
+
+
+ )}
{/* Left Panel - Chat Messages */}
{
{/* Input Footer */}
-
+
+ {/* Prompts Selection Row */}
+
+
+
+ handlePromptSelect(e.target.value)}
+ disabled={isRunning}
+ >
+ Prompt-Vorlage wählen...
+ {prompts?.map((prompt: any) => (
+
+ {prompt.name || prompt.content?.substring(0, 50) + '...'}
+
+ ))}
+
+
+
+
{/* Pending files */}
{pendingFiles && pendingFiles.length > 0 && (
@@ -360,21 +729,29 @@ export const PlaygroundPage: React.FC = () => {
)}
{/* Stats bar */}
- {latestStats && (
+ {latestStats && (latestStats.bytesSent || latestStats.bytesReceived || latestStats.processingTime || latestStats.priceUsd) && (
- {latestStats.promptTokens !== undefined && (
+ {(latestStats.bytesSent !== undefined || latestStats.bytesReceived !== undefined) && (
- Tokens:
+ Daten:
- {latestStats.promptTokens + (latestStats.completionTokens || 0)}
+ {formatBytes(latestStats.bytesSent || 0)} / {formatBytes(latestStats.bytesReceived || 0)}
)}
- {latestStats.totalCost !== undefined && (
+ {latestStats.processingTime !== undefined && latestStats.processingTime > 0 && (
+
+ Zeit:
+
+ {formatDuration(latestStats.processingTime)}
+
+
+ )}
+ {latestStats.priceUsd !== undefined && latestStats.priceUsd > 0 && (
Kosten:
- ${latestStats.totalCost.toFixed(4)}
+ ${latestStats.priceUsd.toFixed(4)}
)}
@@ -389,8 +766,20 @@ export const PlaygroundPage: React.FC = () => {
className={styles.inputTextarea}
value={inputValue}
onChange={(e) => onInputChange(e.target.value)}
- placeholder="Geben Sie Ihre Nachricht ein..."
- disabled={isRunning && !workflowId}
+ placeholder={
+ isRunning
+ ? "Workflow läuft. Neue Eingabe zum Unterbrechen und Neustarten..."
+ : workflowStatus === 'completed'
+ ? "Workflow abgeschlossen. Neue Eingabe zum Fortsetzen..."
+ : workflowStatus === 'failed'
+ ? "Workflow fehlgeschlagen. Neue Eingabe zum Wiederholen..."
+ : workflowStatus === 'stopped'
+ ? "Workflow gestoppt. Neue Eingabe zum Fortfahren..."
+ : !workflowId
+ ? "Geben Sie einen Prompt ein, um zu starten..."
+ : "Geben Sie Ihre Nachricht ein oder ziehen Sie Dateien hierher..."
+ }
+ disabled={false}
rows={3}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -405,34 +794,51 @@ export const PlaygroundPage: React.FC = () => {
+
+ {isRecording ? : }
+
- {isRunning ? (
+ {/* Stop button - only visible when running */}
+ {isRunning && (
- {isSubmitting ? 'Stoppt...' : 'Stoppen'}
-
- ) : (
-
-
- {isSubmitting ? 'Senden...' : 'Senden'}
+ {isStopping ? 'Stoppt...' : 'Stop'}
)}
+ {/* Send button - always visible with dynamic text */}
+
+
+ {isSubmitting
+ ? 'Senden...'
+ : isRunning
+ ? 'Neue Eingabe'
+ : !workflowId
+ ? 'Starten'
+ : 'Senden'
+ }
+
diff --git a/src/pages/workflows/WorkflowsPage.tsx b/src/pages/workflows/WorkflowsPage.tsx
index 66e4608..dff712b 100644
--- a/src/pages/workflows/WorkflowsPage.tsx
+++ b/src/pages/workflows/WorkflowsPage.tsx
@@ -5,7 +5,7 @@
* Follows the pattern established in AdminUsersPage.
*/
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
import { useUserWorkflows, useWorkflowOperations } from '../../hooks/useWorkflows';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
@@ -49,6 +49,11 @@ export const WorkflowsPage: React.FC = () => {
const [editingWorkflow, setEditingWorkflow] = useState
(null);
+ // Initial fetch on mount
+ useEffect(() => {
+ refetch();
+ }, []);
+
// Generate columns from attributes
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
@@ -80,7 +85,7 @@ export const WorkflowsPage: React.FC = () => {
// Handle continue workflow - navigate to playground
const handleContinueWorkflow = (workflow: Workflow) => {
- navigate(`/playground?workflowId=${workflow.id}`);
+ navigate(`/workflows/playground?workflowId=${workflow.id}`);
};
// Handle edit submit