added functionality for workflow selection across platform

This commit is contained in:
Ida Dittrich 2025-08-20 12:21:54 +02:00
parent 22d582f72b
commit e616541ee1
9 changed files with 492 additions and 112 deletions

View file

@ -1,8 +1,7 @@
import React, { useState } from "react";
import React from "react";
import { Prompt } from "../../../hooks/usePrompts";
import { useLanguage } from '../../../contexts/LanguageContext';
import DashboardChatArea from './DashboardChatArea';
import DashboardChatArea from './DashboardChatArea.tsx';
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
@ -13,7 +12,7 @@ interface DashboardChatProps {
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onWorkflowCompletedChange?: (completed: boolean) => void;
onWorkflowResume?: (workflowId: string) => void;
currentWorkflowId?: string | null;
}
const DashboardChat: React.FC<DashboardChatProps> = ({
@ -21,23 +20,11 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
onWorkflowResume
currentWorkflowId
}) => {
const { t } = useLanguage();
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
const handleWorkflowResume = (workflowId: string) => {
// Switch to Chat Area tab first
setResumeWorkflowId(workflowId);
// Then call the parent's resume handler
if (onWorkflowResume) {
onWorkflowResume(workflowId);
}
// Clear the resume ID after a short delay to allow processing
setTimeout(() => {
setResumeWorkflowId(null);
}, 100);
};
// Pass the current workflow ID directly to the chat area
// This handles both dropdown selection and workflow resume scenarios
const effectiveWorkflowId = currentWorkflowId;
return (
<div className={styles.dashboard_chat}>
@ -46,7 +33,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
onPromptUsed={onPromptUsed}
onWorkflowIdChange={onWorkflowIdChange}
onWorkflowCompletedChange={onWorkflowCompletedChange}
resumeWorkflowId={resumeWorkflowId}
resumeWorkflowId={effectiveWorkflowId}
/>
</div>
);

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useRef } from "react";
import MessageList from "./DashboardChatAreaMessageList";
import FilePreview from "./DashboardChatAreaFilePreview";
import InputArea from "./DashboardChatAreaInput";
@ -20,14 +20,23 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
const [selectedFile, setSelectedFile] = useState<any>(null);
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
// Track the last resumeWorkflowId to prevent feedback loops
const lastResumeWorkflowIdRef = useRef<string | null>(resumeWorkflowId);
// Centralized workflow management
const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId);
// Notify parent when workflow ID changes
// Only notify parent when a NEW workflow is created internally (not when selecting existing ones)
// For workflow selection via dropdown, the Dashboard already manages the currentWorkflowId
React.useEffect(() => {
if (onWorkflowIdChange && workflowState.currentWorkflowId !== resumeWorkflowId) {
// Only notify when a workflow is created from scratch (currentWorkflowId exists but resumeWorkflowId was null)
if (onWorkflowIdChange && workflowState.currentWorkflowId && !resumeWorkflowId && !lastResumeWorkflowIdRef.current) {
console.log(`🔔 Notifying parent of NEW workflow created: ${workflowState.currentWorkflowId}`);
onWorkflowIdChange(workflowState.currentWorkflowId);
}
// Update the ref to track the current resumeWorkflowId
lastResumeWorkflowIdRef.current = resumeWorkflowId;
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
// Notify parent when workflow is completed
@ -38,13 +47,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
}
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
// Auto-load workflow when resumeWorkflowId changes externally
React.useEffect(() => {
if (resumeWorkflowId && resumeWorkflowId !== workflowState.currentWorkflowId) {
console.log(`🔄 Loading workflow from external prop: ${resumeWorkflowId}`);
workflowActions.loadWorkflow(resumeWorkflowId);
}
}, [resumeWorkflowId, workflowState.currentWorkflowId, workflowActions]);
// Note: useWorkflowManager handles resumeWorkflowId changes automatically
// No resizing functionality needed

View file

@ -39,7 +39,9 @@ const InputArea: React.FC<InputAreaProps> = ({
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const [isFocused, setIsFocused] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const dropZoneRef = useRef<HTMLDivElement>(null);
// Always use external attached files from parent component
const currentAttachedFiles = externalAttachedFiles;
@ -119,6 +121,25 @@ const InputArea: React.FC<InputAreaProps> = ({
}
};
const handleStop = async () => {
setIsSending(true);
setSendError(null);
try {
console.log('🛑 Stopping workflow...');
const success = await workflowActions.stopWorkflow();
if (!success) {
setSendError('Failed to stop workflow. Please try again.');
}
} catch (error: any) {
console.error('Failed to stop workflow:', error);
setSendError(error.message || 'Failed to stop workflow. Please try again.');
} finally {
setIsSending(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@ -133,10 +154,73 @@ const InputArea: React.FC<InputAreaProps> = ({
}
};
// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes('Files')) {
setIsDragOver(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only hide drag over if we're leaving the drop zone entirely
if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Set the drop effect to copy
e.dataTransfer.dropEffect = 'copy';
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (shouldShowStopButton) {
// Don't allow file drops when workflow is active
return;
}
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// Convert File objects to AttachedFile format
const attachedFiles: AttachedFile[] = files.map((file, index) => ({
id: Date.now() + index, // Simple ID generation
name: file.name,
size: file.size,
type: file.type,
fileData: file,
objectUrl: URL.createObjectURL(file)
}));
// Add to existing attached files
const updatedFiles = [...currentAttachedFiles, ...attachedFiles];
if (onAttachedFilesChange) {
onAttachedFilesChange(updatedFiles);
}
}
};
// Check if workflow is in an active state where we should show stop button
const isWorkflowActive = workflowState.workflow &&
['running', 'processing', 'started'].includes(workflowState.workflow.status);
// Use polling as an indicator that workflow might be active
const shouldShowStopButton = isWorkflowActive || (workflowState.isPolling && workflowState.currentWorkflowId);
// Determine if label should be in focused/moved state
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
@ -147,7 +231,14 @@ const InputArea: React.FC<InputAreaProps> = ({
: t('chat.input.enter_message');
return (
<div className={styles.input_area_container}>
<div
ref={dropZoneRef}
className={`${styles.input_area_container} ${isDragOver ? styles.drag_over : ''}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* Error messages */}
{(sendError || workflowState.error) && (
@ -156,6 +247,23 @@ const InputArea: React.FC<InputAreaProps> = ({
</div>
)}
{/* Drag and drop overlay */}
{isDragOver && (
<div className={`${styles.drag_overlay} ${shouldShowStopButton ? styles.disabled : ''}`}>
<div className={styles.drag_overlay_content}>
<div className={styles.drag_icon}>
{shouldShowStopButton ? '🚫' : '📁'}
</div>
<div className={styles.drag_text}>
{shouldShowStopButton
? t('chat.input.drop_disabled')
: t('chat.input.drop_files_here')
}
</div>
</div>
</div>
)}
{/* Show attached files count */}
{currentAttachedFiles.length > 0 && (
<div className={styles.attached_files_count}>
@ -175,14 +283,13 @@ const InputArea: React.FC<InputAreaProps> = ({
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
// Trigger resize on next frame to ensure DOM is updated
setTimeout(adjustTextareaHeight, 0);
}}
onKeyPress={handleKeyPress}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder=""
disabled={isSending || isWorkflowActive}
disabled={isSending || shouldShowStopButton}
className={styles.message_textarea}
rows={4}
/>
@ -191,27 +298,40 @@ const InputArea: React.FC<InputAreaProps> = ({
<div className={styles.input_actions_row}>
<button
onClick={() => setShowFilePopup(true)}
disabled={isSending || isWorkflowActive}
className={sharedStyles.button_secondary}
disabled={isSending || shouldShowStopButton}
className={`${sharedStyles.button_secondary} ${
(isSending || shouldShowStopButton) ? styles.disabled : ''
}`}
>
{t('chat.input.attach_files')}
</button>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isSending || isWorkflowActive}
className={`${sharedStyles.button_primary} ${
(!inputValue.trim() || isSending || isWorkflowActive)
? styles.disabled
: styles.enabled
}`}
>
{isSending ? t('chat.input.sending') :
isWorkflowActive ? t('chat.input.processing') :
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
</button>
{shouldShowStopButton ? (
<button
onClick={handleStop}
disabled={isSending}
className={`${sharedStyles.button_primary} ${
isSending ? styles.disabled : styles.enabled
}`}
>
{isSending ? t('chat.input.stopping') : t('chat.input.stop')}
</button>
) : (
<button
onClick={handleSend}
disabled={!inputValue.trim() || isSending}
className={`${sharedStyles.button_primary} ${
(!inputValue.trim() || isSending)
? styles.disabled
: styles.enabled
}`}
>
{isSending ? t('chat.input.sending') :
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
</button>
)}
{workflowState.currentWorkflowId && !isWorkflowActive && (
{workflowState.currentWorkflowId && !shouldShowStopButton && (
<button
onClick={() => workflowActions.clearWorkflow()}
className={styles.new_chat_button}

View file

@ -135,3 +135,55 @@
font-size: 12px;
color: var(--color-gray);
}
/* Drag and Drop Styles */
.input_area_container {
position: relative;
}
.drag_over {
border: 2px dashed var(--color-primary);
background-color: rgba(var(--color-primary-rgb), 0.05);
border-radius: 8px;
}
.drag_overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(var(--color-primary-rgb), 0.1);
backdrop-filter: blur(2px);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
border: 2px dashed var(--color-primary);
}
.drag_overlay_content {
text-align: center;
color: var(--color-primary);
font-weight: 500;
}
.drag_icon {
font-size: 48px;
margin-bottom: 12px;
}
.drag_text {
font-size: 16px;
font-weight: 500;
}
.drag_overlay.disabled {
background-color: rgba(255, 0, 0, 0.1);
border-color: #ff6b6b;
}
.drag_overlay.disabled .drag_overlay_content {
color: #ff6b6b;
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useWorkflow, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
export interface WorkflowManagerState {
currentWorkflowId: string | null;
@ -14,25 +14,34 @@ export interface WorkflowManagerActions {
loadWorkflow: (workflowId: string) => Promise<void>;
startNewWorkflow: (prompt: string, fileIds: number[]) => Promise<string | null>;
continueWorkflow: (prompt: string, fileIds: number[]) => Promise<boolean>;
stopWorkflow: () => Promise<boolean>;
clearWorkflow: () => void;
refreshMessages: () => Promise<void>;
setPolling: (enabled: boolean) => void;
}
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] {
console.log('🏭 useWorkflowManager called with initialWorkflowId:', initialWorkflowId);
// Core state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
const [isPolling, setIsPolling] = useState(false);
const pollingIntervalRef = useRef<number | null>(null);
// Hook-based data fetching
const { workflow, loading: workflowLoading, error: workflowError, refetch: refetchWorkflow } = useWorkflow(currentWorkflowId);
const { workflow, loading: workflowLoading, error: workflowError } = useWorkflow(currentWorkflowId);
const { status: workflowStatus, loading: statusLoading, error: statusError, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
const { startWorkflow } = useWorkflowOperations();
const { startWorkflow, stopWorkflow: stopWorkflowRequest } = useWorkflowOperations();
// Use status for real-time updates, fallback to workflow for initial data
const currentWorkflow = workflowStatus || workflow;
// Combined loading and error states
const isLoading = workflowLoading || messagesLoading;
const error = workflowError || messagesError;
const isLoading = workflowLoading || statusLoading || messagesLoading;
const error = workflowError || statusError || messagesError;
// Auto-polling for active workflows and message updates
useEffect(() => {
@ -40,15 +49,16 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
pollingIntervalRef.current = window.setInterval(() => {
console.log('🔄 Auto-polling workflow and messages...');
// Always poll for messages when workflow is active
refetchMessages();
console.log('🔄 Auto-polling status...');
// Always poll for status to detect workflow state changes
refetchStatus();
// Also poll workflow status if we have workflow data
if (workflow) {
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
// Only poll messages if workflow is running
if (currentWorkflow) {
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
if (isActive) {
refetchWorkflow();
console.log('🔄 Workflow is active, also polling messages...');
refetchMessages();
}
}
}, 2000); // Poll every 2 seconds for smoother updates
@ -61,7 +71,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
pollingIntervalRef.current = null;
}
};
}, [isPolling, currentWorkflowId, workflow?.status, refetchWorkflow, refetchMessages]);
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages]);
// Actions
const loadWorkflow = useCallback(async (workflowId: string) => {
@ -129,6 +139,32 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
}
}, [currentWorkflowId, startWorkflow, refetchMessages]);
const stopWorkflow = useCallback(async (): Promise<boolean> => {
if (!currentWorkflowId) {
console.error('❌ Cannot stop workflow: no current workflow ID');
return false;
}
console.log(`🛑 Stopping workflow ${currentWorkflowId}`);
const success = await stopWorkflowRequest(currentWorkflowId);
if (success) {
console.log(`✅ Workflow ${currentWorkflowId} stopped`);
setIsPolling(false); // Stop polling immediately
// Refresh workflow status to get the updated status
setTimeout(() => {
refetchStatus();
}, 500);
return true;
} else {
console.error('❌ Failed to stop workflow');
return false;
}
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus]);
const clearWorkflow = useCallback(() => {
console.log('🧹 Clearing workflow');
setCurrentWorkflowId(null);
@ -145,50 +181,57 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
setIsPolling(enabled);
}, []);
// Initialize workflow on mount if provided
// Sync with external workflow ID changes
useEffect(() => {
if (initialWorkflowId && initialWorkflowId !== currentWorkflowId) {
loadWorkflow(initialWorkflowId);
}
}, [initialWorkflowId, currentWorkflowId, loadWorkflow]);
// Auto-enable polling when workflow becomes active, keep polling until completed
useEffect(() => {
if (currentWorkflowId && workflow) {
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
const isCompleted = ['completed', 'failed', 'stopped'].includes(workflow.status);
if (isActive) {
console.log(`🟢 Workflow ${currentWorkflowId} is active (${workflow.status}), enabling polling`);
setIsPolling(true);
} else if (isCompleted) {
console.log(`🔴 Workflow ${currentWorkflowId} is completed (${workflow.status}), disabling polling`);
if (initialWorkflowId !== currentWorkflowId) {
if (initialWorkflowId) {
console.log(`🔄 useWorkflowManager: Loading workflow ${initialWorkflowId}`);
loadWorkflow(initialWorkflowId);
} else {
console.log(`🧹 useWorkflowManager: Clearing workflow due to null initialWorkflowId`);
setCurrentWorkflowId(null);
setIsPolling(false);
}
} else if (currentWorkflowId && !workflow) {
// If we have a workflow ID but no workflow data yet, start polling to get updates
console.log(`⏳ Workflow ${currentWorkflowId} loaded, starting polling for updates`);
setIsPolling(true);
}
}, [currentWorkflowId, workflow?.status]);
}, [initialWorkflowId]);
// Auto-enable polling only for active workflows
useEffect(() => {
if (currentWorkflowId && currentWorkflow) {
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
if (isActive) {
console.log(`🟢 Workflow ${currentWorkflowId} is active (${currentWorkflow.status}), enabling polling`);
setIsPolling(true);
} else {
console.log(`🔴 Workflow ${currentWorkflowId} is inactive/completed (${currentWorkflow.status}), disabling polling`);
setIsPolling(false);
}
} else {
// No workflow ID or no workflow data, stop polling
console.log(`🛑 No workflow or workflow data, disabling polling`);
setIsPolling(false);
}
}, [currentWorkflowId, currentWorkflow?.status]);
const state: WorkflowManagerState = {
currentWorkflowId,
workflow,
workflow: currentWorkflow,
messages,
isLoading,
error,
isPolling
};
const actions: WorkflowManagerActions = {
const actions: WorkflowManagerActions = useMemo(() => ({
loadWorkflow,
startNewWorkflow,
continueWorkflow,
stopWorkflow,
clearWorkflow,
refreshMessages,
setPolling: setPollingEnabled
};
}), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, refreshMessages, setPollingEnabled]);
return [state, actions];
}

View file

@ -57,6 +57,11 @@ export default {
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
'dashboard.log.level.info': 'INFO',
'dashboard.workflow_dropdown.loading': 'Laden...',
'dashboard.workflow_dropdown.error': 'Fehler',
'dashboard.workflow_dropdown.select_workflow': 'Workflow auswählen',
'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows',
'dashboard.workflow_dropdown.no_workflows': 'Keine Workflows verfügbar',
// Prompt Set
'promptset.loading': 'Prompts werden geladen...',
@ -182,6 +187,10 @@ export default {
'chat.input.processing': 'Wird verarbeitet...',
'chat.input.continue': 'Fortsetzen',
'chat.input.send': 'Senden',
'chat.input.stop': 'Stoppen',
'chat.input.stopping': 'Wird gestoppt...',
'chat.input.drop_files_here': 'Dateien hier ablegen zum Anhängen',
'chat.input.drop_disabled': 'Datei-Ablage während Workflow deaktiviert',
'chat.input.new_chat': 'Neuer Chat',
'chat.input.using_prompt': 'Verwende Vorlage:',

View file

@ -57,6 +57,11 @@ export default {
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
'dashboard.log.fetch_failed': 'Failed to fetch logs',
'dashboard.log.level.info': 'INFO',
'dashboard.workflow_dropdown.loading': 'Loading...',
'dashboard.workflow_dropdown.error': 'Error',
'dashboard.workflow_dropdown.select_workflow': 'Select Workflow',
'dashboard.workflow_dropdown.available_workflows': 'Available Workflows',
'dashboard.workflow_dropdown.no_workflows': 'No workflows available',
// Prompt Set
'promptset.loading': 'Loading prompts...',
@ -183,6 +188,10 @@ export default {
'chat.input.processing': 'Processing...',
'chat.input.continue': 'Continue',
'chat.input.send': 'Send',
'chat.input.stop': 'Stop',
'chat.input.stopping': 'Stopping...',
'chat.input.drop_files_here': 'Drop files here to attach',
'chat.input.drop_disabled': 'File drop disabled during workflow',
'chat.input.new_chat': 'New Chat',
'chat.input.using_prompt': 'Using prompt:',

View file

@ -57,6 +57,11 @@ export default {
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
'dashboard.log.level.info': 'INFO',
'dashboard.workflow_dropdown.loading': 'Chargement...',
'dashboard.workflow_dropdown.error': 'Erreur',
'dashboard.workflow_dropdown.select_workflow': 'Sélectionner un workflow',
'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles',
'dashboard.workflow_dropdown.no_workflows': 'Aucun workflow disponible',
// Prompt Set
'promptset.loading': 'Chargement des prompts...',
@ -182,6 +187,10 @@ export default {
'chat.input.processing': 'Traitement...',
'chat.input.continue': 'Continuer',
'chat.input.send': 'Envoyer',
'chat.input.stop': 'Arrêter',
'chat.input.stopping': 'Arrêt...',
'chat.input.drop_files_here': 'Déposez les fichiers ici pour les joindre',
'chat.input.drop_disabled': 'Dépôt de fichiers désactivé pendant le workflow',
'chat.input.new_chat': 'Nouveau Chat',
'chat.input.using_prompt': 'Utilisation du modèle:',

View file

@ -1,7 +1,8 @@
import { useState, useCallback } from 'react';
import { IoMdRefresh } from 'react-icons/io';
import { useState, useCallback, useRef, useEffect } from 'react';
import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io';
import { Prompt } from '../../hooks/usePrompts';
import { useLanguage } from '../../contexts/LanguageContext';
import { useWorkflows, Workflow } from '../../hooks/useWorkflows';
import styles from './HomeStyles/Dashboard.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
@ -12,23 +13,54 @@ function Dashboard () {
const [isChatExpanded, setIsChatExpanded] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
console.log('🏠 Dashboard render with currentWorkflowId:', currentWorkflowId);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Fetch workflows for dropdown
const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows();
const handleChatToggleExpand = () => {
setIsChatExpanded(!isChatExpanded);
};
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
console.log('🔄 Dashboard.handleWorkflowIdChange called with:', workflowId);
setCurrentWorkflowId(workflowId);
}, []);
const handleWorkflowResume = useCallback((workflowId: string) => {
// Set the workflow ID to resume it
const handleWorkflowSelect = useCallback((workflowId: string) => {
console.log('📋 Dashboard.handleWorkflowSelect called with:', workflowId);
// Set the workflow ID when selected from dropdown
setCurrentWorkflowId(workflowId);
// Switch to Chat Area tab to show the resumed workflow
setIsDropdownOpen(false);
}, []);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Helper functions for workflow dropdown
const getWorkflowDisplayName = (workflow: Workflow) => {
// Use name first, then title, then fallback to id (first 8 chars)
return workflow.name || workflow.title || `${workflow.id.substring(0, 8)}...`;
};
const formatWorkflowId = (id: string) => {
return `${id.substring(0, 8)}...`;
};
const handleResetWorkflow = useCallback(() => {
setCurrentWorkflowId(null);
setSelectedPrompt(null);
@ -48,23 +80,139 @@ function Dashboard () {
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
{currentWorkflowId && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
{t('dashboard.log.workflow')}: {displayWorkflowId}
</span>
{currentWorkflowId ? (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
{t('dashboard.log.workflow')}: {displayWorkflowId}
</span>
</div>
<button
className={sharedStyles.secondaryButton}
onClick={handleResetWorkflow}
aria-label="Reset workflow"
>
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
Reset
</button>
</>
) : (
<div style={{ position: 'relative', display: 'inline-block' }} ref={dropdownRef}>
<button
className={sharedStyles.secondaryButton}
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
disabled={workflowsLoading}
aria-expanded={isDropdownOpen}
aria-haspopup="listbox"
style={{ minWidth: '180px', justifyContent: 'space-between' }}
>
<span>
{workflowsLoading
? t('dashboard.workflow_dropdown.loading')
: workflowsError
? t('dashboard.workflow_dropdown.error')
: t('dashboard.workflow_dropdown.select_workflow')
}
</span>
<IoMdArrowDropdown
className={sharedStyles.buttonIcon}
style={{
transform: isDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
</button>
{isDropdownOpen && !workflowsLoading && !workflowsError && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
marginTop: '4px',
backgroundColor: 'var(--color-bg)',
border: '1px solid var(--color-primary)',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
zIndex: 1000,
maxHeight: '300px',
overflowY: 'auto'
}}>
<div style={{
padding: '12px 16px',
fontSize: '12px',
fontWeight: 600,
color: 'var(--color-text)',
backgroundColor: 'var(--color-gray-disabled)',
borderBottom: '1px solid var(--color-primary)',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
{t('dashboard.workflow_dropdown.available_workflows')}
</div>
{workflows.length === 0 ? (
<div style={{
padding: '12px 16px',
fontSize: '14px',
color: 'var(--color-text-secondary)',
fontStyle: 'italic'
}}>
{t('dashboard.workflow_dropdown.no_workflows')}
</div>
) : (
workflows.map((workflow) => (
<button
key={workflow.id}
onClick={() => handleWorkflowSelect(workflow.id)}
style={{
width: '100%',
padding: '12px 16px',
background: 'none',
border: 'none',
textAlign: 'left',
cursor: 'pointer',
fontFamily: 'var(--font-family)',
transition: 'background-color 0.2s ease',
borderBottom: '1px solid var(--color-gray-disabled)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--color-gray-disabled)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<span style={{
fontSize: '14px',
fontWeight: 500,
color: 'var(--color-text)'
}}>
{getWorkflowDisplayName(workflow)}
</span>
<span style={{
fontSize: '12px',
color: 'var(--color-text-secondary)',
fontFamily: 'monospace'
}}>
ID: {formatWorkflowId(workflow.id)}
</span>
<span style={{
fontSize: '12px',
color: 'var(--color-text-secondary)',
textTransform: 'capitalize'
}}>
{workflow.status}
</span>
</div>
</button>
))
)}
</div>
)}
</div>
)}
{currentWorkflowId && (
<button
className={sharedStyles.secondaryButton}
onClick={handleResetWorkflow}
aria-label="Reset workflow"
>
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
Reset
</button>
)}
</div>
</div>
<div className={sharedStyles.horizontalDivider}></div>
@ -78,7 +226,7 @@ function Dashboard () {
selectedPrompt={selectedPrompt}
onPromptUsed={() => setSelectedPrompt(null)}
onWorkflowIdChange={handleWorkflowIdChange}
onWorkflowResume={handleWorkflowResume}
currentWorkflowId={currentWorkflowId}
/>
</div>
</div>