feat: add date separators between messages from different days

Shows a subtle horizontal line with the date label (Today, Yesterday, or
full date) when messages span multiple days. Helps orient users when
scrolling through long conversation histories.

Includes i18n support (EN/FR) for the date labels.
This commit is contained in:
Nicolas Varrot
2026-02-12 00:40:03 +00:00
parent 788909f0b3
commit 375bd102d4
2 changed files with 41 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import { TypingIndicator } from './TypingIndicator';
import type { ChatMessage, ConnectionStatus } from '../types'; import type { ChatMessage, ConnectionStatus } from '../types';
import { Bot, ArrowDown } from 'lucide-react'; import { Bot, ArrowDown } from 'lucide-react';
import { useT } from '../hooks/useLocale'; import { useT } from '../hooks/useLocale';
import { getLocale, type TranslationKey } from '../lib/i18n';
interface Props { interface Props {
messages: ChatMessage[]; messages: ChatMessage[];
@@ -41,6 +42,24 @@ function hasStreamedText(messages: ChatMessage[]): boolean {
return last.blocks.some(b => b.type === 'text' && b.text.trim().length > 0) || (last.content?.trim().length > 0); return last.blocks.some(b => b.type === 'text' && b.text.trim().length > 0) || (last.content?.trim().length > 0);
} }
function formatDateSeparator(ts: number, t: (k: TranslationKey) => string): string {
const date = new Date(ts);
const now = new Date();
const locale = getLocale();
const bcp47 = locale === 'fr' ? 'fr-FR' : 'en-US';
if (date.toDateString() === now.toDateString()) return t('time.today');
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) return t('time.yesterday');
return date.toLocaleDateString(bcp47, { weekday: 'long', day: 'numeric', month: 'long', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
}
function getDateKey(ts: number): string {
const d = new Date(ts);
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
/** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */ /** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */
const SCROLL_THRESHOLD = 150; const SCROLL_THRESHOLD = 150;
@@ -111,9 +130,26 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
<div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div> <div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div>
</div> </div>
)} )}
{messages.filter(hasVisibleContent).map(msg => ( {(() => {
<ChatMessageComponent key={msg.id} message={msg} onRetry={!isGenerating ? handleSend : undefined} /> let lastDateKey = '';
))} return messages.filter(hasVisibleContent).map(msg => {
const dk = getDateKey(msg.timestamp);
const showSep = dk !== lastDateKey;
lastDateKey = dk;
return (
<div key={msg.id}>
{showSep && (
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
<div className="flex-1 h-px bg-white/8" />
<span className="text-[11px] font-medium text-zinc-500 uppercase tracking-wider">{formatDateSeparator(msg.timestamp, t)}</span>
<div className="flex-1 h-px bg-white/8" />
</div>
)}
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} />
</div>
);
});
})()}
{showTyping && <TypingIndicator />} {showTyping && <TypingIndicator />}
<div ref={bottomRef} /> <div ref={bottomRef} />
</div> </div>

View File

@@ -63,6 +63,7 @@ const en = {
// Timestamps // Timestamps
'time.yesterday': 'Yesterday', 'time.yesterday': 'Yesterday',
'time.today': 'Today',
// Keyboard shortcuts // Keyboard shortcuts
'shortcuts.title': 'Keyboard Shortcuts', 'shortcuts.title': 'Keyboard Shortcuts',
@@ -131,6 +132,7 @@ const fr: Record<keyof typeof en, string> = {
'message.retry': 'Renvoyer le message', 'message.retry': 'Renvoyer le message',
'time.yesterday': 'Hier', 'time.yesterday': 'Hier',
'time.today': "Aujourd'hui",
'shortcuts.title': 'Raccourcis clavier', 'shortcuts.title': 'Raccourcis clavier',
'shortcuts.send': 'Envoyer le message', 'shortcuts.send': 'Envoyer le message',