List the open bot's recent sessions in the rail
Show the latest five sessions under the active bot, with All sessions opening the existing modal once there are more. Share the session modal, row and hook outside the conversation page, and mark the current session with a muted row.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import BotRow from "./bot-row";
|
||||
import RailBotSessions from "./rail-bot-sessions";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
href: string;
|
||||
selected?: boolean;
|
||||
workspace_id?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A worker in the rail, and its sessions while it is the open conversation.
|
||||
*/
|
||||
export default function RailBotEntry({ bot, href, selected, workspace_id }: Props) {
|
||||
return (
|
||||
<>
|
||||
<BotRow bot={bot} href={href} selected={selected} />
|
||||
{selected ? <RailBotSessions key={bot.id} bot={bot} workspace_id={workspace_id} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { BotSessionListItem } from "@/src/types";
|
||||
import relativeTime from "@/src/utils/relative-time";
|
||||
|
||||
type Props = {
|
||||
session: BotSessionListItem;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
onOpen: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One session in the rail's sub-list under the open bot: aligned with the
|
||||
* bot's name, its title and how recent. The active one says so; any other
|
||||
* reopens on click.
|
||||
*/
|
||||
export default function RailBotSessionRow({ session, loading, disabled, onOpen }: Props) {
|
||||
const active = session.status == "active";
|
||||
const at = session.last_activity_at || session.created_at;
|
||||
|
||||
return (
|
||||
<Button
|
||||
title={active ? "This is the current session" : `Reopen "${session.title || "Untitled"}"`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
loading={loading}
|
||||
disabled={active || disabled}
|
||||
onClick={onOpen}
|
||||
buttonContentProps={{ className: "w-full min-w-0 flex-nowrap" }}
|
||||
className={twMerge(
|
||||
"w-full h-8 shrink-0 justify-start! items-center! gap-2! rounded-default px-2! @rail:pl-14! text-left text-ink dark:text-ink hover:bg-ink/5 hover:dark:bg-ink/5 focus-visible:ring-2 focus-visible:ring-ink",
|
||||
active && "bg-sunken hover:bg-sunken hover:dark:bg-sunken opacity-100!",
|
||||
)}
|
||||
>
|
||||
<Span
|
||||
className={twMerge(
|
||||
"grow min-w-0 truncate text-xs leading-4 font-bold",
|
||||
active ? "text-ink-secondary" : "text-ink",
|
||||
)}
|
||||
>
|
||||
{session.title || "Untitled session"}
|
||||
</Span>
|
||||
{session.unread && !active ? (
|
||||
<Span className="shrink-0 text-[11px] font-bold leading-4 text-ink">New</Span>
|
||||
) : null}
|
||||
<Span className="shrink-0 text-[11px] leading-4 text-ink-secondary">
|
||||
{active ? "Current" : at ? relativeTime({ at: Number(at) }) : ""}
|
||||
</Span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import BotSessionsModal from "@/src/components/general/bot-sessions-modal";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import useBotSessions from "@/src/hooks/use-bot-sessions";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import RailBotSessionRow from "./rail-bot-session-row";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
workspace_id?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Under the open bot in the rail: its latest sessions, latest first, and,
|
||||
* once there are more than the ones listed, "All sessions" which opens the
|
||||
* conversation's sessions modal. Hidden when the rail collapses to icons.
|
||||
*/
|
||||
export default function RailBotSessions({ bot, workspace_id }: Props) {
|
||||
const sessions = useBotSessions({
|
||||
bot_id: Number(bot.id),
|
||||
workspace_id,
|
||||
open: true,
|
||||
limit: AppData.RailRecentSessions + 1,
|
||||
});
|
||||
const shown = sessions.sessions.slice(0, AppData.RailRecentSessions);
|
||||
|
||||
if (!shown.length) return null;
|
||||
|
||||
return (
|
||||
<Stack className="hidden @rail:flex w-full shrink-0 flex-col gap-0! items-stretch pb-1">
|
||||
{shown.map((session) => (
|
||||
<RailBotSessionRow
|
||||
key={session.id}
|
||||
session={session}
|
||||
loading={sessions.busy_id == session.id}
|
||||
disabled={Boolean(sessions.busy_id)}
|
||||
onOpen={() => sessions.reopenSession({ session_id: Number(session.id) })}
|
||||
/>
|
||||
))}
|
||||
{sessions.sessions.length > AppData.RailRecentSessions ? (
|
||||
<BotSessionsModal
|
||||
bot={bot}
|
||||
workspace_id={workspace_id}
|
||||
target={
|
||||
<Button
|
||||
title="Open all sessions"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
className="w-full h-8 shrink-0 justify-start! items-center! gap-2! rounded-default px-2! @rail:pl-14! text-left text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink"
|
||||
>
|
||||
<Span className="text-[11px] font-bold leading-4">All sessions</Span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { AppData } from "@/src/data/app-data";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import pickRailBots from "../(utils)/pick-rail-bots";
|
||||
import AllBotsModal from "./all-bots-modal";
|
||||
import BotRow from "./bot-row";
|
||||
import RailBotEntry from "./rail-bot-entry";
|
||||
import RailSectionLabel from "./rail-section-label";
|
||||
import RailSeeAllRow from "./rail-see-all-row";
|
||||
|
||||
@@ -15,6 +15,10 @@ type Props = {
|
||||
*/
|
||||
workers: BotWithLastMessage[];
|
||||
selected_id?: number;
|
||||
/**
|
||||
* Set inside a workspace, whose conversations have their own sessions.
|
||||
*/
|
||||
workspace_id?: number | null;
|
||||
hrefFor: (bot: BotWithLastMessage) => string;
|
||||
/**
|
||||
* Shown when there are no bots yet.
|
||||
@@ -28,7 +32,7 @@ type Props = {
|
||||
* active (and any that want attention), then "See all bots" for the rest,
|
||||
* which lists them all in a modal.
|
||||
*/
|
||||
export default function RailBotsSection({ title, workers, selected_id, hrefFor, empty, action }: Props) {
|
||||
export default function RailBotsSection({ title, workers, selected_id, workspace_id, hrefFor, empty, action }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { shown, hidden } = pickRailBots({ workers, selected_id, limit: AppData.RailRecentBots });
|
||||
|
||||
@@ -36,7 +40,13 @@ export default function RailBotsSection({ title, workers, selected_id, hrefFor,
|
||||
<>
|
||||
<RailSectionLabel title={title} action={action} />
|
||||
{shown.map((bot) => (
|
||||
<BotRow key={bot.id} bot={bot} href={hrefFor(bot)} selected={bot.id == selected_id} />
|
||||
<RailBotEntry
|
||||
key={bot.id}
|
||||
bot={bot}
|
||||
href={hrefFor(bot)}
|
||||
selected={bot.id == selected_id}
|
||||
workspace_id={workspace_id}
|
||||
/>
|
||||
))}
|
||||
{workers.length ? null : (
|
||||
<Span className="hidden @rail:block px-3 py-2 text-[13px] leading-[18px] text-ink-secondary">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppContext } from "@/src/pages/__root";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import type { BotWithLastMessage, WorkspaceWithActivity } from "@/src/types";
|
||||
import CoordinatorRow from "./coordinator-row";
|
||||
import RailBotSessions from "./rail-bot-sessions";
|
||||
import RailBotsSection from "./rail-bots-section";
|
||||
import RailSectionAddButton from "./rail-section-add-button";
|
||||
import RailWorkspacesSection from "./rail-workspaces-section";
|
||||
@@ -27,11 +28,16 @@ export default function RailCrewList({ bots, workspaces, selected_id }: Props) {
|
||||
return (
|
||||
<>
|
||||
{coordinator ? (
|
||||
<CoordinatorRow
|
||||
bot={coordinator}
|
||||
bots={bots}
|
||||
selected={coordinator.id === selected_id}
|
||||
/>
|
||||
<>
|
||||
<CoordinatorRow
|
||||
bot={coordinator}
|
||||
bots={bots}
|
||||
selected={coordinator.id === selected_id}
|
||||
/>
|
||||
{coordinator.id === selected_id ? (
|
||||
<RailBotSessions key={coordinator.id} bot={coordinator} />
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<RailBotsSection
|
||||
action={<RailSectionAddButton title="Add a bot" onClick={() => openForm({ kind: "bot" })} />}
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function RailWorkspaceList({ workspace, bots, selected_id, on_wor
|
||||
title="Workspace bots"
|
||||
workers={workers}
|
||||
selected_id={on_workspace_page ? undefined : selected_id}
|
||||
workspace_id={workspace.id ? Number(workspace.id) : null}
|
||||
hrefFor={(bot) => `${base}/bots/${bot.id}`}
|
||||
empty="No bots here yet."
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user