streamlined workflow process, added workflow stats
This commit is contained in:
parent
e616541ee1
commit
aa5c321f09
12 changed files with 420 additions and 317 deletions
|
|
@ -1,39 +1,26 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Prompt } from "../../../hooks/usePrompts";
|
import { Prompt, WorkflowState, WorkflowActions } from "./dashboardChatAreaTypes";
|
||||||
|
|
||||||
import DashboardChatArea from './DashboardChatArea.tsx';
|
import DashboardChatArea from './DashboardChatArea.tsx';
|
||||||
|
|
||||||
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
interface DashboardChatProps {
|
interface DashboardChatProps {
|
||||||
isExpanded: boolean;
|
|
||||||
onToggleExpand: () => void;
|
|
||||||
selectedPrompt?: Prompt | null;
|
selectedPrompt?: Prompt | null;
|
||||||
onPromptUsed?: () => void;
|
workflowState: WorkflowState;
|
||||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
workflowActions: WorkflowActions;
|
||||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
|
||||||
currentWorkflowId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardChat: React.FC<DashboardChatProps> = ({
|
const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
workflowState,
|
||||||
onWorkflowIdChange,
|
workflowActions
|
||||||
onWorkflowCompletedChange,
|
|
||||||
currentWorkflowId
|
|
||||||
}) => {
|
}) => {
|
||||||
// Pass the current workflow ID directly to the chat area
|
|
||||||
// This handles both dropdown selection and workflow resume scenarios
|
|
||||||
const effectiveWorkflowId = currentWorkflowId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dashboard_chat}>
|
<div className={styles.dashboard_chat}>
|
||||||
<DashboardChatArea
|
<DashboardChatArea
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={onPromptUsed}
|
workflowState={workflowState}
|
||||||
onWorkflowIdChange={onWorkflowIdChange}
|
workflowActions={workflowActions}
|
||||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
|
||||||
resumeWorkflowId={effectiveWorkflowId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,18 @@
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState } from "react";
|
||||||
import MessageList from "./DashboardChatAreaMessageList";
|
import MessageList from "./DashboardChatAreaMessageList";
|
||||||
import FilePreview from "./DashboardChatAreaFilePreview";
|
import FilePreview from "./DashboardChatAreaFilePreview";
|
||||||
import InputArea from "./DashboardChatAreaInput";
|
import InputArea from "./DashboardChatAreaInput";
|
||||||
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
|
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
|
||||||
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||||
import { useWorkflowManager } from "./useWorkflowManager";
|
|
||||||
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
workflowState,
|
||||||
onWorkflowIdChange,
|
workflowActions
|
||||||
onWorkflowCompletedChange,
|
|
||||||
resumeWorkflowId
|
|
||||||
}) => {
|
}) => {
|
||||||
// Fixed grid layout - no resizing
|
|
||||||
|
|
||||||
// File selection state
|
|
||||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||||
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
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 (
|
return (
|
||||||
<div className={styles.chat_grid}>
|
<div className={styles.chat_grid}>
|
||||||
|
|
@ -79,11 +33,10 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
|
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
|
||||||
<InputArea
|
<InputArea
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={onPromptUsed}
|
|
||||||
workflowState={workflowState}
|
workflowState={workflowState}
|
||||||
workflowActions={workflowActions}
|
workflowActions={workflowActions}
|
||||||
onAttachedFilesChange={setAttachedFiles}
|
|
||||||
attachedFiles={attachedFiles}
|
attachedFiles={attachedFiles}
|
||||||
|
onAttachedFilesChange={setAttachedFiles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Prompt } from '../../../hooks/usePrompts';
|
|
||||||
import FileAttachmentPopup from './FileAttachmentPopup';
|
import FileAttachmentPopup from './FileAttachmentPopup';
|
||||||
import { WorkflowManagerState, WorkflowManagerActions } from './useWorkflowManager';
|
import { Prompt, WorkflowState, WorkflowActions } from './dashboardChatAreaTypes';
|
||||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||||
|
|
||||||
import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css';
|
import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css';
|
||||||
|
|
@ -9,11 +8,10 @@ import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
interface InputAreaProps {
|
interface InputAreaProps {
|
||||||
selectedPrompt?: Prompt | null;
|
selectedPrompt?: Prompt | null;
|
||||||
onPromptUsed?: () => void;
|
workflowState: WorkflowState;
|
||||||
workflowState: WorkflowManagerState;
|
workflowActions: WorkflowActions;
|
||||||
workflowActions: WorkflowManagerActions;
|
|
||||||
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
|
||||||
attachedFiles?: AttachedFile[];
|
attachedFiles?: AttachedFile[];
|
||||||
|
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttachedFile {
|
interface AttachedFile {
|
||||||
|
|
@ -27,11 +25,10 @@ interface AttachedFile {
|
||||||
|
|
||||||
const InputArea: React.FC<InputAreaProps> = ({
|
const InputArea: React.FC<InputAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
|
||||||
workflowState,
|
workflowState,
|
||||||
workflowActions,
|
workflowActions,
|
||||||
onAttachedFilesChange,
|
attachedFiles: externalAttachedFiles = [],
|
||||||
attachedFiles: externalAttachedFiles = []
|
onAttachedFilesChange
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
@ -109,7 +106,7 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
if (onAttachedFilesChange) {
|
if (onAttachedFilesChange) {
|
||||||
onAttachedFilesChange([]);
|
onAttachedFilesChange([]);
|
||||||
}
|
}
|
||||||
if (onPromptUsed) onPromptUsed();
|
// Prompt was used
|
||||||
} else {
|
} else {
|
||||||
setSendError('Failed to send message. Please try again.');
|
setSendError('Failed to send message. Please try again.');
|
||||||
}
|
}
|
||||||
|
|
@ -122,22 +119,8 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStop = async () => {
|
const handleStop = async () => {
|
||||||
setIsSending(true);
|
// Stop the workflow but keep it selected
|
||||||
setSendError(null);
|
await workflowActions.stopWorkflow();
|
||||||
|
|
||||||
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) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
|
@ -215,10 +198,7 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
const isWorkflowActive = workflowState.workflow &&
|
const isWorkflowActive = workflowState.workflow &&
|
||||||
['running', 'processing', 'started'].includes(workflowState.workflow.status);
|
['running', 'processing', 'started'].includes(workflowState.workflow.status);
|
||||||
|
|
||||||
|
const shouldShowStopButton = !!isWorkflowActive;
|
||||||
|
|
||||||
// Use polling as an indicator that workflow might be active
|
|
||||||
const shouldShowStopButton = isWorkflowActive || (workflowState.isPolling && workflowState.currentWorkflowId);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import MessageItem from './DashboardChatAreaMessageItem';
|
import MessageItem from './DashboardChatAreaMessageItem';
|
||||||
import { WorkflowMessage, Document } from './dashboardChatAreaTypes';
|
import { WorkflowMessage, Document, WorkflowState } from './dashboardChatAreaTypes';
|
||||||
import { WorkflowManagerState } from './useWorkflowManager';
|
|
||||||
|
|
||||||
interface MessageListProps {
|
interface MessageListProps {
|
||||||
workflowState: WorkflowManagerState;
|
workflowState: WorkflowState;
|
||||||
onFilePreview?: (file: any) => void;
|
onFilePreview?: (file: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,8 +12,20 @@ interface MessageListProps {
|
||||||
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
|
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
|
||||||
let documents: Document[] = [];
|
let documents: Document[] = [];
|
||||||
|
|
||||||
// Fetch file metadata if fileIds exist
|
// Check if we have documents directly from the API or need to fetch via fileIds
|
||||||
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
|
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}`);
|
console.log(`📎 Processing ${workflowMessage.fileIds.length} files for message ${workflowMessage.id}`);
|
||||||
|
|
||||||
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
||||||
|
|
@ -54,9 +65,9 @@ const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, reques
|
||||||
documents = await Promise.all(documentPromises);
|
documents = await Promise.all(documentPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try different possible field names for content
|
// Try different possible field names for content (API uses 'message', some legacy might use 'content')
|
||||||
const possibleContent = workflowMessage.content ||
|
const possibleContent = workflowMessage.message ||
|
||||||
(workflowMessage as any).message ||
|
workflowMessage.content ||
|
||||||
(workflowMessage as any).text ||
|
(workflowMessage as any).text ||
|
||||||
(workflowMessage as any).body ||
|
(workflowMessage as any).body ||
|
||||||
'';
|
'';
|
||||||
|
|
@ -117,14 +128,15 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
const transformed = await Promise.all(
|
const transformed = await Promise.all(
|
||||||
workflowState.messages.map(async (msg: WorkflowMessage, index: number) => {
|
workflowState.messages.map(async (msg: WorkflowMessage, index: number) => {
|
||||||
console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`);
|
console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`);
|
||||||
|
const content = msg.message || msg.content || '';
|
||||||
console.log(`📝 RAW API Message (${msg.role}):`, {
|
console.log(`📝 RAW API Message (${msg.role}):`, {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
rawMessage: msg,
|
rawMessage: msg,
|
||||||
contentType: typeof msg.content,
|
contentType: typeof content,
|
||||||
contentValue: msg.content,
|
contentValue: content,
|
||||||
contentLength: msg.content?.length || 0,
|
contentLength: content.length,
|
||||||
hasContent: !!msg.content,
|
hasContent: !!content,
|
||||||
contentPreview: msg.content?.substring(0, 100) + (msg.content?.length > 100 ? '...' : ''),
|
contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
|
||||||
fileCount: msg.fileIds?.length || 0,
|
fileCount: msg.fileIds?.length || 0,
|
||||||
allKeys: Object.keys(msg)
|
allKeys: Object.keys(msg)
|
||||||
});
|
});
|
||||||
|
|
@ -200,7 +212,7 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
}
|
}
|
||||||
}, [transformedMessages.length, scrollToBottom]);
|
}, [transformedMessages.length, scrollToBottom]);
|
||||||
|
|
||||||
const { currentWorkflowId, workflow, isLoading, error, isPolling } = workflowState;
|
const { currentWorkflowId, workflow, isLoading, error } = workflowState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -255,7 +267,7 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
<strong>Status:</strong> {workflow.status}
|
<strong>Status:</strong> {workflow.status}
|
||||||
{workflow.currentRound && ` (Round ${workflow.currentRound})`}
|
{workflow.currentRound && ` (Round ${workflow.currentRound})`}
|
||||||
{/* Show polling indicator */}
|
{/* Show polling indicator */}
|
||||||
{isPolling && (
|
{workflow && ['running', 'processing', 'started'].includes(workflow.status) && (
|
||||||
<span style={{
|
<span style={{
|
||||||
marginLeft: '8px',
|
marginLeft: '8px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
|
|
|
||||||
|
|
@ -200,11 +200,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button_primary:disabled {
|
.button_primary:disabled {
|
||||||
background-color: var(--color-gray-disabled);
|
background-color: var(--color-secondary-disabled);
|
||||||
border: 1px solid var(--color-gray-disabled);
|
border: 1px solid var(--color-secondary-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
border: 1px solid var(--color-gray-disabled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export interface DashboardChatAreaProps {
|
||||||
selectedPrompt?: Prompt | null;
|
selectedPrompt?: Prompt | null;
|
||||||
onPromptUsed?: () => void;
|
workflowState: WorkflowState;
|
||||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
workflowActions: WorkflowActions;
|
||||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
|
||||||
resumeWorkflowId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
export interface WorkflowMessage {
|
||||||
id: string;
|
id: string;
|
||||||
|
workflowId: string;
|
||||||
|
parentMessageId?: string;
|
||||||
|
documents?: WorkflowDocument[];
|
||||||
|
documentsLabel?: string;
|
||||||
|
message: string;
|
||||||
|
content?: string; // For backward compatibility
|
||||||
role: 'user' | 'assistant' | 'system';
|
role: 'user' | 'assistant' | 'system';
|
||||||
content: string;
|
status: string;
|
||||||
timestamp?: string;
|
sequenceNr: number;
|
||||||
fileIds?: 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 {
|
export interface FileInfo {
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,8 @@
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
||||||
|
import { WorkflowState, WorkflowActions } from './dashboardChatAreaTypes';
|
||||||
|
|
||||||
export interface WorkflowManagerState {
|
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowState, WorkflowActions] {
|
||||||
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);
|
|
||||||
|
|
||||||
// Core state
|
// Core state
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
|
||||||
const [isPolling, setIsPolling] = useState(false);
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
|
@ -46,27 +26,19 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
// Auto-polling for active workflows and message updates
|
// Auto-polling for active workflows and message updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPolling && currentWorkflowId) {
|
if (isPolling && currentWorkflowId) {
|
||||||
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
|
|
||||||
|
|
||||||
pollingIntervalRef.current = window.setInterval(() => {
|
pollingIntervalRef.current = window.setInterval(() => {
|
||||||
console.log('🔄 Auto-polling status...');
|
|
||||||
// Always poll for status to detect workflow state changes
|
|
||||||
refetchStatus();
|
refetchStatus();
|
||||||
|
|
||||||
// Only poll messages if workflow is running
|
|
||||||
if (currentWorkflow) {
|
if (currentWorkflow) {
|
||||||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
console.log('🔄 Workflow is active, also polling messages...');
|
|
||||||
refetchMessages();
|
refetchMessages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000); // Poll every 2 seconds for smoother updates
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (pollingIntervalRef.current) {
|
if (pollingIntervalRef.current) {
|
||||||
console.log('🛑 Stopping auto-polling');
|
|
||||||
clearInterval(pollingIntervalRef.current);
|
clearInterval(pollingIntervalRef.current);
|
||||||
pollingIntervalRef.current = null;
|
pollingIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
@ -75,14 +47,10 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const loadWorkflow = useCallback(async (workflowId: string) => {
|
const loadWorkflow = useCallback(async (workflowId: string) => {
|
||||||
console.log(`📂 Loading workflow: ${workflowId}`);
|
|
||||||
setCurrentWorkflowId(workflowId);
|
setCurrentWorkflowId(workflowId);
|
||||||
// The hooks will automatically fetch data when workflowId changes
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startNewWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<string | null> => {
|
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 = {
|
const workflowData: StartWorkflowRequest = {
|
||||||
prompt,
|
prompt,
|
||||||
listFileId: fileIds
|
listFileId: fileIds
|
||||||
|
|
@ -92,30 +60,17 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const newWorkflowId = result.data.id;
|
const newWorkflowId = result.data.id;
|
||||||
console.log(`✅ New workflow started: ${newWorkflowId}`);
|
|
||||||
setCurrentWorkflowId(newWorkflowId);
|
setCurrentWorkflowId(newWorkflowId);
|
||||||
setIsPolling(true); // Start polling immediately for new workflows
|
setIsPolling(true);
|
||||||
|
setTimeout(() => refetchMessages(), 500);
|
||||||
// Also immediately fetch messages to get the user's message
|
|
||||||
setTimeout(() => {
|
|
||||||
refetchMessages();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return newWorkflowId;
|
return newWorkflowId;
|
||||||
} else {
|
|
||||||
console.error('❌ Failed to start workflow:', result.error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}, [startWorkflow, refetchMessages]);
|
}, [startWorkflow, refetchMessages]);
|
||||||
|
|
||||||
const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<boolean> => {
|
const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<boolean> => {
|
||||||
if (!currentWorkflowId) {
|
if (!currentWorkflowId) return false;
|
||||||
console.error('❌ Cannot continue workflow: no current workflow ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`➡️ Continuing workflow ${currentWorkflowId} with prompt:`, prompt.substring(0, 50) + '...');
|
|
||||||
|
|
||||||
const workflowData: StartWorkflowRequest = {
|
const workflowData: StartWorkflowRequest = {
|
||||||
prompt,
|
prompt,
|
||||||
listFileId: fileIds
|
listFileId: fileIds
|
||||||
|
|
@ -124,71 +79,39 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const result = await startWorkflow(workflowData, currentWorkflowId);
|
const result = await startWorkflow(workflowData, currentWorkflowId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`✅ Workflow ${currentWorkflowId} continued`);
|
setIsPolling(true);
|
||||||
setIsPolling(true); // Ensure polling is enabled
|
setTimeout(() => refetchMessages(), 500);
|
||||||
|
|
||||||
// Immediately start polling for new messages
|
|
||||||
setTimeout(() => {
|
|
||||||
refetchMessages();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
console.error('❌ Failed to continue workflow:', result.error);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
||||||
|
|
||||||
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
||||||
if (!currentWorkflowId) {
|
if (!currentWorkflowId) return false;
|
||||||
console.error('❌ Cannot stop workflow: no current workflow ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🛑 Stopping workflow ${currentWorkflowId}`);
|
const result = await stopWorkflowRequest(currentWorkflowId);
|
||||||
|
if (result) {
|
||||||
const success = await stopWorkflowRequest(currentWorkflowId);
|
// Immediately refresh status to reflect the stopped state
|
||||||
|
|
||||||
if (success) {
|
|
||||||
console.log(`✅ Workflow ${currentWorkflowId} stopped`);
|
|
||||||
setIsPolling(false); // Stop polling immediately
|
|
||||||
|
|
||||||
// Refresh workflow status to get the updated status
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refetchStatus();
|
refetchStatus();
|
||||||
|
refetchMessages();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
console.error('❌ Failed to stop workflow');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus]);
|
return false;
|
||||||
|
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages]);
|
||||||
|
|
||||||
const clearWorkflow = useCallback(() => {
|
const clearWorkflow = useCallback(() => {
|
||||||
console.log('🧹 Clearing workflow');
|
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
setIsPolling(false);
|
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
|
// Sync with external workflow ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialWorkflowId !== currentWorkflowId) {
|
if (initialWorkflowId !== currentWorkflowId) {
|
||||||
if (initialWorkflowId) {
|
if (initialWorkflowId) {
|
||||||
console.log(`🔄 useWorkflowManager: Loading workflow ${initialWorkflowId}`);
|
|
||||||
loadWorkflow(initialWorkflowId);
|
loadWorkflow(initialWorkflowId);
|
||||||
} else {
|
} else {
|
||||||
console.log(`🧹 useWorkflowManager: Clearing workflow due to null initialWorkflowId`);
|
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
setIsPolling(false);
|
setIsPolling(false);
|
||||||
}
|
}
|
||||||
|
|
@ -199,39 +122,27 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentWorkflowId && currentWorkflow) {
|
if (currentWorkflowId && currentWorkflow) {
|
||||||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||||
|
setIsPolling(isActive);
|
||||||
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 {
|
} else {
|
||||||
// No workflow ID or no workflow data, stop polling
|
|
||||||
console.log(`🛑 No workflow or workflow data, disabling polling`);
|
|
||||||
setIsPolling(false);
|
setIsPolling(false);
|
||||||
}
|
}
|
||||||
}, [currentWorkflowId, currentWorkflow?.status]);
|
}, [currentWorkflowId, currentWorkflow?.status]);
|
||||||
|
|
||||||
const state: WorkflowManagerState = {
|
const state: WorkflowState = {
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
workflow: currentWorkflow,
|
workflow: currentWorkflow,
|
||||||
messages,
|
messages,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error
|
||||||
isPolling
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions: WorkflowManagerActions = useMemo(() => ({
|
const actions: WorkflowActions = useMemo(() => ({
|
||||||
loadWorkflow,
|
loadWorkflow,
|
||||||
startNewWorkflow,
|
startNewWorkflow,
|
||||||
continueWorkflow,
|
continueWorkflow,
|
||||||
stopWorkflow,
|
stopWorkflow,
|
||||||
clearWorkflow,
|
clearWorkflow
|
||||||
refreshMessages,
|
}), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow]);
|
||||||
setPolling: setPollingEnabled
|
|
||||||
}), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, refreshMessages, setPollingEnabled]);
|
|
||||||
|
|
||||||
return [state, actions];
|
return [state, actions];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,9 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useApiRequest } from './useApi';
|
import { useApiRequest } from './useApi';
|
||||||
|
|
||||||
// Workflow interfaces (matching backend ChatWorkflow model)
|
// Import the centralized workflow interfaces
|
||||||
export interface Workflow {
|
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
||||||
id: string;
|
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument };
|
||||||
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[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StartWorkflowRequest {
|
export interface StartWorkflowRequest {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,19 @@ export default {
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows',
|
'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows',
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'Keine Workflows verfügbar',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Prompts werden geladen...',
|
'promptset.loading': 'Prompts werden geladen...',
|
||||||
'promptset.error.loading': 'Fehler beim Laden der Prompts',
|
'promptset.error.loading': 'Fehler beim Laden der Prompts',
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,19 @@ export default {
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Available Workflows',
|
'dashboard.workflow_dropdown.available_workflows': 'Available Workflows',
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'No workflows available',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Loading prompts...',
|
'promptset.loading': 'Loading prompts...',
|
||||||
'promptset.error.loading': 'Error loading prompts',
|
'promptset.error.loading': 'Error loading prompts',
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,19 @@ export default {
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles',
|
'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles',
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'Aucun workflow disponible',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Chargement des prompts...',
|
'promptset.loading': 'Chargement des prompts...',
|
||||||
'promptset.error.loading': 'Erreur lors du chargement des prompts',
|
'promptset.error.loading': 'Erreur lors du chargement des prompts',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io';
|
import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io';
|
||||||
import { Prompt } from '../../hooks/usePrompts';
|
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
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 styles from './HomeStyles/Dashboard.module.css'
|
||||||
import sharedStyles from '../../components/PageManager/pages.module.css';
|
import sharedStyles from '../../components/PageManager/pages.module.css';
|
||||||
|
|
||||||
|
|
@ -10,32 +11,25 @@ import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardCha
|
||||||
|
|
||||||
function Dashboard () {
|
function Dashboard () {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
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 [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Central workflow management
|
||||||
|
const [workflowState, workflowActions] = useWorkflowManager();
|
||||||
|
|
||||||
// Fetch workflows for dropdown
|
// Fetch workflows for dropdown
|
||||||
const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows();
|
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) => {
|
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
||||||
console.log('📋 Dashboard.handleWorkflowSelect called with:', workflowId);
|
workflowActions.loadWorkflow(workflowId);
|
||||||
// Set the workflow ID when selected from dropdown
|
|
||||||
setCurrentWorkflowId(workflowId);
|
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
}, []);
|
}, [workflowActions]);
|
||||||
|
|
||||||
|
const handleResetWorkflow = useCallback(() => {
|
||||||
|
workflowActions.clearWorkflow();
|
||||||
|
setSelectedPrompt(null);
|
||||||
|
}, [workflowActions]);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -44,33 +38,99 @@ function Dashboard () {
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => {
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Helper functions for workflow dropdown
|
const getWorkflowDisplayName = (workflow: Workflow) =>
|
||||||
const getWorkflowDisplayName = (workflow: Workflow) => {
|
workflow.name || `${workflow.id.substring(0, 8)}...`;
|
||||||
// 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) => {
|
const formatWorkflowId = (id: string) => `${id.substring(0, 8)}...`;
|
||||||
return `${id.substring(0, 8)}...`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetWorkflow = useCallback(() => {
|
|
||||||
setCurrentWorkflowId(null);
|
|
||||||
setSelectedPrompt(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Format workflow ID for display
|
// Format workflow ID for display
|
||||||
const displayWorkflowId = currentWorkflowId ?
|
const displayWorkflowId = workflowState.currentWorkflowId ?
|
||||||
`${currentWorkflowId.substring(0, 8)}...` :
|
`${workflowState.currentWorkflowId.substring(0, 8)}...` :
|
||||||
t('dashboard.log.no_workflow');
|
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 (
|
return (
|
||||||
<div className={sharedStyles.pageContainer}>
|
<div className={sharedStyles.pageContainer}>
|
||||||
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
|
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
|
||||||
|
|
@ -80,13 +140,87 @@ function Dashboard () {
|
||||||
<div className={sharedStyles.pageHeader}>
|
<div className={sharedStyles.pageHeader}>
|
||||||
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
||||||
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
<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' }}>
|
const stats = getWorkflowStats();
|
||||||
{t('dashboard.log.workflow')}: {displayWorkflowId}
|
return (
|
||||||
</span>
|
<div style={{
|
||||||
</div>
|
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
|
<button
|
||||||
className={sharedStyles.secondaryButton}
|
className={sharedStyles.secondaryButton}
|
||||||
onClick={handleResetWorkflow}
|
onClick={handleResetWorkflow}
|
||||||
|
|
@ -218,15 +352,12 @@ function Dashboard () {
|
||||||
<div className={sharedStyles.horizontalDivider}></div>
|
<div className={sharedStyles.horizontalDivider}></div>
|
||||||
|
|
||||||
<div className={styles.dashboardContentArea}>
|
<div className={styles.dashboardContentArea}>
|
||||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
<div className={styles.chatLogContainer}>
|
||||||
<div className={styles.chatArea}>
|
<div className={styles.chatArea}>
|
||||||
<DashboardChat
|
<DashboardChat
|
||||||
isExpanded={isChatExpanded}
|
|
||||||
onToggleExpand={handleChatToggleExpand}
|
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={() => setSelectedPrompt(null)}
|
workflowState={workflowState}
|
||||||
onWorkflowIdChange={handleWorkflowIdChange}
|
workflowActions={workflowActions}
|
||||||
currentWorkflowId={currentWorkflowId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue