mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix(desktop): refine conversation navigation #782
This commit is contained in:
parent
8cf1783159
commit
74bdb3727d
@ -84,6 +84,7 @@ describe('ConversationNavigator', () => {
|
|||||||
it('renders ordered role markers and identifies the active target', () => {
|
it('renders ordered role markers and identifies the active target', () => {
|
||||||
render(
|
render(
|
||||||
<ConversationNavigator
|
<ConversationNavigator
|
||||||
|
mode="full"
|
||||||
items={[
|
items={[
|
||||||
{ id: 'user-1', renderItemKey: 'user-1', renderIndex: 0, role: 'user', preview: 'First prompt', attachmentCount: 0 },
|
{ id: 'user-1', renderItemKey: 'user-1', renderIndex: 0, role: 'user', preview: 'First prompt', attachmentCount: 0 },
|
||||||
{ id: 'assistant-1', renderItemKey: 'assistant-1', renderIndex: 1, role: 'assistant', preview: 'First answer', attachmentCount: 0 },
|
{ id: 'assistant-1', renderItemKey: 'assistant-1', renderIndex: 1, role: 'assistant', preview: 'First answer', attachmentCount: 0 },
|
||||||
@ -97,6 +98,55 @@ describe('ConversationNavigator', () => {
|
|||||||
expect(markers.map((marker) => marker.getAttribute('data-role'))).toEqual(['user', 'assistant'])
|
expect(markers.map((marker) => marker.getAttribute('data-role'))).toEqual(['user', 'assistant'])
|
||||||
expect(markers[0]?.getAttribute('aria-current')).toBeNull()
|
expect(markers[0]?.getAttribute('aria-current')).toBeNull()
|
||||||
expect(markers[1]?.getAttribute('aria-current')).toBe('location')
|
expect(markers[1]?.getAttribute('aria-current')).toBe('location')
|
||||||
|
|
||||||
|
const markerBars = markers.map((marker) => marker.querySelector('[aria-hidden="true"]'))
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('full')
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('w-3'))).toBe(true)
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('group-hover:w-5'))).toBe(true)
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('group-focus-visible:w-5'))).toBe(true)
|
||||||
|
expect(markerBars[1]?.className).toContain('bg-[var(--color-brand)]')
|
||||||
|
expect(markerBars[1]?.className.split(/\s+/)).not.toContain('w-5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses equal shorter marker geometry in compact mode', () => {
|
||||||
|
render(
|
||||||
|
<ConversationNavigator
|
||||||
|
mode="compact"
|
||||||
|
items={[
|
||||||
|
{ id: 'user-1', renderItemKey: 'user-1', renderIndex: 0, role: 'user', preview: 'First prompt', attachmentCount: 0 },
|
||||||
|
{ id: 'assistant-1', renderItemKey: 'assistant-1', renderIndex: 1, role: 'assistant', preview: 'First answer', attachmentCount: 0 },
|
||||||
|
]}
|
||||||
|
activeItemId="user-1"
|
||||||
|
onNavigate={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const markers = screen.getAllByRole('button')
|
||||||
|
const markerBars = markers.map((marker) => marker.querySelector('[aria-hidden="true"]'))
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('compact')
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('w-2.5'))).toBe(true)
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('group-hover:w-4'))).toBe(true)
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('motion-reduce:transition-none'))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses an edge-sized lane when the transcript becomes narrow', () => {
|
||||||
|
render(
|
||||||
|
<ConversationNavigator
|
||||||
|
mode="edge"
|
||||||
|
items={[
|
||||||
|
{ id: 'user-1', renderItemKey: 'user-1', renderIndex: 0, role: 'user', preview: 'First prompt', attachmentCount: 0 },
|
||||||
|
{ id: 'assistant-1', renderItemKey: 'assistant-1', renderIndex: 1, role: 'assistant', preview: 'First answer', attachmentCount: 0 },
|
||||||
|
]}
|
||||||
|
activeItemId="assistant-1"
|
||||||
|
onNavigate={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const markers = screen.getAllByRole('button')
|
||||||
|
const markerBars = markers.map((marker) => marker.querySelector('[aria-hidden="true"]'))
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('edge')
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('w-1.5'))).toBe(true)
|
||||||
|
expect(markerBars.every((bar) => bar?.className.includes('group-hover:w-3'))).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the preview on hover or focus and navigates on click', () => {
|
it('shows the preview on hover or focus and navigates on click', () => {
|
||||||
@ -111,6 +161,7 @@ describe('ConversationNavigator', () => {
|
|||||||
}
|
}
|
||||||
render(
|
render(
|
||||||
<ConversationNavigator
|
<ConversationNavigator
|
||||||
|
mode="full"
|
||||||
items={[item]}
|
items={[item]}
|
||||||
activeItemId="user-1"
|
activeItemId="user-1"
|
||||||
onNavigate={onNavigate}
|
onNavigate={onNavigate}
|
||||||
|
|||||||
@ -19,6 +19,34 @@ export type ConversationNavigationItem = {
|
|||||||
attachmentCount: number
|
attachmentCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConversationNavigationMode = 'full' | 'compact' | 'edge'
|
||||||
|
|
||||||
|
const NAVIGATION_MODE_STYLES: Record<ConversationNavigationMode, {
|
||||||
|
position: string
|
||||||
|
lane: string
|
||||||
|
button: string
|
||||||
|
marker: string
|
||||||
|
}> = {
|
||||||
|
full: {
|
||||||
|
position: 'left-2',
|
||||||
|
lane: 'w-10',
|
||||||
|
button: 'w-10 pl-1.5',
|
||||||
|
marker: 'w-3 group-hover:w-5 group-focus-visible:w-5',
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
position: 'left-1',
|
||||||
|
lane: 'w-7',
|
||||||
|
button: 'w-7 pl-1',
|
||||||
|
marker: 'w-2.5 group-hover:w-4 group-focus-visible:w-4',
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
position: 'left-0',
|
||||||
|
lane: 'w-5',
|
||||||
|
button: 'w-5 pl-0.5',
|
||||||
|
marker: 'w-1.5 group-hover:w-3 group-focus-visible:w-3',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePreview(content: string) {
|
function normalizePreview(content: string) {
|
||||||
const normalized = content.slice(0, 2_000)
|
const normalized = content.slice(0, 2_000)
|
||||||
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
|
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
|
||||||
@ -51,10 +79,12 @@ export function buildConversationNavigationItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ConversationNavigator({
|
export function ConversationNavigator({
|
||||||
|
mode,
|
||||||
items,
|
items,
|
||||||
activeItemId,
|
activeItemId,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
}: {
|
}: {
|
||||||
|
mode: ConversationNavigationMode
|
||||||
items: ConversationNavigationItem[]
|
items: ConversationNavigationItem[]
|
||||||
activeItemId: string | null
|
activeItemId: string | null
|
||||||
onNavigate: (item: ConversationNavigationItem) => void
|
onNavigate: (item: ConversationNavigationItem) => void
|
||||||
@ -64,6 +94,7 @@ export function ConversationNavigator({
|
|||||||
const [previewPosition, setPreviewPosition] = useState({ left: 0, top: 0 })
|
const [previewPosition, setPreviewPosition] = useState({ left: 0, top: 0 })
|
||||||
const markerRefs = useRef(new Map<string, HTMLButtonElement>())
|
const markerRefs = useRef(new Map<string, HTMLButtonElement>())
|
||||||
const previewItem = items.find((item) => item.id === previewItemId) ?? null
|
const previewItem = items.find((item) => item.id === previewItemId) ?? null
|
||||||
|
const modeStyles = NAVIGATION_MODE_STYLES[mode]
|
||||||
|
|
||||||
const openPreview = (itemId: string, marker: HTMLButtonElement) => {
|
const openPreview = (itemId: string, marker: HTMLButtonElement) => {
|
||||||
const rect = marker.getBoundingClientRect()
|
const rect = marker.getBoundingClientRect()
|
||||||
@ -82,10 +113,11 @@ export function ConversationNavigator({
|
|||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
data-testid="conversation-navigator"
|
data-testid="conversation-navigator"
|
||||||
|
data-mode={mode}
|
||||||
aria-label={t('chat.conversationNavigator.label')}
|
aria-label={t('chat.conversationNavigator.label')}
|
||||||
className="absolute left-2 top-1/2 z-30 flex max-h-[64%] -translate-y-1/2 flex-col overflow-visible"
|
className={`absolute top-1/2 z-30 flex max-h-[64%] -translate-y-1/2 flex-col overflow-visible ${modeStyles.position}`}
|
||||||
>
|
>
|
||||||
<div className="conversation-navigation-scroll flex max-h-full w-10 flex-col items-start gap-0.5 overflow-y-auto py-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
<div className={`conversation-navigation-scroll flex max-h-full flex-col items-start gap-0.5 overflow-y-auto py-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${modeStyles.lane}`}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const roleLabel = item.role === 'user'
|
const roleLabel = item.role === 'user'
|
||||||
? t('chat.userMessageReference')
|
? t('chat.userMessageReference')
|
||||||
@ -111,17 +143,18 @@ export function ConversationNavigator({
|
|||||||
onFocus={(event) => openPreview(item.id, event.currentTarget)}
|
onFocus={(event) => openPreview(item.id, event.currentTarget)}
|
||||||
onBlur={() => setPreviewItemId(null)}
|
onBlur={() => setPreviewItemId(null)}
|
||||||
onClick={() => onNavigate(item)}
|
onClick={() => onNavigate(item)}
|
||||||
className="group flex h-4 w-10 items-center rounded-sm pl-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
|
className={`group flex h-4 items-center rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 ${modeStyles.button}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={[
|
className={[
|
||||||
'block h-0.5 rounded-full transition-[width,background-color,opacity] duration-150',
|
'block h-0.5 rounded-full transition-[width,background-color,opacity] duration-200 ease-out motion-reduce:transition-none',
|
||||||
|
modeStyles.marker,
|
||||||
isActive
|
isActive
|
||||||
? 'w-5 bg-[var(--color-brand)] opacity-100'
|
? 'bg-[var(--color-brand)] opacity-100'
|
||||||
: item.role === 'user'
|
: item.role === 'user'
|
||||||
? 'w-4 bg-[var(--color-text-secondary)] opacity-75 group-hover:w-5 group-hover:bg-[var(--color-text-primary)] group-hover:opacity-100'
|
? 'bg-[var(--color-text-secondary)] opacity-75 group-hover:bg-[var(--color-text-primary)] group-hover:opacity-100 group-focus-visible:bg-[var(--color-text-primary)] group-focus-visible:opacity-100'
|
||||||
: 'w-2.5 bg-[var(--color-outline)] opacity-65 group-hover:w-4 group-hover:bg-[var(--color-text-secondary)] group-hover:opacity-100',
|
: 'bg-[var(--color-outline)] opacity-65 group-hover:bg-[var(--color-text-secondary)] group-hover:opacity-100 group-focus-visible:bg-[var(--color-text-secondary)] group-focus-visible:opacity-100',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -350,7 +350,7 @@ describe('MessageList nested tool calls', () => {
|
|||||||
expect(screen.getByText('latest assistant reply')).toBeTruthy()
|
expect(screen.getByText('latest assistant reply')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the conversation navigator for normal desktop transcripts and hides it in compact mode', () => {
|
it('keeps the conversation navigator available in compact desktop transcripts', () => {
|
||||||
useChatStore.setState({
|
useChatStore.setState({
|
||||||
sessions: {
|
sessions: {
|
||||||
[ACTIVE_TAB]: makeSessionState({
|
[ACTIVE_TAB]: makeSessionState({
|
||||||
@ -368,7 +368,72 @@ describe('MessageList nested tool calls', () => {
|
|||||||
expect(screen.getByRole('navigation', { name: 'Conversation navigation' })).toBeTruthy()
|
expect(screen.getByRole('navigation', { name: 'Conversation navigation' })).toBeTruthy()
|
||||||
|
|
||||||
rerender(<MessageList compact />)
|
rerender(<MessageList compact />)
|
||||||
expect(screen.queryByRole('navigation', { name: 'Conversation navigation' })).toBeNull()
|
expect(screen.getByRole('navigation', { name: 'Conversation navigation' })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adapts the conversation navigator when the chat column is resized by adjacent panels', () => {
|
||||||
|
const observers: Array<{
|
||||||
|
callback: ResizeObserverCallback
|
||||||
|
targets: Element[]
|
||||||
|
}> = []
|
||||||
|
class TestResizeObserver {
|
||||||
|
targets: Element[] = []
|
||||||
|
observe = vi.fn((target: Element) => {
|
||||||
|
this.targets.push(target)
|
||||||
|
})
|
||||||
|
unobserve = vi.fn()
|
||||||
|
disconnect = vi.fn()
|
||||||
|
|
||||||
|
constructor(callback: ResizeObserverCallback) {
|
||||||
|
observers.push({ callback, targets: this.targets })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
||||||
|
|
||||||
|
useChatStore.setState({
|
||||||
|
sessions: {
|
||||||
|
[ACTIVE_TAB]: makeSessionState({
|
||||||
|
messages: [
|
||||||
|
{ id: 'user-1', type: 'user_text', content: 'First prompt', timestamp: 1 },
|
||||||
|
{ id: 'assistant-1', type: 'assistant_text', content: 'First answer', timestamp: 2 },
|
||||||
|
{ id: 'user-2', type: 'user_text', content: 'Second prompt', timestamp: 3 },
|
||||||
|
{ id: 'assistant-2', type: 'assistant_text', content: 'Second answer', timestamp: 4 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<MessageList />)
|
||||||
|
const messageList = screen.getByTestId('message-list')
|
||||||
|
const scroller = messageList.querySelector('.chat-scroll-area') as HTMLElement
|
||||||
|
const layoutObserver = observers.find(({ targets }) => targets.includes(messageList))
|
||||||
|
expect(layoutObserver).toBeTruthy()
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('full')
|
||||||
|
|
||||||
|
const resizeTo = (width: number) => {
|
||||||
|
act(() => {
|
||||||
|
layoutObserver?.callback([{
|
||||||
|
target: messageList,
|
||||||
|
contentRect: { width },
|
||||||
|
} as unknown as ResizeObserverEntry], {} as ResizeObserver)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeTo(900)
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('compact')
|
||||||
|
expect(scroller.className.split(/\s+/)).toContain('pl-9')
|
||||||
|
|
||||||
|
resizeTo(640)
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('compact')
|
||||||
|
|
||||||
|
resizeTo(520)
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('edge')
|
||||||
|
expect(scroller.className.split(/\s+/)).toContain('pl-6')
|
||||||
|
|
||||||
|
resizeTo(1000)
|
||||||
|
expect(screen.getByTestId('conversation-navigator').getAttribute('data-mode')).toBe('full')
|
||||||
|
expect(scroller.className.split(/\s+/)).not.toContain('pl-9')
|
||||||
|
expect(scroller.className.split(/\s+/)).not.toContain('pl-6')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the active conversation marker while the transcript scrolls', () => {
|
it('updates the active conversation marker while the transcript scrolls', () => {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
buildConversationNavigationItems,
|
buildConversationNavigationItems,
|
||||||
ConversationNavigator,
|
ConversationNavigator,
|
||||||
type ConversationNavigationItem,
|
type ConversationNavigationItem,
|
||||||
|
type ConversationNavigationMode,
|
||||||
} from './ConversationNavigator'
|
} from './ConversationNavigator'
|
||||||
import type { AgentTaskNotification, UIMessage } from '../../types/chat'
|
import type { AgentTaskNotification, UIMessage } from '../../types/chat'
|
||||||
import { formatTokenCount } from '../../lib/formatTokenCount'
|
import { formatTokenCount } from '../../lib/formatTokenCount'
|
||||||
@ -952,6 +953,8 @@ const VIRTUAL_MAX_ITEM_HEIGHT = 24_000
|
|||||||
const CONTENT_RESIZE_FOLLOW_MIN_DELTA_PX = 2
|
const CONTENT_RESIZE_FOLLOW_MIN_DELTA_PX = 2
|
||||||
const USER_SCROLL_INTENT_WINDOW_MS = 500
|
const USER_SCROLL_INTENT_WINDOW_MS = 500
|
||||||
const CONVERSATION_NAVIGATION_MIN_ITEMS = 4
|
const CONVERSATION_NAVIGATION_MIN_ITEMS = 4
|
||||||
|
const CONVERSATION_NAVIGATION_FULL_MIN_WIDTH_PX = 960
|
||||||
|
const CONVERSATION_NAVIGATION_COMPACT_MIN_WIDTH_PX = 560
|
||||||
const STREAMING_ASSISTANT_NAVIGATION_KEY = 'streaming-assistant-message'
|
const STREAMING_ASSISTANT_NAVIGATION_KEY = 'streaming-assistant-message'
|
||||||
const EMPTY_MESSAGES: UIMessage[] = []
|
const EMPTY_MESSAGES: UIMessage[] = []
|
||||||
const EMPTY_AGENT_TASK_NOTIFICATIONS: Record<string, AgentTaskNotification> = {}
|
const EMPTY_AGENT_TASK_NOTIFICATIONS: Record<string, AgentTaskNotification> = {}
|
||||||
@ -1466,6 +1469,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
chatState === 'tool_executing' ||
|
chatState === 'tool_executing' ||
|
||||||
hasPendingPermissionCard ||
|
hasPendingPermissionCard ||
|
||||||
(chatState === 'thinking' && Boolean(activeThinkingId))
|
(chatState === 'thinking' && Boolean(activeThinkingId))
|
||||||
|
const messageListRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollContentRef = useRef<HTMLDivElement>(null)
|
const scrollContentRef = useRef<HTMLDivElement>(null)
|
||||||
const virtualItemHeightsRef = useRef<Map<string, number>>(
|
const virtualItemHeightsRef = useRef<Map<string, number>>(
|
||||||
@ -1503,6 +1507,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
})
|
})
|
||||||
const [measuredItemsVersion, setMeasuredItemsVersion] = useState(0)
|
const [measuredItemsVersion, setMeasuredItemsVersion] = useState(0)
|
||||||
const [highlightedNavigationItemKey, setHighlightedNavigationItemKey] = useState<string | null>(null)
|
const [highlightedNavigationItemKey, setHighlightedNavigationItemKey] = useState<string | null>(null)
|
||||||
|
const [messageListWidth, setMessageListWidth] = useState<number | null>(null)
|
||||||
const branchActionsDisabled =
|
const branchActionsDisabled =
|
||||||
isMemberSession ||
|
isMemberSession ||
|
||||||
chatState !== 'idle' ||
|
chatState !== 'idle' ||
|
||||||
@ -1526,6 +1531,27 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const messageList = messageListRef.current
|
||||||
|
if (!messageList) return
|
||||||
|
|
||||||
|
const updateWidth = (width: number) => {
|
||||||
|
const roundedWidth = Math.round(width)
|
||||||
|
if (roundedWidth <= 0) return
|
||||||
|
setMessageListWidth((current) => current === roundedWidth ? current : roundedWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidth(messageList.getBoundingClientRect().width || messageList.clientWidth)
|
||||||
|
if (typeof ResizeObserver === 'undefined') return
|
||||||
|
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
const entry = entries.find((candidate) => candidate.target === messageList)
|
||||||
|
if (entry) updateWidth(entry.contentRect.width)
|
||||||
|
})
|
||||||
|
observer.observe(messageList)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const syncVirtualViewportFromContainer = useCallback((container: HTMLElement) => {
|
const syncVirtualViewportFromContainer = useCallback((container: HTMLElement) => {
|
||||||
const nextScrollTop = container.scrollTop
|
const nextScrollTop = container.scrollTop
|
||||||
const nextViewportHeight = container.clientHeight || VIRTUAL_DEFAULT_VIEWPORT_HEIGHT
|
const nextViewportHeight = container.clientHeight || VIRTUAL_DEFAULT_VIEWPORT_HEIGHT
|
||||||
@ -1920,10 +1946,26 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
),
|
),
|
||||||
[conversationNavigationItems, virtualTranscriptWindow.offsets, virtualViewport],
|
[conversationNavigationItems, virtualTranscriptWindow.offsets, virtualViewport],
|
||||||
)
|
)
|
||||||
|
const conversationNavigationMode: ConversationNavigationMode =
|
||||||
|
messageListWidth === null || messageListWidth >= CONVERSATION_NAVIGATION_FULL_MIN_WIDTH_PX
|
||||||
|
? 'full'
|
||||||
|
: messageListWidth >= CONVERSATION_NAVIGATION_COMPACT_MIN_WIDTH_PX
|
||||||
|
? 'compact'
|
||||||
|
: 'edge'
|
||||||
const showConversationNavigator =
|
const showConversationNavigator =
|
||||||
!compact &&
|
|
||||||
!isTouchH5Document() &&
|
!isTouchH5Document() &&
|
||||||
conversationNavigationItems.length >= CONVERSATION_NAVIGATION_MIN_ITEMS
|
conversationNavigationItems.length >= CONVERSATION_NAVIGATION_MIN_ITEMS
|
||||||
|
const chatScrollPaddingClass = compact
|
||||||
|
? showConversationNavigator && conversationNavigationMode === 'compact'
|
||||||
|
? 'pb-5 pl-9 pr-3 py-3'
|
||||||
|
: showConversationNavigator && conversationNavigationMode === 'edge'
|
||||||
|
? 'pb-5 pl-6 pr-3 py-3'
|
||||||
|
: 'px-3 py-3 pb-5'
|
||||||
|
: showConversationNavigator && conversationNavigationMode === 'compact'
|
||||||
|
? 'pl-9 pr-4 py-4'
|
||||||
|
: showConversationNavigator && conversationNavigationMode === 'edge'
|
||||||
|
? 'pl-6 pr-4 py-4'
|
||||||
|
: 'px-4 py-4'
|
||||||
const confirmTurnCard = useMemo(
|
const confirmTurnCard = useMemo(
|
||||||
() => visibleTurnChangeCards.find((card) => card.target.messageId === turnUndoConfirmTargetId) ?? null,
|
() => visibleTurnChangeCards.find((card) => card.target.messageId === turnUndoConfirmTargetId) ?? null,
|
||||||
[turnUndoConfirmTargetId, visibleTurnChangeCards],
|
[turnUndoConfirmTargetId, visibleTurnChangeCards],
|
||||||
@ -2331,7 +2373,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-0 flex-1">
|
<div ref={messageListRef} data-testid="message-list" className="relative min-h-0 flex-1">
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
onScroll={updateAutoScrollState}
|
onScroll={updateAutoScrollState}
|
||||||
@ -2339,7 +2381,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
onPointerDown={markUserScrollIntent}
|
onPointerDown={markUserScrollIntent}
|
||||||
onTouchStart={markUserScrollIntent}
|
onTouchStart={markUserScrollIntent}
|
||||||
onKeyDown={handleKeyDownScrollIntent}
|
onKeyDown={handleKeyDownScrollIntent}
|
||||||
className={`${CHAT_SCROLL_AREA_CLASS} h-full overflow-y-auto ${compact ? 'px-3 py-3 pb-5' : 'px-4 py-4'}`}
|
className={`${CHAT_SCROLL_AREA_CLASS} h-full overflow-y-auto ${chatScrollPaddingClass}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={scrollContentRef}
|
ref={scrollContentRef}
|
||||||
@ -2410,6 +2452,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
|
|
||||||
{showConversationNavigator ? (
|
{showConversationNavigator ? (
|
||||||
<ConversationNavigator
|
<ConversationNavigator
|
||||||
|
mode={conversationNavigationMode}
|
||||||
items={conversationNavigationItems}
|
items={conversationNavigationItems}
|
||||||
activeItemId={activeConversationNavigationItemId}
|
activeItemId={activeConversationNavigationItemId}
|
||||||
onNavigate={handleNavigateToConversationItem}
|
onNavigate={handleNavigateToConversationItem}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user