From 9e35067aa91be3537acd374972b9cf7289243402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=81=AA?= Date: Fri, 22 May 2026 13:14:26 +0800 Subject: [PATCH] feat(web): show timestamps on chat messages (#664) --- web/src/chat/presentation.test.ts | 17 +++++++++- web/src/chat/presentation.ts | 28 ++++++++++++++++ .../messages/AssistantMessage.tsx | 33 +++++++++++++------ .../messages/MessageTimestamp.tsx | 20 +++++++++++ .../AssistantChat/messages/SystemMessage.tsx | 2 ++ .../AssistantChat/messages/UserMessage.tsx | 13 +++++--- 6 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 web/src/components/AssistantChat/messages/MessageTimestamp.tsx diff --git a/web/src/chat/presentation.test.ts b/web/src/chat/presentation.test.ts index ecf47134..6918fde4 100644 --- a/web/src/chat/presentation.test.ts +++ b/web/src/chat/presentation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { getEventPresentation, formatResetTime } from './presentation' +import { getEventPresentation, formatMessageTimestamp, formatResetTime } from './presentation' describe('getEventPresentation — limit-warning', () => { it('formats five_hour warning', () => { @@ -139,3 +139,18 @@ describe('formatResetTime', () => { expect(result).toBeTruthy() }) }) + +describe('formatMessageTimestamp', () => { + it('formats today without requiring a date prefix', () => { + const now = new Date(2026, 4, 22, 14, 30) + const result = formatMessageTimestamp(new Date(2026, 4, 22, 9, 5), now) + expect(result).toBeTruthy() + expect(result).not.toContain('2026') + }) + + it('includes a year for messages outside the current year', () => { + const now = new Date(2026, 4, 22, 14, 30) + const result = formatMessageTimestamp(new Date(2025, 11, 31, 23, 59), now) + expect(result).toContain('2025') + }) +}) diff --git a/web/src/chat/presentation.ts b/web/src/chat/presentation.ts index 6058c82d..676c6bcc 100644 --- a/web/src/chat/presentation.ts +++ b/web/src/chat/presentation.ts @@ -26,6 +26,34 @@ export function formatResetTime(value: number): string { return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) } +export function formatMessageTimestamp(date: Date, now: Date = new Date()): string { + const sameDay = date.getFullYear() === now.getFullYear() + && date.getMonth() === now.getMonth() + && date.getDate() === now.getDate() + + if (sameDay) { + return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) + } + + const sameYear = date.getFullYear() === now.getFullYear() + if (sameYear) { + return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) + } + + return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) +} + +export function formatMessageTimestampTitle(date: Date): string { + return date.toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit' + }) +} + // Known types: five_hour → "5-hour", seven_day → "7-day". // Unknown types use underscore-to-space fallback (e.g. thirty_day → "thirty day"). function formatLimitType(limitType: string | undefined): string { diff --git a/web/src/components/AssistantChat/messages/AssistantMessage.tsx b/web/src/components/AssistantChat/messages/AssistantMessage.tsx index ce922111..b04bc998 100644 --- a/web/src/components/AssistantChat/messages/AssistantMessage.tsx +++ b/web/src/components/AssistantChat/messages/AssistantMessage.tsx @@ -12,6 +12,7 @@ import { getConversationMessageAnchorId } from '@/chat/outline' import { MessageMetadata } from '@/components/AssistantChat/messages/MessageMetadata' import { isNestedInteractiveEvent } from '@/components/AssistantChat/messages/metadataToggle' import { CodexReviewCard } from '@/components/AssistantChat/messages/CodexReviewCard' +import { MessageTimestamp } from '@/components/AssistantChat/messages/MessageTimestamp' const TOOL_COMPONENTS = { Fallback: HappyToolMessage @@ -85,16 +86,19 @@ export function HappyAssistantMessage() { className="scroll-mt-4 px-1 min-w-0 max-w-full overflow-x-hidden" > - {hasMetadata && ( - - )} +
+ + {hasMetadata && ( + + )} +
{showMetadata && ( +
+ +
{showMetadata && ( +
+ +
{showMetadata && ( +
+ +
{showMetadata && ( message.createdAt) + + return ( + + ) +} diff --git a/web/src/components/AssistantChat/messages/SystemMessage.tsx b/web/src/components/AssistantChat/messages/SystemMessage.tsx index f5ec95c6..ed058095 100644 --- a/web/src/components/AssistantChat/messages/SystemMessage.tsx +++ b/web/src/components/AssistantChat/messages/SystemMessage.tsx @@ -2,6 +2,7 @@ import { MessagePrimitive, useAssistantState } from '@assistant-ui/react' import { getEventPresentation } from '@/chat/presentation' import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime' import { getConversationMessageAnchorId } from '@/chat/outline' +import { MessageTimestamp } from '@/components/AssistantChat/messages/MessageTimestamp' export function HappySystemMessage() { const role = useAssistantState(({ message }) => message.role) @@ -25,6 +26,7 @@ export function HappySystemMessage() { {icon ? : null} {text} + diff --git a/web/src/components/AssistantChat/messages/UserMessage.tsx b/web/src/components/AssistantChat/messages/UserMessage.tsx index 3b458b78..605370cf 100644 --- a/web/src/components/AssistantChat/messages/UserMessage.tsx +++ b/web/src/components/AssistantChat/messages/UserMessage.tsx @@ -11,6 +11,7 @@ import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' import { getConversationMessageAnchorId } from '@/chat/outline' import { MessageMetadata } from '@/components/AssistantChat/messages/MessageMetadata' import { isNestedInteractiveEvent } from '@/components/AssistantChat/messages/metadataToggle' +import { MessageTimestamp } from '@/components/AssistantChat/messages/MessageTimestamp' export function HappyUserMessage() { const ctx = useHappyChatContext() @@ -75,8 +76,9 @@ export function HappyUserMessage() { >
- {hasMetadata && ( -
+
+ + {hasMetadata && ( -
- )} + )} +
{showMetadata && invokedAt != null && ( )} @@ -135,6 +137,9 @@ export function HappyUserMessage() {
)} +
+ +
{showMetadata && invokedAt != null && ( )}