Open computers from the chat, let bots ask for a take-over, show task progress
- Serve each computer to its owner under /computers/<id>/ (HTTP and websockets proxied to the AIO sandbox) and show it in a shared ComputerView with browser, terminal and code modes, opened from the computers modal or a take-over button in the thread. - ask_user_to_take_over lets a bot hand a computer to the user for a login or captcha; the message gets a take-over button and needs-you, and the viewer hands it back with a chat message. - While a task runs the thread shows "<bot> is working..." with the task's latest step; steps read as plain words, not tool names. - A coordinator with no workers no longer claims to delegate, and bots describe started tasks in the first person. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -2,10 +2,12 @@ import React from "react";
|
||||
import type { BotComputers } from "../(hooks)/use-bot-computers";
|
||||
|
||||
/**
|
||||
* The open bot's computers and the switch, shared by the header's
|
||||
* computer select and the computer buttons in its messages.
|
||||
* The open bot's computers, the switch and the viewer, shared by the
|
||||
* header's computers modal and the computer buttons in its messages.
|
||||
*/
|
||||
export const BotComputersContext = React.createContext<BotComputers>({
|
||||
computers: [],
|
||||
switchComputer: async () => {},
|
||||
openComputer: () => {},
|
||||
closeComputer: () => {},
|
||||
});
|
||||
|
||||
@@ -2,8 +2,19 @@ import { useState } from "react";
|
||||
import useServerEvent from "@/src/hooks/use-server-event";
|
||||
import type { BotUpdatedData } from "@/src/types";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import type { ComputerViewMode } from "@/src/dict/computer-dict";
|
||||
import setBotComputer from "../(functions)/set-bot-computer";
|
||||
|
||||
/**
|
||||
* The computer open in the viewer, in which mode, and whether the bot
|
||||
* asked the user to take it over (the viewer then offers to hand it back).
|
||||
*/
|
||||
export type ComputerViewing = {
|
||||
computer_id: number;
|
||||
mode: ComputerViewMode;
|
||||
take_over?: boolean;
|
||||
};
|
||||
|
||||
export type BotComputers = {
|
||||
/**
|
||||
* The bot's computers, the one it's on first.
|
||||
@@ -15,6 +26,9 @@ export type BotComputers = {
|
||||
switching_id?: number;
|
||||
error?: string;
|
||||
switchComputer: (params: { computer_id: number }) => Promise<void>;
|
||||
viewing?: ComputerViewing;
|
||||
openComputer: (params: ComputerViewing) => void;
|
||||
closeComputer: () => void;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
@@ -25,12 +39,14 @@ type Params = {
|
||||
/**
|
||||
* Keeps the open bot's computers and switches the one it's on, in place:
|
||||
* no reload, the list comes back reordered. A switch made in another tab
|
||||
* arrives as `server:bot-updated` with the bot's `computer_id`.
|
||||
* arrives as `server:bot-updated` with the bot's `computer_id`. Also holds
|
||||
* the computer open in the viewer (`ComputerViewerModal`).
|
||||
*/
|
||||
export default function useBotComputers({ bot_id, computers: initial }: Params): BotComputers {
|
||||
const [computers, setComputers] = useState(initial);
|
||||
const [switching_id, setSwitchingId] = useState<number>();
|
||||
const [error, setError] = useState<string>();
|
||||
const [viewing, setViewing] = useState<ComputerViewing>();
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-updated",
|
||||
@@ -66,5 +82,13 @@ export default function useBotComputers({ bot_id, computers: initial }: Params):
|
||||
setSwitchingId(undefined);
|
||||
};
|
||||
|
||||
return { computers, switching_id, error, switchComputer };
|
||||
return {
|
||||
computers,
|
||||
switching_id,
|
||||
error,
|
||||
switchComputer,
|
||||
viewing,
|
||||
openComputer: (params) => setViewing(params),
|
||||
closeComputer: () => setViewing(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import useServerEvent from "@/src/hooks/use-server-event";
|
||||
import type { BotJobUpdateData, BotTaskWithSteps, BotUpdatedData } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
/**
|
||||
* The bot's active task when the page loaded.
|
||||
*/
|
||||
task: BotTaskWithSteps | null;
|
||||
};
|
||||
|
||||
type Progress = {
|
||||
task_id: number | null;
|
||||
text: string | null;
|
||||
};
|
||||
|
||||
function fromTask({ task }: { task: BotTaskWithSteps | null }): Progress {
|
||||
return {
|
||||
task_id: task?.id || null,
|
||||
text: task?.updates[task.updates.length - 1]?.text || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The latest step of the open bot's active task (a `bots_queue_updates`
|
||||
* row's text), kept current from `server:bot-job-update`. Starts over when
|
||||
* `server:bot-updated` brings a different task, or none.
|
||||
*/
|
||||
export default function useTaskProgress({ bot_id, task }: Params) {
|
||||
const [progress, setProgress] = useState<Progress>(() => fromTask({ task }));
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-job-update",
|
||||
onData: (update: BotJobUpdateData) => {
|
||||
if (update.bot_id != bot_id || !update.task_id || !update.text) return;
|
||||
|
||||
setProgress({ task_id: update.task_id, text: update.text });
|
||||
},
|
||||
});
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-updated",
|
||||
onData: (data: BotUpdatedData) => {
|
||||
if (data.id != bot_id) return;
|
||||
|
||||
setProgress((prev) =>
|
||||
(data.task?.id || null) == prev.task_id ? prev : fromTask({ task: data.task }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return progress.text;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { Check } from "lucide-react";
|
||||
import { AppWindow, Check } from "lucide-react";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
|
||||
type Props = {
|
||||
@@ -11,14 +12,21 @@ type Props = {
|
||||
*/
|
||||
current: boolean;
|
||||
onPick: () => void;
|
||||
/**
|
||||
* Opens the computer in the viewer.
|
||||
*/
|
||||
onOpen: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One computer in the header's computers modal, checked when it's the
|
||||
* one the bot is on.
|
||||
* one the bot is on, with a button that opens it.
|
||||
*/
|
||||
export default function BotComputerOption({ computer, current, onPick }: Props) {
|
||||
export default function BotComputerOption({ computer, current, onPick, onOpen }: Props) {
|
||||
const name = computer.name || `Computer ${computer.id}`;
|
||||
|
||||
return (
|
||||
<Row className="w-full gap-1! flex-nowrap items-center pr-2 hover:bg-sunken transition-colors duration-[var(--m-fast)]">
|
||||
<Row
|
||||
role="radio"
|
||||
aria-checked={current}
|
||||
@@ -30,14 +38,14 @@ export default function BotComputerOption({ computer, current, onPick }: Props)
|
||||
onPick();
|
||||
}
|
||||
}}
|
||||
className={`w-full min-h-9 gap-2.5! flex-nowrap items-center px-4 py-1.5 text-ink transition-colors duration-[var(--m-fast)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ink ${
|
||||
current ? "cursor-default" : "cursor-pointer hover:bg-sunken"
|
||||
className={`grow min-w-0 min-h-9 gap-2.5! flex-nowrap items-center px-4 py-1.5 text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ink ${
|
||||
current ? "cursor-default" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
<Check size={16} aria-hidden className={`shrink-0 ${current ? "" : "invisible"}`} />
|
||||
<Stack className="min-w-0 gap-0!">
|
||||
<Span className="text-sm leading-5 text-ink truncate w-full">
|
||||
{computer.name || `Computer ${computer.id}`}
|
||||
{name}
|
||||
</Span>
|
||||
{computer.description ? (
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary truncate w-full">
|
||||
@@ -46,5 +54,17 @@ export default function BotComputerOption({ computer, current, onPick }: Props)
|
||||
) : null}
|
||||
</Stack>
|
||||
</Row>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
title={`Open ${name}`}
|
||||
aria-label={`Open ${name}`}
|
||||
onClick={onOpen}
|
||||
className="shrink-0 p-2! text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink rounded-default focus-visible:ring-2 focus-visible:ring-ink"
|
||||
>
|
||||
<AppWindow size={16} aria-hidden />
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ type Props = {
|
||||
/**
|
||||
* The header's computer icon, beside the model icon: opens what the bot's
|
||||
* computers are, the one it's on checked, and with more than one,
|
||||
* switches it.
|
||||
* switches it. Each can be opened in the viewer.
|
||||
*/
|
||||
export default function BotComputersModal({ bot }: Props) {
|
||||
const { computers, switching_id, error, switchComputer } = useContext(BotComputersContext);
|
||||
const { computers, switching_id, error, switchComputer, openComputer } =
|
||||
useContext(BotComputersContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const current = computers[0];
|
||||
@@ -48,7 +49,7 @@ export default function BotComputersModal({ bot }: Props) {
|
||||
{can_switch ? `${name}'s computers` : `${name}'s computer`}
|
||||
</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`A computer is a sandbox ${name} works in: a shell, a browser and files of its own. Its tasks run on the checked one.`}
|
||||
{`A computer is a sandbox ${name} works in: a shell, a browser and files of its own. Its tasks run on the checked one. Open one to use it yourself.`}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{can_switch
|
||||
@@ -69,6 +70,10 @@ export default function BotComputersModal({ bot }: Props) {
|
||||
onPick={async () => {
|
||||
await switchComputer({ computer_id: computer.id || 0 });
|
||||
}}
|
||||
onOpen={() => {
|
||||
setOpen(false);
|
||||
openComputer({ computer_id: computer.id || 0, mode: "browser" });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import BotStone from "@/src/components/general/bot-stone";
|
||||
import type { BotHue } from "@/src/dict/bot-dict";
|
||||
import { BotReplyJobTitle, BotWaitingStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
@@ -7,28 +8,49 @@ import type { BotWithLastMessage } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
/**
|
||||
* The latest step of the bot's active task.
|
||||
*/
|
||||
progress?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Under the last message while the bot is working on a reply (or waiting
|
||||
* for a model to come back), so a slow reply doesn't look like none.
|
||||
* Under the last message while the bot is working on a reply or a task
|
||||
* (or waiting for a model to come back), so slow work doesn't look like
|
||||
* none. A task shows its latest step, else its title.
|
||||
*/
|
||||
export default function BotThinkingRow({ bot }: Props) {
|
||||
export default function BotThinkingRow({ bot, progress }: Props) {
|
||||
const name = bot.name || "The bot";
|
||||
const thinking = bot.face == "working" && bot.status_line == BotReplyJobTitle;
|
||||
const working = bot.face == "working" && !thinking;
|
||||
const waiting = bot.face == "asleep" && bot.status_line == BotWaitingStatusLine;
|
||||
|
||||
if (!thinking && !waiting) return null;
|
||||
if (!thinking && !working && !waiting) return null;
|
||||
|
||||
const label = thinking
|
||||
? `${name} is thinking…`
|
||||
: working
|
||||
? `${name} is working…`
|
||||
: `${BotWaitingStatusLine}…`;
|
||||
const detail = working ? progress || bot.status_line : null;
|
||||
|
||||
return (
|
||||
<Row role="status" className="w-full gap-3! flex-nowrap items-center">
|
||||
<BotStone
|
||||
hue={(bot.hue || null) as BotHue | null}
|
||||
face={thinking ? "working" : "asleep"}
|
||||
face={waiting ? "asleep" : "working"}
|
||||
size={28}
|
||||
/>
|
||||
<Span className="text-sm leading-5 text-ink-secondary animate-pulse motion-reduce:animate-none">
|
||||
{thinking ? `${bot.name || "The bot"} is thinking…` : `${BotWaitingStatusLine}…`}
|
||||
</Span>
|
||||
<Stack className="min-w-0 gap-0! items-start">
|
||||
<Span className="text-sm leading-5 text-ink-secondary animate-pulse motion-reduce:animate-none">
|
||||
{label}
|
||||
</Span>
|
||||
{detail ? (
|
||||
<Span className="w-full text-xs leading-4 text-ink-secondary truncate">
|
||||
{detail}
|
||||
</Span>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useContext, useState } from "react";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
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 ComputerView from "@/src/components/general/computer-view";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import sendBotMessage from "../(functions)/send-bot-message";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* The computer the user opened (from the computers modal, or a take-over
|
||||
* the bot asked for), large, in its browser, terminal or code editor.
|
||||
* After a take-over the user hands it back from here, which tells the
|
||||
* bot in chat so it carries on.
|
||||
*/
|
||||
export default function ComputerViewerModal({ bot }: Props) {
|
||||
const { computers, viewing, closeComputer } = useContext(BotComputersContext);
|
||||
const [handing_back, setHandingBack] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const computer = computers.find((c) => c.id == viewing?.computer_id);
|
||||
|
||||
if (!viewing || !computer?.id) return null;
|
||||
|
||||
const name = computer.name || `Computer ${computer.id}`;
|
||||
const bot_name = bot.name || "the bot";
|
||||
|
||||
const handBack = async () => {
|
||||
setHandingBack(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await sendBotMessage({
|
||||
bot_id: bot.id || 0,
|
||||
text: `I'm done on ${name}. You can carry on.`,
|
||||
media_ids: [],
|
||||
}).catch(() => null);
|
||||
|
||||
setHandingBack(false);
|
||||
|
||||
if (res?.success) closeComputer();
|
||||
else setError(res?.msg || "Couldn't tell the bot");
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
setOpen={(open) => {
|
||||
if (!open) closeComputer();
|
||||
}}
|
||||
className="max-w-[1280px] h-[90vh] p-0 bg-paper flex flex-col overflow-hidden"
|
||||
>
|
||||
<Row className="w-full gap-3! flex-nowrap items-center pl-5 pr-14 py-3 border-0 border-b border-solid border-hairline">
|
||||
<Stack className="min-w-0 gap-0! items-start">
|
||||
<H2 className="text-base! leading-6! tracking-normal! font-bold text-ink truncate w-full">
|
||||
{name}
|
||||
</H2>
|
||||
{viewing.take_over ? (
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`${bot_name} asked you to take over. Hand it back when you're done.`}
|
||||
</Span>
|
||||
) : null}
|
||||
</Stack>
|
||||
{viewing.take_over ? (
|
||||
<Stack className="ml-auto shrink-0 gap-1! items-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
loading={handing_back}
|
||||
onClick={handBack}
|
||||
title={`Tell ${bot_name} you're done`}
|
||||
>
|
||||
{`Hand back to ${bot_name}`}
|
||||
</Button>
|
||||
{error ? (
|
||||
<Span role="alert" className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{error}
|
||||
</Span>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : null}
|
||||
</Row>
|
||||
<ComputerView
|
||||
key={`${computer.id}-${viewing.mode}`}
|
||||
computer_id={computer.id}
|
||||
name={name}
|
||||
default_mode={viewing.mode}
|
||||
className="grow"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import { ComputerRefHrefPrefix } from "@/src/utils/link-computer-refs";
|
||||
import { ComputerViewModes, type ComputerViewMode } from "@/src/dict/computer-dict";
|
||||
import { ComputerRefHrefPrefix, TakeOverRefHrefPrefix } from "@/src/utils/link-computer-refs";
|
||||
import ComputerRefChip from "./computer-ref-chip";
|
||||
import TakeOverChip from "./take-over-chip";
|
||||
|
||||
/**
|
||||
* A link in a bot message. A computer the bot named (`linkComputerRefs`)
|
||||
* becomes its switch button; anything else opens in a new tab.
|
||||
* becomes its switch button, a take-over request a button that opens the
|
||||
* computer; anything else opens in a new tab.
|
||||
*/
|
||||
export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
if (href?.startsWith(ComputerRefHrefPrefix)) {
|
||||
@@ -14,6 +17,21 @@ export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
if (computer_id) return <ComputerRefChip computer_id={computer_id} />;
|
||||
}
|
||||
|
||||
if (href?.startsWith(TakeOverRefHrefPrefix)) {
|
||||
const [id, mode] = href.slice(TakeOverRefHrefPrefix.length).split("-");
|
||||
const computer_id = Number(id);
|
||||
const known = ComputerViewModes.some((m) => m.value == mode);
|
||||
|
||||
if (computer_id) {
|
||||
return (
|
||||
<TakeOverChip
|
||||
computer_id={computer_id}
|
||||
mode={(known ? mode : ComputerViewModes[0].value) as ComputerViewMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
|
||||
@@ -7,14 +7,19 @@ import MessageGroupBlock from "./message-group-block";
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
messages: BotMessageWithAttachments[];
|
||||
/**
|
||||
* The latest step of the bot's active task, shown while it works.
|
||||
*/
|
||||
progress?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bottom-anchored: the stack sits 24px above the composer rule and grows
|
||||
* upward. Empty space belongs at the top. New messages announce politely.
|
||||
* While the bot works on a reply, a thinking row sits under the last one.
|
||||
* While the bot works on a reply or a task, a status row sits under the
|
||||
* last one.
|
||||
*/
|
||||
export default function MessageThread({ bot, messages }: Props) {
|
||||
export default function MessageThread({ bot, messages, progress }: Props) {
|
||||
const groups = groupMessages({ messages });
|
||||
|
||||
return (
|
||||
@@ -28,7 +33,7 @@ export default function MessageThread({ bot, messages }: Props) {
|
||||
{groups.map((group) => (
|
||||
<MessageGroupBlock key={group.key} bot={bot} group={group} />
|
||||
))}
|
||||
<BotThinkingRow bot={bot} />
|
||||
<BotThinkingRow bot={bot} progress={progress} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useContext } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { Hand } from "lucide-react";
|
||||
import { ComputerViewModes, type ComputerViewMode } from "@/src/dict/computer-dict";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
|
||||
type Props = {
|
||||
computer_id: number;
|
||||
mode: ComputerViewMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bot asked the user to take over a computer (`takeOverRef`): a
|
||||
* button that opens it in the viewer, in the mode the bot asked for. A
|
||||
* computer the bot can't use (any more) is shown by name only.
|
||||
*/
|
||||
export default function TakeOverChip({ computer_id, mode }: Props) {
|
||||
const { computers, openComputer } = useContext(BotComputersContext);
|
||||
|
||||
const computer = computers.find((c) => c.id == computer_id);
|
||||
const name = computer?.name || `computer ${computer_id}`;
|
||||
const mode_title = ComputerViewModes.find((m) => m.value == mode)?.title || mode;
|
||||
|
||||
if (!computer) {
|
||||
return <Span className="font-bold">{name}</Span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="smaller"
|
||||
title={`Open ${name} (${mode_title}) and take over`}
|
||||
onClick={() => openComputer({ computer_id, mode, take_over: true })}
|
||||
className="inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold text-ink whitespace-nowrap"
|
||||
>
|
||||
<Hand size={14} aria-hidden className="shrink-0" />
|
||||
{`Take over ${name} · ${mode_title}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -7,15 +7,18 @@ import useLiveBots from "@/src/hooks/use-live-bots";
|
||||
import useServerEvent from "@/src/hooks/use-server-event";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import useBotComputers from "../(hooks)/use-bot-computers";
|
||||
import useTaskProgress from "../(hooks)/use-task-progress";
|
||||
import ConversationHeader from "../(partials)/conversation-header";
|
||||
import MessageThread from "../(partials)/message-thread";
|
||||
import Composer from "../(partials)/composer";
|
||||
import ComputerViewerModal from "../(partials)/computer-viewer-modal";
|
||||
|
||||
/**
|
||||
* The conversation column: header, bottom-anchored thread, composer, the
|
||||
* thread and composer held to a readable width.
|
||||
* The bot's computers are shared with the header's modal and the computer
|
||||
* buttons in the thread, so a switch from either shows in both.
|
||||
* buttons in the thread, so a switch from either shows in both; either
|
||||
* can open one in the viewer.
|
||||
*/
|
||||
export default function Conversation() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
@@ -25,20 +28,32 @@ export default function Conversation() {
|
||||
pageProps?.bot_messages || [],
|
||||
);
|
||||
|
||||
const bots = useLiveBots({ bots: pageProps?.bots || [], open_bot_id: bot?.id || undefined });
|
||||
const bots = useLiveBots({
|
||||
bots: pageProps?.bots || [],
|
||||
open_bot_id: bot?.id || undefined,
|
||||
});
|
||||
const live_bot = bots.find((b) => b.id == bot?.id) || bot;
|
||||
const bot_computers = useBotComputers({
|
||||
bot_id: bot?.id || 0,
|
||||
computers: pageProps?.bot_computers || [],
|
||||
});
|
||||
|
||||
const progress = useTaskProgress({
|
||||
bot_id: bot?.id || 0,
|
||||
task: pageProps?.bot_task || null,
|
||||
});
|
||||
|
||||
console.log(`Refreshing ...`);
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-message",
|
||||
onData: (message: BotMessageWithAttachments) => {
|
||||
if (message.bot_id != bot?.id) return;
|
||||
|
||||
setMessages((prev) =>
|
||||
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
|
||||
prev.some((m) => m.id == message.id)
|
||||
? prev
|
||||
: [...prev, message],
|
||||
);
|
||||
|
||||
if (message.author == "bot") {
|
||||
@@ -59,16 +74,23 @@ export default function Conversation() {
|
||||
bot={live_bot}
|
||||
harnesses={pageProps?.bot_harness_overview?.harnesses}
|
||||
/>
|
||||
<MessageThread bot={live_bot} messages={messages} />
|
||||
<MessageThread
|
||||
bot={live_bot}
|
||||
messages={messages}
|
||||
progress={progress}
|
||||
/>
|
||||
<Composer
|
||||
bot={bot}
|
||||
onSent={({ message }) =>
|
||||
setMessages((prev) =>
|
||||
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
|
||||
prev.some((m) => m.id == message.id)
|
||||
? prev
|
||||
: [...prev, message],
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<ComputerViewerModal bot={live_bot} />
|
||||
</BotComputersContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabBots from "@/src/functions/backend/db/bots/grab-bots";
|
||||
import grabActiveBotTask from "@/src/functions/backend/db/bots/grab-active-bot-task";
|
||||
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
|
||||
import markBotRead from "@/src/functions/backend/db/bots/mark-bot-read";
|
||||
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
||||
@@ -40,13 +41,14 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
* The header's models modal: a worker's own settings, or for the
|
||||
* coordinator the defaults it runs on (without the providers).
|
||||
*/
|
||||
const [bot_messages, bot_harness_overview, bot_computers] = await Promise.all([
|
||||
const [bot_messages, bot_harness_overview, bot_computers, bot_task] = await Promise.all([
|
||||
grabBotMessages({ bot_id: bot.id }),
|
||||
(bot.kind == "coordinator"
|
||||
? grabHarnessOverview().then(({ harnesses }) => ({ harnesses }))
|
||||
: grabBotHarnessOverview({ bot_id: bot.id })
|
||||
).catch(() => null),
|
||||
grabBotComputers({ bot_id: bot.id, user_id: user.id }),
|
||||
grabActiveBotTask({ bot_id: bot.id }),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -56,6 +58,7 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
bot_messages,
|
||||
bot_harness_overview,
|
||||
bot_computers,
|
||||
bot_task,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user