chat: reposition session actions and refine history drawer

main
sp mac bookpro 2605 2026-06-06 14:11:49 +08:00
parent 3f5b737fbb
commit d7bf0e5c9d
5 changed files with 108 additions and 77 deletions

View File

@ -9,9 +9,10 @@ interface Props {
onChange: (sessionId: string, options?: { highlightMessageId?: string }) => void; onChange: (sessionId: string, options?: { highlightMessageId?: string }) => void;
refreshTick?: number; refreshTick?: number;
theme?: 'light' | 'dark'; theme?: 'light' | 'dark';
showNewButton?: boolean;
} }
export default function SessionSidebar({ agentId, activeSessionId, onChange, refreshTick, theme = 'light' }: Props) { export default function SessionSidebar({ agentId, activeSessionId, onChange, refreshTick, theme = 'light', showNewButton = true }: Props) {
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [tab, setTab] = useState<'active' | 'archived'>('active'); const [tab, setTab] = useState<'active' | 'archived'>('active');
const [active, setActive] = useState<ChatSession[]>([]); const [active, setActive] = useState<ChatSession[]>([]);
@ -269,9 +270,11 @@ export default function SessionSidebar({ agentId, activeSessionId, onChange, ref
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Button type="primary" onClick={handleNew} style={{ marginBottom: 8, background: c.primaryBtn }} block> {showNewButton && (
<Button type="primary" onClick={handleNew} style={{ marginBottom: 8, background: c.primaryBtn }} block>
</Button>
</Button>
)}
<Input.Search <Input.Search
placeholder="搜索会话与消息…" placeholder="搜索会话与消息…"

View File

@ -67,7 +67,7 @@ export function NewChatIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
fill="var(--theme-icon-brand)" fill="currentColor"
d="M128 22C69.458 22 22 69.458 22 128v96c0 5.523 4.477 10 10 10h96c58.542 0 106-47.458 106-106S186.542 22 128 22Zm0 56c5.523 0 10 4.477 10 10v30h30c5.523 0 10 4.477 10 10s-4.477 10-10 10h-30v30c0 5.523-4.477 10-10 10s-10-4.477-10-10v-30H88c-5.523 0-10-4.477-10-10s4.477-10 10-10h30V88c0-5.523 4.477-10 10-10Z" d="M128 22C69.458 22 22 69.458 22 128v96c0 5.523 4.477 10 10 10h96c58.542 0 106-47.458 106-106S186.542 22 128 22Zm0 56c5.523 0 10 4.477 10 10v30h30c5.523 0 10 4.477 10 10s-4.477 10-10 10h-30v30c0 5.523-4.477 10-10 10s-10-4.477-10-10v-30H88c-5.523 0-10-4.477-10-10s4.477-10 10-10h30V88c0-5.523 4.477-10 10-10Z"
/> />
</g> </g>

View File

@ -1,4 +1,4 @@
import { Button, Drawer, Empty, InputNumber, Slider } from 'antd'; import { Button, Drawer, Dropdown, Empty, InputNumber, Slider } from 'antd';
import type { Agent, ModelOverrides } from '../../../api'; import type { Agent, ModelOverrides } from '../../../api';
import McpResourcesDrawer from '../../../components/McpResourcesDrawer'; import McpResourcesDrawer from '../../../components/McpResourcesDrawer';
import SessionSidebar from '../../../components/SessionSidebar'; import SessionSidebar from '../../../components/SessionSidebar';
@ -142,12 +142,25 @@ export default function ChatDrawers(props: {
</Drawer> </Drawer>
<Drawer <Drawer
title="历史对话" title="聊天记录"
placement="right" placement="right"
width={320} width={320}
onClose={() => setHistoryDrawerOpen(false)} onClose={() => setHistoryDrawerOpen(false)}
open={historyDrawerOpen} open={historyDrawerOpen}
styles={{ body: { padding: 0 } }} styles={{ body: { padding: 0 } }}
extra={
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'select', label: '选择', disabled: true },
{ key: 'clear', label: '清除所有对话', disabled: true }
]
}}
>
<Button type="text"></Button>
</Dropdown>
}
> >
{agentId ? ( {agentId ? (
<SessionSidebar <SessionSidebar
@ -159,6 +172,7 @@ export default function ChatDrawers(props: {
setHistoryDrawerOpen(false); setHistoryDrawerOpen(false);
}} }}
refreshTick={sessionRefresh} refreshTick={sessionRefresh}
showNewButton={false}
/> />
) : ( ) : (
<Empty description="未选择 Agent" /> <Empty description="未选择 Agent" />

View File

@ -63,7 +63,7 @@ export default function ChatInput(props: {
))} ))}
</div> </div>
<div className="chat-input-card"> <div className="chat-input-card-wrap">
<div className="chat-input-actions"> <div className="chat-input-actions">
<Tooltip title="历史记录"> <Tooltip title="历史记录">
<Button size="small" type="text" className="chat-input-action-btn" icon={<HistoryIcon />} onClick={onOpenHistory}> <Button size="small" type="text" className="chat-input-action-btn" icon={<HistoryIcon />} onClick={onOpenHistory}>
@ -71,82 +71,84 @@ export default function ChatInput(props: {
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="新增会话"> <Tooltip title="新增会话">
<Button size="small" type="text" className="chat-input-action-btn" icon={<NewChatIcon />} onClick={onNewSession}> <Button size="small" type="text" className="chat-input-action-btn chat-input-action-btn-primary" icon={<NewChatIcon />} onClick={onNewSession}>
</Button> </Button>
</Tooltip> </Tooltip>
</div> </div>
<div className="chat-input-stack"> <div className="chat-input-card">
<Input.TextArea <div className="chat-input-stack">
value={input} <Input.TextArea
onChange={(e) => setInput(e.target.value)} value={input}
placeholder="问我任何问题..." onChange={(e) => setInput(e.target.value)}
autoSize={{ minRows: 3, maxRows: 10 }} placeholder="问我任何问题..."
onKeyDown={(e) => { autoSize={{ minRows: 3, maxRows: 10 }}
if (e.key !== 'Enter') return; onKeyDown={(e) => {
if ((e as any).isComposing) return; if (e.key !== 'Enter') return;
if ((e as any).isComposing) return;
if (e.metaKey || e.ctrlKey) { if (e.metaKey || e.ctrlKey) {
e.preventDefault(); e.preventDefault();
const el = e.currentTarget; const el = e.currentTarget;
const start = el.selectionStart ?? input.length; const start = el.selectionStart ?? input.length;
const end = el.selectionEnd ?? input.length; const end = el.selectionEnd ?? input.length;
const next = input.slice(0, start) + '\n' + input.slice(end); const next = input.slice(0, start) + '\n' + input.slice(end);
setInput(next); setInput(next);
requestAnimationFrame(() => { requestAnimationFrame(() => {
el.selectionStart = el.selectionEnd = start + 1; el.selectionStart = el.selectionEnd = start + 1;
}); });
return; return;
} }
if (!e.shiftKey && !e.altKey) { if (!e.shiftKey && !e.altKey) {
e.preventDefault(); e.preventDefault();
onSend(); onSend();
} }
}} }}
className="chat-input-textarea" className="chat-input-textarea"
disabled={sending} disabled={sending}
/> />
<div className="chat-input-toolbar"> <div className="chat-input-toolbar">
<div className="chat-input-toolbar-left"> <div className="chat-input-toolbar-left">
<Select <Select
value={activeModelValue || undefined} value={activeModelValue || undefined}
className="chat-model-select" className="chat-model-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
options={modelOptions} options={modelOptions}
suffixIcon={<DownOutlined className="chat-model-select-arrow" />} suffixIcon={<DownOutlined className="chat-model-select-arrow" />}
placeholder="选择模型" placeholder="选择模型"
onChange={onChangeModel} onChange={onChangeModel}
/> />
<Upload <Upload
className="chat-upload" className="chat-upload"
multiple multiple
beforeUpload={(_f, files) => { beforeUpload={(_f, files) => {
onAttach(files as File[]); onAttach(files as File[]);
return false; return false;
}} }}
showUploadList={false} showUploadList={false}
accept=".txt,.md,.markdown,.json,.csv,.pdf,.docx,.html,.htm,image/png,image/jpeg,image/webp,image/gif" accept=".txt,.md,.markdown,.json,.csv,.pdf,.docx,.html,.htm,image/png,image/jpeg,image/webp,image/gif"
> >
<Button type="text" className="chat-tool-button" icon={<PaperClipOutlined style={{ fontSize: 18 }} />} /> <Button type="text" className="chat-tool-button" icon={<PaperClipOutlined style={{ fontSize: 18 }} />} />
</Upload> </Upload>
<Button type="text" className="chat-tool-button" icon={<BookOutlined style={{ fontSize: 18 }} />} onClick={onOpenTpl} /> <Button type="text" className="chat-tool-button" icon={<BookOutlined style={{ fontSize: 18 }} />} onClick={onOpenTpl} />
</div>
{sending ? (
<Button danger shape="circle" onClick={onStop} icon={<span className="chat-stop-icon" />} className="chat-send-button" />
) : (
<Button
type="primary"
shape="circle"
onClick={onSend}
icon={<ArrowUpOutlined />}
disabled={!input.trim()}
className="chat-send-button chat-send-button-primary"
/>
)}
</div> </div>
{sending ? (
<Button danger shape="circle" onClick={onStop} icon={<span className="chat-stop-icon" />} className="chat-send-button" />
) : (
<Button
type="primary"
shape="circle"
onClick={onSend}
icon={<ArrowUpOutlined />}
disabled={!input.trim()}
className="chat-send-button chat-send-button-primary"
/>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -749,13 +749,16 @@ body {
padding: 14px 16px 12px; padding: 14px 16px 12px;
min-height: 110px; min-height: 110px;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
}
.chat-input-card-wrap {
position: relative; position: relative;
} }
.chat-input-actions { .chat-input-actions {
position: absolute; position: absolute;
top: 10px; top: -10px;
right: 12px; right: 8px;
display: flex; display: flex;
gap: 4px; gap: 4px;
z-index: 2; z-index: 2;
@ -772,6 +775,15 @@ body {
color: var(--color-text); color: var(--color-text);
} }
.chat-input-action-btn-primary {
color: var(--color-brand);
}
.chat-input-action-btn-primary:hover {
color: var(--color-brand);
opacity: 0.9;
}
.chat-input-stack { .chat-input-stack {
display: flex; display: flex;
flex-direction: column; flex-direction: column;