mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): show timestamps in conversation outline (#1111)
This commit is contained in:
@@ -1,5 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getEventPresentation, formatMessageTimestamp, formatResetTime } from './presentation'
|
||||
import { getEventPresentation, formatMessageTimestamp, formatOutlineTimestamp, formatResetTime } from './presentation'
|
||||
|
||||
describe('formatOutlineTimestamp', () => {
|
||||
it('shows only the time for same-day messages', () => {
|
||||
const date = new Date(2026, 6, 21, 9, 55)
|
||||
const now = new Date(2026, 6, 21, 12, 0)
|
||||
|
||||
expect(formatOutlineTimestamp(date, 'en', now)).toBe(
|
||||
date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
)
|
||||
})
|
||||
|
||||
it('zero-pads Chinese dates within the current year', () => {
|
||||
const date = new Date(2026, 8, 9, 10, 31)
|
||||
const now = new Date(2026, 6, 21, 12, 0)
|
||||
|
||||
expect(formatOutlineTimestamp(date, 'zh-CN', now)).toBe(
|
||||
`09月09日 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })}`
|
||||
)
|
||||
})
|
||||
|
||||
it('uses a zero-padded numeric date in English', () => {
|
||||
const date = new Date(2026, 9, 1, 10, 31)
|
||||
const now = new Date(2026, 6, 21, 12, 0)
|
||||
|
||||
expect(formatOutlineTimestamp(date, 'en', now)).toBe(
|
||||
`10/01 ${date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })}`
|
||||
)
|
||||
})
|
||||
|
||||
it('includes the year for older years in both locales', () => {
|
||||
const date = new Date(2025, 8, 9, 10, 31)
|
||||
const now = new Date(2026, 6, 21, 12, 0)
|
||||
const zhTime = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
const enTime = date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
expect(formatOutlineTimestamp(date, 'zh-CN', now)).toBe(`2025年09月09日 ${zhTime}`)
|
||||
expect(formatOutlineTimestamp(date, 'en', now)).toBe(`2025/09/09 ${enTime}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getEventPresentation — agent errors', () => {
|
||||
it('formats error events with warning icon and message text', () => {
|
||||
|
||||
@@ -43,6 +43,34 @@ export function formatMessageTimestamp(date: Date, now: Date = new Date()): stri
|
||||
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
}
|
||||
|
||||
export function formatOutlineTimestamp(
|
||||
date: Date,
|
||||
locale: 'en' | 'zh-CN',
|
||||
now: Date = new Date()
|
||||
): string {
|
||||
const sameDay = date.getFullYear() === now.getFullYear()
|
||||
&& date.getMonth() === now.getMonth()
|
||||
&& date.getDate() === now.getDate()
|
||||
const time = date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
if (sameDay) {
|
||||
return time
|
||||
}
|
||||
|
||||
const year = String(date.getFullYear())
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const sameYear = date.getFullYear() === now.getFullYear()
|
||||
|
||||
if (locale === 'zh-CN') {
|
||||
const dateLabel = sameYear ? `${month}月${day}日` : `${year}年${month}月${day}日`
|
||||
return `${dateLabel} ${time}`
|
||||
}
|
||||
|
||||
const dateLabel = sameYear ? `${month}/${day}` : `${year}/${month}/${day}`
|
||||
return `${dateLabel} ${time}`
|
||||
}
|
||||
|
||||
export function formatMessageTimestampTitle(date: Date): string {
|
||||
return date.toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
|
||||
@@ -69,6 +69,16 @@ describe('ConversationOutlinePanel', () => {
|
||||
expect(onSelect).toHaveBeenCalledWith(outlineItems[0])
|
||||
})
|
||||
|
||||
it('shows each message time instead of a redundant user label', () => {
|
||||
const { container } = renderPanel()
|
||||
|
||||
const timestamps = container.querySelectorAll('time')
|
||||
expect(timestamps).toHaveLength(outlineItems.length)
|
||||
expect(timestamps[0]).toHaveAttribute('dateTime', new Date(outlineItems[0].createdAt).toISOString())
|
||||
expect(timestamps[0]).toHaveAttribute('title')
|
||||
expect(screen.queryByText('User')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows load earlier when older messages exist', () => {
|
||||
const onLoadMore = vi.fn()
|
||||
renderPanel({ hasMoreMessages: true, onLoadMore })
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ApiClient } from '@/api/client'
|
||||
import type { SessionMetadataSummary } from '@/types/api'
|
||||
import type { ConversationOutlineItem } from '@/chat/outline'
|
||||
import { getConversationMessageAnchorId } from '@/chat/outline'
|
||||
import { formatMessageTimestampTitle, formatOutlineTimestamp } from '@/chat/presentation'
|
||||
import { HappyChatProvider } from '@/components/AssistantChat/context'
|
||||
import { HappyAssistantMessage } from '@/components/AssistantChat/messages/AssistantMessage'
|
||||
import { HappyUserMessage } from '@/components/AssistantChat/messages/UserMessage'
|
||||
@@ -208,7 +209,7 @@ export function ConversationOutlinePanel(props: {
|
||||
onSelect: (item: ConversationOutlineItem) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { t, locale } = useTranslation()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const normalizedSearchQuery = searchQuery.trim().toLocaleLowerCase()
|
||||
const filteredItems = useMemo(() => {
|
||||
@@ -302,21 +303,26 @@ export function ConversationOutlinePanel(props: {
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{filteredItems.map((item) => {
|
||||
const createdAt = new Date(item.createdAt)
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => props.onSelect(item)}
|
||||
className="group flex w-full min-w-0 items-start gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-[var(--app-subtle-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||
className="group block w-full min-w-0 rounded-md px-2 py-2 text-left transition-colors hover:bg-[var(--app-subtle-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||
>
|
||||
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-[var(--app-button)]" aria-hidden="true" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[11px] font-medium uppercase text-[var(--app-hint)]">
|
||||
{t('session.outline.kind.user')}
|
||||
</span>
|
||||
<span className="line-clamp-2 text-sm leading-snug text-[var(--app-fg)]">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="flex min-w-0 items-center gap-2 text-[11px] font-medium tabular-nums text-[var(--app-hint)]">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-[var(--app-button)]" aria-hidden="true" />
|
||||
<time
|
||||
dateTime={createdAt.toISOString()}
|
||||
title={formatMessageTimestampTitle(createdAt)}
|
||||
className="truncate"
|
||||
>
|
||||
{formatOutlineTimestamp(createdAt, locale)}
|
||||
</time>
|
||||
</span>
|
||||
<span className="mt-0.5 line-clamp-2 pl-4 text-sm leading-snug text-[var(--app-fg)]">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -180,7 +180,6 @@ export default {
|
||||
'session.outline.searchLabel': 'Search outline items',
|
||||
'session.outline.searchResults': '{matched} of {total} items',
|
||||
'session.outline.noSearchResults': 'No matching outline items',
|
||||
'session.outline.kind.user': 'User',
|
||||
|
||||
// Session actions
|
||||
'session.action.rename': 'Rename',
|
||||
|
||||
@@ -180,7 +180,6 @@ export default {
|
||||
'session.outline.searchLabel': '搜索大纲条目',
|
||||
'session.outline.searchResults': '{matched}/{total} 个条目',
|
||||
'session.outline.noSearchResults': '没有匹配的大纲条目',
|
||||
'session.outline.kind.user': '用户',
|
||||
|
||||
// Session actions
|
||||
'session.action.rename': '重命名',
|
||||
|
||||
Reference in New Issue
Block a user