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:
@@ -87,7 +87,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
||||
|
||||
- **Access (the crew is shared):** bots, workspaces and computers belong to the install, not to a user (their `user_id` is only who created them, not a foreign key). Nobody reaches a bot until a super admin or admin grants it: `bot_users` (a worker assigned to a user, for its own chat) and `workspace_users` (a user added to a workspace, to chat with every worker in it there). `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the only one who ever sees, chats with or edits the coordinator; `admin` reaches every worker and workspace; anyone else (`member`) only their grants. `canManageCrew` (super admins and admins) creates, edits and deletes workers and workspaces, sets their models and pictures, and hands out access (the Bots and Workspaces groups on a user's settings page, `user-access-groups.tsx`, each added or taken away at once by `api/admin/users/add-access` and `remove-access` through `setUserGrant`); the structural DB helpers check it themselves (`assertCrewManager`, `src/functions/backend/db/access/`). The SQL conditions live in `src/utils/crew-access/` (`chatBotsSql`, `workspacesSql`, `workspaceBotsSql`, `computersSql`, tested against SQLite) and back `grabChatBot` (its own chat), `grabUserBot` (anywhere: its own chat or a workspace), `grabUserWorkspace(s)`, `grabWorkspaceBot`, `grabBots` and `grabUserComputer(s)`. Conversations are per user (`bot_sessions.user_id`): a job runs for the user whose session it's in (`runBotJob` reads it), with that user's memory and only what they reach. A bot's face and status line are per user too (`bot_user_faces`, set by `setBotFace` for the job's user, `grabJobUserId`; no row is idle), so needs-you or working only shows to the user the job is for; every bot read for a page overlays the viewer's (`botFaceSql`, `src/utils/bot-face-sql.ts`). Live updates go to the session's user (`pushToSessionUser`), a face change to its user, and a bot change (its computer) to everyone who sees it (`pushBotUpdated`, `grabBotAudience`, each with their own face and active task); its status line never names a task (`BotTaskStatusLine`). Bot and workspace pictures are served to whoever sees them (`grabSharedPictureMedia`). A member with nothing granted lands on an empty `/admin/bots`; a workspace's dashboard is open to everyone in it, its settings link and computers' settings links only to `canManageCrew`. `src/scripts/share-crew.ts` (safe to rerun) moved an install from one crew per user to this.
|
||||
- **Tables:** `bots` (`kind` worker|coordinator, `hue`, `role`, `system_prompt` (the bot's core instructions, markdown)), `bot_user_faces` (`face`, `status_line` per bot and user), `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
|
||||
- **Threads and sessions:** each user has one conversation with a bot in its own chat and one per workspace it's in (the coordinator in every workspace). Each is split into `bot_sessions` (`user_id`, `bot_id`, `workspace_id` null for its own chat, `status` active|closed, `title` from the first user message, `summary`, `previous_session_id`, `unread`, `last_activity_at`); `bot_messages`, `bot_tasks` and `bots_queue` carry `session_id` and `workspace_id`. Helpers in `src/functions/backend/db/bot-sessions/`: `ensureBotSession` (the active one, else a new one; `rollover` starts the next once it has `AppData.BotSessionMaxMessages`), `startBotSession`, `reopenBotSession`, `grabBotSessions` (search over title, summary and message text), `grabUserConversation` (access and membership), `grabConversationProps` (the conversation pages' props). A new session's first reply gets the previous one's summary (`grab-session-carry-over.ts`, written once with the text harness). The header's sessions icon (`(partials)/bot-sessions-modal.tsx`) lists, searches, starts and reopens sessions, and the new-session icon beside it (`(partials)/new-session-button.tsx`) starts one (`api/admin/bots/list-sessions`, `new-session`, `reopen-session`). Unread, the rail's last message and activity are per thread, from its sessions. Job results go into the job's own session, even after it closes.
|
||||
- **Threads and sessions:** each user has one conversation with a bot in its own chat and one per workspace it's in (the coordinator in every workspace). Each is split into `bot_sessions` (`user_id`, `bot_id`, `workspace_id` null for its own chat, `status` active|closed, `title` from the first user message, `summary`, `previous_session_id`, `unread`, `last_activity_at`); `bot_messages`, `bot_tasks` and `bots_queue` carry `session_id` and `workspace_id`. Helpers in `src/functions/backend/db/bot-sessions/`: `ensureBotSession` (the active one, else a new one; `rollover` starts the next once it has `AppData.BotSessionMaxMessages`), `startBotSession`, `reopenBotSession`, `grabBotSessions` (search over title, summary and message text), `grabUserConversation` (access and membership), `grabConversationProps` (the conversation pages' props). A new session's first reply gets the previous one's summary (`grab-session-carry-over.ts`, written once with the text harness). The header's sessions icon (`src/components/general/bot-sessions-modal.tsx`) lists, searches, starts and reopens sessions, and the new-session icon beside it (`(partials)/new-session-button.tsx`) starts one (`api/admin/bots/list-sessions`, `new-session`, `reopen-session`). The rail lists the open bot's latest `AppData.RailRecentSessions` sessions under its row (`rail-bot-sessions.tsx`), and once there are more, "All sessions" opens the same modal. Unread, the rail's last message and activity are per thread, from its sessions. Job results go into the job's own session, even after it closes.
|
||||
- **Coordinator:** the install has exactly one `kind: coordinator` bot, the super admin's alone, that coordinates the crew and delegates to every worker. It is created at signup (`bun run seed`) and, as a fallback, by the super admin's `/admin/bots`, all via `ensureCoordinatorBot` (`src/functions/backend/db/bots/ensure-coordinator-bot.ts`, idempotent). Name in `CoordinatorBotName` (`src/dict/bot-dict.ts`). Its default prompt is `CoordinatorSystemPrompt` (`src/data/coordinator-system-prompt.ts`): attached at creation, and backfilled onto a coordinator whose prompt is empty, never over an edited one.
|
||||
- **Routes:** `/admin/bots` ensures the coordinator and redirects the super admin to its chat (`/admin/bots/1` on a fresh install), anyone else to their latest bot, else their first workspace, else an empty page; `/admin/bots/[bot_id]` renders the conversation (header, thread and composer, the last two held to 760px; there is no right panel); `/admin/bots/[bot_id]/models` is a worker bot's own model settings, for super admins and admins (the coordinator's redirects to its chat); `/admin/workspaces/[workspace_id]` is a workspace's dashboard (`(sections)/workspace-dashboard.tsx`: read-only picture, name, description and context, latest activity, bots with their per-workspace instructions and memory, computers; its header links into `/admin/settings/workspaces/[workspace_id]`, whose `workspace-overview.tsx` has the editable groups) and `/admin/workspaces/[workspace_id]/bots/[bot_id]` is the same `Conversation` section scoped to the workspace (`pageProps.workspace`; only the coordinator and member bots). All render in the full-bleed `src/layouts/bots/` layout (rail of conversations + user popover), which `__root.tsx` uses for every `/admin/*` page.
|
||||
- **Server:** `src/functions/backend/db/bots/` (`grab-bots`, `grab-bot-messages`, `grab-active-bot-task`, `grab-user-bot`, `create-bot`, `insert-user-message`). `api/admin/bots/send-message.ts` posts a user message to a bot the session user chats with (zod: `src/schemas/bot-schema.ts`), queues the bot's reply (`enqueueBotReply`) and wakes the queue (`src/bots-queue/wake-bots-queue.ts`; never import `src/bots-queue/index.ts` from a page or API route, it pulls the bunext runtime into the page bundles). `upload-attachment.ts` stores one file for the next message (private `media` row, paradigm `bot-attachment`, type from `attachmentFileKind`, documents get `text_content`, limits in `AppData.BotAttachment*`) and `send-message` takes their `media_ids` (checked to be the user's own by `grabAttachableMedia`), linked in `bot_message_media`; messages carry `attachments` (`BotMessageWithAttachments`). `mark-read.ts` clears a session's unread when a message arrives in the open conversation; `session-messages.ts` returns a session's latest messages, which the open conversation polls every `AppData.BotReplyCatchUpMs` while a reply is due (`use-reply-catch-up`), so a reply whose live push was missed (socket down) still lands without a reload (replayed as a `server:bot-message`, so the rail and unread take it too); `create-bot.ts` adds a worker (and puts it in a workspace when given one); `save-harness-settings.ts` saves a worker bot's models.
|
||||
|
||||
+9
-3
@@ -33,14 +33,20 @@ export default function BotSessionRow({ session, loading, disabled, onOpen }: Pr
|
||||
loading={loading}
|
||||
disabled={active || disabled}
|
||||
onClick={onOpen}
|
||||
buttonContentProps={{ className: "w-full min-w-0 flex-nowrap" }}
|
||||
className={twMerge(
|
||||
"w-full justify-start! items-start! gap-0! px-3! py-2.5! rounded-default text-left text-ink dark:text-ink hover:bg-sunken hover:dark:bg-sunken focus-visible:ring-2 focus-visible:ring-ink",
|
||||
active && "bg-ink/5 opacity-100!",
|
||||
"w-full justify-start! items-start! gap-0! px-3! py-2.5! rounded-default 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!",
|
||||
)}
|
||||
>
|
||||
<Stack className="w-full min-w-0 gap-0.5! items-stretch">
|
||||
<Row className="w-full gap-2! flex-nowrap items-center">
|
||||
<Span className="grow min-w-0 truncate text-sm font-bold leading-5 text-ink">
|
||||
<Span
|
||||
className={twMerge(
|
||||
"grow min-w-0 truncate text-sm font-bold leading-5",
|
||||
active ? "text-ink-secondary" : "text-ink",
|
||||
)}
|
||||
>
|
||||
{session.title || "Untitled session"}
|
||||
</Span>
|
||||
{session.unread && !active ? (
|
||||
+29
-11
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, type ComponentProps, type ReactNode } from "react";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
@@ -6,12 +6,11 @@ import H2 from "@moduletrace/twui/layout/H2";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { History, Plus } from "lucide-react";
|
||||
import { History, Plus, CircleAlert } from "lucide-react";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import SettingsStatus from "@/src/pages/admin/settings/(partials)/settings-status";
|
||||
import useBotSessions from "../(hooks)/use-bot-sessions";
|
||||
import useBotSessions from "@/src/hooks/use-bot-sessions";
|
||||
import BotSessionRow from "./bot-session-row";
|
||||
import HeaderIconButton from "@/src/components/general/header-icon-button";
|
||||
import HeaderIconButton from "./header-icon-button";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
@@ -20,14 +19,26 @@ type Props = {
|
||||
* A bot message arrived in another session of this conversation.
|
||||
*/
|
||||
has_other_unread?: boolean;
|
||||
/**
|
||||
* What opens it; the conversation header's sessions icon by default.
|
||||
*/
|
||||
target?: ReactNode;
|
||||
target_wrapper_props?: ComponentProps<typeof Modal>["targetWrapperProps"];
|
||||
};
|
||||
|
||||
/**
|
||||
* The header's sessions icon. A conversation is split into sessions so a
|
||||
* bot's context never grows without bound; this lists them, searches
|
||||
* them, starts a fresh one, or reopens an old one to carry on.
|
||||
* The sessions icon (or another target) that opens a conversation's
|
||||
* sessions. A conversation is split into sessions so a bot's context never
|
||||
* grows without bound; this lists them, searches them, starts a fresh one,
|
||||
* or reopens an old one to carry on.
|
||||
*/
|
||||
export default function BotSessionsModal({ bot, workspace_id, has_other_unread }: Props) {
|
||||
export default function BotSessionsModal({
|
||||
bot,
|
||||
workspace_id,
|
||||
has_other_unread,
|
||||
target,
|
||||
target_wrapper_props,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const sessions = useBotSessions({ bot_id: Number(bot.id), workspace_id, open });
|
||||
const name = bot.name || "the bot";
|
||||
@@ -35,12 +46,14 @@ export default function BotSessionsModal({ bot, workspace_id, has_other_unread }
|
||||
return (
|
||||
<Modal
|
||||
target={
|
||||
target ?? (
|
||||
<HeaderIconButton
|
||||
icon={History}
|
||||
title={has_other_unread ? "Sessions: new messages in another session" : "Sessions"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
targetWrapperProps={{ className: "shrink-0" }}
|
||||
targetWrapperProps={target_wrapper_props ?? { className: target ? "w-full" : "shrink-0" }}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
className="max-w-lg bg-paper p-6"
|
||||
@@ -74,7 +87,12 @@ export default function BotSessionsModal({ bot, workspace_id, has_other_unread }
|
||||
onChange={(e) => sessions.setQuery(e.target.value)}
|
||||
wrapperWrapperProps={{ className: "mt-4" }}
|
||||
/>
|
||||
<SettingsStatus status={sessions.error ? { error: true, msg: sessions.error } : undefined} />
|
||||
{sessions.error ? (
|
||||
<Span role="alert" className="mt-2 flex items-center gap-1.5 text-[13px] leading-[18px] text-ink">
|
||||
<CircleAlert size={14} aria-hidden className="shrink-0" />
|
||||
{sessions.error}
|
||||
</Span>
|
||||
) : null}
|
||||
<Stack className="w-full mt-2 max-h-[60vh] overflow-y-auto gap-0! items-stretch">
|
||||
{sessions.sessions.map((session) => (
|
||||
<BotSessionRow
|
||||
@@ -251,6 +251,11 @@ export const AppData = {
|
||||
* Stones shown beside "See all bots".
|
||||
*/
|
||||
RailSeeAllStones: 4,
|
||||
/**
|
||||
* Recent sessions the rail lists under the open bot, before "All
|
||||
* sessions".
|
||||
*/
|
||||
RailRecentSessions: 5,
|
||||
/**
|
||||
* Recent sessions a workspace's dashboard shows.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,10 @@ type Params = {
|
||||
*/
|
||||
query?: string;
|
||||
page?: number;
|
||||
/**
|
||||
* How many to return; a page by default.
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,10 +31,11 @@ export default async function grabBotSessions({
|
||||
workspace_id,
|
||||
query,
|
||||
page = 1,
|
||||
limit,
|
||||
}: Params): Promise<BotSessionListItem[]> {
|
||||
const search = query?.trim();
|
||||
const like = search ? `%${search.replace(/[\\%_]/g, (c) => `\\${c}`)}%` : null;
|
||||
const limit = AppData.BotSessionsPageSize;
|
||||
const page_size = limit || AppData.BotSessionsPageSize;
|
||||
|
||||
let sql = ``;
|
||||
|
||||
@@ -56,8 +61,8 @@ export default async function grabBotSessions({
|
||||
bot_id,
|
||||
workspace_id ? Number(workspace_id) : null,
|
||||
...(like ? [like, like, like] : []),
|
||||
limit,
|
||||
(Math.max(1, page) - 1) * limit,
|
||||
page_size,
|
||||
(Math.max(1, page) - 1) * page_size,
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
+9
-5
@@ -1,21 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { BotSessionListItem } from "@/src/types";
|
||||
import postSessionApi from "../(functions)/post-session-api";
|
||||
import postSessionApi from "@/src/functions/frontend/post-session-api";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
workspace_id?: number | null;
|
||||
/**
|
||||
* Loads only while the modal is open.
|
||||
* Loads only while the caller needs the sessions.
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* How many to load, latest first; the modal leaves it out for a page.
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A conversation's sessions, latest first, searched as the user types
|
||||
* (debounced). Starting or reopening a session reloads the page onto it.
|
||||
*/
|
||||
export default function useBotSessions({ bot_id, workspace_id, open }: Params) {
|
||||
export default function useBotSessions({ bot_id, workspace_id, open, limit }: Params) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [sessions, setSessions] = useState<BotSessionListItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -31,7 +35,7 @@ export default function useBotSessions({ bot_id, workspace_id, open }: Params) {
|
||||
|
||||
const res = await postSessionApi<BotSessionListItem>({
|
||||
path: "list-sessions",
|
||||
body: { bot_id, workspace_id, query: query.trim() || undefined },
|
||||
body: { bot_id, workspace_id, query: query.trim() || undefined, limit },
|
||||
}).catch(() => null);
|
||||
|
||||
if (stale) return;
|
||||
@@ -50,7 +54,7 @@ export default function useBotSessions({ bot_id, workspace_id, open }: Params) {
|
||||
stale = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [open, query, bot_id, workspace_id]);
|
||||
}, [open, query, bot_id, workspace_id, limit]);
|
||||
|
||||
const act = async ({ id, path, body }: { id: number | "new"; path: "new-session" | "reopen-session"; body: Record<string, unknown> }) => {
|
||||
if (busy_id) return;
|
||||
@@ -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}
|
||||
/>
|
||||
{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."
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { BotMessageWithAttachments, WebSocketDataType } from "@/src/types";
|
||||
import postSessionApi from "../(functions)/post-session-api";
|
||||
import postSessionApi from "@/src/functions/frontend/post-session-api";
|
||||
|
||||
type Params = {
|
||||
session_id?: number | null;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import BotComputersModal from "./bot-computers-modal";
|
||||
import BotModelsModal from "./bot-models-modal";
|
||||
import BotProfileModal from "./bot-profile-modal";
|
||||
import BotSessionsModal from "./bot-sessions-modal";
|
||||
import BotSessionsModal from "@/src/components/general/bot-sessions-modal";
|
||||
import NewSessionButton from "./new-session-button";
|
||||
import HeaderIconButton from "@/src/components/general/header-icon-button";
|
||||
import RailDrawerButton from "@/src/layouts/bots/(partials)/rail-drawer-button";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { MessageSquarePlus } from "lucide-react";
|
||||
import postSessionApi from "../(functions)/post-session-api";
|
||||
import postSessionApi from "@/src/functions/frontend/post-session-api";
|
||||
import HeaderIconButton from "@/src/components/general/header-icon-button";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const handler: BunextAPIRouteHandler<
|
||||
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
||||
}
|
||||
|
||||
const { bot_id, workspace_id, query, page } = parseWithSchema({
|
||||
const { bot_id, workspace_id, query, page, limit } = parseWithSchema({
|
||||
schema: ListBotSessionsRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
@@ -44,6 +44,7 @@ export const handler: BunextAPIRouteHandler<
|
||||
workspace_id: conversation.workspace_id,
|
||||
query,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -63,6 +63,10 @@ export type BotConversationRequest = z.infer<typeof BotConversationRequestSchema
|
||||
export const ListBotSessionsRequestSchema = BotConversationRequestSchema.extend({
|
||||
query: z.string().trim().max(200).optional(),
|
||||
page: z.coerce.number().int().positive().optional(),
|
||||
/**
|
||||
* A handful for the rail's sub-list; the modal leaves it out for a page.
|
||||
*/
|
||||
limit: z.coerce.number().int().positive().max(AppData.BotSessionsPageSize).optional(),
|
||||
});
|
||||
|
||||
export type ListBotSessionsRequest = z.infer<typeof ListBotSessionsRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user