feat(frontend):优化前端页面设计
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork, Radio } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { WorkflowDetail } from '../../types';
|
||||
import { WorkflowDiagram } from './WorkflowDiagram';
|
||||
@@ -9,6 +10,7 @@ interface RightPanelProps {
|
||||
}
|
||||
|
||||
export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [detail, setDetail] = useState<WorkflowDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
@@ -37,33 +39,18 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
setLogs([]);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchDetail(selectedWorkflow);
|
||||
setLogs([]);
|
||||
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || `${protocol}//${host}`;
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || `${window.location.protocol}//${window.location.host}`;
|
||||
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onopen = () => setSseConnected(true);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
setLogs(prev => [...prev, event.data]);
|
||||
};
|
||||
|
||||
es.onmessage = (event) => setLogs((prev) => [...prev, event.data]);
|
||||
es.onerror = () => setSseConnected(false);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDetail(selectedWorkflow);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
clearInterval(interval);
|
||||
};
|
||||
const interval = setInterval(() => fetchDetail(selectedWorkflow), 3000);
|
||||
return () => { es.close(); eventSourceRef.current = null; clearInterval(interval); };
|
||||
}, [selectedWorkflow]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -75,113 +62,87 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const handleReplySubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!replyText.trim() || !selectedWorkflow) return;
|
||||
|
||||
const message = replyText.trim();
|
||||
setReplyText('');
|
||||
setLogs(prev => [...prev, `[You]: ${message}`]);
|
||||
|
||||
setLogs((prev) => [...prev, `[You]: ${message}`]);
|
||||
try {
|
||||
await apiClient.post(`/api/v1/workflow/reply/${selectedWorkflow}`, { message });
|
||||
} catch (err) {
|
||||
console.error("Failed to send reply", err);
|
||||
setLogs(prev => [...prev, `[System Error]: Failed to send reply.`]);
|
||||
} catch {
|
||||
setLogs((prev) => [...prev, `[System Error]: Failed to send reply.`]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedWorkflow) return null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-white border-l border-slate-200 flex flex-col z-0 relative">
|
||||
<div className="h-14 border-b border-slate-100 flex items-center px-6 justify-between bg-white z-10 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<Terminal size={18} className="text-blue-500" />
|
||||
<span className="truncate max-w-[200px]" title={detail?.title || 'Loading...'}>
|
||||
{detail?.title || 'Workflow Details'}
|
||||
</span>
|
||||
</h2>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full font-medium ${sseConnected ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
|
||||
{sseConnected ? 'Live' : 'Disconnected'}
|
||||
<div className="flex-1 bg-bg-card border-l border-border-primary flex flex-col relative">
|
||||
<div className="h-12 border-b border-border-primary/60 bg-bg-card/80 backdrop-blur flex items-center px-5 justify-between shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<Terminal size={14} className="text-accent" />
|
||||
</div>
|
||||
<h2 className="font-semibold text-sm text-text-primary truncate max-w-[180px]">{detail?.title || t('workflow.workflowDetails')}</h2>
|
||||
<span className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium ${sseConnected ? 'bg-success-bg text-success' : 'bg-bg-secondary text-text-muted'}`}>
|
||||
<Radio size={10} className={sseConnected ? 'animate-pulse' : ''} />
|
||||
{sseConnected ? t('workflow.live') : t('workflow.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<LayoutList size={16} />
|
||||
交流日志
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('diagram')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'diagram' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<GitFork size={16} />
|
||||
流程图
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center bg-bg-secondary rounded-lg p-0.5">
|
||||
<button onClick={() => setActiveTab('chat')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'chat' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<LayoutList size={12} /> {t('workflow.chatLog')}
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('diagram')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'diagram' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<GitFork size={12} /> {t('workflow.diagram')}
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => fetchDetail(selectedWorkflow)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all">
|
||||
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => fetchDetail(selectedWorkflow)}
|
||||
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Refresh Data"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden bg-slate-50 relative">
|
||||
<div className="flex-1 flex overflow-hidden bg-bg-secondary relative">
|
||||
{activeTab === 'diagram' ? (
|
||||
<div className="absolute inset-0">
|
||||
{detail?.steps && detail.steps.length > 0 ? (
|
||||
<WorkflowDiagram steps={detail.steps} currentStep={0} status={detail.status} />
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Workflow steps are not yet generated.
|
||||
</div>
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">Workflow steps are not yet generated.</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col p-6 overflow-hidden">
|
||||
{detail?.command && (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
|
||||
<p className="text-slate-700 text-sm">{detail.command}</p>
|
||||
<div className="flex-1 flex flex-col p-5 overflow-hidden">
|
||||
{detail?.command && (
|
||||
<div className="bg-bg-card border border-border-primary rounded-xl p-4 mb-4 shadow-sm shrink-0">
|
||||
<h3 className="text-[10px] font-bold text-text-muted uppercase tracking-widest mb-1.5">{t('workflow.originalCommand')}</h3>
|
||||
<p className="text-text-secondary text-sm">{detail.command}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 bg-bg-card border border-border-primary rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-2 font-mono text-xs">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-text-muted">
|
||||
{t('workflow.waitingEvents')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Waiting for events...
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`p-2.5 rounded-lg text-xs ${log.startsWith('[You]') ? 'bg-accent-light/50 text-accent-text ml-8' : 'bg-bg-secondary text-text-secondary mr-8'}`}>
|
||||
{log}
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`p-3 rounded-lg ${log.startsWith('[You]') ? 'bg-blue-50 border border-blue-100 text-blue-800 self-end ml-12' : 'bg-slate-50 border border-slate-100 text-slate-700 mr-12'}`}>
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleReplySubmit} className="relative shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Reply to the workflow..."
|
||||
className="w-full bg-white border border-slate-200 rounded-xl pl-4 pr-12 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!replyText.trim()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<SendHorizontal size={16} />
|
||||
</button>
|
||||
</form>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
<form onSubmit={handleReplySubmit} className="relative shrink-0">
|
||||
<input type="text" value={replyText} onChange={(e) => 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" />
|
||||
<button type="submit" disabled={!replyText.trim()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-accent text-white rounded-lg hover:bg-accent-hover disabled:opacity-30 transition-colors">
|
||||
<SendHorizontal size={14} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user