feat(sidebar): add conversation sort mode toggle with created-time default

This commit is contained in:
xintaofei
2026-04-22 22:34:23 +08:00
parent 8ef29680d2
commit 5ac9cee2ff
13 changed files with 113 additions and 4 deletions

View File

@@ -42,6 +42,7 @@ import type { ConversationStatus, DbConversationSummary } from "@/lib/types"
import { import {
loadFolderExpanded, loadFolderExpanded,
saveFolderExpanded, saveFolderExpanded,
type SidebarSortMode,
} from "@/lib/sidebar-view-mode-storage" } from "@/lib/sidebar-view-mode-storage"
import { SidebarConversationCard } from "./sidebar-conversation-card" import { SidebarConversationCard } from "./sidebar-conversation-card"
import { ConversationManageDialog } from "./conversation-manage-dialog" import { ConversationManageDialog } from "./conversation-manage-dialog"
@@ -89,6 +90,21 @@ function compareByUpdatedAtDesc(
return right.id - left.id 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 { function formatRelative(iso: string): string {
const ts = parseTimestamp(iso) const ts = parseTimestamp(iso)
if (!ts) return "" if (!ts) return ""
@@ -259,11 +275,13 @@ export interface SidebarConversationListHandle {
export interface SidebarConversationListProps { export interface SidebarConversationListProps {
showCompleted?: boolean showCompleted?: boolean
sortMode?: SidebarSortMode
} }
export function SidebarConversationList({ export function SidebarConversationList({
ref, ref,
showCompleted = true, showCompleted = true,
sortMode = "created",
}: SidebarConversationListProps & { }: SidebarConversationListProps & {
ref?: Ref<SidebarConversationListHandle> ref?: Ref<SidebarConversationListHandle>
}) { }) {
@@ -368,9 +386,11 @@ export function SidebarConversationList({
if (list) list.push(conv) if (list) list.push(conv)
else map.set(conv.folder_id, [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 return map
}, [filteredConversations]) }, [filteredConversations, sortMode])
const orderedFolderIds = useMemo(() => { const orderedFolderIds = useMemo(() => {
const seen = new Set<number>() const seen = new Set<number>()
@@ -821,7 +841,11 @@ export function SidebarConversationList({
isOpenInTab={openTabConversationKeys.has( isOpenInTab={openTabConversationKeys.has(
`${conv.agent_type}:${conv.id}` `${conv.agent_type}:${conv.id}`
)} )}
timeLabel={formatRelative(conv.updated_at)} timeLabel={formatRelative(
sortMode === "updated"
? conv.updated_at
: conv.created_at
)}
onSelect={handleSelect} onSelect={handleSelect}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
onRename={handleRename} onRename={handleRename}

View File

@@ -18,6 +18,10 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { import {
@@ -29,7 +33,10 @@ import {
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"
import { import {
loadShowCompleted, loadShowCompleted,
loadSortMode,
saveShowCompleted, saveShowCompleted,
saveSortMode,
type SidebarSortMode,
} from "@/lib/sidebar-view-mode-storage" } from "@/lib/sidebar-view-mode-storage"
export function Sidebar() { export function Sidebar() {
@@ -39,12 +46,14 @@ export function Sidebar() {
const listRef = useRef<SidebarConversationListHandle>(null) const listRef = useRef<SidebarConversationListHandle>(null)
const [showCompleted, setShowCompleted] = useState(false) const [showCompleted, setShowCompleted] = useState(false)
const [sortMode, setSortMode] = useState<SidebarSortMode>("created")
const [allExpanded, setAllExpanded] = useState(true) const [allExpanded, setAllExpanded] = useState(true)
useEffect(() => { useEffect(() => {
// Hydrate from localStorage after mount to keep SSR/CSR markup consistent. // Hydrate from localStorage after mount to keep SSR/CSR markup consistent.
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setShowCompleted(loadShowCompleted()) setShowCompleted(loadShowCompleted())
setSortMode(loadSortMode())
}, []) }, [])
const handleSetShowCompleted = useCallback((value: boolean) => { const handleSetShowCompleted = useCallback((value: boolean) => {
@@ -52,6 +61,12 @@ export function Sidebar() {
saveShowCompleted(value) saveShowCompleted(value)
}, []) }, [])
const handleSetSortMode = useCallback((value: string) => {
const mode: SidebarSortMode = value === "updated" ? "updated" : "created"
setSortMode(mode)
saveSortMode(mode)
}, [])
const handleToggleExpandAll = useCallback(() => { const handleToggleExpandAll = useCallback(() => {
if (allExpanded) { if (allExpanded) {
listRef.current?.collapseAll() listRef.current?.collapseAll()
@@ -122,6 +137,19 @@ export function Sidebar() {
> >
{t("showCompleted")} {t("showCompleted")}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t("sortBy")}</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={sortMode}
onValueChange={handleSetSortMode}
>
<DropdownMenuRadioItem value="created">
{t("sortByCreatedAt")}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="updated">
{t("sortByUpdatedAt")}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@@ -142,7 +170,11 @@ export function Sidebar() {
: undefined : undefined
} }
> >
<SidebarConversationList ref={listRef} showCompleted={showCompleted} /> <SidebarConversationList
ref={listRef}
showCompleted={showCompleted}
sortMode={sortMode}
/>
</div> </div>
</aside> </aside>
) )

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "بحث عن محادثات...", "searchPlaceholder": "بحث عن محادثات...",
"showCompleted": "عرض المحادثات المكتملة", "showCompleted": "عرض المحادثات المكتملة",
"moreOptions": "المزيد من الخيارات", "moreOptions": "المزيد من الخيارات",
"sortBy": "الترتيب حسب",
"sortByCreatedAt": "وقت الإنشاء",
"sortByUpdatedAt": "وقت التحديث",
"statusRunningBadge": "قيد التشغيل", "statusRunningBadge": "قيد التشغيل",
"statusFailedBadge": "فشل", "statusFailedBadge": "فشل",
"conversationCountUnit": "{count} محادثة", "conversationCountUnit": "{count} محادثة",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "Konversationen suchen...", "searchPlaceholder": "Konversationen suchen...",
"showCompleted": "Abgeschlossene Konversationen anzeigen", "showCompleted": "Abgeschlossene Konversationen anzeigen",
"moreOptions": "Weitere Optionen", "moreOptions": "Weitere Optionen",
"sortBy": "Sortieren nach",
"sortByCreatedAt": "Erstellungszeit",
"sortByUpdatedAt": "Aktualisierungszeit",
"statusRunningBadge": "Läuft", "statusRunningBadge": "Läuft",
"statusFailedBadge": "Fehlgeschlagen", "statusFailedBadge": "Fehlgeschlagen",
"conversationCountUnit": "{count, plural, one {# Konversation} other {# Konversationen}}", "conversationCountUnit": "{count, plural, one {# Konversation} other {# Konversationen}}",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "Search conversations...", "searchPlaceholder": "Search conversations...",
"showCompleted": "Show completed conversations", "showCompleted": "Show completed conversations",
"moreOptions": "More options", "moreOptions": "More options",
"sortBy": "Sort by",
"sortByCreatedAt": "Created time",
"sortByUpdatedAt": "Updated time",
"statusRunningBadge": "Running", "statusRunningBadge": "Running",
"statusFailedBadge": "Failed", "statusFailedBadge": "Failed",
"conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}", "conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "Buscar conversaciones...", "searchPlaceholder": "Buscar conversaciones...",
"showCompleted": "Mostrar conversaciones completadas", "showCompleted": "Mostrar conversaciones completadas",
"moreOptions": "Más opciones", "moreOptions": "Más opciones",
"sortBy": "Ordenar por",
"sortByCreatedAt": "Fecha de creación",
"sortByUpdatedAt": "Fecha de actualización",
"statusRunningBadge": "Ejecutando", "statusRunningBadge": "Ejecutando",
"statusFailedBadge": "Fallido", "statusFailedBadge": "Fallido",
"conversationCountUnit": "{count, plural, one {# conversación} other {# conversaciones}}", "conversationCountUnit": "{count, plural, one {# conversación} other {# conversaciones}}",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "Rechercher des conversations...", "searchPlaceholder": "Rechercher des conversations...",
"showCompleted": "Afficher les conversations terminées", "showCompleted": "Afficher les conversations terminées",
"moreOptions": "Plus d'options", "moreOptions": "Plus d'options",
"sortBy": "Trier par",
"sortByCreatedAt": "Date de création",
"sortByUpdatedAt": "Date de mise à jour",
"statusRunningBadge": "En cours", "statusRunningBadge": "En cours",
"statusFailedBadge": "Échec", "statusFailedBadge": "Échec",
"conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}", "conversationCountUnit": "{count, plural, one {# conversation} other {# conversations}}",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "会話を検索...", "searchPlaceholder": "会話を検索...",
"showCompleted": "完了した会話を表示", "showCompleted": "完了した会話を表示",
"moreOptions": "その他のオプション", "moreOptions": "その他のオプション",
"sortBy": "並び替え",
"sortByCreatedAt": "作成時刻順",
"sortByUpdatedAt": "更新時刻順",
"statusRunningBadge": "実行中", "statusRunningBadge": "実行中",
"statusFailedBadge": "失敗", "statusFailedBadge": "失敗",
"conversationCountUnit": "{count} 件", "conversationCountUnit": "{count} 件",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "대화 검색...", "searchPlaceholder": "대화 검색...",
"showCompleted": "완료된 대화 표시", "showCompleted": "완료된 대화 표시",
"moreOptions": "더 많은 옵션", "moreOptions": "더 많은 옵션",
"sortBy": "정렬 기준",
"sortByCreatedAt": "생성 시간순",
"sortByUpdatedAt": "업데이트 시간순",
"statusRunningBadge": "실행 중", "statusRunningBadge": "실행 중",
"statusFailedBadge": "실패", "statusFailedBadge": "실패",
"conversationCountUnit": "{count}개", "conversationCountUnit": "{count}개",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "Buscar conversas...", "searchPlaceholder": "Buscar conversas...",
"showCompleted": "Mostrar conversas concluídas", "showCompleted": "Mostrar conversas concluídas",
"moreOptions": "Mais opções", "moreOptions": "Mais opções",
"sortBy": "Ordenar por",
"sortByCreatedAt": "Data de criação",
"sortByUpdatedAt": "Data de atualização",
"statusRunningBadge": "Executando", "statusRunningBadge": "Executando",
"statusFailedBadge": "Falhou", "statusFailedBadge": "Falhou",
"conversationCountUnit": "{count, plural, one {# conversa} other {# conversas}}", "conversationCountUnit": "{count, plural, one {# conversa} other {# conversas}}",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "搜索会话...", "searchPlaceholder": "搜索会话...",
"showCompleted": "显示已完成会话", "showCompleted": "显示已完成会话",
"moreOptions": "更多选项", "moreOptions": "更多选项",
"sortBy": "排序方式",
"sortByCreatedAt": "按创建时间排序",
"sortByUpdatedAt": "按更新时间排序",
"statusRunningBadge": "运行中", "statusRunningBadge": "运行中",
"statusFailedBadge": "失败", "statusFailedBadge": "失败",
"conversationCountUnit": "{count} 条", "conversationCountUnit": "{count} 条",

View File

@@ -788,6 +788,9 @@
"searchPlaceholder": "搜尋對話...", "searchPlaceholder": "搜尋對話...",
"showCompleted": "顯示已完成對話", "showCompleted": "顯示已完成對話",
"moreOptions": "更多選項", "moreOptions": "更多選項",
"sortBy": "排序方式",
"sortByCreatedAt": "按建立時間排序",
"sortByUpdatedAt": "按更新時間排序",
"statusRunningBadge": "運行中", "statusRunningBadge": "運行中",
"statusFailedBadge": "失敗", "statusFailedBadge": "失敗",
"conversationCountUnit": "{count} 條", "conversationCountUnit": "{count} 條",

View File

@@ -2,6 +2,9 @@
const FOLDER_EXPANDED_KEY = "workspace:sidebar-folder-expanded" const FOLDER_EXPANDED_KEY = "workspace:sidebar-folder-expanded"
const SHOW_COMPLETED_KEY = "workspace:sidebar-show-completed" 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<number, boolean> { export function loadFolderExpanded(): Record<number, boolean> {
if (typeof window === "undefined") return {} if (typeof window === "undefined") return {}
@@ -51,3 +54,23 @@ export function saveShowCompleted(value: boolean): void {
/* ignore */ /* 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 */
}
}