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';
|
import { AgentAPI } from './api';
|
||||||
|
|
||||||
function HomeRedirect() {
|
function HomeRedirect() {
|
||||||
const navigate = useNavigate();
|
return <Navigate to="/chat" replace />;
|
||||||
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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
@ -54,10 +42,12 @@ export default function App() {
|
||||||
const mainContent = (
|
const mainContent = (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomeRedirect />} />
|
<Route path="/" element={<HomeRedirect />} />
|
||||||
|
<Route path="/chat" element={<ChatPage />} />
|
||||||
|
<Route path="/chat/:id" element={<ChatPage />} />
|
||||||
<Route path="/agents" element={<AgentList />} />
|
<Route path="/agents" element={<AgentList />} />
|
||||||
<Route path="/agents/new" element={<AgentEditor />} />
|
<Route path="/agents/new" element={<AgentEditor />} />
|
||||||
<Route path="/agents/:id" 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="/marketplace" element={<MarketplacePage />} />
|
||||||
<Route path="/teams" element={<TeamsPage />} />
|
<Route path="/teams" element={<TeamsPage />} />
|
||||||
<Route path="/prompts" element={<PromptLibraryPage />} />
|
<Route path="/prompts" element={<PromptLibraryPage />} />
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const NAV_GROUPS: Array<{
|
||||||
{
|
{
|
||||||
label: '工作台',
|
label: '工作台',
|
||||||
items: [
|
items: [
|
||||||
{ to: '/', icon: <MessageOutlined />, label: '聊天', end: true },
|
{ to: '/chat', icon: <MessageOutlined />, label: '聊天' },
|
||||||
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
|
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
|
||||||
{ to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
|
{ to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -226,9 +226,9 @@ export default function AgentList() {
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', paddingTop: 16, borderTop: '1px solid var(--color-border)' }}>
|
<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 type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
|
||||||
对话
|
聊天
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={`/agents/${a.id}`} style={{ flex: 1 }}>
|
<Link to={`/agents/${a.id}`} style={{ flex: 1 }}>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export default function ChatPage() {
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
const [highlightId, setHighlightId] = useState<string | null>(null);
|
const [highlightId, setHighlightId] = useState<string | null>(null);
|
||||||
const [sessionRefresh, setSessionRefresh] = useState(0);
|
const [sessionRefresh, setSessionRefresh] = useState(0);
|
||||||
|
const [agentList, setAgentList] = useState<Agent[]>([]);
|
||||||
|
const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
|
||||||
const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false);
|
const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false);
|
||||||
const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false);
|
const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false);
|
||||||
const [tplDrawerOpen, setTplDrawerOpen] = useState(false);
|
const [tplDrawerOpen, setTplDrawerOpen] = useState(false);
|
||||||
|
|
@ -85,11 +87,24 @@ export default function ChatPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAgent = async () => {
|
const loadAgent = async () => {
|
||||||
if (!id) return;
|
if (!id) {
|
||||||
|
setAgent(null);
|
||||||
|
setMessages([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const a = await AgentAPI.detail(id);
|
const a = await AgentAPI.detail(id);
|
||||||
setAgent(a);
|
setAgent(a);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAgentList = async () => {
|
||||||
|
try {
|
||||||
|
const list = await AgentAPI.list();
|
||||||
|
setAgentList(list);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
try {
|
try {
|
||||||
const list = await LLMProviderAPI.list();
|
const list = await LLMProviderAPI.list();
|
||||||
|
|
@ -117,6 +132,10 @@ export default function ChatPage() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAgentList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAgent();
|
loadAgent();
|
||||||
loadProviders();
|
loadProviders();
|
||||||
|
|
@ -125,9 +144,10 @@ export default function ChatPage() {
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
loadMessages();
|
loadMessages();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [sessionId, highlightId]);
|
}, [sessionId, highlightId, id]);
|
||||||
|
|
||||||
/** 把附件文本拼成 system 注入字符串 */
|
/** 把附件文本拼成 system 注入字符串 */
|
||||||
const buildAttachmentsText = () => {
|
const buildAttachmentsText = () => {
|
||||||
|
|
@ -395,11 +415,9 @@ export default function ChatPage() {
|
||||||
(activeModelValue.includes('/') ? providers.find((item) => item.id === activeModelValue.split('/')[0])?.name : defaultProvider?.name) ||
|
(activeModelValue.includes('/') ? providers.find((item) => item.id === activeModelValue.split('/')[0])?.name : defaultProvider?.name) ||
|
||||||
(agentModel ? '当前 Agent 配置' : defaultProvider?.name || '系统默认');
|
(agentModel ? '当前 Agent 配置' : defaultProvider?.name || '系统默认');
|
||||||
|
|
||||||
if (!agent) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-shell">
|
<div className="chat-shell">
|
||||||
{/* 1. 二级侧边栏:会话列表 */}
|
{/* 1. 二级侧边栏:智能体列表 */}
|
||||||
<aside
|
<aside
|
||||||
className="chat-side"
|
className="chat-side"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -407,139 +425,105 @@ export default function ChatPage() {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 0,
|
gap: 0,
|
||||||
background: 'var(--color-surface)',
|
background: 'var(--color-surface)',
|
||||||
borderRight: '1px solid var(--color-border)'
|
borderRight: '1px solid var(--color-border)',
|
||||||
|
width: 260
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<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
|
<div
|
||||||
|
key={a.id}
|
||||||
|
onClick={() => navigate(`/chat/${a.id}`)}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
marginBottom: 20,
|
padding: '10px 16px',
|
||||||
padding: '8px 0 16px',
|
cursor: 'pointer',
|
||||||
borderBottom: '1px solid var(--color-border)'
|
background: isActive ? 'var(--color-surface-2)' : 'transparent',
|
||||||
|
borderLeft: `3px solid ${isActive ? 'var(--color-brand)' : 'transparent'}`,
|
||||||
|
transition: 'background 0.2s'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="avatar"
|
|
||||||
style={{
|
style={{
|
||||||
width: 42,
|
width: 32,
|
||||||
height: 42,
|
height: 32,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: agent.avatar || 'var(--gradient-brand)',
|
background: a.avatar || 'var(--gradient-brand)',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: 18,
|
fontSize: 14,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden'
|
||||||
boxShadow: 'var(--shadow-sm)'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isImageUrl(agent.avatar) ? (
|
{isImageUrl(a.avatar) ? (
|
||||||
<img src={agent.avatar} className="w-full h-full object-cover" alt="avatar" />
|
<img src={a.avatar} className="w-full h-full object-cover" alt="avatar" />
|
||||||
) : (
|
) : (
|
||||||
(agent.name?.charAt(0) || '?').toUpperCase()
|
(a.name?.charAt(0) || '?').toUpperCase()
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 15, color: 'var(--color-text)' }}>{agent.name}</div>
|
<div style={{ fontWeight: isActive ? 600 : 500, fontSize: 14, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
<div
|
{a.name}
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: 'var(--color-text-secondary)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{agent.description || '智能体'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</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>
|
</aside>
|
||||||
|
|
||||||
{/* 2. 主聊天区 */}
|
{/* 2. 主聊天区 */}
|
||||||
<section className="chat-main">
|
<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 */}
|
{/* Header */}
|
||||||
<div className="chat-header">
|
<div className="chat-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 24px' }}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: 16, color: 'var(--color-text)' }}>{agent.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 16, color: 'var(--color-text)' }}>{agent.name}</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)' }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)' }}>
|
||||||
{agent.model || '默认模型'} · T={agent.temperature}
|
{agent.model || '默认模型'} · T={agent.temperature}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
|
|
@ -780,6 +764,8 @@ export default function ChatPage() {
|
||||||
AI 可能会产生错误信息,请核实重要信息。
|
AI 可能会产生错误信息,请核实重要信息。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---- 抽屉组件保持不变 ---- */}
|
{/* ---- 抽屉组件保持不变 ---- */}
|
||||||
|
|
@ -893,6 +879,28 @@ export default function ChatPage() {
|
||||||
🔄 全部恢复默认
|
🔄 全部恢复默认
|
||||||
</Button>
|
</Button>
|
||||||
</Drawer>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue