feat(chat): refactor chat page layout with secondary agent list sidebar
- 聊天页移至 /chat 路由,不再强制依赖 URL id - 左侧增加二级侧边栏:展示用户所有 Agent 列表 - 当未选择 Agent 时显示空白状态占位 - 历史会话迁移至顶部栏『历史对话』Drawer 内main
parent
e474b4578d
commit
cf4791b34d
18
src/App.tsx
18
src/App.tsx
|
|
@ -18,19 +18,7 @@ import { useAuth } from './store/auth';
|
|||
import { AgentAPI } from './api';
|
||||
|
||||
function HomeRedirect() {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
AgentAPI.list().then(list => {
|
||||
if (list.length > 0) {
|
||||
navigate(`/agents/${list[0].id}/chat`, { replace: true });
|
||||
} else {
|
||||
navigate('/agents', { replace: true });
|
||||
}
|
||||
}).catch(() => {
|
||||
navigate('/agents', { replace: true });
|
||||
});
|
||||
}, [navigate]);
|
||||
return <div className="flex items-center justify-center h-full"><Spin size="large" /></div>;
|
||||
return <Navigate to="/chat" replace />;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
|
|
@ -54,10 +42,12 @@ export default function App() {
|
|||
const mainContent = (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomeRedirect />} />
|
||||
<Route path="/chat" element={<ChatPage />} />
|
||||
<Route path="/chat/:id" element={<ChatPage />} />
|
||||
<Route path="/agents" element={<AgentList />} />
|
||||
<Route path="/agents/new" element={<AgentEditor />} />
|
||||
<Route path="/agents/:id" element={<AgentEditor />} />
|
||||
<Route path="/agents/:id/chat" element={<ChatPage />} />
|
||||
<Route path="/agents/:id/chat" element={<Navigate to="/chat" replace />} />
|
||||
<Route path="/marketplace" element={<MarketplacePage />} />
|
||||
<Route path="/teams" element={<TeamsPage />} />
|
||||
<Route path="/prompts" element={<PromptLibraryPage />} />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const NAV_GROUPS: Array<{
|
|||
{
|
||||
label: '工作台',
|
||||
items: [
|
||||
{ to: '/', icon: <MessageOutlined />, label: '聊天', end: true },
|
||||
{ to: '/chat', icon: <MessageOutlined />, label: '聊天' },
|
||||
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
|
||||
{ to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -226,10 +226,10 @@ export default function AgentList() {
|
|||
</Space>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', paddingTop: 16, borderTop: '1px solid var(--color-border)' }}>
|
||||
<Link to={`/agents/${a.id}/chat`} style={{ flex: 1 }}>
|
||||
<Link to={`/chat/${a.id}`} style={{ flex: 1 }}>
|
||||
<Button type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
|
||||
对话
|
||||
</Button>
|
||||
聊天
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={`/agents/${a.id}`} style={{ flex: 1 }}>
|
||||
<Button block icon={<EditOutlined />} style={{ borderRadius: 12, height: 40 }}>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ export default function ChatPage() {
|
|||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const [highlightId, setHighlightId] = useState<string | null>(null);
|
||||
const [sessionRefresh, setSessionRefresh] = useState(0);
|
||||
const [agentList, setAgentList] = useState<Agent[]>([]);
|
||||
const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
|
||||
const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false);
|
||||
const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false);
|
||||
const [tplDrawerOpen, setTplDrawerOpen] = useState(false);
|
||||
|
|
@ -84,11 +86,24 @@ export default function ChatPage() {
|
|||
});
|
||||
};
|
||||
|
||||
const loadAgent = async () => {
|
||||
if (!id) return;
|
||||
const a = await AgentAPI.detail(id);
|
||||
setAgent(a);
|
||||
};
|
||||
const loadAgent = async () => {
|
||||
if (!id) {
|
||||
setAgent(null);
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
const a = await AgentAPI.detail(id);
|
||||
setAgent(a);
|
||||
};
|
||||
|
||||
const loadAgentList = async () => {
|
||||
try {
|
||||
const list = await AgentAPI.list();
|
||||
setAgentList(list);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
|
|
@ -117,17 +132,22 @@ export default function ChatPage() {
|
|||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAgent();
|
||||
useEffect(() => {
|
||||
loadAgentList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAgent();
|
||||
loadProviders();
|
||||
return () => abortRef.current?.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMessages();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, highlightId]);
|
||||
return () => abortRef.current?.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
loadMessages();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, highlightId, id]);
|
||||
|
||||
/** 把附件文本拼成 system 注入字符串 */
|
||||
const buildAttachmentsText = () => {
|
||||
|
|
@ -394,12 +414,10 @@ export default function ChatPage() {
|
|||
activeModelOption?.providerName ||
|
||||
(activeModelValue.includes('/') ? providers.find((item) => item.id === activeModelValue.split('/')[0])?.name : defaultProvider?.name) ||
|
||||
(agentModel ? '当前 Agent 配置' : defaultProvider?.name || '系统默认');
|
||||
|
||||
if (!agent) return null;
|
||||
|
||||
return (
|
||||
<div className="chat-shell">
|
||||
{/* 1. 二级侧边栏:会话列表 */}
|
||||
|
||||
return (
|
||||
<div className="chat-shell">
|
||||
{/* 1. 二级侧边栏:智能体列表 */}
|
||||
<aside
|
||||
className="chat-side"
|
||||
style={{
|
||||
|
|
@ -407,143 +425,109 @@ export default function ChatPage() {
|
|||
flexDirection: 'column',
|
||||
gap: 0,
|
||||
background: 'var(--color-surface)',
|
||||
borderRight: '1px solid var(--color-border)'
|
||||
borderRight: '1px solid var(--color-border)',
|
||||
width: 260
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
padding: '8px 0 16px',
|
||||
borderBottom: '1px solid var(--color-border)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="avatar"
|
||||
style={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: '50%',
|
||||
background: agent.avatar || 'var(--gradient-brand)',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'var(--shadow-sm)'
|
||||
}}
|
||||
>
|
||||
{isImageUrl(agent.avatar) ? (
|
||||
<img src={agent.avatar} className="w-full h-full object-cover" alt="avatar" />
|
||||
) : (
|
||||
(agent.name?.charAt(0) || '?').toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 15, color: 'var(--color-text)' }}>{agent.name}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-secondary)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{agent.description || '智能体'}
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid var(--color-border)' }}>
|
||||
<Button block type="dashed" onClick={() => navigate('/agents/new')}>+ 创建智能体</Button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
|
||||
{agentList.map((a) => {
|
||||
const isActive = a.id === id;
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
onClick={() => navigate(`/chat/${a.id}`)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '10px 16px',
|
||||
cursor: 'pointer',
|
||||
background: isActive ? 'var(--color-surface-2)' : 'transparent',
|
||||
borderLeft: `3px solid ${isActive ? 'var(--color-brand)' : 'transparent'}`,
|
||||
transition: 'background 0.2s'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
background: a.avatar || 'var(--gradient-brand)',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontSize: 14,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{isImageUrl(a.avatar) ? (
|
||||
<img src={a.avatar} className="w-full h-full object-cover" alt="avatar" />
|
||||
) : (
|
||||
(a.name?.charAt(0) || '?').toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: isActive ? 600 : 500, fontSize: 14, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{a.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 2. 主聊天区 */}
|
||||
<section className="chat-main" style={{ flex: 1, display: 'flex', flexDirection: 'column', position: 'relative' }}>
|
||||
{!agent ? (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Empty description="请在左侧选择一个智能体开始对话" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="chat-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 24px' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 16, color: 'var(--color-text)' }}>{agent.name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)' }}>
|
||||
{agent.model || '默认模型'} · T={agent.temperature}
|
||||
</div>
|
||||
</div>
|
||||
<Space>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginRight: 12 }}>
|
||||
<span style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>流式输出</span>
|
||||
<Switch size="small" checked={useStream} onChange={setUseStream} />
|
||||
</div>
|
||||
<Button size="small" onClick={() => setHistoryDrawerOpen(true)}>历史对话</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'params', label: '模型参数', icon: <SettingOutlined />, onClick: () => setParamsDrawerOpen(true) },
|
||||
{ key: 'mcp', label: 'MCP 资源', icon: <ApiOutlined />, onClick: () => setMcpDrawerOpen(true) },
|
||||
{ key: 'edit', label: '管理 Agent', icon: <EditOutlined />, onClick: () => navigate(`/agents/${id}`) },
|
||||
{ type: 'divider' },
|
||||
{ key: 'clear', label: '清空对话', icon: <DeleteOutlined />, danger: true, onClick: () => {
|
||||
Modal.confirm({
|
||||
title: '清空当前会话所有消息?',
|
||||
onOk: handleClear
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<Button size="small">更多 <DownOutlined /></Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 会话切换 */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
{id && (
|
||||
<SessionSidebar
|
||||
agentId={id}
|
||||
activeSessionId={sessionId}
|
||||
onChange={(sid, opts) => {
|
||||
setSessionId(sid);
|
||||
setHighlightId(opts?.highlightMessageId || null);
|
||||
}}
|
||||
refreshTick={sessionRefresh}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0', borderColor: 'var(--color-border)' }} />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={4}>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--color-surface-2)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--color-border)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: 'var(--color-text-secondary)', fontWeight: 500 }}>
|
||||
⚡ 流式输出
|
||||
</span>
|
||||
<Switch size="small" checked={useStream} onChange={setUseStream} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
block
|
||||
size="middle"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => setParamsDrawerOpen(true)}
|
||||
style={{ borderRadius: 10, height: 38 }}
|
||||
>
|
||||
模型参数
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="middle"
|
||||
icon={<ApiOutlined />}
|
||||
onClick={() => setMcpDrawerOpen(true)}
|
||||
style={{ borderRadius: 10, height: 38 }}
|
||||
>
|
||||
MCP 资源
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="middle"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/agents/${id}`)}
|
||||
style={{ borderRadius: 10, height: 38 }}
|
||||
>
|
||||
管理 Agent
|
||||
</Button>
|
||||
<Popconfirm title="清空当前会话所有消息?" onConfirm={handleClear}>
|
||||
<Button danger block size="middle" icon={<DeleteOutlined />} style={{ borderRadius: 10, height: 38 }}>
|
||||
清空对话
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</aside>
|
||||
|
||||
{/* 2. 主聊天区 */}
|
||||
<section className="chat-main">
|
||||
{/* Header */}
|
||||
<div className="chat-header">
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 16, color: 'var(--color-text)' }}>{agent.name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)' }}>
|
||||
{agent.model || '默认模型'} · T={agent.temperature}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div ref={bodyRef} className="chat-body">
|
||||
|
||||
{/* Body */}
|
||||
<div ref={bodyRef} className="chat-body">
|
||||
<div className="messages-container">
|
||||
{messages.length === 0 && !streaming.active ? (
|
||||
<div style={{ textAlign: 'center', marginTop: 120 }}>
|
||||
|
|
@ -778,12 +762,14 @@ export default function ChatPage() {
|
|||
</div>
|
||||
<div style={{ textAlign: 'center', fontSize: 11, color: 'var(--color-text-tertiary)', marginTop: 8 }}>
|
||||
AI 可能会产生错误信息,请核实重要信息。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---- 抽屉组件保持不变 ---- */}
|
||||
{id && (
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ---- 抽屉组件保持不变 ---- */}
|
||||
{id && (
|
||||
<McpResourcesDrawer
|
||||
agentId={id}
|
||||
open={mcpDrawerOpen}
|
||||
|
|
@ -889,12 +875,34 @@ export default function ChatPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Button block onClick={() => setOverrides({})}>
|
||||
🔄 全部恢复默认
|
||||
</Button>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
<Button block onClick={() => setOverrides({})}>
|
||||
🔄 全部恢复默认
|
||||
</Button>
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="历史对话"
|
||||
placement="right"
|
||||
width={320}
|
||||
onClose={() => setHistoryDrawerOpen(false)}
|
||||
open={historyDrawerOpen}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{id && (
|
||||
<SessionSidebar
|
||||
agentId={id}
|
||||
activeSessionId={sessionId}
|
||||
onChange={(sid, opts) => {
|
||||
setSessionId(sid);
|
||||
setHighlightId(opts?.highlightMessageId || null);
|
||||
setHistoryDrawerOpen(false);
|
||||
}}
|
||||
refreshTick={sessionRefresh}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 输入栏子组件 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue