feat(frontend):优化前端页面设计
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user