ui-nyla/src/pages/Home/Dashboard.tsx

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;