streamlined workflow process, added workflow stats

This commit is contained in:
Ida Dittrich 2025-08-20 13:20:52 +02:00
parent e616541ee1
commit aa5c321f09
12 changed files with 420 additions and 317 deletions

View file

@ -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<DashboardChatProps> = ({
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 (
<div className={styles.dashboard_chat}>
<DashboardChatArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
onWorkflowIdChange={onWorkflowIdChange}
onWorkflowCompletedChange={onWorkflowCompletedChange}
resumeWorkflowId={effectiveWorkflowId}
selectedPrompt={selectedPrompt}
workflowState={workflowState}
workflowActions={workflowActions}
/>
</div>
);

View file

@ -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<DashboardChatAreaProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
resumeWorkflowId
workflowState,
workflowActions
}) => {
// Fixed grid layout - no resizing
// File selection state
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);
// 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 (
<div className={styles.chat_grid}>
@ -79,11 +33,10 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
<InputArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
workflowState={workflowState}
workflowActions={workflowActions}
onAttachedFilesChange={setAttachedFiles}
attachedFiles={attachedFiles}
onAttachedFilesChange={setAttachedFiles}
/>
</div>

View file

@ -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<InputAreaProps> = ({
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<InputAreaProps> = ({
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<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);
}
// Stop the workflow but keep it selected
await workflowActions.stopWorkflow();
};
const handleKeyPress = (e: React.KeyboardEvent) => {
@ -215,10 +198,7 @@ const InputArea: React.FC<InputAreaProps> = ({
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;

View file

@ -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<any> => {
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<MessageListProps> = ({
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<MessageListProps> = ({
}
}, [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<MessageListProps> = ({
<strong>Status:</strong> {workflow.status}
{workflow.currentRound && ` (Round ${workflow.currentRound})`}
{/* Show polling indicator */}
{isPolling && (
{workflow && ['running', 'processing', 'started'].includes(workflow.status) && (
<span style={{
marginLeft: '8px',
fontSize: '12px',

View file

@ -200,11 +200,10 @@
}
.button_primary:disabled {
background-color: var(--color-gray-disabled);
border: 1px solid var(--color-gray-disabled);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary-disabled);
cursor: not-allowed;
opacity: 0.6;
border: 1px solid var(--color-gray-disabled);
}

View file

@ -1,19 +1,134 @@
import { Prompt } from "../../../../hooks/usePrompts";
// Simplified types - everything in one place
export interface Prompt {
id: number;
name: string;
content: string;
createdAt?: string;
isShared?: boolean;
}
export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => 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<string | null>;
continueWorkflow: (prompt: string, fileIds?: number[]) => Promise<boolean>;
stopWorkflow: () => Promise<boolean>;
clearWorkflow: () => void;
}
export interface FileInfo {

View file

@ -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<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);
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowState, WorkflowActions] {
// Core state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(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<string | null> => {
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<boolean> => {
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<boolean> => {
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];
}

View file

@ -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<string, any>;
tasks?: any[];
dataStats?: Record<string, any>; // 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;

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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<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);
// 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 (
<div className={sharedStyles.pageContainer}>
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
@ -80,13 +140,87 @@ function Dashboard () {
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
{currentWorkflowId ? (
{workflowState.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>
{(() => {
const stats = getWorkflowStats();
return (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '20px',
padding: '12px 20px',
backgroundColor: 'var(--color-gray-disabled)',
borderRadius: '12px',
fontSize: '13px',
fontWeight: '500'
}}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.workflow')}</span>
<span style={{ color: 'var(--color-text)', fontFamily: 'monospace' }}>{displayWorkflowId}</span>
</div>
{stats && (
<>
<div style={{ width: '1px', height: '40px', backgroundColor: 'var(--color-primary)', opacity: 0.3 }}></div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.status')}</span>
<span style={{ color: getStatusColor(stats.status), fontWeight: '600' }}>{formatStatus(stats.status)}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.rounds')}</span>
<span style={{ color: 'var(--color-text)' }}>{stats.rounds}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.messages')}</span>
<span style={{ color: 'var(--color-text)' }}>{stats.messageCount}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.files')}</span>
<span style={{ color: 'var(--color-text)' }}>{stats.fileCount}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.tokens')}</span>
<span style={{ color: 'var(--color-text)' }}>{stats.tokenCount.toLocaleString()}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.data_sent')}</span>
<span style={{ color: 'var(--color-text)' }}>{formatBytes(stats.bytesSent)}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.data_received')}</span>
<span style={{ color: 'var(--color-text)' }}>{formatBytes(stats.bytesReceived)}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.success_rate')}</span>
<span style={{
color: stats.successRate >= 90 ? 'var(--color-success)' :
stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)'
}}>{stats.successRate}%</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.errors')}</span>
<span style={{ color: stats.errorCount > 0 ? 'var(--color-error)' : 'var(--color-text)' }}>{stats.errorCount}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.started')}</span>
<span style={{ color: 'var(--color-text)', fontSize: '12px' }}>{formatDate(stats.startedAt)}</span>
</div>
</>
)}
</div>
);
})()}
<button
className={sharedStyles.secondaryButton}
onClick={handleResetWorkflow}
@ -218,15 +352,12 @@ function Dashboard () {
<div className={sharedStyles.horizontalDivider}></div>
<div className={styles.dashboardContentArea}>
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
<div className={styles.chatLogContainer}>
<div className={styles.chatArea}>
<DashboardChat
isExpanded={isChatExpanded}
onToggleExpand={handleChatToggleExpand}
selectedPrompt={selectedPrompt}
onPromptUsed={() => setSelectedPrompt(null)}
onWorkflowIdChange={handleWorkflowIdChange}
currentWorkflowId={currentWorkflowId}
workflowState={workflowState}
workflowActions={workflowActions}
/>
</div>
</div>