import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork, Radio, PlayCircle, ListChecks } from 'lucide-react'; import apiClient from '../../api/client'; import { connectSSE } from '../../api/sse'; import type { SSEConnection } from '../../api/sse'; import type { WorkflowDetail } from '../../types'; import { ErrorBoundary } from '../ErrorBoundary'; import { WorkflowDiagram } from './WorkflowDiagram'; interface RightPanelProps { selectedWorkflow: string | null; } export function RightPanel({ selectedWorkflow }: RightPanelProps) { const { t } = useTranslation(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [logs, setLogs] = useState([]); const [sseConnected, setSseConnected] = useState(false); const [replyText, setReplyText] = useState(''); const [resuming, setResuming] = useState(false); const [activeTab, setActiveTab] = useState<'chat' | 'steps' | 'diagram'>('chat'); const eventSourceRef = useRef(null); const logsEndRef = useRef(null); const fetchDetail = async (traceId: string) => { setLoading(true); try { const response = await apiClient.get(`/api/v1/workflow/${traceId}`); setDetail(response.data); } catch (err) { console.error('Failed to fetch workflow detail', err); setDetail(null); } finally { setLoading(false); } }; useEffect(() => { if (!selectedWorkflow) { setDetail(null); setLogs([]); return; } fetchDetail(selectedWorkflow); setLogs([]); const apiBase = import.meta.env.VITE_API_BASE_URL || `${window.location.protocol}//${window.location.host}`; // 用 fetch-based SSE,token 走标准 Authorization header,不进 URL const token = localStorage.getItem('token') || ''; const conn = connectSSE( `${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`, token, { onOpen: () => setSseConnected(true), onMessage: (data) => setLogs((prev) => [...prev, data]), onError: () => setSseConnected(false), onReconnect: (delayMs) => { setSseConnected(false); setLogs((prev) => [ ...prev, `[System]: ${t('workflow.sseReconnecting', { seconds: Math.round(delayMs / 1000) })}`, ]); }, }, ); eventSourceRef.current = conn; const interval = setInterval(() => fetchDetail(selectedWorkflow), 3000); return () => { conn.close(); eventSourceRef.current = null; clearInterval(interval); }; }, [selectedWorkflow]); useEffect(() => { if (activeTab === 'chat' && logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logs, activeTab]); const handleReplySubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!replyText.trim() || !selectedWorkflow) return; const message = replyText.trim(); setReplyText(''); setLogs((prev) => [...prev, `[You]: ${message}`]); try { await apiClient.post(`/api/v1/workflow/reply/${selectedWorkflow}`, { message }); } catch { setLogs((prev) => [...prev, `[System Error]: Failed to send reply.`]); } }; const handleResume = async () => { if (!selectedWorkflow || resuming) return; setResuming(true); try { await apiClient.post(`/api/v1/workflow/${selectedWorkflow}/resume`); setLogs((prev) => [...prev, `[System]: ${t('workflow.resumeTriggered')}`]); fetchDetail(selectedWorkflow); } catch (err: any) { const detailMsg = err?.response?.data?.detail || t('workflow.resumeFailed'); setLogs((prev) => [...prev, `[System Error]: ${detailMsg}`]); } finally { setResuming(false); } }; // 只有非终态(未 completed/failed)的工作流才允许 resume const canResume = !!detail && detail.status !== 'completed' && detail.status !== 'failed'; if (!selectedWorkflow) return null; return (

{detail?.title || t('workflow.workflowDetails')}

{sseConnected ? t('workflow.live') : t('workflow.disconnected')}
{canResume && ( )}
{activeTab === 'diagram' ? (
{detail?.steps && detail.steps.length > 0 ? ( ) : (
Workflow steps are not yet generated.
)}
) : activeTab === 'steps' ? (
{detail?.steps && detail.steps.length > 0 ? ( detail.steps.map((step: any, idx: number) => { const isCurrent = idx === (detail.current_step ?? 0); const isDone = idx < (detail.current_step ?? 0); return (
{isDone ? '✓' : idx + 1} {step.node_name || step.name || `Step ${idx + 1}`} {isCurrent && {t('workflow.status.running')}}
{step.output && (
                        {typeof step.output === 'string' ? step.output : JSON.stringify(step.output, null, 2)}
                      
)} {step.error && (
{step.error}
)}
); }) ) : (
{t('workflow.noStepsYet')}
)}
) : loading && !detail ? (
) : (
{detail?.command && (

{t('workflow.originalCommand')}

{detail.command}

)}
{logs.length === 0 ? (
{t('workflow.waitingEvents')}
) : ( logs.map((log, index) => (
{log}
)) )}
setReplyText(e.target.value)} placeholder={t('workflow.replyPlaceholder')} className="w-full bg-bg-card border border-border-primary rounded-xl pl-4 pr-11 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 text-text-primary placeholder:text-text-muted/50" />
)}
); }