mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Merge ba34ca5226b25fef489ee5ddb1242ce866f7da79 into d1e684c419b4f88fee62490b0c2b4fcf65469e59
This commit is contained in:
commit
43df0eea0c
@ -7,6 +7,7 @@ import { listPendingPermissions, useChatStore } from '../../stores/chatStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useWorkspaceChatContextStore } from '../../stores/workspaceChatContextStore'
|
||||
import { SETTINGS_TAB_ID, useTabStore } from '../../stores/tabStore'
|
||||
import { MessageNavigation } from './MessageNavigation'
|
||||
import { useTeamStore } from '../../stores/teamStore'
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@ -529,6 +530,7 @@ function SelectableChatMessage({
|
||||
<div
|
||||
ref={rootRef}
|
||||
data-chat-selectable-message={role}
|
||||
data-message-id={messageId}
|
||||
onPointerDown={(event) => {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return
|
||||
lastSelectionPointerRef.current = getSelectionPointer(event)
|
||||
@ -1640,6 +1642,38 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
||||
scrollToBottom('auto')
|
||||
}, [scrollToBottom])
|
||||
|
||||
const handleNavigateToMessage = useCallback((messageId: string) => {
|
||||
const scrollToElement = () => {
|
||||
const el = document.querySelector(`[data-message-id="${messageId}"]`)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (scrollToElement()) return
|
||||
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
const messageIndex = messages.findIndex((m) => m.id === messageId)
|
||||
if (messageIndex === -1) return
|
||||
|
||||
const totalMessages = messages.length
|
||||
if (totalMessages === 0) return
|
||||
|
||||
const scrollHeight = container.scrollHeight - container.clientHeight
|
||||
const estimatedPosition = (messageIndex / totalMessages) * scrollHeight
|
||||
container.scrollTop = Math.max(0, estimatedPosition - container.clientHeight / 3)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToElement()
|
||||
})
|
||||
})
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
const content = scrollContentRef.current
|
||||
if (!content || typeof ResizeObserver === 'undefined') return
|
||||
@ -2080,6 +2114,11 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<MessageNavigation
|
||||
messages={messages}
|
||||
onNavigate={handleNavigateToMessage}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(confirmTurnCard)}
|
||||
onClose={() => {
|
||||
|
||||
66
desktop/src/components/chat/MessageNavigation.tsx
Normal file
66
desktop/src/components/chat/MessageNavigation.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import { User } from 'lucide-react'
|
||||
import type { UIMessage } from '../../types/chat'
|
||||
|
||||
type MessageNavigationProps = {
|
||||
messages: UIMessage[]
|
||||
onNavigate: (messageId: string) => void
|
||||
}
|
||||
|
||||
type NavigationItem = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
function getNavigationLabel(message: UIMessage): string {
|
||||
if (message.type === 'user_text') {
|
||||
const text = message.content.trim()
|
||||
return text.length > 30 ? text.slice(0, 30) + '...' : text || '用户消息'
|
||||
}
|
||||
return '消息'
|
||||
}
|
||||
|
||||
export const MessageNavigation = memo(function MessageNavigation({
|
||||
messages,
|
||||
onNavigate,
|
||||
}: MessageNavigationProps) {
|
||||
const navItems = useMemo(() => {
|
||||
const items: NavigationItem[] = []
|
||||
for (const message of messages) {
|
||||
if (message.type === 'user_text') {
|
||||
items.push({
|
||||
id: message.id,
|
||||
label: getNavigationLabel(message),
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}, [messages])
|
||||
|
||||
if (navItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 bottom-0 w-[220px] overflow-y-auto py-4 pl-2 pr-4">
|
||||
<div className="sticky top-0 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] p-2 shadow-sm">
|
||||
<div className="mb-2 px-2 text-[11px] font-semibold uppercase tracking-wider text-[var(--color-text-tertiary)]">
|
||||
消息导航
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className="group flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12px] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
<User size={12} strokeWidth={2} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
||||
<span className="min-w-0 truncate">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user