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
+103 -150
View File
@@ -1,33 +1,29 @@
import { useState, useEffect } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Plus, Trash2, MessageSquare, Workflow as WorkflowIcon } from 'lucide-react';
import apiClient from '../../api/client';
import type { Workflow } from '../../types';
import type { ChatSession } from '../../App';
import { useChatStore } from '../../store/useChatStore';
interface LeftPanelProps {
activeTab: string;
selectedWorkflow: string | null;
setSelectedWorkflow: (id: string | null) => void;
chatSessions?: ChatSession[];
setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId?: string | null;
setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>;
onSessionsChanged?: () => void;
}
export function LeftPanel({
activeTab,
selectedWorkflow,
setSelectedWorkflow,
chatSessions,
setChatSessions,
activeSessionId,
setActiveSessionId,
onSessionsChanged,
}: LeftPanelProps) {
export function LeftPanel({ activeTab }: LeftPanelProps) {
const { t } = useTranslation();
const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [loadingWorkflows, setLoadingWorkflows] = useState(false);
const {
sessions,
activeSessionId,
setActiveSessionId,
removeSession,
createChat,
selectedWorkflow,
setSelectedWorkflow,
} = useChatStore();
useEffect(() => {
let intervalId: ReturnType<typeof setInterval>;
@@ -62,148 +58,105 @@ export function LeftPanel({
}, [activeTab]);
const handleNewChat = async () => {
if (!setChatSessions || !setActiveSessionId || !onSessionsChanged) return;
try {
const response = await apiClient.post('/api/v1/chat', {
title: '新对话',
initial_message: '你好',
});
const chatId: string = response.data.chat_id;
const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
const newSession: ChatSession = {
id: chatId,
title: '新对话',
messages: [
{ id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() },
{ id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
],
updatedAt: Date.now(),
};
setChatSessions((prev) => [newSession, ...prev]);
setActiveSessionId(chatId);
onSessionsChanged();
} catch (error) {
console.error('Failed to create chat session', error);
}
await createChat(t('chat.newChat'), '你好');
};
const handleDeleteChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
if (!setChatSessions || !setActiveSessionId || !chatSessions) return;
const updated = chatSessions.filter((s) => s.id !== id);
setChatSessions(updated);
if (activeSessionId === id) {
setActiveSessionId(updated.length > 0 ? updated[0].id : null);
}
removeSession(id);
};
const isChats = activeTab === 'chats';
return (
<div className="w-72 bg-white border-r border-slate-100 flex flex-col z-0 shrink-0">
<div className="flex-1 flex flex-col overflow-hidden">
<div className="w-64 bg-bg-sidebar border-r border-border-primary flex flex-col shrink-0">
<div className="flex items-center justify-between px-4 py-3 border-b border-border-primary">
<span className="text-[11px] font-bold text-text-muted uppercase tracking-widest">
{isChats ? t('chat.chatHistory') : t('nav.workflow')}
</span>
<button
onClick={() => {
if (isChats) handleNewChat();
else setSelectedWorkflow('new');
}}
className="p-1.5 rounded-lg bg-bg-hover text-text-muted hover:text-accent hover:bg-accent-light transition-all"
title={isChats ? t('chat.newChat') : t('workflow.createWorkflow')}
>
<Plus size={14} />
</button>
</div>
<div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
<span className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
{activeTab === 'chats' ? 'Chat History' : 'Workflows'}
</span>
<button
onClick={() => {
if (activeTab === 'chats') {
handleNewChat();
} else {
setSelectedWorkflow('new');
}
}}
className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
title={activeTab === 'chats' ? 'New Chat' : 'New Workflow'}
>
<Plus size={16} />
</button>
</div>
<div className="flex-1 p-3 overflow-y-auto">
{activeTab === 'workflows' && (
<div className="space-y-2">
{loadingWorkflows ? (
<div className="text-center text-slate-400 text-sm py-4">Loading workflows...</div>
) : workflows.length === 0 ? (
<div className="text-center text-slate-400 text-sm py-4"><br/> + </div>
) : (
workflows.map((wf) => (
<div
key={wf.trace_id}
onClick={() => setSelectedWorkflow(wf.trace_id)}
className={`p-3 rounded-lg border cursor-pointer transition-all ${
selectedWorkflow === wf.trace_id
? 'border-blue-300 bg-blue-50 shadow-sm'
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
}`}
>
<div className="flex justify-between items-center mb-1">
<span
className={`font-medium text-sm ${
selectedWorkflow === wf.trace_id ? 'text-blue-700' : 'text-slate-700'
}`}
>
{wf.title || 'Unnamed Workflow'}
</span>
<span
className={`flex h-2 w-2 rounded-full ${
wf.status && (wf.status.includes('working'))
? 'bg-green-400 animate-pulse'
: wf.status === 'failed'
? 'bg-red-400'
: wf.status === 'completed'
? 'bg-green-500'
: 'bg-slate-300'
}`}
></span>
</div>
<p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.trace_id}</p>
</div>
))
)}
<div className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
{isChats ? (
sessions.length === 0 ? (
<div className="px-3 py-8 text-center text-text-muted text-xs">
{t('chat.noHistory')}
</div>
)}
{activeTab === 'chats' && chatSessions && (
<div className="space-y-2">
{chatSessions.length === 0 ? (
<div className="text-center text-slate-400 text-sm py-8">
No chat history.<br/>Click + to start a new chat.
) : (
sessions.map((session) => (
<div
key={session.id}
onClick={() => setActiveSessionId(session.id)}
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
activeSessionId === session.id
? 'bg-accent-light text-accent'
: 'hover:bg-bg-hover text-text-secondary'
}`}
>
<MessageSquare size={14} className={`flex-shrink-0 ${activeSessionId === session.id ? 'text-accent' : 'text-text-muted'}`} />
<div className="flex-1 min-w-0">
<h3 className={`text-xs font-medium truncate ${activeSessionId === session.id ? 'text-accent' : 'text-text-secondary'}`}>
{session.title}
</h3>
<p className="text-[10px] text-text-muted mt-0.5">
{new Date(session.updatedAt).toLocaleDateString()}
</p>
</div>
) : (
chatSessions.map((session) => (
<div
key={session.id}
onClick={() => setActiveSessionId?.(session.id)}
className={`group flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-all ${
activeSessionId === session.id
? 'border-blue-300 bg-blue-50 shadow-sm'
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
}`}
>
<div className="flex-1 min-w-0 mr-2">
<h3 className={`font-medium text-sm truncate ${
activeSessionId === session.id ? 'text-blue-700' : 'text-slate-700'
}`}>
{session.title}
</h3>
<p className="text-xs text-slate-400 mt-1">
{new Date(session.updatedAt).toLocaleDateString()}
</p>
</div>
<button
onClick={(e) => handleDeleteChat(e, session.id)}
className="text-slate-400 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-all"
>
<Trash2 size={16} />
</button>
</div>
))
)}
<button
onClick={(e) => handleDeleteChat(e, session.id)}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-text-muted hover:text-danger hover:bg-danger-bg transition-all"
>
<Trash2 size={12} />
</button>
</div>
))
)
) : (
loadingWorkflows ? (
<div className="px-3 py-8 text-center text-text-muted text-xs">{t('workflow.loading')}</div>
) : workflows.length === 0 ? (
<div className="px-3 py-8 text-center text-text-muted text-xs">
{t('workflow.noWorkflows')}
</div>
)}
</div>
) : (
workflows.map((wf) => (
<div
key={wf.trace_id}
onClick={() => setSelectedWorkflow(wf.trace_id)}
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
selectedWorkflow === wf.trace_id
? 'bg-accent-light'
: 'hover:bg-bg-hover'
}`}
>
<WorkflowIcon size={14} className={`flex-shrink-0 ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-muted'}`} />
<div className="flex-1 min-w-0">
<h3 className={`text-xs font-medium truncate ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-secondary'}`}>
{wf.title || 'Unnamed'}
</h3>
<div className="flex items-center gap-1.5 mt-0.5">
<span className={`w-1.5 h-1.5 rounded-full ${
wf.status?.includes('working') ? 'bg-accent animate-pulse' :
wf.status === 'failed' ? 'bg-danger' :
wf.status === 'completed' ? 'bg-success' : 'bg-text-muted'
}`} />
<span className="text-[10px] text-text-muted font-mono truncate">{wf.trace_id.slice(-8)}</span>
</div>
</div>
</div>
))
)
)}
</div>
</div>
);