chat: add right-side outline for quick navigation

main
sp mac bookpro 2605 2026-06-05 23:31:11 +08:00
parent 088ab7ab73
commit 6b393a7214
3 changed files with 144 additions and 15 deletions

View File

@ -7,6 +7,7 @@ import ChatHeader from './components/ChatHeader';
import ChatBody from './components/ChatBody';
import ChatInput from './components/ChatInput';
import ChatDrawers from './components/ChatDrawers';
import ChatOutline from './components/ChatOutline';
import { useChatScroll } from './hooks/useChatScroll';
import { useChatData } from './hooks/useChatData';
import { useChatSender } from './hooks/useChatSender';
@ -97,21 +98,35 @@ export default function ChatPage() {
onClear={sender.handleClear}
/>
<ChatBody
bodyRef={bodyRef}
agent={agent}
messages={messages}
branches={branches}
highlightId={highlightId}
sending={sender.sending}
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
return navigator.clipboard?.writeText(content).then(() => message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)'));
}}
/>
<div className="chat-content-row">
<ChatBody
bodyRef={bodyRef}
agent={agent}
messages={messages}
branches={branches}
highlightId={highlightId}
sending={sender.sending}
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
return navigator.clipboard
?.writeText(content)
.then(() => message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)'));
}}
/>
<ChatOutline
messages={messages}
activeId={highlightId}
onJump={(msgId) => {
setHighlightId(msgId);
const el = document.getElementById('msg-' + msgId);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
}}
/>
</div>
<ChatInput
input={sender.input}

View File

@ -0,0 +1,41 @@
import type { ChatMessage } from '../../../api';
import { markdownToPlainText } from '../utils/copy';
function summarize(content: string) {
const plain = markdownToPlainText(content);
const firstLine = plain
.split('\n')
.map((s) => s.trim())
.filter(Boolean)[0];
const text = (firstLine || plain.trim()).replace(/\s+/g, ' ');
if (text.length <= 44) return text;
return text.slice(0, 44) + '…';
}
export default function ChatOutline(props: { messages: ChatMessage[]; onJump: (id: string) => void; activeId?: string | null }) {
const { messages, onJump, activeId } = props;
const items = messages.filter((m) => m.role === 'assistant');
if (items.length === 0) return null;
return (
<aside className="chat-outline">
<div className="chat-outline-title"></div>
<div className="chat-outline-list">
{items.map((m, idx) => (
<button
key={m.id}
type="button"
className={`chat-outline-item${activeId === m.id ? ' active' : ''}`}
onClick={() => onJump(m.id)}
title={summarize(m.content)}
>
<span className="chat-outline-index">{idx + 1}</span>
<span className="chat-outline-text">{summarize(m.content)}</span>
</button>
))}
</div>
</aside>
);
}

View File

@ -482,6 +482,12 @@ body {
position: relative;
}
.chat-content-row {
flex: 1;
min-height: 0;
display: flex;
}
.chat-header {
height: 60px;
padding: 0 24px;
@ -552,6 +558,73 @@ body {
background: var(--color-bg);
}
.chat-outline {
width: 260px;
border-left: 1px solid var(--color-border);
background: var(--color-surface);
padding: 14px 12px;
overflow: auto;
}
.chat-outline-title {
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 10px;
}
.chat-outline-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.chat-outline-item {
border: 1px solid var(--color-border);
background: var(--color-surface);
border-radius: 10px;
padding: 8px 10px;
text-align: left;
cursor: pointer;
display: flex;
gap: 8px;
align-items: flex-start;
color: var(--color-text);
}
.chat-outline-item:hover {
border-color: var(--color-border-strong);
box-shadow: var(--shadow-xs);
}
.chat-outline-item.active {
border-color: var(--color-brand);
box-shadow: var(--shadow-focus);
}
.chat-outline-index {
font-size: 12px;
color: var(--color-text-tertiary);
line-height: 1.4;
flex: 0 0 auto;
}
.chat-outline-text {
font-size: 12.5px;
line-height: 1.4;
color: var(--color-text);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
@media (max-width: 1100px) {
.chat-outline {
display: none;
}
}
.chat-body .messages-container {
max-width: 780px;
width: 100%;