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<string | null | undefined>): 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<SidebarProjectSortOrder, "manual">,): number { if (sortOrder === "created_at") { return ( getFirstSortableTimestamp(thread.createdAt, thread.updatedAt) ?? Number.NEGATIVE_INFINITY ); } return getLatestUserMessageTimestamp(thread);} export function sortThreads<T extends { readonly id: string } & ThreadSortInput>( 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 );}
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<string | null | undefined>): 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<SidebarProjectSortOrder, "manual">,
): number {
if (sortOrder === "created_at") {
return (
getFirstSortableTimestamp(thread.createdAt, thread.updatedAt) ?? Number.NEGATIVE_INFINITY
);
}
return getLatestUserMessageTimestamp(thread);
}
export function sortThreads<T extends { readonly id: string } & ThreadSortInput>(
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
);
}