import type { ProjectId } from "@t3tools/contracts"; import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; export interface ThreadSortInput { readonly createdAt: string; readonly updatedAt: string; readonly latestUserMessageAt?: string | null; readonly messages?: ReadonlyArray<{ readonly createdAt: string; readonly role: string; }>; } export function toSortableTimestamp(iso: string | undefined): number | null { if (!iso) return null; const ms = Date.parse(iso); return Number.isFinite(ms) ? ms : null; } function getFirstSortableTimestamp(...values: Array): number | null { for (const value of values) { const timestamp = toSortableTimestamp(value ?? undefined); if (timestamp !== null) { return timestamp; } } return null; } function getLatestUserMessageTimestamp(thread: ThreadSortInput): number { if (thread.latestUserMessageAt) { const latestUserMessageTimestamp = toSortableTimestamp(thread.latestUserMessageAt); if (latestUserMessageTimestamp !== null) { return latestUserMessageTimestamp; } } let latestUserMessageTimestamp: number | null = null; for (const message of thread.messages ?? []) { if (message.role !== "user") continue; const messageTimestamp = toSortableTimestamp(message.createdAt); if (messageTimestamp === null) continue; latestUserMessageTimestamp = latestUserMessageTimestamp === null ? messageTimestamp : Math.max(latestUserMessageTimestamp, messageTimestamp); } if (latestUserMessageTimestamp !== null) { return latestUserMessageTimestamp; } return getFirstSortableTimestamp(thread.updatedAt, thread.createdAt) ?? Number.NEGATIVE_INFINITY; } export function getThreadSortTimestamp( thread: ThreadSortInput, sortOrder: SidebarThreadSortOrder | Exclude, ): number { if (sortOrder === "created_at") { return ( getFirstSortableTimestamp(thread.createdAt, thread.updatedAt) ?? Number.NEGATIVE_INFINITY ); } return getLatestUserMessageTimestamp(thread); } export function sortThreads( threads: readonly T[], sortOrder: SidebarThreadSortOrder, ): T[] { return Arr.sort( threads, Order.mapInput( Order.Struct({ timestamp: Order.flip(Order.Number), id: Order.flip(Order.String), }), (thread: T) => ({ timestamp: getThreadSortTimestamp(thread, sortOrder), id: thread.id, }), ), ); } export function getLatestThreadForProject< T extends { readonly id: string; readonly projectId: ProjectId; readonly archivedAt: string | null; } & ThreadSortInput, >(threads: readonly T[], projectId: ProjectId, sortOrder: SidebarThreadSortOrder): T | null { return ( sortThreads( threads.filter((thread) => thread.projectId === projectId && thread.archivedAt === null), sortOrder, )[0] ?? null ); }