chat: add right-side outline for quick navigation
parent
088ab7ab73
commit
6b393a7214
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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%;
|
||||
|
|
|
|||
Loading…
Reference in New Issue