From 5ac9cee2ff2481c3d89fa9b16f2c94117d04e3c6 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 22 Apr 2026 22:34:23 +0800 Subject: [PATCH] feat(sidebar): add conversation sort mode toggle with created-time default --- .../sidebar-conversation-list.tsx | 30 ++++++++++++++-- src/components/layout/sidebar.tsx | 34 ++++++++++++++++++- src/i18n/messages/ar.json | 3 ++ src/i18n/messages/de.json | 3 ++ src/i18n/messages/en.json | 3 ++ src/i18n/messages/es.json | 3 ++ src/i18n/messages/fr.json | 3 ++ src/i18n/messages/ja.json | 3 ++ src/i18n/messages/ko.json | 3 ++ src/i18n/messages/pt.json | 3 ++ src/i18n/messages/zh-CN.json | 3 ++ src/i18n/messages/zh-TW.json | 3 ++ src/lib/sidebar-view-mode-storage.ts | 23 +++++++++++++ 13 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/components/conversations/sidebar-conversation-list.tsx b/src/components/conversations/sidebar-conversation-list.tsx index b57fcf7..347d35d 100644 --- a/src/components/conversations/sidebar-conversation-list.tsx +++ b/src/components/conversations/sidebar-conversation-list.tsx @@ -42,6 +42,7 @@ import type { ConversationStatus, DbConversationSummary } from "@/lib/types" import { loadFolderExpanded, saveFolderExpanded, + type SidebarSortMode, } from "@/lib/sidebar-view-mode-storage" import { SidebarConversationCard } from "./sidebar-conversation-card" import { ConversationManageDialog } from "./conversation-manage-dialog" @@ -89,6 +90,21 @@ function compareByUpdatedAtDesc( return right.id - left.id } +function compareByCreatedAtDesc( + left: DbConversationSummary, + right: DbConversationSummary +): number { + const createdDiff = + parseTimestamp(right.created_at) - parseTimestamp(left.created_at) + if (createdDiff !== 0) return createdDiff + + const updatedDiff = + parseTimestamp(right.updated_at) - parseTimestamp(left.updated_at) + if (updatedDiff !== 0) return updatedDiff + + return right.id - left.id +} + function formatRelative(iso: string): string { const ts = parseTimestamp(iso) if (!ts) return "" @@ -259,11 +275,13 @@ export interface SidebarConversationListHandle { export interface SidebarConversationListProps { showCompleted?: boolean + sortMode?: SidebarSortMode } export function SidebarConversationList({ ref, showCompleted = true, + sortMode = "created", }: SidebarConversationListProps & { ref?: Ref }) { @@ -368,9 +386,11 @@ export function SidebarConversationList({ if (list) list.push(conv) else map.set(conv.folder_id, [conv]) } - for (const list of map.values()) list.sort(compareByUpdatedAtDesc) + const comparator = + sortMode === "updated" ? compareByUpdatedAtDesc : compareByCreatedAtDesc + for (const list of map.values()) list.sort(comparator) return map - }, [filteredConversations]) + }, [filteredConversations, sortMode]) const orderedFolderIds = useMemo(() => { const seen = new Set() @@ -821,7 +841,11 @@ export function SidebarConversationList({ isOpenInTab={openTabConversationKeys.has( `${conv.agent_type}:${conv.id}` )} - timeLabel={formatRelative(conv.updated_at)} + timeLabel={formatRelative( + sortMode === "updated" + ? conv.updated_at + : conv.created_at + )} onSelect={handleSelect} onDoubleClick={handleDoubleClick} onRename={handleRename} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index ed59b56..4d8ef24 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -18,6 +18,10 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { @@ -29,7 +33,10 @@ import { import { useIsMobile } from "@/hooks/use-mobile" import { loadShowCompleted, + loadSortMode, saveShowCompleted, + saveSortMode, + type SidebarSortMode, } from "@/lib/sidebar-view-mode-storage" export function Sidebar() { @@ -39,12 +46,14 @@ export function Sidebar() { const listRef = useRef(null) const [showCompleted, setShowCompleted] = useState(false) + const [sortMode, setSortMode] = useState("created") const [allExpanded, setAllExpanded] = useState(true) useEffect(() => { // Hydrate from localStorage after mount to keep SSR/CSR markup consistent. // eslint-disable-next-line react-hooks/set-state-in-effect setShowCompleted(loadShowCompleted()) + setSortMode(loadSortMode()) }, []) const handleSetShowCompleted = useCallback((value: boolean) => { @@ -52,6 +61,12 @@ export function Sidebar() { saveShowCompleted(value) }, []) + const handleSetSortMode = useCallback((value: string) => { + const mode: SidebarSortMode = value === "updated" ? "updated" : "created" + setSortMode(mode) + saveSortMode(mode) + }, []) + const handleToggleExpandAll = useCallback(() => { if (allExpanded) { listRef.current?.collapseAll() @@ -122,6 +137,19 @@ export function Sidebar() { > {t("showCompleted")} + + {t("sortBy")} + + + {t("sortByCreatedAt")} + + + {t("sortByUpdatedAt")} + + @@ -142,7 +170,11 @@ export function Sidebar() { : undefined } > - + ) diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index f701f2e..9d5529d 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -788,6 +788,9 @@ "searchPlaceholder": "بحث عن محادثات...", "showCompleted": "عرض المحادثات المكتملة", "moreOptions": "المزيد من الخيارات", + "sortBy": "الترتيب حسب", + "sortByCreatedAt": "وقت الإنشاء", + "sortByUpdatedAt": "وقت التحديث", "statusRunningBadge": "قيد التشغيل", "statusFailedBadge": "فشل", "conversationCountUnit": "{count} محادثة", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 3455b9c..44c9504 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -788,6 +788,9 @@ "searchPlaceholder": "Konversationen suchen...", "showCompleted": "Abgeschlossene Konversationen anzeigen", "moreOptions": "Weitere Optionen", + "sortBy": "Sortieren nach", + "sortByCreatedAt": "Erstellungszeit", + "sortByUpdatedAt": "Aktualisierungszeit", "statusRunningBadge": "Läuft", "statusFailedBadge": "Fehlgeschlagen", "conversationCountUnit": "{count, plural, one {# Konversation} other {# Konversationen}}", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index a941361..2a35f4b 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -788,6 +788,9 @@ "searchPlaceholder": "Search conversations...", "showCompleted": "Show completed conversations", "moreOptions": "More options", + "sortBy": "Sort by", + "sortByCreatedAt": "Created time", + "sortByUpdatedAt": "Updated time", "statusRunningBadge": "Running", "statusFailedBadge": "Failed", "conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index c3b5fee..eccbe70 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -788,6 +788,9 @@ "searchPlaceholder": "Buscar conversaciones...", "showCompleted": "Mostrar conversaciones completadas", "moreOptions": "Más opciones", + "sortBy": "Ordenar por", + "sortByCreatedAt": "Fecha de creación", + "sortByUpdatedAt": "Fecha de actualización", "statusRunningBadge": "Ejecutando", "statusFailedBadge": "Fallido", "conversationCountUnit": "{count, plural, one {# conversación} other {# conversaciones}}", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index a220aaa..2d50309 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -788,6 +788,9 @@ "searchPlaceholder": "Rechercher des conversations...", "showCompleted": "Afficher les conversations terminées", "moreOptions": "Plus d'options", + "sortBy": "Trier par", + "sortByCreatedAt": "Date de création", + "sortByUpdatedAt": "Date de mise à jour", "statusRunningBadge": "En cours", "statusFailedBadge": "Échec", "conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a527743..bed3a36 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -788,6 +788,9 @@ "searchPlaceholder": "会話を検索...", "showCompleted": "完了した会話を表示", "moreOptions": "その他のオプション", + "sortBy": "並び替え", + "sortByCreatedAt": "作成時刻順", + "sortByUpdatedAt": "更新時刻順", "statusRunningBadge": "実行中", "statusFailedBadge": "失敗", "conversationCountUnit": "{count} 件", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 82141ee..3d5dc0a 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -788,6 +788,9 @@ "searchPlaceholder": "대화 검색...", "showCompleted": "완료된 대화 표시", "moreOptions": "더 많은 옵션", + "sortBy": "정렬 기준", + "sortByCreatedAt": "생성 시간순", + "sortByUpdatedAt": "업데이트 시간순", "statusRunningBadge": "실행 중", "statusFailedBadge": "실패", "conversationCountUnit": "{count}개", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index d1e5d5a..45ada4f 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -788,6 +788,9 @@ "searchPlaceholder": "Buscar conversas...", "showCompleted": "Mostrar conversas concluídas", "moreOptions": "Mais opções", + "sortBy": "Ordenar por", + "sortByCreatedAt": "Data de criação", + "sortByUpdatedAt": "Data de atualização", "statusRunningBadge": "Executando", "statusFailedBadge": "Falhou", "conversationCountUnit": "{count, plural, one {# conversa} other {# conversas}}", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index a9af9c8..fe1f198 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -788,6 +788,9 @@ "searchPlaceholder": "搜索会话...", "showCompleted": "显示已完成会话", "moreOptions": "更多选项", + "sortBy": "排序方式", + "sortByCreatedAt": "按创建时间排序", + "sortByUpdatedAt": "按更新时间排序", "statusRunningBadge": "运行中", "statusFailedBadge": "失败", "conversationCountUnit": "{count} 条", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 15c5b18..9f17b81 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -788,6 +788,9 @@ "searchPlaceholder": "搜尋對話...", "showCompleted": "顯示已完成對話", "moreOptions": "更多選項", + "sortBy": "排序方式", + "sortByCreatedAt": "按建立時間排序", + "sortByUpdatedAt": "按更新時間排序", "statusRunningBadge": "運行中", "statusFailedBadge": "失敗", "conversationCountUnit": "{count} 條", diff --git a/src/lib/sidebar-view-mode-storage.ts b/src/lib/sidebar-view-mode-storage.ts index 21efef0..7aecbea 100644 --- a/src/lib/sidebar-view-mode-storage.ts +++ b/src/lib/sidebar-view-mode-storage.ts @@ -2,6 +2,9 @@ const FOLDER_EXPANDED_KEY = "workspace:sidebar-folder-expanded" const SHOW_COMPLETED_KEY = "workspace:sidebar-show-completed" +const SORT_MODE_KEY = "workspace:sidebar-sort-mode" + +export type SidebarSortMode = "created" | "updated" export function loadFolderExpanded(): Record { if (typeof window === "undefined") return {} @@ -51,3 +54,23 @@ export function saveShowCompleted(value: boolean): void { /* ignore */ } } + +export function loadSortMode(): SidebarSortMode { + if (typeof window === "undefined") return "created" + try { + const raw = localStorage.getItem(SORT_MODE_KEY) + if (raw === "updated" || raw === "created") return raw + } catch { + /* ignore */ + } + return "created" +} + +export function saveSortMode(value: SidebarSortMode): void { + if (typeof window === "undefined") return + try { + localStorage.setItem(SORT_MODE_KEY, value) + } catch { + /* ignore */ + } +}