feat(sidebar): add conversation sort mode toggle with created-time default
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -788,6 +788,9 @@
|
|||||||
"searchPlaceholder": "بحث عن محادثات...",
|
"searchPlaceholder": "بحث عن محادثات...",
|
||||||
"showCompleted": "عرض المحادثات المكتملة",
|
"showCompleted": "عرض المحادثات المكتملة",
|
||||||
"moreOptions": "المزيد من الخيارات",
|
"moreOptions": "المزيد من الخيارات",
|
||||||
|
"sortBy": "الترتيب حسب",
|
||||||
|
"sortByCreatedAt": "وقت الإنشاء",
|
||||||
|
"sortByUpdatedAt": "وقت التحديث",
|
||||||
"statusRunningBadge": "قيد التشغيل",
|
"statusRunningBadge": "قيد التشغيل",
|
||||||
"statusFailedBadge": "فشل",
|
"statusFailedBadge": "فشل",
|
||||||
"conversationCountUnit": "{count} محادثة",
|
"conversationCountUnit": "{count} محادثة",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -788,6 +788,9 @@
|
|||||||
"searchPlaceholder": "会話を検索...",
|
"searchPlaceholder": "会話を検索...",
|
||||||
"showCompleted": "完了した会話を表示",
|
"showCompleted": "完了した会話を表示",
|
||||||
"moreOptions": "その他のオプション",
|
"moreOptions": "その他のオプション",
|
||||||
|
"sortBy": "並び替え",
|
||||||
|
"sortByCreatedAt": "作成時刻順",
|
||||||
|
"sortByUpdatedAt": "更新時刻順",
|
||||||
"statusRunningBadge": "実行中",
|
"statusRunningBadge": "実行中",
|
||||||
"statusFailedBadge": "失敗",
|
"statusFailedBadge": "失敗",
|
||||||
"conversationCountUnit": "{count} 件",
|
"conversationCountUnit": "{count} 件",
|
||||||
|
|||||||
@@ -788,6 +788,9 @@
|
|||||||
"searchPlaceholder": "대화 검색...",
|
"searchPlaceholder": "대화 검색...",
|
||||||
"showCompleted": "완료된 대화 표시",
|
"showCompleted": "완료된 대화 표시",
|
||||||
"moreOptions": "더 많은 옵션",
|
"moreOptions": "더 많은 옵션",
|
||||||
|
"sortBy": "정렬 기준",
|
||||||
|
"sortByCreatedAt": "생성 시간순",
|
||||||
|
"sortByUpdatedAt": "업데이트 시간순",
|
||||||
"statusRunningBadge": "실행 중",
|
"statusRunningBadge": "실행 중",
|
||||||
"statusFailedBadge": "실패",
|
"statusFailedBadge": "실패",
|
||||||
"conversationCountUnit": "{count}개",
|
"conversationCountUnit": "{count}개",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -788,6 +788,9 @@
|
|||||||
"searchPlaceholder": "搜索会话...",
|
"searchPlaceholder": "搜索会话...",
|
||||||
"showCompleted": "显示已完成会话",
|
"showCompleted": "显示已完成会话",
|
||||||
"moreOptions": "更多选项",
|
"moreOptions": "更多选项",
|
||||||
|
"sortBy": "排序方式",
|
||||||
|
"sortByCreatedAt": "按创建时间排序",
|
||||||
|
"sortByUpdatedAt": "按更新时间排序",
|
||||||
"statusRunningBadge": "运行中",
|
"statusRunningBadge": "运行中",
|
||||||
"statusFailedBadge": "失败",
|
"statusFailedBadge": "失败",
|
||||||
"conversationCountUnit": "{count} 条",
|
"conversationCountUnit": "{count} 条",
|
||||||
|
|||||||
@@ -788,6 +788,9 @@
|
|||||||
"searchPlaceholder": "搜尋對話...",
|
"searchPlaceholder": "搜尋對話...",
|
||||||
"showCompleted": "顯示已完成對話",
|
"showCompleted": "顯示已完成對話",
|
||||||
"moreOptions": "更多選項",
|
"moreOptions": "更多選項",
|
||||||
|
"sortBy": "排序方式",
|
||||||
|
"sortByCreatedAt": "按建立時間排序",
|
||||||
|
"sortByUpdatedAt": "按更新時間排序",
|
||||||
"statusRunningBadge": "運行中",
|
"statusRunningBadge": "運行中",
|
||||||
"statusFailedBadge": "失敗",
|
"statusFailedBadge": "失敗",
|
||||||
"conversationCountUnit": "{count} 條",
|
"conversationCountUnit": "{count} 條",
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user