315 lines
17 KiB
TypeScript
315 lines
17 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { IoMdArrowDropdown } from 'react-icons/io';
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
import { useWorkflows } from '../../hooks/useWorkflows';
|
|
import { Workflow } from '../../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
|
import { useWorkflowManager } from '../../components/Dashboard/DashboardChat/useWorkflowManager';
|
|
import styles from './HomeStyles/Dashboard.module.css'
|
|
import sharedStyles from '../../core/PageManager/pages.module.css';
|
|
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
|
|
import { IoMdClose } from 'react-icons/io';
|
|
|
|
function Dashboard() {
|
|
const { t } = useLanguage();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Get workflow ID from URL parameters
|
|
const workflowIdFromUrl = searchParams.get('workflowId');
|
|
const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl);
|
|
const { workflows, loading: workflowsLoading, error: workflowsError, refetch: refetchWorkflows } = useWorkflows();
|
|
|
|
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
|
workflowActions.loadWorkflow(workflowId);
|
|
setIsDropdownOpen(false);
|
|
// Clear the URL parameter once workflow is loaded
|
|
setSearchParams({});
|
|
}, [workflowActions, setSearchParams]);
|
|
|
|
const handleResetWorkflow = useCallback(() => {
|
|
workflowActions.clearWorkflow();
|
|
// Clear the URL parameter when resetting
|
|
setSearchParams({});
|
|
}, [workflowActions, setSearchParams]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsDropdownOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
// Listen for workflow updates from other components (like WorkflowsTable)
|
|
useEffect(() => {
|
|
const handleWorkflowUpdated = (event: CustomEvent) => {
|
|
console.log('Dashboard received workflow update event:', event.detail);
|
|
// Refetch workflows to update the dropdown
|
|
refetchWorkflows();
|
|
};
|
|
|
|
window.addEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
|
|
return () => window.removeEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
|
|
}, [refetchWorkflows]);
|
|
|
|
const getWorkflowDisplayName = (workflow: Workflow) =>
|
|
workflow.name || `${workflow.id.substring(0, 8)}...`;
|
|
|
|
const formatWorkflowId = (id: string) => `${id.substring(0, 8)}...`;
|
|
|
|
const displayWorkflowId = workflowState.currentWorkflowId ?
|
|
`${workflowState.currentWorkflowId.substring(0, 8)}...` :
|
|
t('dashboard.log.no_workflow');
|
|
|
|
const getWorkflowStats = () => {
|
|
if (!workflowState.workflow) return null;
|
|
|
|
const { workflow, messages } = workflowState;
|
|
const messageCount = messages?.length || 0;
|
|
const fileCount = messages?.reduce((count, msg) =>
|
|
count + (msg.documents?.length || msg.fileIds?.length || 0), 0) || 0;
|
|
|
|
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);
|
|
|
|
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 = (dateValue: string | number) => {
|
|
if (typeof dateValue === 'number') {
|
|
// Convert from seconds to milliseconds for Date constructor
|
|
return new Date(dateValue * 1000).toLocaleString();
|
|
}
|
|
return new Date(dateValue).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) =>
|
|
status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
|
|
const stats = getWorkflowStats();
|
|
|
|
return (
|
|
<div className={sharedStyles.pageContainer}>
|
|
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
|
|
<div className={styles.verticalDivider}></div>
|
|
|
|
<div className={sharedStyles.pageHeader}>
|
|
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
|
<div className={styles.headerControls}>
|
|
{workflowState.currentWorkflowId ? (
|
|
<>
|
|
<div className={styles.workflowStats}>
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.workflow')}</span>
|
|
<span className={styles.statValueMonospace}>{displayWorkflowId}</span>
|
|
</div>
|
|
|
|
{stats && (
|
|
<>
|
|
<div className={styles.statDivider}></div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.status')}</span>
|
|
<span className={`${styles.statValue} ${styles.statValueBold}`} style={{ color: getStatusColor(stats.status) }}>
|
|
{formatStatus(stats.status)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.rounds')}</span>
|
|
<span className={styles.statValue}>{stats.rounds}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.messages')}</span>
|
|
<span className={styles.statValue}>{stats.messageCount}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.files')}</span>
|
|
<span className={styles.statValue}>{stats.fileCount}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.tokens')}</span>
|
|
<span className={styles.statValue}>{stats.tokenCount.toLocaleString()}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.data_sent')}</span>
|
|
<span className={styles.statValue}>{formatBytes(stats.bytesSent)}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.data_received')}</span>
|
|
<span className={styles.statValue}>{formatBytes(stats.bytesReceived)}</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.success_rate')}</span>
|
|
<span className={styles.statValue} style={{
|
|
color: stats.successRate >= 90 ? 'var(--color-success)' :
|
|
stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)'
|
|
}}>{stats.successRate}%</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.errors')}</span>
|
|
<span className={`${styles.statValue} ${stats.errorCount > 0 ? styles.statValueError : ''}`}>
|
|
{stats.errorCount}
|
|
</span>
|
|
</div>
|
|
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>{t('dashboard.stats.started')}</span>
|
|
<span className={`${styles.statValue} ${styles.statValueSmall}`}>
|
|
{formatDate(stats.startedAt)}
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<button
|
|
className={sharedStyles.primaryButton}
|
|
onClick={handleResetWorkflow}
|
|
aria-label="Reset workflow"
|
|
>
|
|
<span className={sharedStyles.buttonIcon}><IoMdClose /></span>
|
|
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
|
<button
|
|
className={`${sharedStyles.primaryButton} ${styles.dropdownButton}`}
|
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
disabled={workflowsLoading}
|
|
aria-expanded={isDropdownOpen}
|
|
aria-haspopup="listbox"
|
|
>
|
|
<span>
|
|
{workflowsLoading
|
|
? t('dashboard.workflow_dropdown.loading')
|
|
: workflowsError
|
|
? t('dashboard.workflow_dropdown.error')
|
|
: t('dashboard.workflow_dropdown.select_workflow')
|
|
}
|
|
</span>
|
|
<IoMdArrowDropdown
|
|
className={`${sharedStyles.buttonIcon} ${styles.dropdownIcon} ${
|
|
isDropdownOpen ? styles.dropdownIconOpen : ''
|
|
}`}
|
|
/>
|
|
</button>
|
|
|
|
{isDropdownOpen && !workflowsLoading && !workflowsError && (
|
|
<div className={styles.dropdownMenu}>
|
|
<div className={styles.dropdownHeader}>
|
|
{t('dashboard.workflow_dropdown.available_workflows')}
|
|
</div>
|
|
|
|
{workflows.length === 0 ? (
|
|
<div className={styles.dropdownEmpty}>
|
|
{t('dashboard.workflow_dropdown.no_workflows')}
|
|
</div>
|
|
) : (
|
|
workflows.map((workflow) => (
|
|
<button
|
|
key={workflow.id}
|
|
onClick={() => handleWorkflowSelect(workflow.id)}
|
|
className={styles.dropdownItem}
|
|
>
|
|
<div className={styles.workflowInfo}>
|
|
<span className={styles.workflowName}>
|
|
{getWorkflowDisplayName(workflow)}
|
|
</span>
|
|
<span className={styles.workflowId}>
|
|
ID: {formatWorkflowId(workflow.id)}
|
|
</span>
|
|
<span className={styles.workflowStatus}>
|
|
{workflow.status}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className={sharedStyles.horizontalDivider}></div>
|
|
|
|
<div className={styles.dashboardContentArea}>
|
|
<div className={styles.chatLogContainer}>
|
|
<div className={styles.chatArea}>
|
|
<DashboardChat
|
|
workflowState={workflowState}
|
|
workflowActions={workflowActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default Dashboard;
|