feat(frontend):优化前端页面设计

This commit is contained in:
2026-05-29 16:44:17 +00:00
parent a83c5fa5bd
commit affe460180
80 changed files with 2670 additions and 2678 deletions
+62 -101
View File
@@ -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>