feat(chat): show message timestamp (relative time, full datetime on hover) (#157)

- Add formatRelativeTime() to date-time utils (Intl.RelativeTimeFormat, locale-aware)
- Display relative time under each message in message-item.vue
- Show full datetime in title attribute on hover

Made-with: Cursor
This commit is contained in:
RoomWithOutRoof
2026-03-02 15:09:01 +08:00
committed by GitHub
parent 874ca5fac7
commit 567a1f3761
2 changed files with 48 additions and 0 deletions
@@ -86,6 +86,12 @@
:on-open-media="onOpenMedia"
/>
</div>
<p
class="text-xs text-muted-foreground/80 mt-1"
:title="fullTimestamp"
>
{{ relativeTimestamp }}
</p>
</div>
<!-- Assistant message blocks -->
@@ -148,6 +154,12 @@
/>
{{ $t('chat.thinking') }}
</div>
<p
class="text-xs text-muted-foreground/80 mt-1"
:title="fullTimestamp"
>
{{ relativeTimestamp }}
</p>
</div>
</div>
@@ -176,6 +188,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { formatRelativeTime, formatDateTime } from '@/utils/date-time'
import { Avatar, AvatarImage, AvatarFallback } from '@memoh/ui'
import MarkdownRender, { enableKatex, enableMermaid } from 'markstream-vue'
import ThinkingBlock from './thinking-block.vue'
@@ -253,4 +266,11 @@ const contentClass = computed(() => {
if (props.message.role === 'user') return 'max-w-[80%]'
return 'flex-1 max-w-full'
})
const relativeTimestamp = computed(() =>
formatRelativeTime(props.message.timestamp),
)
const fullTimestamp = computed(() =>
formatDateTime(props.message.timestamp.toISOString()),
)
</script>
+28
View File
@@ -53,3 +53,31 @@ export function formatDateTimeSeconds(
const seconds = String(parsed.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
/**
* Returns a locale-aware relative time string such as "3 minutes ago" or
* "in 2 days". Falls back to `toLocaleDateString()` for dates older than a
* week. Accepts either an ISO string or a `Date` object.
*
* Uses `Intl.RelativeTimeFormat` so the output language follows the browser
* locale automatically — no hardcoded English strings.
*/
export function formatRelativeTime(
value: string | Date | null | undefined,
options: FormatDateOptions = {},
): string {
if (!value) return options.fallback ?? ''
const date = value instanceof Date ? value : parseDate(value)
if (!date) return options.fallback ?? ''
const diffMs = date.getTime() - Date.now()
const absDiffSec = Math.abs(diffMs) / 1000
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
if (absDiffSec < 60) return rtf.format(Math.round(diffMs / 1000), 'second')
if (absDiffSec < 3_600) return rtf.format(Math.round(diffMs / 60_000), 'minute')
if (absDiffSec < 86_400) return rtf.format(Math.round(diffMs / 3_600_000), 'hour')
if (absDiffSec < 604_800) return rtf.format(Math.round(diffMs / 86_400_000), 'day')
return date.toLocaleDateString()
}