Let a user stop a bot, and say the app is still alpha
The thinking row and the header menu both carry Stop while a bot has work open for the user anywhere: every pending or processing job of that bot in their conversations is failed as stopped, its task closed, a Stopped step recorded and one closing message posted per session, so the thread stops waiting and the bot goes idle. The handler running the job is aborted from this process through a registry, so a job stuck waiting on a model stops at once instead of on its next heartbeat, and a late face change after the abort is dropped. The release stage is now said in the app too: Settings > General reports "Cobble v0.1.5 (alpha)" next to development, and the README and RELEASING.md say what alpha means for installs.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import type { StopBot } from "../(hooks)/use-stop-bot";
|
||||
|
||||
export type StopBotState = StopBot & {
|
||||
/**
|
||||
* The bot has something open for the signed-in user right now (a reply
|
||||
* it owes, a task it's on, a wait for a model or a computer), so
|
||||
* stopping it has something to pull the plug on.
|
||||
*/
|
||||
can_stop: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation's stop control, shared by the thinking row under the
|
||||
* thread and the header's menu, so both pull the same plug.
|
||||
*/
|
||||
export const StopBotContext = React.createContext<StopBotState>({
|
||||
stop: async () => {},
|
||||
stopping: false,
|
||||
can_stop: false,
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import type { BotConversationRequest } from "@/src/schemas/bot-schema";
|
||||
import type { BotStoppedPayload } from "@/src/types";
|
||||
|
||||
/**
|
||||
* Pulls the plug on the bot for the signed-in user: every job of it that's
|
||||
* queued or running stops, wherever they chat with it. The messages that
|
||||
* end the wait arrive over the WebSocket, so nothing here reloads.
|
||||
*/
|
||||
export default async function stopBot({ bot_id, workspace_id }: BotConversationRequest) {
|
||||
return await fetchApi<
|
||||
BotConversationRequest,
|
||||
APIResponseObject<BotStoppedPayload>
|
||||
>(`/api/admin/bots/stop`, {
|
||||
method: "POST",
|
||||
body: { bot_id, workspace_id },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useRef, useState } from "react";
|
||||
import stopBot from "../(functions)/stop-bot";
|
||||
|
||||
export type StopBot = {
|
||||
/**
|
||||
* Stops every job of the bot for the signed-in user.
|
||||
*/
|
||||
stop: () => Promise<void>;
|
||||
/**
|
||||
* A stop is on its way to the server.
|
||||
*/
|
||||
stopping: boolean;
|
||||
/**
|
||||
* Why the last stop didn't work, for the button's title.
|
||||
*/
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
workspace_id?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation's stop button: pulls the plug on the bot for the
|
||||
* signed-in user, wherever they chat with it. It's quiet on the way out:
|
||||
* the bot's closing message and idle face arrive as pushes, so nothing
|
||||
* here reloads and a failed stop only surfaces as the button's title.
|
||||
*/
|
||||
export default function useStopBot({ bot_id, workspace_id }: Params): StopBot {
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
/**
|
||||
* A ref, not the state: two clicks in one tick both read the old
|
||||
* `stopping` and would send two stops.
|
||||
*/
|
||||
const in_flight = useRef(false);
|
||||
|
||||
const stop = async () => {
|
||||
if (in_flight.current) return;
|
||||
|
||||
in_flight.current = true;
|
||||
setStopping(true);
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
const res = await stopBot({
|
||||
bot_id,
|
||||
workspace_id: workspace_id || null,
|
||||
}).catch(() => null);
|
||||
|
||||
/**
|
||||
* A message without a failure is the count ("Stopped 2 jobs."),
|
||||
* so only an unsuccessful answer is an error.
|
||||
*/
|
||||
if (!res?.success) setError(res?.msg || "Couldn't stop the bot.");
|
||||
} finally {
|
||||
in_flight.current = false;
|
||||
setStopping(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { stop, stopping, error };
|
||||
}
|
||||
@@ -2,15 +2,11 @@ import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import BotAvatar from "@/src/components/general/bot-avatar";
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
import botStoneProps from "@/src/utils/bot-stone-props";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotGettingMessageStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
import { BotGettingMessageStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import botBusyState from "../(utils)/bot-busy-state";
|
||||
import StopBotButton from "./stop-bot-button";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
@@ -45,7 +41,7 @@ type Props = {
|
||||
* reply shows from the thread alone: every user message queues one, so while
|
||||
* the user has the last word a reply is due here, even while the bot works
|
||||
* elsewhere. Only the waits (for a model, for its computer) come from the
|
||||
* face.
|
||||
* face. While it shows, a stop button pulls the plug on the bot's work.
|
||||
*/
|
||||
export default function BotThinkingRow({
|
||||
bot,
|
||||
@@ -56,17 +52,14 @@ export default function BotThinkingRow({
|
||||
has_task,
|
||||
}: Props) {
|
||||
const name = bot.name || "The bot";
|
||||
const asleep = bot.face == "asleep";
|
||||
const waiting_model = asleep && bot.status_line == BotWaitingStatusLine && Boolean(awaiting_reply || has_task);
|
||||
const waiting_computer =
|
||||
asleep && bot.status_line == BotComputerBusyStatusLine && Boolean(has_task) && !awaiting_reply;
|
||||
const waiting = waiting_model || waiting_computer;
|
||||
const getting = Boolean(receiving) && Boolean(awaiting_reply);
|
||||
const thinking = Boolean(awaiting_reply) && !waiting && !getting;
|
||||
const working =
|
||||
!awaiting_reply && bot.face == "working" && bot.status_line != BotReplyJobTitle && Boolean(has_task);
|
||||
const { waiting, getting, thinking, working, busy, stoppable } = botBusyState({
|
||||
bot,
|
||||
awaiting_reply,
|
||||
receiving,
|
||||
has_task,
|
||||
});
|
||||
|
||||
if (!getting && !thinking && !working && !waiting) return null;
|
||||
if (!busy) return null;
|
||||
|
||||
const label = getting
|
||||
? `${name} is ${BotGettingMessageStatusLine.toLowerCase()}…`
|
||||
@@ -95,6 +88,7 @@ export default function BotThinkingRow({
|
||||
</Span>
|
||||
) : null}
|
||||
</Stack>
|
||||
<StopBotButton bot_name={bot.name} disabled={!stoppable} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import BotRoutinesModal from "./bot-routines-modal";
|
||||
import BotModelsModal from "./bot-models-modal";
|
||||
import HeaderMenuItem from "./header-menu-item";
|
||||
import NewSessionButton from "./new-session-button";
|
||||
import StopBotButton from "./stop-bot-button";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
@@ -48,8 +49,9 @@ type Props = {
|
||||
|
||||
/**
|
||||
* The conversation header's menu: the sessions, a new session, the bot's
|
||||
* computer, its models and its settings. A row that opens a modal closes
|
||||
* the menu first, so the modal never sits behind an open menu.
|
||||
* computer, its models, its settings and, while it works, stopping it. A
|
||||
* row that opens a modal closes the menu first, so the modal never sits
|
||||
* behind an open menu.
|
||||
*/
|
||||
export default function ConversationHeaderMenu({
|
||||
bot,
|
||||
@@ -136,6 +138,8 @@ export default function ConversationHeaderMenu({
|
||||
onSaved={on_model_saved}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider className="my-1 mx-3 w-auto border-hairline dark:border-hairline" />
|
||||
<StopBotButton menu bot_name={bot.name} />
|
||||
{can_open_settings ? (
|
||||
<>
|
||||
<Divider className="my-1 mx-3 w-auto border-hairline dark:border-hairline" />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useContext } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { CircleStop } from "lucide-react";
|
||||
import { BotStopButtonLabel } from "@/src/dict/bot-queue-dict";
|
||||
import { StopBotContext } from "../(data)/stop-bot-context";
|
||||
import HeaderMenuItem from "./header-menu-item";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Whose work stops, named in the tooltip so it's clear this pulls the
|
||||
* bot's plug everywhere the user chats with it.
|
||||
*/
|
||||
bot_name?: string | null;
|
||||
/**
|
||||
* Shown as a row in the conversation header's menu instead of a button
|
||||
* in the thinking row.
|
||||
*/
|
||||
menu?: boolean;
|
||||
/**
|
||||
* The caller already knows there's nothing to pull (a row that says the
|
||||
* bot is only collecting the message), so the button sits out.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pulls the plug on the bot for the signed-in user: its queued and running
|
||||
* work stops wherever they chat with it, so a bot stuck waiting for a model
|
||||
* (or anything else) comes back. The thinking row carries it while
|
||||
* something is running; the header's menu carries it too, disabled when
|
||||
* there's nothing to stop.
|
||||
*/
|
||||
export default function StopBotButton({ bot_name, menu, disabled }: Props) {
|
||||
const { stop, stopping, error, can_stop } = useContext(StopBotContext);
|
||||
const name = bot_name || "the bot";
|
||||
const off = Boolean(disabled) || !can_stop || stopping;
|
||||
const title = error
|
||||
? error
|
||||
: off && !stopping
|
||||
? `Nothing of ${name}'s is running for you.`
|
||||
: `Stop ${name}. Its queued and running work stops, everywhere you chat with it.`;
|
||||
|
||||
if (menu) {
|
||||
return (
|
||||
<HeaderMenuItem
|
||||
icon={CircleStop}
|
||||
title={BotStopButtonLabel}
|
||||
description={title}
|
||||
hint={stopping ? "Stopping" : undefined}
|
||||
loading={stopping}
|
||||
disabled={off}
|
||||
onClick={() => {
|
||||
void stop();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
title={title}
|
||||
aria-label={title}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
size="smaller"
|
||||
disabled={off}
|
||||
loading={stopping}
|
||||
loadingIconSize="small"
|
||||
beforeIcon={<CircleStop size={14} aria-hidden />}
|
||||
onClick={() => {
|
||||
void stop();
|
||||
}}
|
||||
className="shrink-0 text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken rounded-default transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink"
|
||||
>
|
||||
{BotStopButtonLabel}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -15,9 +15,15 @@ import { CrewBotsContext } from "../(data)/crew-bots-context";
|
||||
import { CrewWorkspacesContext } from "../(data)/crew-workspaces-context";
|
||||
import { BotMessageActionsContext } from "../(data)/message-actions-context";
|
||||
import { SpeakMessageContext } from "../(data)/speak-message-context";
|
||||
import {
|
||||
StopBotContext,
|
||||
type StopBotState,
|
||||
} from "../(data)/stop-bot-context";
|
||||
import reactToBotMessage from "../(functions)/react-to-bot-message";
|
||||
import botBusyState from "../(utils)/bot-busy-state";
|
||||
import dropSentMessage from "../(utils)/drop-sent-message";
|
||||
import useBotComputers from "../(hooks)/use-bot-computers";
|
||||
import useStopBot from "../(hooks)/use-stop-bot";
|
||||
import useTaskProgress from "../(hooks)/use-task-progress";
|
||||
import useReplyProgress from "../(hooks)/use-reply-progress";
|
||||
import useReplyCatchUp from "../(hooks)/use-reply-catch-up";
|
||||
@@ -90,6 +96,10 @@ export default function Conversation() {
|
||||
workspace_id,
|
||||
computers: pageProps?.bot_computers || [],
|
||||
});
|
||||
const stop_bot = useStopBot({
|
||||
bot_id: bot?.id || 0,
|
||||
workspace_id,
|
||||
});
|
||||
|
||||
const speak = useSpeakMessage({
|
||||
enabled: Boolean(pageProps?.bot_voice?.speech),
|
||||
@@ -233,7 +243,26 @@ export default function Conversation() {
|
||||
|
||||
if (!bot?.id || !live_bot) return null;
|
||||
|
||||
return (
|
||||
/**
|
||||
* What the stop control pulls the plug on, read the way the thinking
|
||||
* row reads it: the same conditions, so the header's row is enabled
|
||||
* exactly while the thread shows the bot at work.
|
||||
*/
|
||||
const stop_state: StopBotState = {
|
||||
...stop_bot,
|
||||
can_stop: botBusyState({
|
||||
bot: live_bot,
|
||||
awaiting_reply,
|
||||
receiving: pending_messages.length > 0,
|
||||
has_task: Boolean(progress.task_id),
|
||||
}).stoppable,
|
||||
};
|
||||
|
||||
/**
|
||||
* The thread, kept intact so the stop control can wrap it without
|
||||
* reindenting every provider.
|
||||
*/
|
||||
const thread = (
|
||||
<CrewBotsContext.Provider value={bots}>
|
||||
<CrewWorkspacesContext.Provider value={workspaces}>
|
||||
<BotComputersContext.Provider value={bot_computers}>
|
||||
@@ -316,4 +345,10 @@ export default function Conversation() {
|
||||
</CrewWorkspacesContext.Provider>
|
||||
</CrewBotsContext.Provider>
|
||||
);
|
||||
|
||||
return (
|
||||
<StopBotContext.Provider value={stop_state}>
|
||||
{thread}
|
||||
</StopBotContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotGettingMessageStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import botBusyState from "./bot-busy-state";
|
||||
|
||||
function bot(overrides: Partial<BotWithLastMessage>): BotWithLastMessage {
|
||||
return { id: 1, name: "Ember", ...overrides } as BotWithLastMessage;
|
||||
}
|
||||
|
||||
describe("botBusyState", () => {
|
||||
test("a reply owed here is thinking", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "idle" }), awaiting_reply: true });
|
||||
|
||||
expect(state.thinking).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("a message still on its way is getting, not thinking", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working" }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.thinking).toBe(false);
|
||||
});
|
||||
|
||||
test("a task running while the bot has the last word is working", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: "Working" }),
|
||||
awaiting_reply: false,
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.working).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("a reply's own turn never shows as working on a task", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: BotReplyJobTitle }),
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.working).toBe(false);
|
||||
expect(state.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("waiting for a model is a wait, with a reply owed here", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
awaiting_reply: true,
|
||||
});
|
||||
|
||||
expect(state.waiting).toBe(true);
|
||||
expect(state.thinking).toBe(false);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("waiting for a model is a wait, with a task running", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.waiting).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("waiting for its computer is a wait only while a task runs", () => {
|
||||
const waiting = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotComputerBusyStatusLine }),
|
||||
has_task: true,
|
||||
});
|
||||
const idle = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotComputerBusyStatusLine }),
|
||||
});
|
||||
|
||||
expect(waiting.waiting).toBe(true);
|
||||
expect(idle.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("a message still on its way wins over the wait, so the row says what the user just did", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.waiting).toBe(true);
|
||||
});
|
||||
|
||||
test("an idle bot with nothing owed and no task shows nothing", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "idle" }) });
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("a done face with no task shows nothing", () => {
|
||||
expect(botBusyState({ bot: bot({ face: "done" }) }).busy).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("botBusyState stoppable", () => {
|
||||
test("busy in this thread is stoppable", () => {
|
||||
expect(
|
||||
botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: true,
|
||||
}).stoppable,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("working somewhere else is stoppable too, since the face is per user", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "working" }) });
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
expect(state.stoppable).toBe(true);
|
||||
});
|
||||
|
||||
test("asleep on a wait line is stoppable, even when this thread shows nothing", () => {
|
||||
for (const status_line of [BotWaitingStatusLine, BotComputerBusyStatusLine]) {
|
||||
expect(botBusyState({ bot: bot({ face: "asleep", status_line }) }).stoppable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("a bot simply resting is not stoppable", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: "Until tomorrow" }),
|
||||
});
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("a message only being collected is not stoppable: its reply is queued after it", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("done, needs-you and idle mean nothing is running", () => {
|
||||
for (const face of ["done", "needs-you", "idle"] as const) {
|
||||
const state = botBusyState({ bot: bot({ face }) });
|
||||
|
||||
expect(state.stoppable).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("a silently stopped bot is not stoppable", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: false,
|
||||
has_task: false,
|
||||
});
|
||||
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("a reply job's own turn with no task owed is not stoppable", () => {
|
||||
/**
|
||||
* The face says working while a reply runs somewhere else, which is
|
||||
* real work: it's stoppable even though this thread owes nothing.
|
||||
*/
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: BotReplyJobTitle }),
|
||||
});
|
||||
|
||||
expect(state.stoppable).toBe(true);
|
||||
});
|
||||
|
||||
test("the getting line still labels the row", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle", status_line: BotGettingMessageStatusLine }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
|
||||
type Params = {
|
||||
bot: BotWithLastMessage;
|
||||
/**
|
||||
* The last message in the open thread is the user's, so a reply is due
|
||||
* here.
|
||||
*/
|
||||
awaiting_reply?: boolean;
|
||||
/**
|
||||
* A message is still on its way to the server.
|
||||
*/
|
||||
receiving?: boolean;
|
||||
/**
|
||||
* A task of the bot's is running.
|
||||
*/
|
||||
has_task?: boolean;
|
||||
};
|
||||
|
||||
export type BotBusyState = {
|
||||
/**
|
||||
* Waiting for a model to come back, or for the bot's computer.
|
||||
*/
|
||||
waiting: boolean;
|
||||
/**
|
||||
* The user's message hasn't reached the server yet.
|
||||
*/
|
||||
getting: boolean;
|
||||
/**
|
||||
* A reply is due in this thread.
|
||||
*/
|
||||
thinking: boolean;
|
||||
/**
|
||||
* A task is running and the bot has the last word.
|
||||
*/
|
||||
working: boolean;
|
||||
/**
|
||||
* Anything of the above, so the row has something to say.
|
||||
*/
|
||||
busy: boolean;
|
||||
/**
|
||||
* There's work of the bot's to pull the plug on. A message still on
|
||||
* its way doesn't count: its reply is queued after it lands, so a stop
|
||||
* now would cancel nothing and be overtaken a moment later. The face
|
||||
* covers work in the user's other conversations too, since it's one
|
||||
* per user; `asleep` only counts on the two status lines the queue
|
||||
* sets while it waits, never a bot that's simply resting.
|
||||
*/
|
||||
stoppable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a bot is doing in a conversation, read from the thread and its face.
|
||||
* The thread decides the reply states: every user message queues one, so
|
||||
* while the user has the last word a reply is due here, even while the bot
|
||||
* works elsewhere. Only the waits (for a model, for its computer) come from
|
||||
* the face. The thinking row reads it for its line, and the conversation
|
||||
* reads it for the stop control.
|
||||
*/
|
||||
export default function botBusyState({
|
||||
bot,
|
||||
awaiting_reply,
|
||||
receiving,
|
||||
has_task,
|
||||
}: Params): BotBusyState {
|
||||
const asleep = bot.face == "asleep";
|
||||
const waiting_model =
|
||||
asleep && bot.status_line == BotWaitingStatusLine && Boolean(awaiting_reply || has_task);
|
||||
const waiting_computer =
|
||||
asleep &&
|
||||
bot.status_line == BotComputerBusyStatusLine &&
|
||||
Boolean(has_task) &&
|
||||
!awaiting_reply;
|
||||
const waiting = waiting_model || waiting_computer;
|
||||
const getting = Boolean(receiving) && Boolean(awaiting_reply);
|
||||
const thinking = Boolean(awaiting_reply) && !waiting && !getting;
|
||||
const working =
|
||||
!awaiting_reply &&
|
||||
bot.face == "working" &&
|
||||
bot.status_line != BotReplyJobTitle &&
|
||||
Boolean(has_task);
|
||||
|
||||
/**
|
||||
* Work runs here (a reply owed, a task of this thread's) or somewhere
|
||||
* else of this user's (the face says `working`, or `asleep` on a wait
|
||||
* line).
|
||||
*/
|
||||
const stoppable =
|
||||
waiting ||
|
||||
thinking ||
|
||||
working ||
|
||||
bot.face == "working" ||
|
||||
(asleep &&
|
||||
(bot.status_line == BotWaitingStatusLine ||
|
||||
bot.status_line == BotComputerBusyStatusLine));
|
||||
|
||||
return {
|
||||
waiting,
|
||||
getting,
|
||||
thinking,
|
||||
working,
|
||||
busy: getting || thinking || working || waiting,
|
||||
stoppable,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user