Group conversations by date in chat sidebar
Conversations are bucketed into: Today, Yesterday, This week, This month, and older entries sub-grouped by calendar month (e.g. "February 2026"). Group labels are sticky so they stay visible while scrolling. Older buckets use month+year sub-grouping rather than a single "Older" catch-all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,18 +64,49 @@ function formatConvDate(dateStr: string): string {
|
|||||||
const diffMin = Math.floor(diffMs / 60_000);
|
const diffMin = Math.floor(diffMs / 60_000);
|
||||||
const diffHrs = Math.floor(diffMs / 3_600_000);
|
const diffHrs = Math.floor(diffMs / 3_600_000);
|
||||||
|
|
||||||
// Relative time for < 10 hours
|
|
||||||
if (diffMin < 1) return "Just now";
|
if (diffMin < 1) return "Just now";
|
||||||
if (diffMin < 60) return `${diffMin}m ago`;
|
if (diffMin < 60) return `${diffMin}m ago`;
|
||||||
if (diffHrs < 10) return `${diffHrs}h ago`;
|
if (diffHrs < 10) return `${diffHrs}h ago`;
|
||||||
|
|
||||||
// Date for older
|
|
||||||
if (date.getFullYear() === now.getFullYear()) {
|
if (date.getFullYear() === now.getFullYear()) {
|
||||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||||
}
|
}
|
||||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConvGroup { label: string; convs: typeof store.conversations }
|
||||||
|
|
||||||
|
const groupedConversations = computed((): ConvGroup[] => {
|
||||||
|
const now = new Date();
|
||||||
|
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
const startOfYesterday = new Date(startOfToday.getTime() - 86_400_000);
|
||||||
|
const startOfWeek = new Date(startOfToday.getTime() - 6 * 86_400_000);
|
||||||
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
|
||||||
|
const buckets: Record<string, typeof store.conversations> = {};
|
||||||
|
const order: string[] = [];
|
||||||
|
|
||||||
|
for (const conv of store.conversations) {
|
||||||
|
const d = new Date(conv.updated_at);
|
||||||
|
let key: string;
|
||||||
|
if (d >= startOfToday) {
|
||||||
|
key = "Today";
|
||||||
|
} else if (d >= startOfYesterday) {
|
||||||
|
key = "Yesterday";
|
||||||
|
} else if (d >= startOfWeek) {
|
||||||
|
key = "This week";
|
||||||
|
} else if (d >= startOfMonth) {
|
||||||
|
key = "This month";
|
||||||
|
} else {
|
||||||
|
// Sub-group older by "Month Year"
|
||||||
|
key = d.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
||||||
|
}
|
||||||
|
if (!buckets[key]) { buckets[key] = []; order.push(key); }
|
||||||
|
buckets[key].push(conv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return order.map((label) => ({ label, convs: buckets[label] }));
|
||||||
|
});
|
||||||
|
|
||||||
const streamingRendered = computed(() => {
|
const streamingRendered = computed(() => {
|
||||||
if (!store.streamingContent) return "";
|
if (!store.streamingContent) return "";
|
||||||
return renderMarkdown(store.streamingContent);
|
return renderMarkdown(store.streamingContent);
|
||||||
@@ -422,25 +453,28 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="conv-list">
|
<div class="conv-list">
|
||||||
<div
|
<template v-for="group in groupedConversations" :key="group.label">
|
||||||
v-for="conv in store.conversations"
|
<div class="conv-group-label">{{ group.label }}</div>
|
||||||
:key="conv.id"
|
<div
|
||||||
class="conv-item"
|
v-for="conv in group.convs"
|
||||||
:class="{ active: convId === conv.id }"
|
:key="conv.id"
|
||||||
@click="selectConversation(conv.id)"
|
class="conv-item"
|
||||||
>
|
:class="{ active: convId === conv.id }"
|
||||||
<div class="conv-info">
|
@click="selectConversation(conv.id)"
|
||||||
<span class="conv-title">{{ conv.title || "Untitled" }}</span>
|
|
||||||
<span class="conv-date">{{ formatConvDate(conv.updated_at) }}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn-delete-conv"
|
|
||||||
@click.stop="removeConversation(conv.id)"
|
|
||||||
title="Delete conversation"
|
|
||||||
>
|
>
|
||||||
×
|
<div class="conv-info">
|
||||||
</button>
|
<span class="conv-title">{{ conv.title || "Untitled" }}</span>
|
||||||
</div>
|
<span class="conv-date">{{ formatConvDate(conv.updated_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-delete-conv"
|
||||||
|
@click.stop="removeConversation(conv.id)"
|
||||||
|
title="Delete conversation"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<p v-if="!store.conversations.length" class="empty-msg">
|
<p v-if="!store.conversations.length" class="empty-msg">
|
||||||
No conversations yet
|
No conversations yet
|
||||||
</p>
|
</p>
|
||||||
@@ -763,7 +797,20 @@ onUnmounted(() => {
|
|||||||
.conv-list {
|
.conv-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-group-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 0.6rem 0.75rem 0.2rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-item {
|
.conv-item {
|
||||||
|
|||||||
Reference in New Issue
Block a user