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
+153 -102
View File
@@ -1,31 +1,47 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { MessageSquare, Activity, Terminal, ChevronRight, Plus } from 'lucide-react';
import apiClient from '../../api/client';
import type { ChatSession, Message } from '../../App';
interface ChatMessage {
id: string;
sender: 'user' | 'ai';
text: string;
timestamp: Date;
eventId?: string;
interface ChatPanelProps {
chatSessions: ChatSession[];
setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId: string | null;
setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
}
export function ChatPanel() {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
sender: 'ai',
text: "Hello! I am Pretor Assistant. How can I help you today?",
timestamp: new Date()
}
]);
export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) {
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const activeSession = chatSessions.find((s) => s.id === activeSessionId) || null;
const messages = activeSession ? activeSession.messages : [];
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const updateSessionMessages = (newMessages: Message[]) => {
if (!activeSessionId) return;
setChatSessions((prev) =>
prev.map((s) =>
s.id === activeSessionId
? { ...s, messages: newMessages, updatedAt: Date.now() }
: s
)
);
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file || !activeSessionId) return;
const formData = new FormData();
formData.append('file', file);
@@ -33,26 +49,24 @@ export function ChatPanel() {
setLoading(true);
try {
const response = await apiClient.post('/api/v1/adapter/client/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
headers: { 'Content-Type': 'multipart/form-data' },
});
const aiMessage: ChatMessage = {
const aiMessage: Message = {
id: Date.now().toString(),
sender: 'ai',
text: `已上传文件: ${response.data.filename}`,
timestamp: new Date()
role: 'assistant',
content: `已上传文件: ${response.data.filename}`,
timestamp: Date.now(),
};
setMessages(prev => [...prev, aiMessage]);
updateSessionMessages([...messages, aiMessage]);
} catch (error) {
console.error("Error uploading file", error);
const errorMessage: ChatMessage = {
console.error('Error uploading file', error);
const errorMessage: Message = {
id: Date.now().toString(),
sender: 'ai',
text: "文件上传失败。",
timestamp: new Date()
role: 'assistant',
content: '文件上传失败。',
timestamp: Date.now(),
};
setMessages(prev => [...prev, errorMessage]);
updateSessionMessages([...messages, errorMessage]);
} finally {
setLoading(false);
if (fileInputRef.current) {
@@ -62,77 +76,116 @@ export function ChatPanel() {
};
const handleSendMessage = async () => {
if (!input.trim()) return;
if (!input.trim() || !activeSessionId) return;
const userMessage: ChatMessage = {
const userText = input;
const userMessage: Message = {
id: Date.now().toString(),
sender: 'user',
text: input,
timestamp: new Date()
role: 'user',
content: userText,
timestamp: Date.now(),
};
setMessages(prev => [...prev, userMessage]);
updateSessionMessages([...messages, userMessage]);
setInput('');
setLoading(true);
try {
// Assuming a token might be needed, apiClient should handle it if set
const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : '';
const response = await apiClient.post('/api/v1/adapter/client', {
message: promptModifier + userMessage.text
message: promptModifier + userMessage.content,
});
const aiMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
sender: 'ai',
text: typeof response.data.message === 'string' && response.data.message.includes('-')
? "Task has been created." // It's an event ID
: response.data.message || "I received your message.",
eventId: typeof response.data.message === 'string' && response.data.message.includes('-') ? response.data.message : undefined,
timestamp: new Date()
};
const responseData = response.data.message;
let aiContent = responseData || 'I received your message.';
setMessages(prev => [...prev, aiMessage]);
// If we got an event_id, we could potentially open a websocket to listen to its stream
if (aiMessage.eventId) {
console.log(`Open WS to track event: ${aiMessage.eventId}`);
// Implement WS tracking if needed
// Auto-update title if it's the first user message
if (messages.length <= 1 && userText.length > 0) {
setChatSessions((prev) =>
prev.map((s) =>
s.id === activeSessionId
? { ...s, title: userText.slice(0, 20) + (userText.length > 20 ? '...' : '') }
: s
)
);
}
} catch (error) {
console.error("Error sending message", error);
const errorMessage: ChatMessage = {
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
sender: 'ai',
text: "Sorry, I encountered an error communicating with the server.",
timestamp: new Date()
role: 'assistant',
content: aiContent,
timestamp: Date.now(),
};
setMessages(prev => [...prev, errorMessage]);
updateSessionMessages([...messages, userMessage, aiMessage]);
} catch (error) {
console.error('Error sending message', error);
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Sorry, I encountered an error communicating with the server.',
timestamp: Date.now(),
};
updateSessionMessages([...messages, userMessage, errorMessage]);
} finally {
setLoading(false);
}
};
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
if (!activeSessionId) {
return (
<div className="flex-1 flex flex-col bg-white overflow-hidden items-center justify-center">
<Activity size={48} className="text-slate-300 mb-4" />
<h2 className="text-xl font-semibold text-slate-600">Pretor Assistant</h2>
<p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p>
<button
onClick={() => {
const newSession: ChatSession = {
id: Date.now().toString(),
title: 'New Chat',
messages: [
{
id: Date.now().toString(),
role: 'assistant',
content: 'Hello! I am Pretor Assistant. How can I help you today?',
timestamp: Date.now(),
},
],
updatedAt: Date.now(),
};
setChatSessions([newSession, ...chatSessions]);
setActiveSessionId(newSession.id);
}}
className="mt-6 px-6 py-2 bg-blue-200 text-slate-800 rounded-xl shadow-sm hover:bg-blue-300 transition-colors"
>
Start New Chat
</button>
</div>
);
}
// Notice we removed the outer padded div, since App.tsx is handling that layout now
return (
<div className="flex-1 flex flex-col bg-slate-50">
<div className="h-14 border-b border-slate-200 bg-white flex items-center justify-between px-6 shadow-sm z-10">
<div className="flex-1 flex flex-col bg-white overflow-hidden relative">
<div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0">
<div className="flex items-center">
<MessageSquare size={18} className="text-blue-600 mr-3" />
<h1 className="font-semibold text-slate-800">Pretor Assistant</h1>
<h1 className="font-semibold text-slate-800">{activeSession?.title || 'Chat'}</h1>
</div>
<div className="flex space-x-2 bg-slate-100 p-1 rounded-lg">
<div className="flex space-x-2 bg-slate-50 p-1 rounded-lg">
<button
onClick={() => setMode('chat')}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
Chat
</button>
<button
onClick={() => setMode('deploy')}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
Deploy Task
</button>
@@ -140,54 +193,52 @@ export function ChatPanel() {
</div>
{/* Chat History */}
<div className="flex-1 p-6 overflow-y-auto space-y-6">
<div className="flex justify-center">
<span className="text-xs text-slate-400 bg-slate-200/50 px-3 py-1 rounded-full">Today</span>
</div>
<div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.sender === 'ai' && (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600" />
</div>
)}
<div className={`${msg.sender === 'user' ? 'bg-blue-600 text-white rounded-2xl rounded-tr-sm' : 'bg-white border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'} p-4 max-w-[80%] shadow-sm`}>
<p className="text-sm leading-relaxed mb-3">{msg.text}</p>
{msg.eventId && (
<div className="bg-slate-50 border border-slate-100 rounded-lg p-3 flex items-center text-sm">
<Terminal size={16} className="text-slate-400 mr-2" />
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.eventId}</span>
</div>
<div
className={`${
msg.role === 'user'
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
} p-4 max-w-[80%] shadow-sm`}
>
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
{typeof msg.content === 'string' && msg.content.includes('-') && msg.role === 'assistant' && (msg.content.length === 36 || msg.content.includes('任务已创建')) && (
<div className="mt-2 bg-white border border-slate-100 rounded-lg p-3 flex items-center text-sm shadow-sm">
<Terminal size={16} className="text-slate-400 mr-2" />
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.content.substring(0, 36)}</span>
</div>
)}
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600 animate-spin" />
</div>
<div className="bg-white border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
<span className="flex space-x-1">
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
</span>
</div>
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600 animate-spin" />
</div>
<div className="bg-slate-50 border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
<span className="flex space-x-1">
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Chat Input */}
<div className="p-4 bg-white border-t border-slate-200">
<div className="p-4 bg-white border-t border-slate-100 shrink-0">
<div className="relative flex items-center">
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
/>
<input type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" />
<button
onClick={() => fileInputRef.current?.click()}
className="absolute left-2 p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors z-10 cursor-pointer"
@@ -201,12 +252,12 @@ export function ChatPanel() {
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Ask Pretor to do something..."
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-12 pr-12 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-inner"
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-2xl pl-12 pr-12 py-3.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all"
/>
<button
onClick={handleSendMessage}
disabled={loading || !input.trim()}
className="absolute right-2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
className="absolute right-2 p-1.5 bg-blue-200 text-slate-800 rounded-xl hover:bg-blue-300 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
>
<ChevronRight size={18} />
</button>