157 lines
7.2 KiB
TypeScript
157 lines
7.2 KiB
TypeScript
import { WorkflowLog, WorkflowProgress, TimelineItem } from './dashboardChatAreaTypes';
|
|
|
|
const parseLogProgress = (msg: string) => {
|
|
const m = msg.trim();
|
|
|
|
const taskStart = m.match(/^Executing task (\d+)\/(\d+)$/i);
|
|
if (taskStart) return { taskNumber: parseInt(taskStart[1]), totalTasks: parseInt(taskStart[2]), type: 'task_start' as const };
|
|
|
|
const actionStart = m.match(/^Task (\d+) - Starting action (\d+)\/(\d+)$/i);
|
|
if (actionStart) return { taskNumber: parseInt(actionStart[1]), actionNumber: parseInt(actionStart[2]), totalActions: parseInt(actionStart[3]), type: 'action_start' as const };
|
|
|
|
const actionComplete = m.match(/^(?:✅\s+)?Task (\d+) - Action (\d+)\/(\d+) completed$/i);
|
|
if (actionComplete) return { taskNumber: parseInt(actionComplete[1]), actionNumber: parseInt(actionComplete[2]), totalActions: parseInt(actionComplete[3]), isCompleted: true, type: 'action_complete' as const };
|
|
|
|
const taskComplete = m.match(/^(?:🎯\s+)?Task (\d+)\/(\d+) completed$/i);
|
|
if (taskComplete) return { taskNumber: parseInt(taskComplete[1]), totalTasks: parseInt(taskComplete[2]), isCompleted: true, type: 'task_complete' as const };
|
|
|
|
return { type: 'unknown' as const };
|
|
};
|
|
|
|
export const calculateWorkflowProgress = (messages: any[], logs: WorkflowLog[] = []): WorkflowProgress | null => {
|
|
if (messages.length === 0 && logs.length === 0) return null;
|
|
|
|
const lastMessage = messages[messages.length - 1];
|
|
if (lastMessage?.role === 'user') return { current: 0, total: 0, percentage: 0, isLoading: true };
|
|
|
|
let totalTasks = 0;
|
|
const taskCounts: { [key: number]: { total: number; completed: Set<number> } } = {};
|
|
|
|
logs.forEach(log => {
|
|
if (!log.message) return;
|
|
const p = parseLogProgress(log.message);
|
|
|
|
if (p.type === 'task_start' && p.totalTasks) totalTasks = Math.max(totalTasks, p.totalTasks);
|
|
|
|
if (p.type === 'action_start' && p.taskNumber && p.totalActions) {
|
|
if (!taskCounts[p.taskNumber]) taskCounts[p.taskNumber] = { total: 0, completed: new Set() };
|
|
taskCounts[p.taskNumber].total = Math.max(taskCounts[p.taskNumber].total, p.totalActions);
|
|
}
|
|
|
|
if (p.type === 'action_complete' && p.taskNumber && p.actionNumber && p.totalActions) {
|
|
if (!taskCounts[p.taskNumber]) taskCounts[p.taskNumber] = { total: p.totalActions, completed: new Set() };
|
|
taskCounts[p.taskNumber].completed.add(p.actionNumber);
|
|
taskCounts[p.taskNumber].total = Math.max(taskCounts[p.taskNumber].total, p.totalActions);
|
|
}
|
|
|
|
if (p.type === 'task_complete' && p.taskNumber && p.totalTasks) {
|
|
totalTasks = Math.max(totalTasks, p.totalTasks);
|
|
if (taskCounts[p.taskNumber]) {
|
|
const task = taskCounts[p.taskNumber];
|
|
for (let i = 1; i <= task.total; i++) task.completed.add(i);
|
|
}
|
|
}
|
|
});
|
|
|
|
messages.forEach(msg => {
|
|
if (msg.role !== 'assistant' || !msg.content) return;
|
|
const match = msg.content.match(/✅\s+Task (\d+) - Action\s+(?:(\d+)\/(\d+)|.*completed)/i);
|
|
if (match) {
|
|
const taskNum = parseInt(match[1]);
|
|
if (match[2] && match[3]) {
|
|
const actionNum = parseInt(match[2]);
|
|
const totalActions = parseInt(match[3]);
|
|
if (!taskCounts[taskNum]) taskCounts[taskNum] = { total: totalActions, completed: new Set() };
|
|
taskCounts[taskNum].completed.add(actionNum);
|
|
taskCounts[taskNum].total = Math.max(taskCounts[taskNum].total, totalActions);
|
|
} else {
|
|
if (!taskCounts[taskNum]) taskCounts[taskNum] = { total: 1, completed: new Set() };
|
|
taskCounts[taskNum].completed.add(1);
|
|
if (taskCounts[taskNum].total === 0) taskCounts[taskNum].total = 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
let completed = 0;
|
|
for (let i = 1; i <= totalTasks; i++) {
|
|
const task = taskCounts[i];
|
|
if (task && task.total > 0 && task.completed.size === task.total) completed++;
|
|
}
|
|
|
|
return totalTasks > 0 ? { current: completed, total: totalTasks, percentage: Math.round((completed / totalTasks) * 100), isLoading: false } : null;
|
|
};
|
|
|
|
export const safeParseDate = (ts: any, fallback = Date.now()): Date => {
|
|
if (!ts) return new Date(fallback);
|
|
|
|
let d = ts;
|
|
if (typeof ts === 'number') d = ts < 10000000000 ? ts * 1000 : ts;
|
|
else if (typeof ts === 'string' && /^\d+$/.test(ts)) {
|
|
const n = parseInt(ts);
|
|
d = n < 10000000000 ? n * 1000 : n;
|
|
}
|
|
|
|
const date = new Date(d);
|
|
return isNaN(date.getTime()) ? new Date(fallback) : date;
|
|
};
|
|
|
|
export const mergeMessagesAndLogs = (messages: any[], logs: WorkflowLog[]): TimelineItem[] => {
|
|
const items: TimelineItem[] = [];
|
|
|
|
messages.forEach((msg, i) => {
|
|
const ts = safeParseDate(msg.timestamp || msg.publishedAt, Date.now() - ((messages.length + logs.length) - i) * 1000);
|
|
items.push({ type: 'message', item: msg, timestamp: ts });
|
|
});
|
|
|
|
logs.forEach((log, i) => {
|
|
const ts = safeParseDate(log.timestamp, Date.now() - (logs.length - i) * 1000);
|
|
items.push({ type: 'log', item: log, timestamp: ts });
|
|
});
|
|
|
|
return items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
};
|
|
|
|
export const transformWorkflowMessage = async (msg: any, request: any): Promise<any> => {
|
|
let docs: any[] = [];
|
|
|
|
if (msg.documents?.length > 0) {
|
|
docs = msg.documents.map((d: any) => ({
|
|
id: d.id || d.fileId,
|
|
fileId: typeof d.fileId === 'string' ? parseInt(d.fileId) : d.fileId,
|
|
name: d.filename || `File_${d.id || d.fileId || 'unknown'}`,
|
|
ext: (d.filename && d.filename.includes('.')) ? d.filename.split('.').pop() : 'unknown',
|
|
type: d.mimeType,
|
|
size: d.fileSize,
|
|
downloadUrl: `/api/workflows/files/${d.fileId}/download`
|
|
}));
|
|
} else if (msg.fileIds?.length > 0) {
|
|
const promises = msg.fileIds.map(async (id: number) => {
|
|
try {
|
|
const res = await request({ url: `/api/workflows/files/${id}/preview`, method: 'get' });
|
|
return {
|
|
id: id.toString(),
|
|
fileId: id,
|
|
name: res.name || res.fileName || `File_${id}`,
|
|
ext: res.extension || res.ext || (res.name ? res.name.split('.').pop() : 'txt'),
|
|
type: res.mimeType || res.type || 'application/octet-stream',
|
|
size: res.size || 0,
|
|
downloadUrl: res.downloadUrl || res.url
|
|
};
|
|
} catch {
|
|
return { id: id.toString(), fileId: id, name: `File_${id}`, ext: 'unknown', type: 'application/octet-stream', size: 0 };
|
|
}
|
|
});
|
|
docs = await Promise.all(promises);
|
|
}
|
|
|
|
return {
|
|
id: msg.id,
|
|
role: msg.role,
|
|
agentName: msg.role === 'user' ? 'You' : 'Assistant',
|
|
content: msg.message || msg.content || msg.text || msg.body || '',
|
|
timestamp: msg.publishedAt
|
|
? (typeof msg.publishedAt === 'number' ? new Date(msg.publishedAt * 1000).toISOString() : msg.publishedAt)
|
|
: msg.timestamp,
|
|
documents: docs
|
|
};
|
|
};
|