fix: add mobile navigation and restore chat outline/history access
parent
faef8b6aea
commit
deab555b3a
36
src/App.tsx
36
src/App.tsx
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Spin } from 'antd';
|
import { Button, Drawer, Spin } from 'antd';
|
||||||
|
import { MenuOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import Sidebar from './components/Sidebar';
|
import Sidebar from './components/Sidebar';
|
||||||
import CommandPalette from './components/CommandPalette';
|
import CommandPalette from './components/CommandPalette';
|
||||||
import AgentList from './pages/AgentList';
|
import AgentList from './pages/AgentList';
|
||||||
|
|
@ -25,6 +26,8 @@ export default function App() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||||
|
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(() => (typeof window === 'undefined' ? false : window.innerWidth < 768));
|
||||||
|
|
||||||
// 全局快捷键 Ctrl/⌘ + K
|
// 全局快捷键 Ctrl/⌘ + K
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -38,6 +41,12 @@ export default function App() {
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onResize = () => setIsMobile(window.innerWidth < 768);
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => window.removeEventListener('resize', onResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const mainContent = (
|
const mainContent = (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
@ -73,13 +82,32 @@ export default function App() {
|
||||||
) : (
|
) : (
|
||||||
<div className="layout-shell">
|
<div className="layout-shell">
|
||||||
{/* 只有编辑器全屏显示,其他页面均保留侧边栏 */}
|
{/* 只有编辑器全屏显示,其他页面均保留侧边栏 */}
|
||||||
{!location.pathname.startsWith('/agents/') || location.pathname.includes('/chat') ? (
|
{!isMobile && (!location.pathname.startsWith('/agents/') || location.pathname.includes('/chat')) ? (
|
||||||
<Sidebar onOpenPalette={() => setPaletteOpen(true)} />
|
<Sidebar onOpenPalette={() => setPaletteOpen(true)} />
|
||||||
) : null}
|
) : null}
|
||||||
<main className="main">
|
<main className="main">
|
||||||
{mainContent}
|
{isMobile && (
|
||||||
|
<div className="mobile-topbar">
|
||||||
|
<Button type="text" icon={<MenuOutlined />} onClick={() => setMobileNavOpen(true)} />
|
||||||
|
<div className="mobile-topbar-title">Agent Studio</div>
|
||||||
|
<Button type="text" icon={<SearchOutlined />} onClick={() => setPaletteOpen(true)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="main-content">{mainContent}</div>
|
||||||
</main>
|
</main>
|
||||||
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
|
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
|
||||||
|
{isMobile && (
|
||||||
|
<Drawer
|
||||||
|
title="导航"
|
||||||
|
placement="left"
|
||||||
|
open={mobileNavOpen}
|
||||||
|
onClose={() => setMobileNavOpen(false)}
|
||||||
|
width={280}
|
||||||
|
styles={{ body: { padding: 0 } }}
|
||||||
|
>
|
||||||
|
<Sidebar onOpenPalette={() => setPaletteOpen(true)} onNavigate={() => setMobileNavOpen(false)} />
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useTheme } from '../main';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onOpenPalette?: () => void;
|
onOpenPalette?: () => void;
|
||||||
|
onNavigate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAV_GROUPS: Array<{
|
const NAV_GROUPS: Array<{
|
||||||
|
|
@ -54,7 +55,7 @@ const NAV_GROUPS: Array<{
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Sidebar({ onOpenPalette }: Props) {
|
export default function Sidebar({ onOpenPalette, onNavigate }: Props) {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { mode, toggle } = useTheme();
|
const { mode, toggle } = useTheme();
|
||||||
|
|
@ -77,7 +78,10 @@ export default function Sidebar({ onOpenPalette }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={onOpenPalette}
|
onClick={() => {
|
||||||
|
onOpenPalette?.();
|
||||||
|
onNavigate?.();
|
||||||
|
}}
|
||||||
className="nav-item"
|
className="nav-item"
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|
@ -103,6 +107,7 @@ export default function Sidebar({ onOpenPalette }: Props) {
|
||||||
to={it.to}
|
to={it.to}
|
||||||
end={it.end}
|
end={it.end}
|
||||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||||
|
onClick={onNavigate}
|
||||||
>
|
>
|
||||||
<span className="nav-icon">{it.icon}</span>
|
<span className="nav-icon">{it.icon}</span>
|
||||||
<span>{it.label}</span>
|
<span>{it.label}</span>
|
||||||
|
|
@ -135,6 +140,7 @@ export default function Sidebar({ onOpenPalette }: Props) {
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await logout();
|
await logout();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
|
onNavigate?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,14 @@ export default function ChatOutline(props: { messages: ChatMessage[]; onJump: (i
|
||||||
const { messages, onJump, activeId } = props;
|
const { messages, onJump, activeId } = props;
|
||||||
const items = messages.filter((m) => ((m as any)?.speaker?.type ? (m as any).speaker.type === 'agent' : m.role === 'assistant'));
|
const items = messages.filter((m) => ((m as any)?.speaker?.type ? (m as any).speaker.type === 'agent' : m.role === 'assistant'));
|
||||||
|
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<aside className="chat-outline">
|
||||||
|
<div className="chat-outline-title">聊天段落</div>
|
||||||
|
<div style={{ padding: 12, color: 'var(--color-text-tertiary)', fontSize: 12 }}>暂无</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="chat-outline">
|
<aside className="chat-outline">
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
|
||||||
<Button size="small" onClick={() => setOutlineDrawerOpen(true)}>
|
<Button size="small" onClick={() => setOutlineDrawerOpen(true)}>
|
||||||
大纲
|
大纲
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="small" onClick={() => setHistoryDrawerOpen(true)}>
|
||||||
|
历史对话
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -620,12 +620,42 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.layout-shell > .sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-topbar {
|
||||||
|
height: 48px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-topbar-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-shell {
|
.chat-shell {
|
||||||
|
|
@ -677,6 +707,10 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-body .messages-container {
|
.chat-body .messages-container {
|
||||||
max-width: 780px;
|
max-width: 780px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue