"use client";
import { useMemo, useState } from "react";
import { IconColumns } from "@tabler/icons-react";
import type { ActiveThread } from "@/lib/threads/active-threads";
import { useTranslation } from "react-i18next";
import { ThreadColumn } from "./thread-column";
import { useThreadColumnActivation } from "./use-thread-column-activation";
type ThreadsBoardProps = {
threads: ActiveThread[];
isLoading?: boolean;
/** Column a deep link asked for; scrolled into view and ringed on arrival. */
focusedTaskId?: string | null;
/** Session a task-detail link asked the target column to select. */
focusedSessionId?: string | null;
/** Removes a target session query after the target column proves it invalid. */
onInvalidRequestedSession?: (taskId: string, sessionId: string) => void;
onOpenTask: (taskId: string) => void;
};
function ThreadsPlaceholder({ testId, children }: { testId: string; children: React.ReactNode }) {
return (
{children}
);
}
function ThreadsEmptyState() {
const { t } = useTranslation();
return (
{t("threads:emptyTitle")}
{t("threads:emptyBody")}
);
}
function ThreadsLoadingState() {
const { t } = useTranslation();
return (
{t("threads:loading")}
);
}
/**
* The deep-link mark answers "where is the column I asked for", so it retires
* the moment the reader starts using the deck rather than sitting on a column
* they have since moved away from. A later deep link earns a fresh mark, which
* is why dismissal is keyed to the requested id rather than latched forever.
*
* Uses the store-previous-props pattern instead of an effect so the mark never
* paints for a frame after a new request has already been dismissed.
*/
function useRetiringFocusMark(focusedTaskId: string | null) {
const [retired, setRetired] = useState(false);
const [requested, setRequested] = useState(focusedTaskId);
if (requested !== focusedTaskId) {
setRequested(focusedTaskId);
setRetired(false);
}
return {
markedTaskId: retired ? null : focusedTaskId,
retire: () => setRetired(true),
};
}
/**
* The deck: every live agent conversation as its own column, scrolled
* horizontally. Columns keep the order the selector gave them, so a thread the
* reader is following does not jump while they read it.
*/
export function ThreadsBoard({
threads,
isLoading = false,
focusedTaskId = null,
focusedSessionId = null,
onInvalidRequestedSession,
onOpenTask,
}: ThreadsBoardProps) {
const { markedTaskId, retire } = useRetiringFocusMark(focusedTaskId);
const orderedIds = useMemo(() => threads.map((thread) => thread.taskId), [threads]);
const { boardRef, registerColumn, preloadTaskIds, detailTaskIds } = useThreadColumnActivation(
orderedIds,
focusedTaskId,
);
if (threads.length === 0) {
return isLoading ? : ;
}
return (
{threads.map((thread) => (
))}
);
}