chore(release): v0.1.1-alpha

##前端美化和bug修复
#### 💄 美化
- **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。

#### 🐛 修复
- **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。 
- **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
This commit is contained in:
2026-05-04 16:38:21 +08:00
committed by GitHub
parent d84212f780
commit d30c7e37a6
92 changed files with 2449 additions and 863 deletions
+117 -130
View File
@@ -1,35 +1,25 @@
import { useState, useEffect, useRef } from 'react';
import { Terminal, Activity, RefreshCw, CheckCircle2, Circle, XCircle, Clock, Loader2 } from 'lucide-react';
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork } from 'lucide-react';
import apiClient from '../../api/client';
import type { WorkflowDetail, WorkflowStep } from '../../types';
import type { WorkflowDetail } from '../../types';
import { WorkflowDiagram } from './WorkflowDiagram';
interface RightPanelProps {
selectedWorkflow: string | null;
}
function stepStatusIcon(status: string) {
switch (status) {
case 'completed':
return <CheckCircle2 size={14} className="text-green-500" />;
case 'running':
return <Loader2 size={14} className="text-blue-500 animate-spin" />;
case 'failed':
return <XCircle size={14} className="text-red-500" />;
default:
return <Circle size={14} className="text-slate-300" />;
}
}
export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const [detail, setDetail] = useState<WorkflowDetail | null>(null);
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState<string[]>([]);
const [sseConnected, setSseConnected] = useState(false);
const [replyText, setReplyText] = useState('');
const [activeTab, setActiveTab] = useState<'chat' | 'diagram'>('chat');
const eventSourceRef = useRef<EventSource | null>(null);
const logsEndRef = useRef<HTMLDivElement>(null);
const fetchDetail = async (traceId: string) => {
setLoading(true);
setLogs([]);
try {
const response = await apiClient.get(`/api/v1/workflow/${traceId}`);
setDetail(response.data);
@@ -48,6 +38,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
}
fetchDetail(selectedWorkflow);
setLogs([]); // Reset logs when changing workflow
const protocol = window.location.protocol;
const host = window.location.host;
@@ -55,17 +46,13 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
eventSourceRef.current = es;
es.onopen = () => {
setSseConnected(true);
};
es.onopen = () => setSseConnected(true);
es.onmessage = (event) => {
setLogs(prev => [...prev, event.data]);
};
es.onerror = () => {
setSseConnected(false);
};
es.onerror = () => setSseConnected(false);
const interval = setInterval(() => {
fetchDetail(selectedWorkflow);
@@ -78,127 +65,127 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
};
}, [selectedWorkflow]);
const isActive = detail?.status === 'llm_working' || detail?.status === 'tool_working';
useEffect(() => {
if (activeTab === 'chat' && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, activeTab]);
if (!selectedWorkflow) {
return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0 justify-center items-center p-6 text-center">
<Activity size={32} className="text-slate-300 mb-4" />
<h3 className="text-sm font-semibold text-slate-600">No Workflow Selected</h3>
<p className="text-xs text-slate-400 mt-2">Select a workflow from the left panel to view its details.</p>
</div>
);
}
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 (err) {
console.error("Failed to send reply", err);
setLogs(prev => [...prev, `[System Error]: Failed to send reply.`]);
}
};
if (!selectedWorkflow) return null;
return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0">
<div className="h-14 border-b border-slate-100 flex items-center px-4 justify-between bg-slate-50/50">
<h2 className="font-semibold text-slate-800 text-sm flex items-center">
<Terminal size={16} className="mr-2 text-slate-500" />
Workflow Detail
</h2>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs rounded-md font-medium border ${sseConnected ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500 border-slate-200'}`}>
{sseConnected ? 'Live' : '--'}
<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?.workflow_title || 'Loading...'}>
{detail?.workflow_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'}
</span>
<button
onClick={() => selectedWorkflow && fetchDetail(selectedWorkflow)}
className="p-1 text-slate-400 hover:text-blue-600 rounded transition-colors"
title="Refresh"
>
<RefreshCw size={14} />
</button>
</div>
{/* Navigation Tabs */}
<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>
<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 p-4 overflow-y-auto">
{loading && !detail ? (
<div className="text-center text-slate-400 text-sm py-8">
<Loader2 size={24} className="animate-spin mx-auto mb-2" />
Loading...
<div className="flex-1 flex overflow-hidden bg-slate-50 relative">
{activeTab === 'diagram' ? (
<div className="absolute inset-0">
{detail?.steps && detail.steps.length > 0 ? (
<WorkflowDiagram steps={detail.steps} currentStep={detail.current_step} status={detail.status} />
) : (
<div className="h-full flex items-center justify-center text-slate-400">
Workflow steps are not yet generated.
</div>
)}
</div>
) : !detail ? (
<div className="text-center text-slate-400 text-sm py-8">Failed to load workflow details</div>
) : (
<>
{/* Header */}
<div className="mb-4">
<h3 className="text-base font-bold text-slate-800">
{detail.workflow_title || 'Workflow'}
</h3>
<p className="text-xs text-slate-500 font-mono mt-1">ID: {detail.event_id}</p>
{detail.command && (
<p className="text-xs text-slate-500 mt-1 truncate">Command: {detail.command}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
detail.status === 'failed' ? 'bg-red-100 text-red-700' :
isActive ? 'bg-blue-100 text-blue-700' :
detail.status === 'waiting_llm_working' || detail.status === 'waiting_tool_working' ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
{detail.status}
</span>
<span className="text-xs text-slate-400">
Step {detail.current_step}/{detail.steps.length}
</span>
</div>
</div>
{/* Steps */}
{detail.steps.length > 0 && (
<div className="mb-4">
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Steps</h4>
<div className="space-y-1.5">
{detail.steps.map((step: WorkflowStep) => (
<div
key={step.step}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs border ${
step.step === detail.current_step && isActive
? 'border-blue-200 bg-blue-50'
: step.status === 'completed'
? 'border-green-100 bg-green-50/50'
: step.status === 'failed'
? 'border-red-100 bg-red-50/50'
: 'border-slate-100 bg-white'
}`}
>
{stepStatusIcon(step.status)}
<span className="font-medium text-slate-700 w-5 text-right">{step.step}</span>
<span className="text-slate-600 truncate flex-1">{step.name}</span>
<span className="text-slate-400 text-[10px]">{step.node}</span>
</div>
))}
<div className="flex-1 flex flex-col p-6 overflow-hidden">
{/* Command Header */}
{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>
</div>
)}
)}
{detail.steps.length === 0 && (
<div className="text-center py-4">
<Clock size={24} className="text-slate-300 mx-auto mb-2" />
<p className="text-xs text-slate-400">Workflow is being generated...</p>
</div>
)}
{/* SSE Logs */}
{logs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Live Logs</h4>
<div className="relative border-l-2 border-slate-200 ml-3 pl-5 space-y-3">
{logs.map((msg, idx) => (
<div key={idx} className="relative">
<div className={`absolute -left-[27px] top-1 w-3 h-3 rounded-full border-2 border-white shadow-sm ${idx === logs.length - 1 && sseConnected ? 'bg-blue-500 animate-pulse' : 'bg-green-500'}`} />
<p className="text-[11px] font-mono text-slate-600 leading-relaxed break-all">{msg}</p>
{/* Live Chat / Logs Area */}
<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...
</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>
</div>
)}
))
)}
<div ref={logsEndRef} />
</div>
{logs.length === 0 && sseConnected && isActive && (
<div className="text-xs text-slate-400 italic mt-2">Waiting for live events...</div>
)}
</>
{/* Input Area */}
<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>
)}
</div>
</div>