diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.tsx b/src/components/Dashboard/DashboardChat/DashboardChat.tsx index 8ce12c3..3aa0ced 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChat.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChat.tsx @@ -1,39 +1,26 @@ import React from "react"; -import { Prompt } from "../../../hooks/usePrompts"; - +import { Prompt, WorkflowState, WorkflowActions } from "./dashboardChatAreaTypes"; import DashboardChatArea from './DashboardChatArea.tsx'; import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; interface DashboardChatProps { - isExpanded: boolean; - onToggleExpand: () => void; selectedPrompt?: Prompt | null; - onPromptUsed?: () => void; - onWorkflowIdChange?: (workflowId: string | null) => void; - onWorkflowCompletedChange?: (completed: boolean) => void; - currentWorkflowId?: string | null; + workflowState: WorkflowState; + workflowActions: WorkflowActions; } const DashboardChat: React.FC = ({ selectedPrompt, - onPromptUsed, - onWorkflowIdChange, - onWorkflowCompletedChange, - currentWorkflowId + workflowState, + workflowActions }) => { - // Pass the current workflow ID directly to the chat area - // This handles both dropdown selection and workflow resume scenarios - const effectiveWorkflowId = currentWorkflowId; - return (
); diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx index c93e05f..82951d1 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx @@ -1,64 +1,18 @@ -import React, { useState, useRef } from "react"; +import React, { useState } from "react"; import MessageList from "./DashboardChatAreaMessageList"; import FilePreview from "./DashboardChatAreaFilePreview"; import InputArea from "./DashboardChatAreaInput"; import ConnectedFiles from "./DashboardChatAreaConnectedFiles"; import { DashboardChatAreaProps } from "./dashboardChatAreaTypes"; -import { useWorkflowManager } from "./useWorkflowManager"; import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; const DashboardChatArea: React.FC = ({ selectedPrompt, - onPromptUsed, - onWorkflowIdChange, - onWorkflowCompletedChange, - resumeWorkflowId + workflowState, + workflowActions }) => { - // Fixed grid layout - no resizing - - // File selection state const [selectedFile, setSelectedFile] = useState(null); const [attachedFiles, setAttachedFiles] = useState([]); - - // Track the last resumeWorkflowId to prevent feedback loops - const lastResumeWorkflowIdRef = useRef(resumeWorkflowId); - - // Centralized workflow management - const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId); - - // 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(() => { - // 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 - React.useEffect(() => { - if (onWorkflowCompletedChange && workflowState.workflow) { - const isCompleted = ['completed', 'failed', 'stopped'].includes(workflowState.workflow.status); - onWorkflowCompletedChange(isCompleted); - } - }, [workflowState.workflow?.status, onWorkflowCompletedChange]); - - // Note: useWorkflowManager handles resumeWorkflowId changes automatically - - // No resizing functionality needed - - console.log('🎯 DashboardChatArea render:', { - currentWorkflowId: workflowState.currentWorkflowId, - resumeWorkflowId, - workflowStatus: workflowState.workflow?.status, - messagesCount: workflowState.messages?.length || 0, - isLoading: workflowState.isLoading, - isPolling: workflowState.isPolling - }); return (
@@ -79,11 +33,10 @@ const DashboardChatArea: React.FC = ({
diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx index 4fe2288..c00fdea 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Prompt } from '../../../hooks/usePrompts'; import FileAttachmentPopup from './FileAttachmentPopup'; -import { WorkflowManagerState, WorkflowManagerActions } from './useWorkflowManager'; +import { Prompt, WorkflowState, WorkflowActions } from './dashboardChatAreaTypes'; import { useLanguage } from '../../../contexts/LanguageContext'; import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css'; @@ -9,11 +8,10 @@ import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css'; interface InputAreaProps { selectedPrompt?: Prompt | null; - onPromptUsed?: () => void; - workflowState: WorkflowManagerState; - workflowActions: WorkflowManagerActions; - onAttachedFilesChange?: (files: AttachedFile[]) => void; + workflowState: WorkflowState; + workflowActions: WorkflowActions; attachedFiles?: AttachedFile[]; + onAttachedFilesChange?: (files: AttachedFile[]) => void; } interface AttachedFile { @@ -27,11 +25,10 @@ interface AttachedFile { const InputArea: React.FC = ({ selectedPrompt, - onPromptUsed, workflowState, workflowActions, - onAttachedFilesChange, - attachedFiles: externalAttachedFiles = [] + attachedFiles: externalAttachedFiles = [], + onAttachedFilesChange }) => { const { t } = useLanguage(); const [inputValue, setInputValue] = useState(''); @@ -109,7 +106,7 @@ const InputArea: React.FC = ({ if (onAttachedFilesChange) { onAttachedFilesChange([]); } - if (onPromptUsed) onPromptUsed(); + // Prompt was used } else { setSendError('Failed to send message. Please try again.'); } @@ -122,22 +119,8 @@ const InputArea: React.FC = ({ }; 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); - } + // Stop the workflow but keep it selected + await workflowActions.stopWorkflow(); }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -215,10 +198,7 @@ const InputArea: React.FC = ({ 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); + const shouldShowStopButton = !!isWorkflowActive; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx index f5d7d9a..ba12b51 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { useApiRequest } from '../../../hooks/useApi'; import MessageItem from './DashboardChatAreaMessageItem'; -import { WorkflowMessage, Document } from './dashboardChatAreaTypes'; -import { WorkflowManagerState } from './useWorkflowManager'; +import { WorkflowMessage, Document, WorkflowState } from './dashboardChatAreaTypes'; interface MessageListProps { - workflowState: WorkflowManagerState; + workflowState: WorkflowState; onFilePreview?: (file: any) => void; } @@ -13,8 +12,20 @@ interface MessageListProps { const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise => { let documents: Document[] = []; - // Fetch file metadata if fileIds exist - if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) { + // Check if we have documents directly from the API or need to fetch via fileIds + if (workflowMessage.documents && workflowMessage.documents.length > 0) { + // Use documents directly from the API + documents = workflowMessage.documents.map(doc => ({ + id: doc.id || doc.fileId, + fileId: parseInt(doc.fileId), + name: doc.filename, + ext: doc.filename.split('.').pop() || 'unknown', + type: doc.mimeType, + size: doc.fileSize, + downloadUrl: `/api/workflows/files/${doc.fileId}/download` + })); + } else if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) { + // Fallback to legacy fileIds approach console.log(`📎 Processing ${workflowMessage.fileIds.length} files for message ${workflowMessage.id}`); const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => { @@ -54,9 +65,9 @@ const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, reques documents = await Promise.all(documentPromises); } - // Try different possible field names for content - const possibleContent = workflowMessage.content || - (workflowMessage as any).message || + // Try different possible field names for content (API uses 'message', some legacy might use 'content') + const possibleContent = workflowMessage.message || + workflowMessage.content || (workflowMessage as any).text || (workflowMessage as any).body || ''; @@ -117,14 +128,15 @@ const MessageList: React.FC = ({ const transformed = await Promise.all( workflowState.messages.map(async (msg: WorkflowMessage, index: number) => { console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`); + const content = msg.message || msg.content || ''; console.log(`📝 RAW API Message (${msg.role}):`, { id: msg.id, rawMessage: msg, - contentType: typeof msg.content, - contentValue: msg.content, - contentLength: msg.content?.length || 0, - hasContent: !!msg.content, - contentPreview: msg.content?.substring(0, 100) + (msg.content?.length > 100 ? '...' : ''), + contentType: typeof content, + contentValue: content, + contentLength: content.length, + hasContent: !!content, + contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''), fileCount: msg.fileIds?.length || 0, allKeys: Object.keys(msg) }); @@ -200,7 +212,7 @@ const MessageList: React.FC = ({ } }, [transformedMessages.length, scrollToBottom]); - const { currentWorkflowId, workflow, isLoading, error, isPolling } = workflowState; + const { currentWorkflowId, workflow, isLoading, error } = workflowState; return ( <> @@ -255,7 +267,7 @@ const MessageList: React.FC = ({ Status: {workflow.status} {workflow.currentRound && ` (Round ${workflow.currentRound})`} {/* Show polling indicator */} - {isPolling && ( + {workflow && ['running', 'processing', 'started'].includes(workflow.status) && ( void; - onWorkflowIdChange?: (workflowId: string | null) => void; - onWorkflowCompletedChange?: (completed: boolean) => void; - resumeWorkflowId?: string | null; + workflowState: WorkflowState; + workflowActions: WorkflowActions; } +// Workflow interfaces - Updated to match full API response +export interface WorkflowStats { + id: string; + processingTime: number; + tokenCount: number; + bytesSent: number; + bytesReceived: number; + successRate: number; + errorCount: number; +} + +export interface WorkflowDocument { + id: string; + fileId: string; + filename: string; + fileSize: number; + mimeType: string; +} + +export interface WorkflowMessageStats extends WorkflowStats {} + export interface WorkflowMessage { id: string; + workflowId: string; + parentMessageId?: string; + documents?: WorkflowDocument[]; + documentsLabel?: string; + message: string; + content?: string; // For backward compatibility role: 'user' | 'assistant' | 'system'; - content: string; - timestamp?: string; - fileIds?: number[]; + status: string; + sequenceNr: number; + publishedAt: string; + timestamp?: string; // For backward compatibility + fileIds?: number[]; // For backward compatibility + stats?: WorkflowMessageStats; + success: boolean; + actionId?: string; + actionMethod?: string; + actionName?: string; +} + +export interface WorkflowLog { + id: string; + workflowId: string; + message: string; + type: string; + timestamp: string; + status: string; + progress: number; + performance: any; +} + +export interface WorkflowAction { + id: string; + execMethod: string; + execAction: string; + execParameters: any; + execResultLabel: string; + expectedDocumentFormats?: any[]; + status: 'pending' | 'running' | 'completed' | 'failed'; + error?: string; + retryCount: number; + retryMax: number; + processingTime: number; + timestamp: string; + result?: string; + resultDocuments?: WorkflowDocument[]; +} + +export interface WorkflowTask { + id: string; + workflowId: string; + userInput: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + error?: string; + startedAt: string; + finishedAt?: string; + actionList: WorkflowAction[]; + retryCount: number; + retryMax: number; + rollbackOnFailure: boolean; + dependencies: string[]; + feedback?: string; + processingTime: number; + resultLabels: any; +} + +export interface Workflow { + id: string; + mandateId: string; + status: string; + name?: string; + currentRound: number; + lastActivity: string; + startedAt: string; + logs?: WorkflowLog[]; + messages?: WorkflowMessage[]; + stats?: WorkflowStats; + tasks?: WorkflowTask[]; +} + +export interface WorkflowState { + currentWorkflowId: string | null; + workflow: Workflow | null; + messages: WorkflowMessage[]; + isLoading: boolean; + error: string | null; +} + +export interface WorkflowActions { + loadWorkflow: (workflowId: string) => void; + startNewWorkflow: (prompt: string, fileIds?: number[]) => Promise; + continueWorkflow: (prompt: string, fileIds?: number[]) => Promise; + stopWorkflow: () => Promise; + clearWorkflow: () => void; } export interface FileInfo { diff --git a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts index 7360434..3374a3b 100644 --- a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts +++ b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts @@ -1,28 +1,8 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows'; +import { WorkflowState, WorkflowActions } from './dashboardChatAreaTypes'; -export interface WorkflowManagerState { - currentWorkflowId: string | null; - workflow: any; - messages: any[]; - isLoading: boolean; - error: string | null; - isPolling: boolean; -} - -export interface WorkflowManagerActions { - loadWorkflow: (workflowId: string) => Promise; - startNewWorkflow: (prompt: string, fileIds: number[]) => Promise; - continueWorkflow: (prompt: string, fileIds: number[]) => Promise; - stopWorkflow: () => Promise; - clearWorkflow: () => void; - refreshMessages: () => Promise; - setPolling: (enabled: boolean) => void; -} - -export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] { - console.log('🏭 useWorkflowManager called with initialWorkflowId:', initialWorkflowId); - +export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowState, WorkflowActions] { // Core state const [currentWorkflowId, setCurrentWorkflowId] = useState(initialWorkflowId || null); const [isPolling, setIsPolling] = useState(false); @@ -46,27 +26,19 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow // Auto-polling for active workflows and message updates useEffect(() => { if (isPolling && currentWorkflowId) { - console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`); - pollingIntervalRef.current = window.setInterval(() => { - console.log('🔄 Auto-polling status...'); - // Always poll for status to detect workflow state changes refetchStatus(); - - // Only poll messages if workflow is running if (currentWorkflow) { const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status); if (isActive) { - console.log('🔄 Workflow is active, also polling messages...'); refetchMessages(); } } - }, 2000); // Poll every 2 seconds for smoother updates + }, 2000); } return () => { if (pollingIntervalRef.current) { - console.log('🛑 Stopping auto-polling'); clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } @@ -75,14 +47,10 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow // Actions const loadWorkflow = useCallback(async (workflowId: string) => { - console.log(`📂 Loading workflow: ${workflowId}`); setCurrentWorkflowId(workflowId); - // The hooks will automatically fetch data when workflowId changes }, []); const startNewWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise => { - console.log('🚀 Starting new workflow with prompt:', prompt.substring(0, 50) + '...'); - const workflowData: StartWorkflowRequest = { prompt, listFileId: fileIds @@ -92,30 +60,17 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow if (result.success && result.data) { const newWorkflowId = result.data.id; - console.log(`✅ New workflow started: ${newWorkflowId}`); setCurrentWorkflowId(newWorkflowId); - setIsPolling(true); // Start polling immediately for new workflows - - // Also immediately fetch messages to get the user's message - setTimeout(() => { - refetchMessages(); - }, 500); - + setIsPolling(true); + setTimeout(() => refetchMessages(), 500); return newWorkflowId; - } else { - console.error('❌ Failed to start workflow:', result.error); - return null; } + return null; }, [startWorkflow, refetchMessages]); const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise => { - if (!currentWorkflowId) { - console.error('❌ Cannot continue workflow: no current workflow ID'); - return false; - } + if (!currentWorkflowId) return false; - console.log(`➡️ Continuing workflow ${currentWorkflowId} with prompt:`, prompt.substring(0, 50) + '...'); - const workflowData: StartWorkflowRequest = { prompt, listFileId: fileIds @@ -124,71 +79,39 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow const result = await startWorkflow(workflowData, currentWorkflowId); if (result.success) { - console.log(`✅ Workflow ${currentWorkflowId} continued`); - setIsPolling(true); // Ensure polling is enabled - - // Immediately start polling for new messages - setTimeout(() => { - refetchMessages(); - }, 500); - + setIsPolling(true); + setTimeout(() => refetchMessages(), 500); return true; - } else { - console.error('❌ Failed to continue workflow:', result.error); - return false; } + return false; }, [currentWorkflowId, startWorkflow, refetchMessages]); const stopWorkflow = useCallback(async (): Promise => { - if (!currentWorkflowId) { - console.error('❌ Cannot stop workflow: no current workflow ID'); - return false; - } + if (!currentWorkflowId) 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 + const result = await stopWorkflowRequest(currentWorkflowId); + if (result) { + // Immediately refresh status to reflect the stopped state setTimeout(() => { refetchStatus(); + refetchMessages(); }, 500); - return true; - } else { - console.error('❌ Failed to stop workflow'); - return false; } - }, [currentWorkflowId, stopWorkflowRequest, refetchStatus]); + return false; + }, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages]); const clearWorkflow = useCallback(() => { - console.log('🧹 Clearing workflow'); setCurrentWorkflowId(null); setIsPolling(false); }, []); - const refreshMessages = useCallback(async () => { - console.log('🔄 Manually refreshing messages'); - await refetchMessages(); - }, [refetchMessages]); - - const setPollingEnabled = useCallback((enabled: boolean) => { - console.log(`🔄 Setting polling to: ${enabled}`); - setIsPolling(enabled); - }, []); - // Sync with external workflow ID changes useEffect(() => { 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); } @@ -199,39 +122,27 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow 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); - } + setIsPolling(isActive); } 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 = { + const state: WorkflowState = { currentWorkflowId, workflow: currentWorkflow, messages, isLoading, - error, - isPolling + error }; - const actions: WorkflowManagerActions = useMemo(() => ({ + const actions: WorkflowActions = useMemo(() => ({ loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, - clearWorkflow, - refreshMessages, - setPolling: setPollingEnabled - }), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, refreshMessages, setPollingEnabled]); + clearWorkflow + }), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow]); return [state, actions]; } diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index 7e9e0ea..ef4971b 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -1,33 +1,9 @@ import { useState, useEffect } from 'react'; import { useApiRequest } from './useApi'; -// Workflow interfaces (matching backend ChatWorkflow model) -export interface Workflow { - id: string; - mandateId: string; - status: string; - name?: string; - title?: string; // Keep for backward compatibility - currentRound: number; - lastActivity: string; - startedAt: string; - logs?: any[]; - messages?: any[]; - stats?: Record; - tasks?: any[]; - dataStats?: Record; // Keep for backward compatibility - userId?: number; // Keep for backward compatibility - messageIds?: string[]; // Keep for backward compatibility -} - -export interface WorkflowMessage { - id: string; - content: string; - role: 'user' | 'assistant' | 'system'; - timestamp?: string; - sequenceNo?: number; - fileIds?: number[]; -} +// Import the centralized workflow interfaces +import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes'; +export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument }; export interface StartWorkflowRequest { prompt: string; diff --git a/src/locales/de.ts b/src/locales/de.ts index d9eb910..6b859f0 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -63,6 +63,19 @@ export default { 'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows', 'dashboard.workflow_dropdown.no_workflows': 'Keine Workflows verfügbar', + // Workflow Stats + 'dashboard.stats.workflow': 'Workflow', + 'dashboard.stats.status': 'Status', + 'dashboard.stats.rounds': 'Runden', + 'dashboard.stats.messages': 'Nachrichten', + 'dashboard.stats.files': 'Dateien', + 'dashboard.stats.tokens': 'Token', + 'dashboard.stats.data_sent': 'Daten gesendet', + 'dashboard.stats.data_received': 'Daten empfangen', + 'dashboard.stats.success_rate': 'Erfolgsrate', + 'dashboard.stats.errors': 'Fehler', + 'dashboard.stats.started': 'Gestartet', + // Prompt Set 'promptset.loading': 'Prompts werden geladen...', 'promptset.error.loading': 'Fehler beim Laden der Prompts', diff --git a/src/locales/en.ts b/src/locales/en.ts index 3e2a982..62a5ed9 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -63,6 +63,19 @@ export default { 'dashboard.workflow_dropdown.available_workflows': 'Available Workflows', 'dashboard.workflow_dropdown.no_workflows': 'No workflows available', + // Workflow Stats + 'dashboard.stats.workflow': 'Workflow', + 'dashboard.stats.status': 'Status', + 'dashboard.stats.rounds': 'Rounds', + 'dashboard.stats.messages': 'Messages', + 'dashboard.stats.files': 'Files', + 'dashboard.stats.tokens': 'Tokens', + 'dashboard.stats.data_sent': 'Data Sent', + 'dashboard.stats.data_received': 'Data Received', + 'dashboard.stats.success_rate': 'Success Rate', + 'dashboard.stats.errors': 'Errors', + 'dashboard.stats.started': 'Started', + // Prompt Set 'promptset.loading': 'Loading prompts...', 'promptset.error.loading': 'Error loading prompts', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index d522ee3..47c44f6 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -63,6 +63,19 @@ export default { 'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles', 'dashboard.workflow_dropdown.no_workflows': 'Aucun workflow disponible', + // Workflow Stats + 'dashboard.stats.workflow': 'Workflow', + 'dashboard.stats.status': 'Statut', + 'dashboard.stats.rounds': 'Tours', + 'dashboard.stats.messages': 'Messages', + 'dashboard.stats.files': 'Fichiers', + 'dashboard.stats.tokens': 'Jetons', + 'dashboard.stats.data_sent': 'Données envoyées', + 'dashboard.stats.data_received': 'Données reçues', + 'dashboard.stats.success_rate': 'Taux de succès', + 'dashboard.stats.errors': 'Erreurs', + 'dashboard.stats.started': 'Démarré', + // Prompt Set 'promptset.loading': 'Chargement des prompts...', 'promptset.error.loading': 'Erreur lors du chargement des prompts', diff --git a/src/pages/Home/Dashboard.tsx b/src/pages/Home/Dashboard.tsx index 54f8932..cb5faa3 100644 --- a/src/pages/Home/Dashboard.tsx +++ b/src/pages/Home/Dashboard.tsx @@ -1,8 +1,9 @@ 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 { useWorkflows } from '../../hooks/useWorkflows'; +import { Prompt, Workflow } from '../../components/Dashboard/DashboardChat/dashboardChatAreaTypes'; +import { useWorkflowManager } from '../../components/Dashboard/DashboardChat/useWorkflowManager'; import styles from './HomeStyles/Dashboard.module.css' import sharedStyles from '../../components/PageManager/pages.module.css'; @@ -10,32 +11,25 @@ import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardCha function Dashboard () { const { t } = useLanguage(); - const [isChatExpanded, setIsChatExpanded] = useState(false); const [selectedPrompt, setSelectedPrompt] = useState(null); - const [currentWorkflowId, setCurrentWorkflowId] = useState(null); - - console.log('🏠 Dashboard render with currentWorkflowId:', currentWorkflowId); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); + // Central workflow management + const [workflowState, workflowActions] = useWorkflowManager(); + // 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 handleWorkflowSelect = useCallback((workflowId: string) => { - console.log('📋 Dashboard.handleWorkflowSelect called with:', workflowId); - // Set the workflow ID when selected from dropdown - setCurrentWorkflowId(workflowId); + workflowActions.loadWorkflow(workflowId); setIsDropdownOpen(false); - }, []); + }, [workflowActions]); + + const handleResetWorkflow = useCallback(() => { + workflowActions.clearWorkflow(); + setSelectedPrompt(null); + }, [workflowActions]); // Close dropdown when clicking outside useEffect(() => { @@ -44,33 +38,99 @@ function Dashboard () { setIsDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('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 getWorkflowDisplayName = (workflow: Workflow) => + workflow.name || `${workflow.id.substring(0, 8)}...`; - const formatWorkflowId = (id: string) => { - return `${id.substring(0, 8)}...`; - }; - - const handleResetWorkflow = useCallback(() => { - setCurrentWorkflowId(null); - setSelectedPrompt(null); - }, []); + const formatWorkflowId = (id: string) => `${id.substring(0, 8)}...`; // Format workflow ID for display - const displayWorkflowId = currentWorkflowId ? - `${currentWorkflowId.substring(0, 8)}...` : + const displayWorkflowId = workflowState.currentWorkflowId ? + `${workflowState.currentWorkflowId.substring(0, 8)}...` : t('dashboard.log.no_workflow'); + // Calculate workflow stats + const getWorkflowStats = () => { + if (!workflowState.workflow) return null; + + const workflow = workflowState.workflow; + const messages = workflowState.messages; // Use messages from workflowState, not workflow.messages + const messageCount = messages?.length || 0; + const fileCount = messages?.reduce((count, msg) => { + return count + (msg.documents?.length || msg.fileIds?.length || 0); + }, 0) || 0; + + // Aggregate stats from workflow stats and message stats + const totalTokens = (workflow.stats?.tokenCount || 0) + + (messages?.reduce((sum, msg) => sum + (msg.stats?.tokenCount || 0), 0) || 0); + const totalBytesSent = (workflow.stats?.bytesSent || 0) + + (messages?.reduce((sum, msg) => sum + (msg.stats?.bytesSent || 0), 0) || 0); + const totalBytesReceived = (workflow.stats?.bytesReceived || 0) + + (messages?.reduce((sum, msg) => sum + (msg.stats?.bytesReceived || 0), 0) || 0); + const totalErrors = (workflow.stats?.errorCount || 0) + + (messages?.reduce((sum, msg) => sum + (msg.stats?.errorCount || 0), 0) || 0); + + // Calculate overall success rate + const successfulMessages = messages?.filter(msg => msg.success)?.length || 0; + const successRate = messageCount > 0 ? (successfulMessages / messageCount) * 100 : 100; + + return { + status: workflow.status, + rounds: workflow.currentRound, + startedAt: workflow.startedAt, + messageCount, + fileCount, + tokenCount: totalTokens, + bytesSent: totalBytesSent, + bytesReceived: totalBytesReceived, + successRate: Math.round(successRate), + errorCount: totalErrors + }; + }; + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString(); + }; + + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'completed': + case 'success': + case 'finished': + return 'var(--color-success)'; + case 'running': + case 'processing': + case 'started': + case 'in_progress': + return 'var(--color-warning)'; + case 'failed': + case 'error': + case 'cancelled': + return 'var(--color-error)'; + case 'pending': + case 'queued': + case 'waiting': + return 'var(--color-text-secondary)'; + default: + return 'var(--color-text)'; + } + }; + + const formatStatus = (status: string) => { + return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + }; + return (
@@ -80,13 +140,87 @@ function Dashboard () {

{t('nav.dashboard')}

- {currentWorkflowId ? ( + {workflowState.currentWorkflowId ? ( <> -
- - {t('dashboard.log.workflow')}: {displayWorkflowId} - -
+ {(() => { + const stats = getWorkflowStats(); + return ( +
+
+ {t('dashboard.stats.workflow')} + {displayWorkflowId} +
+ + {stats && ( + <> +
+ +
+ {t('dashboard.stats.status')} + {formatStatus(stats.status)} +
+ +
+ {t('dashboard.stats.rounds')} + {stats.rounds} +
+ +
+ {t('dashboard.stats.messages')} + {stats.messageCount} +
+ +
+ {t('dashboard.stats.files')} + {stats.fileCount} +
+ +
+ {t('dashboard.stats.tokens')} + {stats.tokenCount.toLocaleString()} +
+ +
+ {t('dashboard.stats.data_sent')} + {formatBytes(stats.bytesSent)} +
+ +
+ {t('dashboard.stats.data_received')} + {formatBytes(stats.bytesReceived)} +
+ +
+ {t('dashboard.stats.success_rate')} + = 90 ? 'var(--color-success)' : + stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)' + }}>{stats.successRate}% +
+ +
+ {t('dashboard.stats.errors')} + 0 ? 'var(--color-error)' : 'var(--color-text)' }}>{stats.errorCount} +
+ +
+ {t('dashboard.stats.started')} + {formatDate(stats.startedAt)} +
+ + )} +
+ ); + })()}
-
+
setSelectedPrompt(null)} - onWorkflowIdChange={handleWorkflowIdChange} - currentWorkflowId={currentWorkflowId} + workflowState={workflowState} + workflowActions={workflowActions} />