smoother progress bar and message rendering
This commit is contained in:
parent
18d03e4e78
commit
574e3e1cfa
10 changed files with 586 additions and 613 deletions
|
|
@ -1,27 +1,12 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import FileAttachmentPopup from './FileAttachmentPopup';
|
import FileAttachmentPopup from './FileAttachmentPopup';
|
||||||
import { Prompt, WorkflowState, WorkflowActions } from './dashboardChatAreaTypes';
|
import { Prompt, WorkflowState, WorkflowActions, InputAreaProps, AttachedFile } 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';
|
||||||
import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
interface InputAreaProps {
|
|
||||||
selectedPrompt?: Prompt | null;
|
|
||||||
workflowState: WorkflowState;
|
|
||||||
workflowActions: WorkflowActions;
|
|
||||||
attachedFiles?: AttachedFile[];
|
|
||||||
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttachedFile {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
type: string;
|
|
||||||
fileData?: File;
|
|
||||||
objectUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InputArea: React.FC<InputAreaProps> = ({
|
const InputArea: React.FC<InputAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
|
|
@ -91,12 +76,8 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
let success = false;
|
let success = false;
|
||||||
|
|
||||||
if (workflowState.currentWorkflowId) {
|
if (workflowState.currentWorkflowId) {
|
||||||
// Continue existing workflow
|
|
||||||
console.log(`➡️ Continuing workflow ${workflowState.currentWorkflowId}`);
|
|
||||||
success = await workflowActions.continueWorkflow(inputValue, fileIds);
|
success = await workflowActions.continueWorkflow(inputValue, fileIds);
|
||||||
} else {
|
} else {
|
||||||
// Start new workflow
|
|
||||||
console.log('🚀 Starting new workflow');
|
|
||||||
const newWorkflowId = await workflowActions.startNewWorkflow(inputValue, fileIds);
|
const newWorkflowId = await workflowActions.startNewWorkflow(inputValue, fileIds);
|
||||||
success = !!newWorkflowId;
|
success = !!newWorkflowId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,42 +106,6 @@ const LogItem: React.FC<LogItemProps> = ({ log }) => {
|
||||||
Progress: {log.progress}%
|
Progress: {log.progress}%
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(log.details || log.performance) && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDetails(!showDetails)}
|
|
||||||
style={{
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
color: 'var(--color-secondary)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '11px',
|
|
||||||
padding: '4px 0',
|
|
||||||
textDecoration: 'underline'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showDetails ? 'Hide Details' : 'Show Details'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showDetails && (
|
|
||||||
<div className={messageStyles.log_details}>
|
|
||||||
{log.details && (
|
|
||||||
<div>
|
|
||||||
<strong>Details:</strong>
|
|
||||||
<pre>{JSON.stringify(log.details, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{log.performance && (
|
|
||||||
<div>
|
|
||||||
<strong>Performance:</strong>
|
|
||||||
<pre>{JSON.stringify(log.performance, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,480 +1,96 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MessageListProps } from './dashboardChatAreaTypes';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import MessageItem from './DashboardChatAreaMessageItem';
|
import MessageItem from './DashboardChatAreaMessageItem';
|
||||||
import LogItem from './DashboardChatAreaLogItem';
|
import LogItem from './DashboardChatAreaLogItem';
|
||||||
import { WorkflowMessage, Document, WorkflowState, WorkflowLog } from './dashboardChatAreaTypes';
|
import {
|
||||||
|
calculateWorkflowProgress,
|
||||||
|
mergeMessagesAndLogs,
|
||||||
|
transformWorkflowMessage
|
||||||
|
} from './dashboardChatAreaProgressBar';
|
||||||
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
|
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
|
||||||
|
import { IoIosArrowDown, IoIosChatbubbles } from 'react-icons/io';
|
||||||
|
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||||
|
|
||||||
// Helper function to parse task and action progress from log messages
|
|
||||||
const parseLogProgress = (logMessage: string): {
|
|
||||||
taskNumber?: number;
|
|
||||||
totalTasks?: number;
|
|
||||||
actionNumber?: number;
|
|
||||||
totalActions?: number;
|
|
||||||
isCompleted?: boolean;
|
|
||||||
type: 'task_start' | 'action_start' | 'action_complete' | 'task_complete' | 'unknown';
|
|
||||||
} => {
|
|
||||||
const message = logMessage.trim();
|
|
||||||
|
|
||||||
// Pattern: "Executing task X/Y"
|
|
||||||
const taskStartPattern = /^Executing task (\d+)\/(\d+)$/i;
|
|
||||||
const taskStartMatch = message.match(taskStartPattern);
|
|
||||||
if (taskStartMatch) {
|
|
||||||
return {
|
|
||||||
taskNumber: parseInt(taskStartMatch[1]),
|
|
||||||
totalTasks: parseInt(taskStartMatch[2]),
|
|
||||||
type: 'task_start'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern: "Task X - Starting action Y/Z"
|
|
||||||
const actionStartPattern = /^Task (\d+) - Starting action (\d+)\/(\d+)$/i;
|
|
||||||
const actionStartMatch = message.match(actionStartPattern);
|
|
||||||
if (actionStartMatch) {
|
|
||||||
return {
|
|
||||||
taskNumber: parseInt(actionStartMatch[1]),
|
|
||||||
actionNumber: parseInt(actionStartMatch[2]),
|
|
||||||
totalActions: parseInt(actionStartMatch[3]),
|
|
||||||
type: 'action_start'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern: "Task X - Action Y/Z completed" or "✅ Task X - Action Y/Z completed"
|
|
||||||
const actionCompletePattern = /^(?:✅\s+)?Task (\d+) - Action (\d+)\/(\d+) completed$/i;
|
|
||||||
const actionCompleteMatch = message.match(actionCompletePattern);
|
|
||||||
if (actionCompleteMatch) {
|
|
||||||
return {
|
|
||||||
taskNumber: parseInt(actionCompleteMatch[1]),
|
|
||||||
actionNumber: parseInt(actionCompleteMatch[2]),
|
|
||||||
totalActions: parseInt(actionCompleteMatch[3]),
|
|
||||||
isCompleted: true,
|
|
||||||
type: 'action_complete'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern: "Task X/Y completed" or "🎯 Task X/Y completed"
|
|
||||||
const taskCompletePattern = /^(?:🎯\s+)?Task (\d+)\/(\d+) completed$/i;
|
|
||||||
const taskCompleteMatch = message.match(taskCompletePattern);
|
|
||||||
if (taskCompleteMatch) {
|
|
||||||
return {
|
|
||||||
taskNumber: parseInt(taskCompleteMatch[1]),
|
|
||||||
totalTasks: parseInt(taskCompleteMatch[2]),
|
|
||||||
isCompleted: true,
|
|
||||||
type: 'task_complete'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { type: 'unknown' };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to analyze workflow progress from messages and logs
|
|
||||||
const analyzeWorkflowProgressFromMessages = (messages: any[], logs: any[] = []): {
|
|
||||||
current: number;
|
|
||||||
total: number;
|
|
||||||
percentage: number;
|
|
||||||
isLoading: boolean;
|
|
||||||
taskBreakdown?: string;
|
|
||||||
} | null => {
|
|
||||||
console.log('🚨 analyzeWorkflowProgressFromMessages called:', {
|
|
||||||
messagesLength: messages.length,
|
|
||||||
logsLength: logs.length,
|
|
||||||
logMessages: logs.map(l => l.message)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (messages.length === 0 && logs.length === 0) return null;
|
|
||||||
|
|
||||||
// Check if the last message is from user (indicating we're waiting for assistant response)
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
const isWaitingForAssistant = lastMessage?.role === 'user';
|
|
||||||
|
|
||||||
// If we're waiting for assistant response, show loading state
|
|
||||||
if (isWaitingForAssistant) {
|
|
||||||
return {
|
|
||||||
current: 0,
|
|
||||||
total: 0,
|
|
||||||
percentage: 0,
|
|
||||||
isLoading: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse logs first (they contain the structure info)
|
|
||||||
let totalTasks = 0;
|
|
||||||
const taskActionCounts: { [taskNumber: number]: { total: number; completedActions: Set<number> } } = {};
|
|
||||||
|
|
||||||
// Parse logs for structure information
|
|
||||||
logs.forEach((log) => {
|
|
||||||
if (!log.message) return;
|
|
||||||
|
|
||||||
const content = log.message.trim();
|
|
||||||
|
|
||||||
// Debug: log message content for analysis
|
|
||||||
if (content.includes('✅') || content.includes('🚀') || content.includes('⚡') || content.includes('🎯') || content.includes('Executing task') || content.includes('Starting action')) {
|
|
||||||
console.log('📊 Progress: Analyzing log content:', content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for task execution patterns: "Executing task X/Y"
|
|
||||||
const taskExecMatch = content.match(/Executing task (\d+)\/(\d+)/i);
|
|
||||||
if (taskExecMatch) {
|
|
||||||
const totalTasksFound = parseInt(taskExecMatch[2]);
|
|
||||||
if (totalTasksFound > totalTasks) {
|
|
||||||
totalTasks = totalTasksFound;
|
|
||||||
}
|
|
||||||
console.log('📊 Found total tasks:', totalTasksFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for action start patterns: "Task X - Starting action Y/Z"
|
|
||||||
const actionStartMatch = content.match(/Task (\d+) - Starting action (\d+)\/(\d+)/i);
|
|
||||||
if (actionStartMatch) {
|
|
||||||
const taskNumber = parseInt(actionStartMatch[1]);
|
|
||||||
const totalActions = parseInt(actionStartMatch[3]);
|
|
||||||
if (!taskActionCounts[taskNumber]) {
|
|
||||||
taskActionCounts[taskNumber] = { total: 0, completedActions: new Set() };
|
|
||||||
}
|
|
||||||
if (totalActions > taskActionCounts[taskNumber].total) {
|
|
||||||
taskActionCounts[taskNumber].total = totalActions;
|
|
||||||
}
|
|
||||||
console.log(`📊 Task ${taskNumber} has ${totalActions} actions`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for action completion patterns: "✅ Task X - Action Y/Z completed"
|
|
||||||
const actionCompleteMatch = content.match(/✅\s+Task (\d+) - Action (\d+)\/(\d+) completed/i);
|
|
||||||
if (actionCompleteMatch) {
|
|
||||||
const taskNumber = parseInt(actionCompleteMatch[1]);
|
|
||||||
const actionNumber = parseInt(actionCompleteMatch[2]);
|
|
||||||
const totalActions = parseInt(actionCompleteMatch[3]);
|
|
||||||
|
|
||||||
if (!taskActionCounts[taskNumber]) {
|
|
||||||
taskActionCounts[taskNumber] = { total: totalActions, completedActions: new Set() };
|
|
||||||
}
|
|
||||||
// Add this specific action to the completed set
|
|
||||||
taskActionCounts[taskNumber].completedActions.add(actionNumber);
|
|
||||||
// Update total if needed
|
|
||||||
if (totalActions > taskActionCounts[taskNumber].total) {
|
|
||||||
taskActionCounts[taskNumber].total = totalActions;
|
|
||||||
}
|
|
||||||
console.log(`📊 Task ${taskNumber} action ${actionNumber} completed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for task completion patterns: "🎯 Task X/Y completed"
|
|
||||||
const taskCompleteMatch = content.match(/🎯\s+Task (\d+)\/(\d+) completed/i);
|
|
||||||
if (taskCompleteMatch) {
|
|
||||||
const taskNumber = parseInt(taskCompleteMatch[1]);
|
|
||||||
const totalTasksFound = parseInt(taskCompleteMatch[2]);
|
|
||||||
if (totalTasksFound > totalTasks) {
|
|
||||||
totalTasks = totalTasksFound;
|
|
||||||
}
|
|
||||||
// Mark all actions for this task as completed
|
|
||||||
if (taskActionCounts[taskNumber]) {
|
|
||||||
const task = taskActionCounts[taskNumber];
|
|
||||||
for (let i = 1; i <= task.total; i++) {
|
|
||||||
task.completedActions.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`📊 Task ${taskNumber} completed (all actions)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also parse assistant messages for any additional patterns
|
|
||||||
messages.forEach((message) => {
|
|
||||||
if (message.role !== 'assistant' || !message.content) return;
|
|
||||||
|
|
||||||
const content = message.content.trim();
|
|
||||||
|
|
||||||
// Look for action completion in messages: "✅ Task X - Action document.generateReport completed"
|
|
||||||
const actionCompleteMatch = content.match(/✅\s+Task (\d+) - Action\s+(?:(\d+)\/(\d+)|.*completed)/i);
|
|
||||||
if (actionCompleteMatch) {
|
|
||||||
const taskNumber = parseInt(actionCompleteMatch[1]);
|
|
||||||
|
|
||||||
// If we have the action number format (e.g., "1/1")
|
|
||||||
if (actionCompleteMatch[2] && actionCompleteMatch[3]) {
|
|
||||||
const actionNumber = parseInt(actionCompleteMatch[2]);
|
|
||||||
const totalActions = parseInt(actionCompleteMatch[3]);
|
|
||||||
|
|
||||||
if (!taskActionCounts[taskNumber]) {
|
|
||||||
taskActionCounts[taskNumber] = { total: totalActions, completedActions: new Set() };
|
|
||||||
}
|
|
||||||
taskActionCounts[taskNumber].completedActions.add(actionNumber);
|
|
||||||
if (totalActions > taskActionCounts[taskNumber].total) {
|
|
||||||
taskActionCounts[taskNumber].total = totalActions;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If it's a named action without numbers, treat it as action 1/1 for this task
|
|
||||||
if (!taskActionCounts[taskNumber]) {
|
|
||||||
taskActionCounts[taskNumber] = { total: 1, completedActions: new Set() };
|
|
||||||
}
|
|
||||||
taskActionCounts[taskNumber].completedActions.add(1);
|
|
||||||
if (taskActionCounts[taskNumber].total === 0) {
|
|
||||||
taskActionCounts[taskNumber].total = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`📊 Message: Task ${taskNumber} action completed`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate completed tasks (a task is complete when all its actions are done)
|
|
||||||
let completedTasks = 0;
|
|
||||||
const taskStatuses = [];
|
|
||||||
|
|
||||||
// Check each task's completion status
|
|
||||||
for (let taskNum = 1; taskNum <= totalTasks; taskNum++) {
|
|
||||||
const task = taskActionCounts[taskNum];
|
|
||||||
if (task) {
|
|
||||||
const isTaskComplete = task.total > 0 && task.completedActions.size === task.total;
|
|
||||||
if (isTaskComplete) {
|
|
||||||
completedTasks++;
|
|
||||||
}
|
|
||||||
taskStatuses.push(`Task ${taskNum}: ${task.completedActions.size}/${task.total} actions`);
|
|
||||||
} else {
|
|
||||||
taskStatuses.push(`Task ${taskNum}: Not started`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📊 Final calculation (task-based):', {
|
|
||||||
totalTasks,
|
|
||||||
completedTasks,
|
|
||||||
taskActionCounts: Object.fromEntries(
|
|
||||||
Object.entries(taskActionCounts).map(([taskNum, task]) => [
|
|
||||||
taskNum,
|
|
||||||
{
|
|
||||||
total: task.total,
|
|
||||||
completed: task.completedActions.size,
|
|
||||||
completedActions: Array.from(task.completedActions),
|
|
||||||
isComplete: task.total > 0 && task.completedActions.size === task.total
|
|
||||||
}
|
|
||||||
])
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create task breakdown string
|
|
||||||
const taskBreakdown = taskStatuses.length > 0 ? taskStatuses.join(', ') : undefined;
|
|
||||||
|
|
||||||
// If we have total tasks, always use task-based progress
|
|
||||||
if (totalTasks > 0) {
|
|
||||||
const percentage = Math.round((completedTasks / totalTasks) * 100);
|
|
||||||
console.log(`📊 Task-based progress: ${completedTasks}/${totalTasks} tasks (${percentage}%)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
current: completedTasks,
|
|
||||||
total: totalTasks,
|
|
||||||
percentage: percentage,
|
|
||||||
isLoading: false,
|
|
||||||
taskBreakdown: taskBreakdown || `${completedTasks}/${totalTasks} tasks completed`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to safely parse timestamps
|
|
||||||
const safeParseDate = (timestamp: any, fallback: number = Date.now()): Date => {
|
|
||||||
if (!timestamp) {
|
|
||||||
console.warn(`⚠️ No timestamp provided, using fallback:`, { fallback });
|
|
||||||
return new Date(fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different timestamp formats
|
|
||||||
let dateToTry = timestamp;
|
|
||||||
|
|
||||||
// If it's a number, check if it's in seconds or milliseconds
|
|
||||||
if (typeof timestamp === 'number') {
|
|
||||||
// If it's a 10-digit number, it's likely seconds since epoch
|
|
||||||
if (timestamp < 10000000000) {
|
|
||||||
dateToTry = timestamp * 1000; // Convert seconds to milliseconds
|
|
||||||
} else {
|
|
||||||
dateToTry = timestamp; // Already in milliseconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it's a string that looks like a number, parse it and handle seconds/milliseconds
|
|
||||||
else if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) {
|
|
||||||
const numericTimestamp = parseInt(timestamp);
|
|
||||||
// If it's a 10-digit number, it's likely seconds since epoch
|
|
||||||
if (numericTimestamp < 10000000000) {
|
|
||||||
dateToTry = numericTimestamp * 1000; // Convert seconds to milliseconds
|
|
||||||
} else {
|
|
||||||
dateToTry = numericTimestamp; // Already in milliseconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it's already a Date object
|
|
||||||
else if (timestamp instanceof Date) {
|
|
||||||
dateToTry = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the timestamp
|
|
||||||
const date = new Date(dateToTry);
|
|
||||||
|
|
||||||
// Check if the date is valid
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
console.warn(`⚠️ Invalid timestamp detected:`, {
|
|
||||||
originalTimestamp: timestamp,
|
|
||||||
type: typeof timestamp,
|
|
||||||
processedTimestamp: dateToTry,
|
|
||||||
fallback
|
|
||||||
});
|
|
||||||
return new Date(fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to merge and sort messages and logs by timestamp
|
|
||||||
const mergeMessagesAndLogs = (messages: any[], logs: WorkflowLog[]): Array<{type: 'message' | 'log', item: any, timestamp: Date}> => {
|
|
||||||
const combined: Array<{type: 'message' | 'log', item: any, timestamp: Date}> = [];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add messages
|
|
||||||
messages.forEach((message, index) => {
|
|
||||||
const rawTimestamp = message.timestamp || message.publishedAt;
|
|
||||||
|
|
||||||
// Use current time minus index for fallback to maintain order
|
|
||||||
const fallbackTime = Date.now() - ((messages.length + logs.length) - index) * 1000;
|
|
||||||
const timestamp = safeParseDate(rawTimestamp, fallbackTime);
|
|
||||||
|
|
||||||
combined.push({
|
|
||||||
type: 'message',
|
|
||||||
item: message,
|
|
||||||
timestamp
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add logs
|
|
||||||
logs.forEach((log, index) => {
|
|
||||||
|
|
||||||
// Use current time minus index for fallback to maintain order
|
|
||||||
const fallbackTime = Date.now() - ((logs.length) - index) * 1000;
|
|
||||||
const timestamp = safeParseDate(log.timestamp, fallbackTime);
|
|
||||||
|
|
||||||
combined.push({
|
|
||||||
type: 'log',
|
|
||||||
item: log,
|
|
||||||
timestamp
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort by timestamp (chronological order)
|
|
||||||
combined.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
||||||
|
|
||||||
|
|
||||||
return combined;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MessageListProps {
|
|
||||||
workflowState: WorkflowState;
|
|
||||||
onFilePreview?: (file: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to transform WorkflowMessage to display Message
|
|
||||||
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
|
|
||||||
let documents: Document[] = [];
|
|
||||||
|
|
||||||
// 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: typeof doc.fileId === 'string' ? parseInt(doc.fileId) : 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
|
|
||||||
|
|
||||||
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
|
||||||
try {
|
|
||||||
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
|
|
||||||
const response = await request({
|
|
||||||
url: `/api/workflows/files/${fileId}/preview`,
|
|
||||||
method: 'get'
|
|
||||||
});
|
|
||||||
|
|
||||||
const document: Document = {
|
|
||||||
id: fileId.toString(),
|
|
||||||
fileId: fileId,
|
|
||||||
name: response.name || response.fileName || `File_${fileId}`,
|
|
||||||
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
|
|
||||||
type: response.mimeType || response.type || 'application/octet-stream',
|
|
||||||
size: response.size || 0,
|
|
||||||
downloadUrl: response.downloadUrl || response.url
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`✅ File ${fileId} metadata processed:`, document.name);
|
|
||||||
return document;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
|
|
||||||
// Return a fallback object for failed requests
|
|
||||||
return {
|
|
||||||
id: fileId.toString(),
|
|
||||||
fileId: fileId,
|
|
||||||
name: `File_${fileId}`,
|
|
||||||
ext: 'unknown',
|
|
||||||
type: 'application/octet-stream',
|
|
||||||
size: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
documents = await Promise.all(documentPromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
const transformedMessage = {
|
|
||||||
id: workflowMessage.id,
|
|
||||||
role: workflowMessage.role,
|
|
||||||
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
|
|
||||||
content: possibleContent,
|
|
||||||
timestamp: workflowMessage.publishedAt || workflowMessage.timestamp,
|
|
||||||
documents: documents
|
|
||||||
};
|
|
||||||
|
|
||||||
return transformedMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageList: React.FC<MessageListProps> = ({
|
const MessageList: React.FC<MessageListProps> = ({
|
||||||
workflowState,
|
workflowState,
|
||||||
onFilePreview
|
onFilePreview
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
const [transformedMessages, setTransformedMessages] = React.useState<any[]>([]);
|
const [transformedMessages, setTransformedMessages] = React.useState<any[]>([]);
|
||||||
const [isTransforming, setIsTransforming] = React.useState(false);
|
const [isTransforming, setIsTransforming] = React.useState(false);
|
||||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
|
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
|
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
|
||||||
const lastMessageCountRef = React.useRef(0);
|
const lastMessageCountRef = React.useRef(0);
|
||||||
const [workflowProgress, setWorkflowProgress] = React.useState<{
|
const [showProgressBar, setShowProgressBar] = React.useState(false);
|
||||||
current: number;
|
const [progressText, setProgressText] = React.useState('');
|
||||||
total: number;
|
const [progressPercentage, setProgressPercentage] = React.useState(0);
|
||||||
percentage: number;
|
const [maxCompletedTasks, setMaxCompletedTasks] = React.useState(0);
|
||||||
isLoading: boolean;
|
|
||||||
taskBreakdown?: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
// Calculate workflow progress when messages or logs change
|
// ONE single effect to handle all progress logic
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('🔄 Progress calculation triggered:', {
|
const pendingMessages = workflowState.pendingMessages || [];
|
||||||
messagesCount: transformedMessages.length,
|
const logs = workflowState.logs || [];
|
||||||
logsCount: workflowState.logs?.length || 0,
|
|
||||||
logs: workflowState.logs?.map(l => l.message) || []
|
|
||||||
});
|
|
||||||
|
|
||||||
const progress = analyzeWorkflowProgressFromMessages(transformedMessages, workflowState.logs || []);
|
// Show progress bar if user sent message or logs exist
|
||||||
|
if ((pendingMessages.length > 0 || logs.length > 0) && !showProgressBar) {
|
||||||
|
setShowProgressBar(true);
|
||||||
|
setProgressPercentage(0);
|
||||||
|
setMaxCompletedTasks(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📊 Progress result:', progress);
|
// Update text based on current state
|
||||||
|
if (logs.length === 0 && pendingMessages.length > 0) {
|
||||||
|
setProgressText(t('chat.messages.loading_progress'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setWorkflowProgress(progress);
|
if (logs.length > 0) {
|
||||||
}, [transformedMessages.length, JSON.stringify(transformedMessages.map(m => m.content)), workflowState.logs?.length, JSON.stringify(workflowState.logs?.map(l => l.message))]);
|
let totalTasks = 0;
|
||||||
|
let completedTasks = 0;
|
||||||
|
|
||||||
|
logs.forEach(log => {
|
||||||
|
const message = log.message || '';
|
||||||
|
|
||||||
|
const taskStartMatch = message.match(/Executing task (\d+)\/(\d+)/i);
|
||||||
|
if (taskStartMatch) {
|
||||||
|
totalTasks = Math.max(totalTasks, parseInt(taskStartMatch[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskCompleteMatch = message.match(/✅.*Task (\d+).*completed/i);
|
||||||
|
if (taskCompleteMatch) {
|
||||||
|
completedTasks = Math.max(completedTasks, parseInt(taskCompleteMatch[1]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalTasks > 0) {
|
||||||
|
const currentPercentage = Math.round((completedTasks / totalTasks) * 100);
|
||||||
|
setProgressText(`${completedTasks}/${totalTasks} ${t('chat.messages.tasks')} (${currentPercentage}%)`);
|
||||||
|
|
||||||
|
// ONLY update percentage if we completed more tasks than before
|
||||||
|
if (completedTasks > maxCompletedTasks) {
|
||||||
|
setMaxCompletedTasks(completedTasks);
|
||||||
|
setProgressPercentage(currentPercentage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setProgressText(`${t('chat.messages.analyzing_workflow')} (${logs.length} ${t('chat.messages.logs')})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [workflowState.pendingMessages, workflowState.logs, showProgressBar, maxCompletedTasks, t]);
|
||||||
|
|
||||||
|
// Clear progress bar when workflow changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!workflowState.currentWorkflowId) {
|
||||||
|
setShowProgressBar(false);
|
||||||
|
setProgressText('');
|
||||||
|
setProgressPercentage(0);
|
||||||
|
setMaxCompletedTasks(0);
|
||||||
|
}
|
||||||
|
}, [workflowState.currentWorkflowId]);
|
||||||
|
|
||||||
// Create merged timeline of messages and logs
|
|
||||||
const timeline = React.useMemo(() => {
|
const timeline = React.useMemo(() => {
|
||||||
return mergeMessagesAndLogs(transformedMessages, workflowState.logs || []);
|
return mergeMessagesAndLogs(transformedMessages, workflowState.logs || []);
|
||||||
}, [transformedMessages, workflowState.logs]);
|
}, [transformedMessages, workflowState.logs]);
|
||||||
|
|
@ -482,23 +98,33 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
// Transform messages when workflow messages change
|
// Transform messages when workflow messages change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const transformMessages = async () => {
|
const transformMessages = async () => {
|
||||||
if (!workflowState.messages || workflowState.messages.length === 0) {
|
// Use pending messages for immediate display, backend messages for confirmed content
|
||||||
|
// Since the workflow manager already filters out duplicate user messages,
|
||||||
|
// we need to combine them properly: pending first, then backend messages
|
||||||
|
const pendingMessages = workflowState.pendingMessages || [];
|
||||||
|
const backendMessages = workflowState.messages || [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Create a simple combined list - pending messages will be user messages,
|
||||||
|
// backend messages will be mostly assistant messages (user messages filtered out)
|
||||||
|
const allMessages = [...pendingMessages, ...backendMessages];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (allMessages.length === 0) {
|
||||||
setTransformedMessages([]);
|
setTransformedMessages([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsTransforming(true);
|
setIsTransforming(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transformed = await Promise.all(
|
const transformed = await Promise.all(
|
||||||
workflowState.messages.map(async (msg: WorkflowMessage) => {
|
allMessages.map(msg => transformWorkflowMessage(msg, request))
|
||||||
|
|
||||||
return await transformWorkflowMessage(msg, request);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
setTransformedMessages(transformed);
|
setTransformedMessages(transformed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error transforming messages:', error);
|
console.error('Error transforming messages:', error);
|
||||||
setTransformedMessages([]);
|
setTransformedMessages([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsTransforming(false);
|
setIsTransforming(false);
|
||||||
|
|
@ -506,23 +132,18 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
transformMessages();
|
transformMessages();
|
||||||
}, [workflowState.messages, request]);
|
}, [workflowState.messages, workflowState.pendingMessages, request]);
|
||||||
|
|
||||||
// Check if user is scrolled near the bottom
|
// Scroll handling
|
||||||
const checkScrollPosition = React.useCallback(() => {
|
const checkScrollPosition = React.useCallback(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
|
setIsUserScrolledUp(distanceFromBottom > 100);
|
||||||
// Consider "near bottom" if within 100px of the bottom
|
|
||||||
const isNearBottom = distanceFromBottom < 100;
|
|
||||||
setIsUserScrolledUp(!isNearBottom);
|
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Scroll to bottom function
|
|
||||||
const scrollToBottom = React.useCallback(() => {
|
const scrollToBottom = React.useCallback(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
|
|
@ -530,34 +151,27 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Auto-scroll when new items arrive (only if user is near bottom)
|
// Auto-scroll on new messages
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const currentTimelineCount = timeline.length;
|
const currentCount = timeline.length;
|
||||||
const hadItems = lastMessageCountRef.current > 0;
|
const hasNewItems = currentCount > lastMessageCountRef.current;
|
||||||
const hasNewItems = currentTimelineCount > lastMessageCountRef.current;
|
|
||||||
|
|
||||||
if (hasNewItems && hadItems && !isUserScrolledUp) {
|
if (hasNewItems && !isUserScrolledUp) {
|
||||||
// Small delay to ensure DOM is updated
|
|
||||||
setTimeout(scrollToBottom, 100);
|
setTimeout(scrollToBottom, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessageCountRef.current = currentTimelineCount;
|
lastMessageCountRef.current = currentCount;
|
||||||
}, [timeline.length, isUserScrolledUp, scrollToBottom]);
|
}, [timeline.length, isUserScrolledUp, scrollToBottom]);
|
||||||
|
|
||||||
// Scroll to bottom on initial load
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (timeline.length > 0 && lastMessageCountRef.current === 0) {
|
|
||||||
setTimeout(scrollToBottom, 100);
|
|
||||||
}
|
|
||||||
}, [timeline.length, scrollToBottom]);
|
|
||||||
|
|
||||||
const { currentWorkflowId, isLoading, error } = workflowState;
|
const { currentWorkflowId, isLoading, error } = workflowState;
|
||||||
|
|
||||||
|
const isEmpty = timeline.length === 0 && !isLoading && !isTransforming && !currentWorkflowId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={messageStyles.message_list_container}>
|
<div className={messageStyles.message_list_container}>
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className={messageStyles.chat_messages}
|
className={isEmpty ? messageStyles.chat_messages_empty : messageStyles.chat_messages}
|
||||||
onScroll={checkScrollPosition}
|
onScroll={checkScrollPosition}
|
||||||
>
|
>
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -565,28 +179,25 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<div className={messageStyles.messages_container}>
|
<div className={messageStyles.messages_container}>
|
||||||
{timeline.map((timelineItem, index) => {
|
{timeline.map((timelineItem, index) => {
|
||||||
if (timelineItem.type === 'message') {
|
const { type, item } = timelineItem;
|
||||||
const message = timelineItem.item;
|
|
||||||
|
|
||||||
|
if (type === 'message') {
|
||||||
return (
|
return (
|
||||||
<MessageItem
|
<MessageItem
|
||||||
key={`message-${message.id}`}
|
key={`message-${item.id}`}
|
||||||
message={message}
|
message={item}
|
||||||
index={index}
|
index={index}
|
||||||
onFilePreview={onFilePreview}
|
onFilePreview={onFilePreview}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (timelineItem.type === 'log') {
|
} else if (type === 'log') {
|
||||||
const log = timelineItem.item;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogItem
|
<LogItem
|
||||||
key={`log-${log.id}`}
|
key={`log-${item.id}`}
|
||||||
log={log}
|
log={item}
|
||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -595,82 +206,41 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(isLoading || isTransforming) && (
|
{isEmpty && (
|
||||||
<div className={messageStyles.message_loading}>
|
|
||||||
{isTransforming ? 'Processing messages...' : 'Loading messages...'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{timeline.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
|
|
||||||
<div className={messageStyles.message_empty_state}>
|
<div className={messageStyles.message_empty_state}>
|
||||||
No workflow selected. Start a conversation to create a new workflow.
|
<div className={messageStyles.message_empty_state_icon}>
|
||||||
|
<IoIosChatbubbles />
|
||||||
|
</div>
|
||||||
|
<h3>{t('chat.messages.no_workflow_selected')}</h3>
|
||||||
|
<p>{t('chat.messages.no_workflow_selected_description')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{timeline.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
|
|
||||||
<div className={messageStyles.message_empty_state}>
|
|
||||||
No messages or logs in this workflow yet.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workflow Progress Bar - positioned outside scrollable area */}
|
{/* Progress Bar */}
|
||||||
{(workflowProgress || workflowState.logs?.length > 0) && (
|
{showProgressBar && (
|
||||||
<div className={messageStyles.workflow_progress_container}>
|
<div className={messageStyles.workflow_progress_container}>
|
||||||
{workflowProgress ? (
|
|
||||||
<>
|
|
||||||
<div className={messageStyles.workflow_progress_label}>
|
<div className={messageStyles.workflow_progress_label}>
|
||||||
<span>Workflow Progress</span>
|
<span>{t('chat.messages.workflow_progress')}</span>
|
||||||
<span>
|
<span>{progressText}</span>
|
||||||
{workflowProgress.isLoading
|
|
||||||
? 'Loading tasks...'
|
|
||||||
: `${workflowProgress.current}/${workflowProgress.total} Actions (${workflowProgress.percentage}%)`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{workflowProgress.taskBreakdown && !workflowProgress.isLoading && (
|
|
||||||
<div className={messageStyles.workflow_progress_breakdown}>
|
|
||||||
{workflowProgress.taskBreakdown}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={messageStyles.workflow_progress_bar}>
|
|
||||||
<div
|
|
||||||
className={`${messageStyles.workflow_progress_fill} ${
|
|
||||||
workflowProgress.isLoading ? messageStyles.loading : ''
|
|
||||||
}`}
|
|
||||||
style={{ width: workflowProgress.isLoading ? '0%' : `${workflowProgress.percentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={messageStyles.workflow_progress_label}>
|
|
||||||
<span>Workflow Progress</span>
|
|
||||||
<span>Analyzing workflow... ({workflowState.logs?.length || 0} logs)</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={messageStyles.workflow_progress_bar}>
|
<div className={messageStyles.workflow_progress_bar}>
|
||||||
<div
|
<div
|
||||||
className={`${messageStyles.workflow_progress_fill} ${messageStyles.loading}`}
|
className={messageStyles.workflow_progress_fill}
|
||||||
style={{ width: '0%' }}
|
style={{ width: `${progressPercentage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scroll to bottom button - positioned relative to message list container */}
|
|
||||||
<button
|
<button
|
||||||
className={`${messageStyles.scroll_to_bottom_btn} ${
|
className={`${messageStyles.scroll_to_bottom_btn} ${
|
||||||
(isUserScrolledUp && timeline.length > 0)
|
(isUserScrolledUp && timeline.length > 0) ? messageStyles.visible : messageStyles.hidden
|
||||||
? messageStyles.visible
|
|
||||||
: messageStyles.hidden
|
|
||||||
}`}
|
}`}
|
||||||
onClick={scrollToBottom}
|
onClick={scrollToBottom}
|
||||||
title="Scroll to bottom"
|
title={t('chat.messages.scroll_to_bottom_btn')}
|
||||||
>
|
>
|
||||||
⬇️
|
<IoIosArrowDown className={messageStyles.scroll_to_bottom_btn_icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,44 @@
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
min-height: 0;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat_messages_empty {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.message_empty_state_icon {
|
||||||
|
font-size: 90px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message_empty_state h3 {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message_empty_state p {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.chat_messages::-webkit-scrollbar {
|
.chat_messages::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
@ -308,6 +340,7 @@
|
||||||
.message_empty_state {
|
.message_empty_state {
|
||||||
color: var(--color-gray);
|
color: var(--color-gray);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
import { WorkflowLog, WorkflowProgress, TimelineItem } from './dashboardChatAreaTypes';
|
||||||
|
|
||||||
|
// Helper function to parse task and action progress from log messages
|
||||||
|
const parseLogProgress = (logMessage: string) => {
|
||||||
|
const message = logMessage.trim();
|
||||||
|
|
||||||
|
// Task start: "Executing task X/Y"
|
||||||
|
const taskStartMatch = message.match(/^Executing task (\d+)\/(\d+)$/i);
|
||||||
|
if (taskStartMatch) {
|
||||||
|
return {
|
||||||
|
taskNumber: parseInt(taskStartMatch[1]),
|
||||||
|
totalTasks: parseInt(taskStartMatch[2]),
|
||||||
|
type: 'task_start' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action start: "Task X - Starting action Y/Z"
|
||||||
|
const actionStartMatch = message.match(/^Task (\d+) - Starting action (\d+)\/(\d+)$/i);
|
||||||
|
if (actionStartMatch) {
|
||||||
|
return {
|
||||||
|
taskNumber: parseInt(actionStartMatch[1]),
|
||||||
|
actionNumber: parseInt(actionStartMatch[2]),
|
||||||
|
totalActions: parseInt(actionStartMatch[3]),
|
||||||
|
type: 'action_start' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action complete: "✅ Task X - Action Y/Z completed"
|
||||||
|
const actionCompleteMatch = message.match(/^(?:✅\s+)?Task (\d+) - Action (\d+)\/(\d+) completed$/i);
|
||||||
|
if (actionCompleteMatch) {
|
||||||
|
return {
|
||||||
|
taskNumber: parseInt(actionCompleteMatch[1]),
|
||||||
|
actionNumber: parseInt(actionCompleteMatch[2]),
|
||||||
|
totalActions: parseInt(actionCompleteMatch[3]),
|
||||||
|
isCompleted: true,
|
||||||
|
type: 'action_complete' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task complete: "🎯 Task X/Y completed"
|
||||||
|
const taskCompleteMatch = message.match(/^(?:🎯\s+)?Task (\d+)\/(\d+) completed$/i);
|
||||||
|
if (taskCompleteMatch) {
|
||||||
|
return {
|
||||||
|
taskNumber: parseInt(taskCompleteMatch[1]),
|
||||||
|
totalTasks: parseInt(taskCompleteMatch[2]),
|
||||||
|
isCompleted: true,
|
||||||
|
type: 'task_complete' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'unknown' as const };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate workflow progress from messages and logs
|
||||||
|
export const calculateWorkflowProgress = (messages: any[], logs: WorkflowLog[] = []): WorkflowProgress | null => {
|
||||||
|
if (messages.length === 0 && logs.length === 0) return null;
|
||||||
|
|
||||||
|
// Check if waiting for assistant response
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
if (lastMessage?.role === 'user') {
|
||||||
|
return { current: 0, total: 0, percentage: 0, isLoading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse logs for task structure
|
||||||
|
let totalTasks = 0;
|
||||||
|
const taskActionCounts: { [taskNumber: number]: { total: number; completedActions: Set<number> } } = {};
|
||||||
|
|
||||||
|
// Analyze logs
|
||||||
|
logs.forEach((log) => {
|
||||||
|
if (!log.message) return;
|
||||||
|
|
||||||
|
const progress = parseLogProgress(log.message);
|
||||||
|
|
||||||
|
if (progress.type === 'task_start' && progress.totalTasks) {
|
||||||
|
totalTasks = Math.max(totalTasks, progress.totalTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.type === 'action_start' && progress.taskNumber && progress.totalActions) {
|
||||||
|
const taskNum = progress.taskNumber;
|
||||||
|
if (!taskActionCounts[taskNum]) {
|
||||||
|
taskActionCounts[taskNum] = { total: 0, completedActions: new Set() };
|
||||||
|
}
|
||||||
|
taskActionCounts[taskNum].total = Math.max(taskActionCounts[taskNum].total, progress.totalActions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.type === 'action_complete' && progress.taskNumber && progress.actionNumber && progress.totalActions) {
|
||||||
|
const taskNum = progress.taskNumber;
|
||||||
|
if (!taskActionCounts[taskNum]) {
|
||||||
|
taskActionCounts[taskNum] = { total: progress.totalActions, completedActions: new Set() };
|
||||||
|
}
|
||||||
|
taskActionCounts[taskNum].completedActions.add(progress.actionNumber);
|
||||||
|
taskActionCounts[taskNum].total = Math.max(taskActionCounts[taskNum].total, progress.totalActions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.type === 'task_complete' && progress.taskNumber && progress.totalTasks) {
|
||||||
|
const taskNum = progress.taskNumber;
|
||||||
|
totalTasks = Math.max(totalTasks, progress.totalTasks);
|
||||||
|
// Mark all actions complete for this task
|
||||||
|
if (taskActionCounts[taskNum]) {
|
||||||
|
const task = taskActionCounts[taskNum];
|
||||||
|
for (let i = 1; i <= task.total; i++) {
|
||||||
|
task.completedActions.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze messages for additional action completion patterns
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (message.role !== 'assistant' || !message.content) return;
|
||||||
|
|
||||||
|
const actionCompleteMatch = message.content.match(/✅\s+Task (\d+) - Action\s+(?:(\d+)\/(\d+)|.*completed)/i);
|
||||||
|
if (actionCompleteMatch) {
|
||||||
|
const taskNumber = parseInt(actionCompleteMatch[1]);
|
||||||
|
|
||||||
|
if (actionCompleteMatch[2] && actionCompleteMatch[3]) {
|
||||||
|
const actionNumber = parseInt(actionCompleteMatch[2]);
|
||||||
|
const totalActions = parseInt(actionCompleteMatch[3]);
|
||||||
|
|
||||||
|
if (!taskActionCounts[taskNumber]) {
|
||||||
|
taskActionCounts[taskNumber] = { total: totalActions, completedActions: new Set() };
|
||||||
|
}
|
||||||
|
taskActionCounts[taskNumber].completedActions.add(actionNumber);
|
||||||
|
taskActionCounts[taskNumber].total = Math.max(taskActionCounts[taskNumber].total, totalActions);
|
||||||
|
} else {
|
||||||
|
// Named action without numbers - treat as 1/1
|
||||||
|
if (!taskActionCounts[taskNumber]) {
|
||||||
|
taskActionCounts[taskNumber] = { total: 1, completedActions: new Set() };
|
||||||
|
}
|
||||||
|
taskActionCounts[taskNumber].completedActions.add(1);
|
||||||
|
if (taskActionCounts[taskNumber].total === 0) {
|
||||||
|
taskActionCounts[taskNumber].total = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate completed tasks
|
||||||
|
let completedTasks = 0;
|
||||||
|
for (let taskNum = 1; taskNum <= totalTasks; taskNum++) {
|
||||||
|
const task = taskActionCounts[taskNum];
|
||||||
|
if (task && task.total > 0 && task.completedActions.size === task.total) {
|
||||||
|
completedTasks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalTasks > 0) {
|
||||||
|
const percentage = Math.round((completedTasks / totalTasks) * 100);
|
||||||
|
return {
|
||||||
|
current: completedTasks,
|
||||||
|
total: totalTasks,
|
||||||
|
percentage: percentage,
|
||||||
|
isLoading: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safe date parsing with fallback
|
||||||
|
export const safeParseDate = (timestamp: any, fallback: number = Date.now()): Date => {
|
||||||
|
if (!timestamp) return new Date(fallback);
|
||||||
|
|
||||||
|
let dateToTry = timestamp;
|
||||||
|
|
||||||
|
if (typeof timestamp === 'number') {
|
||||||
|
dateToTry = timestamp < 10000000000 ? timestamp * 1000 : timestamp;
|
||||||
|
} else if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) {
|
||||||
|
const numericTimestamp = parseInt(timestamp);
|
||||||
|
dateToTry = numericTimestamp < 10000000000 ? numericTimestamp * 1000 : numericTimestamp;
|
||||||
|
} else if (timestamp instanceof Date) {
|
||||||
|
dateToTry = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(dateToTry);
|
||||||
|
return isNaN(date.getTime()) ? new Date(fallback) : date;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge and sort messages and logs by timestamp
|
||||||
|
export const mergeMessagesAndLogs = (messages: any[], logs: WorkflowLog[]): TimelineItem[] => {
|
||||||
|
const combined: TimelineItem[] = [];
|
||||||
|
|
||||||
|
// Add messages
|
||||||
|
messages.forEach((message, index) => {
|
||||||
|
const rawTimestamp = message.timestamp || message.publishedAt;
|
||||||
|
const fallbackTime = Date.now() - ((messages.length + logs.length) - index) * 1000;
|
||||||
|
const timestamp = safeParseDate(rawTimestamp, fallbackTime);
|
||||||
|
|
||||||
|
combined.push({
|
||||||
|
type: 'message',
|
||||||
|
item: message,
|
||||||
|
timestamp
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add logs
|
||||||
|
logs.forEach((log, index) => {
|
||||||
|
const fallbackTime = Date.now() - ((logs.length) - index) * 1000;
|
||||||
|
const timestamp = safeParseDate(log.timestamp, fallbackTime);
|
||||||
|
|
||||||
|
combined.push({
|
||||||
|
type: 'log',
|
||||||
|
item: log,
|
||||||
|
timestamp
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by timestamp
|
||||||
|
combined.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||||
|
|
||||||
|
return combined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform workflow message to display message
|
||||||
|
export const transformWorkflowMessage = async (workflowMessage: any, request: any): Promise<any> => {
|
||||||
|
let documents: any[] = [];
|
||||||
|
|
||||||
|
// Handle documents
|
||||||
|
if (workflowMessage.documents && workflowMessage.documents.length > 0) {
|
||||||
|
documents = workflowMessage.documents.map((doc: any) => ({
|
||||||
|
id: doc.id || doc.fileId,
|
||||||
|
fileId: typeof doc.fileId === 'string' ? parseInt(doc.fileId) : 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) {
|
||||||
|
// Legacy fileIds approach
|
||||||
|
const documentPromises = workflowMessage.fileIds.map(async (fileId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: `/api/workflows/files/${fileId}/preview`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: fileId.toString(),
|
||||||
|
fileId: fileId,
|
||||||
|
name: response.name || response.fileName || `File_${fileId}`,
|
||||||
|
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
|
||||||
|
type: response.mimeType || response.type || 'application/octet-stream',
|
||||||
|
size: response.size || 0,
|
||||||
|
downloadUrl: response.downloadUrl || response.url
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
id: fileId.toString(),
|
||||||
|
fileId: fileId,
|
||||||
|
name: `File_${fileId}`,
|
||||||
|
ext: 'unknown',
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
size: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
documents = await Promise.all(documentPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = workflowMessage.message ||
|
||||||
|
workflowMessage.content ||
|
||||||
|
(workflowMessage as any).text ||
|
||||||
|
(workflowMessage as any).body ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: workflowMessage.id,
|
||||||
|
role: workflowMessage.role,
|
||||||
|
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
|
||||||
|
content: content,
|
||||||
|
timestamp: workflowMessage.publishedAt || workflowMessage.timestamp,
|
||||||
|
documents: documents
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -122,6 +122,7 @@ export interface WorkflowState {
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
workflow: Workflow | null;
|
workflow: Workflow | null;
|
||||||
messages: WorkflowMessage[];
|
messages: WorkflowMessage[];
|
||||||
|
pendingMessages: WorkflowMessage[]; // Messages sent but not yet confirmed by backend
|
||||||
logs: WorkflowLog[];
|
logs: WorkflowLog[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
@ -165,4 +166,42 @@ export interface Message {
|
||||||
content: string;
|
content: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
documents?: Document[];
|
documents?: Document[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InputAreaProps {
|
||||||
|
selectedPrompt?: Prompt | null;
|
||||||
|
workflowState: WorkflowState;
|
||||||
|
workflowActions: WorkflowActions;
|
||||||
|
attachedFiles?: AttachedFile[];
|
||||||
|
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachedFile {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
fileData?: File;
|
||||||
|
objectUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageListProps {
|
||||||
|
workflowState: WorkflowState;
|
||||||
|
onFilePreview?: (file: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress bar interfaces
|
||||||
|
export interface WorkflowProgress {
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
percentage: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
taskBreakdown?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeline item for merged messages and logs
|
||||||
|
export interface TimelineItem {
|
||||||
|
type: 'message' | 'log';
|
||||||
|
item: any;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
// 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);
|
||||||
|
const [pendingMessages, setPendingMessages] = useState<any[]>([]);
|
||||||
|
const [sentUserMessages, setSentUserMessages] = useState<Set<string>>(new Set()); // Track sent user messages
|
||||||
const pollingIntervalRef = useRef<number | null>(null);
|
const pollingIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Hook-based data fetching
|
// Hook-based data fetching
|
||||||
|
|
@ -18,12 +20,40 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
// Use status for real-time updates, fallback to workflow for initial data
|
// Use status for real-time updates, fallback to workflow for initial data
|
||||||
const currentWorkflow = workflowStatus || workflow;
|
const currentWorkflow = workflowStatus || workflow;
|
||||||
|
|
||||||
|
// Helper to create optimistic user message
|
||||||
|
const createOptimisticMessage = useCallback((prompt: string, fileIds: number[] = []) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
id: `temp-${Date.now()}`,
|
||||||
|
workflowId: currentWorkflowId || 'pending',
|
||||||
|
message: prompt,
|
||||||
|
role: 'user' as const,
|
||||||
|
status: 'pending',
|
||||||
|
sequenceNr: 0,
|
||||||
|
publishedAt: timestamp,
|
||||||
|
success: true,
|
||||||
|
fileIds: fileIds.length > 0 ? fileIds : undefined
|
||||||
|
};
|
||||||
|
}, [currentWorkflowId]);
|
||||||
|
|
||||||
// Combined loading and error states
|
// Combined loading and error states
|
||||||
const isLoading = workflowLoading || statusLoading || messagesLoading || logsLoading;
|
const isLoading = workflowLoading || statusLoading || messagesLoading || logsLoading;
|
||||||
const error = workflowError || statusError || messagesError || logsError;
|
const error = workflowError || statusError || messagesError || logsError;
|
||||||
|
|
||||||
|
// Filter out ALL user messages from backend - we only show user messages from pendingMessages
|
||||||
|
const filteredMessages = useMemo(() => {
|
||||||
|
if (!messages) return [];
|
||||||
|
|
||||||
|
// If we've tracked ANY sent messages, always filter out user messages from backend
|
||||||
|
// This prevents flickering during new workflow creation
|
||||||
|
if (sentUserMessages.size > 0) {
|
||||||
|
return messages.filter(msg => msg.role !== 'user');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tracked sent messages, this is a historical workflow - show all messages
|
||||||
|
return messages;
|
||||||
|
}, [messages, sentUserMessages]);
|
||||||
|
|
||||||
// Auto-polling for active workflows and message updates
|
// Auto-polling for active workflows and message updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPolling && currentWorkflowId) {
|
if (isPolling && currentWorkflowId) {
|
||||||
|
|
@ -53,6 +83,13 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startNewWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<string | null> => {
|
const startNewWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<string | null> => {
|
||||||
|
// Add optimistic message immediately
|
||||||
|
const optimisticMessage = createOptimisticMessage(prompt, fileIds);
|
||||||
|
setPendingMessages(prev => [...prev, optimisticMessage]);
|
||||||
|
|
||||||
|
// Track this message as sent
|
||||||
|
setSentUserMessages(prev => new Set(prev).add(prompt.trim()));
|
||||||
|
|
||||||
const workflowData: StartWorkflowRequest = {
|
const workflowData: StartWorkflowRequest = {
|
||||||
prompt,
|
prompt,
|
||||||
listFileId: fileIds
|
listFileId: fileIds
|
||||||
|
|
@ -69,13 +106,28 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
refetchLogs();
|
refetchLogs();
|
||||||
}, 500);
|
}, 500);
|
||||||
return newWorkflowId;
|
return newWorkflowId;
|
||||||
|
} else {
|
||||||
|
// Remove optimistic message and sent tracking on failure
|
||||||
|
setPendingMessages(prev => prev.filter(msg => msg.id !== optimisticMessage.id));
|
||||||
|
setSentUserMessages(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(prompt.trim());
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [startWorkflow, refetchMessages]);
|
}, [startWorkflow, refetchMessages, createOptimisticMessage]);
|
||||||
|
|
||||||
const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<boolean> => {
|
const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<boolean> => {
|
||||||
if (!currentWorkflowId) return false;
|
if (!currentWorkflowId) return false;
|
||||||
|
|
||||||
|
// Add optimistic message immediately
|
||||||
|
const optimisticMessage = createOptimisticMessage(prompt, fileIds);
|
||||||
|
setPendingMessages(prev => [...prev, optimisticMessage]);
|
||||||
|
|
||||||
|
// Track this message as sent
|
||||||
|
setSentUserMessages(prev => new Set(prev).add(prompt.trim()));
|
||||||
|
|
||||||
const workflowData: StartWorkflowRequest = {
|
const workflowData: StartWorkflowRequest = {
|
||||||
prompt,
|
prompt,
|
||||||
listFileId: fileIds
|
listFileId: fileIds
|
||||||
|
|
@ -90,9 +142,17 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
refetchLogs();
|
refetchLogs();
|
||||||
}, 500);
|
}, 500);
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
// Remove optimistic message and sent tracking on failure
|
||||||
|
setPendingMessages(prev => prev.filter(msg => msg.id !== optimisticMessage.id));
|
||||||
|
setSentUserMessages(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(prompt.trim());
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [currentWorkflowId, startWorkflow, refetchMessages, refetchLogs]);
|
}, [currentWorkflowId, startWorkflow, refetchMessages, refetchLogs, createOptimisticMessage]);
|
||||||
|
|
||||||
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
||||||
if (!currentWorkflowId) return false;
|
if (!currentWorkflowId) return false;
|
||||||
|
|
@ -113,8 +173,32 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const clearWorkflow = useCallback(() => {
|
const clearWorkflow = useCallback(() => {
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
setIsPolling(false);
|
setIsPolling(false);
|
||||||
|
setPendingMessages([]);
|
||||||
|
setSentUserMessages(new Set());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Only clear pending messages when workflow changes to a different ID
|
||||||
|
// (not when creating a new workflow)
|
||||||
|
const previousWorkflowId = useRef(currentWorkflowId);
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = previousWorkflowId.current;
|
||||||
|
const current = currentWorkflowId;
|
||||||
|
|
||||||
|
// If we're switching between different existing workflows, clear state
|
||||||
|
if (prev && current && prev !== current) {
|
||||||
|
setPendingMessages([]);
|
||||||
|
setSentUserMessages(new Set());
|
||||||
|
}
|
||||||
|
// If we're going from a workflow to no workflow, clear state
|
||||||
|
else if (prev && !current) {
|
||||||
|
setPendingMessages([]);
|
||||||
|
setSentUserMessages(new Set());
|
||||||
|
}
|
||||||
|
// If we're going from no workflow to a workflow (new workflow creation), keep pending messages
|
||||||
|
|
||||||
|
previousWorkflowId.current = current;
|
||||||
|
}, [currentWorkflowId]);
|
||||||
|
|
||||||
// Sync with external workflow ID changes
|
// Sync with external workflow ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialWorkflowId !== currentWorkflowId) {
|
if (initialWorkflowId !== currentWorkflowId) {
|
||||||
|
|
@ -140,8 +224,9 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const state: WorkflowState = {
|
const state: WorkflowState = {
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
workflow: currentWorkflow,
|
workflow: currentWorkflow,
|
||||||
messages,
|
messages: filteredMessages,
|
||||||
logs,
|
pendingMessages,
|
||||||
|
logs: logs || [],
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,15 @@ export default {
|
||||||
'chat_history.resume_tooltip': 'Workflow fortsetzen',
|
'chat_history.resume_tooltip': 'Workflow fortsetzen',
|
||||||
'chat_history.delete_tooltip': 'Workflow löschen',
|
'chat_history.delete_tooltip': 'Workflow löschen',
|
||||||
'chat_history.deleting': 'Workflow wird gelöscht...',
|
'chat_history.deleting': 'Workflow wird gelöscht...',
|
||||||
|
|
||||||
|
// Chat Messages
|
||||||
|
'chat.messages.no_workflow_selected': 'Noch keinen Workflow ausgewählt',
|
||||||
|
'chat.messages.no_workflow_selected_description': 'Wähle einen Workflow aus der Liste aus oder starte einen neuen Workflow',
|
||||||
|
'chat.messages.loading_progress': 'Lade Fortschritt...',
|
||||||
|
'chat.messages.tasks': 'Aufgaben',
|
||||||
|
'chat.messages.workflow_progress': 'Workflow Fortschritt',
|
||||||
|
'chat.messages.analyzing_workflow': 'Analysiere Workflow...',
|
||||||
|
'chat.messages.scroll_to_bottom_btn': 'Nach unten scrollen',
|
||||||
// Workflow Status
|
// Workflow Status
|
||||||
'status.error': 'FEHLER',
|
'status.error': 'FEHLER',
|
||||||
'status.failed': 'FEHLGESCHLAGEN',
|
'status.failed': 'FEHLGESCHLAGEN',
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,14 @@ export default {
|
||||||
'chat_history.delete_tooltip': 'Delete workflow',
|
'chat_history.delete_tooltip': 'Delete workflow',
|
||||||
'chat_history.deleting': 'Deleting workflow...',
|
'chat_history.deleting': 'Deleting workflow...',
|
||||||
|
|
||||||
|
// Chat Messages
|
||||||
|
'chat.messages.no_workflow_selected': 'No workflow selected',
|
||||||
|
'chat.messages.no_workflow_selected_description': 'Select a workflow from the list or start a new workflow',
|
||||||
|
'chat.messages.loading_progress': 'Loading progress...',
|
||||||
|
'chat.messages.tasks': 'Tasks',
|
||||||
|
'chat.messages.workflow_progress': 'Workflow Progress',
|
||||||
|
'chat.messages.analyzing_workflow': 'Analyzing workflow...',
|
||||||
|
'chat.messages.scroll_to_bottom_btn': 'Scroll to bottom',
|
||||||
// Workflow Status
|
// Workflow Status
|
||||||
'status.error': 'ERROR',
|
'status.error': 'ERROR',
|
||||||
'status.failed': 'FAILED',
|
'status.failed': 'FAILED',
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,15 @@ export default {
|
||||||
'chat_history.delete_tooltip': 'Supprimer le workflow',
|
'chat_history.delete_tooltip': 'Supprimer le workflow',
|
||||||
'chat_history.deleting': 'Suppression du workflow...',
|
'chat_history.deleting': 'Suppression du workflow...',
|
||||||
|
|
||||||
|
// Chat Messages
|
||||||
|
'chat.messages.no_workflow_selected': 'Aucun workflow sélectionné',
|
||||||
|
'chat.messages.no_workflow_selected_description': 'Sélectionnez un workflow dans la liste ou démarrez un nouveau workflow',
|
||||||
|
'chat.messages.loading_progress': 'Chargement du progrès...',
|
||||||
|
'chat.messages.tasks': 'Tâches',
|
||||||
|
'chat.messages.workflow_progress': 'Progression du workflow',
|
||||||
|
'chat.messages.analyzing_workflow': 'Analyse du workflow...',
|
||||||
|
'chat.messages.scroll_to_bottom_btn': 'Faire défiler vers le bas',
|
||||||
|
|
||||||
// Workflow Status
|
// Workflow Status
|
||||||
'status.error': 'ERREUR',
|
'status.error': 'ERREUR',
|
||||||
'status.failed': 'ÉCHEC',
|
'status.failed': 'ÉCHEC',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue