import React, { useCallback, useMemo, useState } from 'react'; import { Badge, Button, useToast, Tabs, TabsBar, TabsPanel, type Tab as TabDefinition } from '@vertesia/ui/core'; import { useUITranslation } from '../../../i18n/index.js'; import { CheckCircleIcon, ClipboardCopyIcon, DownloadCloudIcon, FileTextIcon, Loader2Icon, LayoutListIcon, XCircleIcon, XIcon, } from 'lucide-react'; import { FileProcessingStatus } from '@vertesia/common'; import type { Plan, ConversationFile, AgentMessage } from '@vertesia/common'; import { useUserSession } from '@vertesia/ui/session'; import InlineSlidingPlanPanel from './ModernAgentOutput/InlineSlidingPlanPanel'; import { getConversationUrl } from './ModernAgentOutput/utils.js'; import { DocumentPanel } from './DocumentPanel.js'; import { ArtifactsTab } from './ArtifactsTab.js'; import type { OpenDocument } from './types/document.js'; // --------------------------------------------------------------------------- // Workstream list types // --------------------------------------------------------------------------- export interface WorkstreamInfo { workstream_id: string; launch_id: string; elapsed_ms: number; deadline_ms: number; remaining_ms: number; status: 'running' | 'canceling' | 'completed' | 'canceled'; phase?: string; child_workflow_id?: string; child_workflow_run_id?: string; } // --------------------------------------------------------------------------- // UploadedDocuments (moved from WorkflowPayloadForm) // --------------------------------------------------------------------------- interface UploadedDocumentsProps { files?: Map; } function UploadedDocumentsTab({ files }: UploadedDocumentsProps) { const { t } = useUITranslation(); const filesArray = useMemo(() => { return files ? Array.from(files.values()) : []; }, [files]); const getStatusIcon = (status: FileProcessingStatus) => { switch (status) { case FileProcessingStatus.UPLOADING: case FileProcessingStatus.PROCESSING: return ; case FileProcessingStatus.READY: return ; case FileProcessingStatus.ERROR: return ; default: return ; } }; const getStatusBadge = (status: FileProcessingStatus) => { switch (status) { case FileProcessingStatus.UPLOADING: return {t('agent.uploading')}; case FileProcessingStatus.PROCESSING: return {t('agent.processing')}; case FileProcessingStatus.READY: return {t('agent.ready')}; case FileProcessingStatus.ERROR: return {t('agent.error')}; default: return null; } }; return (
{filesArray.length === 0 ? (
{t('agent.noFilesUploadedYet')}
) : (
{filesArray.map((file) => (
{getStatusIcon(file.status)}
{file.name} {getStatusBadge(file.status)}
{file.error && (
{file.error}
)}
))}
)}
); } // --------------------------------------------------------------------------- // Workstreams tab // --------------------------------------------------------------------------- interface WorkstreamsTabProps { workstreams: WorkstreamInfo[]; messages: AgentMessage[]; runId?: string; } function WorkstreamsTab({ workstreams, messages: _messages }: WorkstreamsTabProps) { const { t } = useUITranslation(); const { client } = useUserSession(); const toast = useToast(); const copyRunId = useCallback((runId: string) => { navigator.clipboard.writeText(runId); toast({ status: 'success', title: t('agent.runIdCopied'), duration: 2000 }); }, [toast]); const downloadConversation = useCallback(async (runId: string) => { try { const url = await getConversationUrl(client, runId); if (url) window.open(url, '_blank'); } catch { toast({ status: 'error', title: t('agent.failedToDownload') }); } }, [client, toast]); if (workstreams.length === 0) { return (
{t('agent.noActiveWorkstreams')}
); } return (
{workstreams.map((ws) => { const isActive = ws.status === 'running' || ws.status === 'canceling'; const elapsed = Math.round(ws.elapsed_ms / 1000); const remaining = Math.round(ws.remaining_ms / 1000); const progress = ws.deadline_ms > 0 ? Math.min(100, Math.round((ws.elapsed_ms / ws.deadline_ms) * 100)) : 0; const statusBadge = ws.status === 'running' ? {ws.phase || 'running'} : ws.status === 'canceling' ? canceling : ws.status === 'completed' ? {t('agent.completed')} : {t('agent.canceled')}; return (
{ws.workstream_id} {statusBadge}
{/* Progress bar — only for active workstreams */} {isActive && ( <>
{t('agent.elapsed', { seconds: elapsed })} {t('agent.remaining', { seconds: remaining })}
)} {/* Actions */} {ws.child_workflow_run_id && (
)}
); })}
); } // --------------------------------------------------------------------------- // Right panel tabs // --------------------------------------------------------------------------- type RightPanelTab = 'plan' | 'workstreams' | 'documents' | 'uploads' | 'artifacts' | 'payload' | 'conversation'; export interface AgentRightPanelProps { /** Optional payload content to show as a "Payload" tab */ payloadContent?: React.ReactNode; /** Optional conversation content to show as a "Conversation" tab */ conversationContent?: React.ReactNode; // Plan plan?: Plan; workstreamStatus?: Map; plans?: Array<{ plan: Plan; timestamp: number }>; activePlanIndex?: number; onChangePlan?: (index: number) => void; showPlan?: boolean; // Workstreams activeWorkstreams?: WorkstreamInfo[]; messages?: AgentMessage[]; hideWorkstreams?: boolean; // Documents openDocuments?: OpenDocument[]; activeDocumentId?: string | null; onSelectDocument?: (id: string) => void; onCloseDocument?: (id: string) => void; onUpdateDocumentTitle?: (id: string, title: string) => void; docRefreshKey?: number; runId?: string; // Uploads processingFiles?: Map; // Artifacts /** Show the Artifacts tab (opt-in, default false) */ showArtifacts?: boolean; artifactRefreshKey?: number; // Panel control onClose: () => void; /** Which tab to auto-activate when panel opens */ defaultTab?: RightPanelTab; /** Controlled active tab (overrides internal state when provided) */ activeTab?: RightPanelTab; /** Callback when the active tab changes */ onTabChange?: (tab: RightPanelTab) => void; } function AgentRightPanelComponent({ // Plan plan, workstreamStatus, plans = [], activePlanIndex = 0, onChangePlan, showPlan, // Workstreams activeWorkstreams = [], messages = [], hideWorkstreams = false, // Documents openDocuments = [], activeDocumentId, onSelectDocument, onCloseDocument, onUpdateDocumentTitle, docRefreshKey = 0, runId, // Uploads processingFiles, // Artifacts showArtifacts = false, artifactRefreshKey = 0, // Payload payloadContent, // Conversation conversationContent, // Panel onClose, defaultTab, activeTab: activeTabProp, onTabChange, }: AgentRightPanelProps) { const { t } = useUITranslation(); const [internalActiveTab, setInternalActiveTab] = useState(defaultTab || 'plan'); const activeTab = activeTabProp ?? internalActiveTab; const handleTabChange = (name: string) => { setInternalActiveTab(name as RightPanelTab); onTabChange?.(name as RightPanelTab); }; // Determine which tabs have content (for badges/indicators) const hasWorkstreams = !hideWorkstreams && activeWorkstreams.length > 0; const hasDocuments = openDocuments.length > 0; const hasUploads = processingFiles ? processingFiles.size > 0 : false; const hasPlan = showPlan && plan; const conversationTab: TabDefinition = { name: 'conversation', label: 'Conversation', content: conversationContent ?
{conversationContent}
: null, is_allowed: !!conversationContent, }; const tabs: TabDefinition[] = [ ...(conversationContent ? [conversationTab] : []), { name: 'plan', label: hasPlan ? {t('agent.plan')} : t('agent.plan'), content: plan ? ( ) : (
{t('agent.noPlanAvailable')}
), is_allowed: true, }, { name: 'workstreams', label: hasWorkstreams ? {t('agent.workstreams')} {activeWorkstreams.length} : t('agent.workstreams'), content: , is_allowed: !hideWorkstreams, }, { name: 'documents', label: hasDocuments ? {t('agent.documents')} {openDocuments.length} : t('agent.documents'), content: openDocuments.length > 0 && onSelectDocument && onCloseDocument ? ( ) : (
{t('agent.noDocumentsOpen')}
), is_allowed: true, }, { name: 'uploads', label: hasUploads ? {t('agent.uploads')} : t('agent.uploads'), content: , is_allowed: true, }, { name: 'artifacts', label: t('agent.artifacts'), content: , is_allowed: showArtifacts, }, { name: 'payload', label: t('agent.payload'), content: payloadContent ?
{payloadContent}
: null, is_allowed: !!payloadContent, }, ]; return (
{!conversationContent && ( )}
); } export const AgentRightPanel = React.memo(AgentRightPanelComponent);