Let crew managers edit a bot's workspace instructions where they work with it
A bot's instructions and memory for one workspace were only reachable
from the workspace's settings. The editor is now shared and also opens
from the conversation menu in that workspace ("Instructions here",
loaded on open through api/admin/workspaces/workspace-bot) and from the
workspace rows on the bot's settings page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -123,7 +123,7 @@ Server push only. `pushBotNotice` (`server:bot-message`, `server:bot-updated`),
|
||||
|
||||
### Workspaces
|
||||
|
||||
`workspaces` (`description`, shared `system_prompt`, default `computer_id` (not an FK), `archived`), `workspace_bots` (per-bot instructions and memory there; coordinator is an implicit member), `workspace_computers`. First computer linked becomes the default. Workspace models win over a bot's own, then the defaults. Helpers in `src/functions/backend/db/workspaces/` are access-checked; structural ones are crew-manager only.
|
||||
`workspaces` (`description`, shared `system_prompt`, default `computer_id` (not an FK), `archived`), `workspace_bots` (per-bot instructions and memory there; coordinator is an implicit member), `workspace_computers`. First computer linked becomes the default. Workspace models win over a bot's own, then the defaults. A bot's instructions and memory there (`WorkspaceBotModal`/`WorkspaceBotForm`, crew managers) open from the workspace's settings, the bot's settings and the conversation menu's "Instructions here" (loaded on open). Helpers in `src/functions/backend/db/workspaces/` are access-checked; structural ones are crew-manager only.
|
||||
|
||||
### Computers
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from "react";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import CodeEditor from "@/src/components/general/code-editor";
|
||||
import type { WorkspaceBot } from "@/src/types";
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
|
||||
|
||||
type Props = {
|
||||
workspace_id: number;
|
||||
bot_id: number;
|
||||
bot_name?: string | null;
|
||||
/**
|
||||
* What the bot has for this workspace now.
|
||||
*/
|
||||
system_prompt?: string | null;
|
||||
memory?: string | null;
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* After the save, with the workspace's crew as it is now.
|
||||
*/
|
||||
onSaved: (bots: WorkspaceBot[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The form for what one bot has for a workspace alone: its instructions
|
||||
* there, on top of its own, and its memory there
|
||||
* (`api/admin/workspaces/update-bot`, crew managers). It asks first.
|
||||
*/
|
||||
export default function WorkspaceBotForm({
|
||||
workspace_id,
|
||||
bot_id,
|
||||
bot_name,
|
||||
system_prompt: initial_prompt,
|
||||
memory: initial_memory,
|
||||
onCancel,
|
||||
onSaved,
|
||||
}: Props) {
|
||||
const [system_prompt, setSystemPrompt] = useState(initial_prompt || "");
|
||||
const [memory, setMemory] = useState(initial_memory || "");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const submit = async () => {
|
||||
if (loading) return;
|
||||
if (!window.confirm(`Save ${bot_name || "this bot"}'s instructions and memory here?`)) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await postAdminApi<WorkspaceBot>({
|
||||
path: "workspaces/update-bot",
|
||||
body: { workspace_id, bot_id, system_prompt, memory },
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res.success) {
|
||||
setError(res.msg || "Couldn't save.");
|
||||
return;
|
||||
}
|
||||
|
||||
onSaved(res.payload || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={submit}>
|
||||
<CodeEditor
|
||||
mode="markdown"
|
||||
className="h-44"
|
||||
title="Instructions here"
|
||||
content={system_prompt}
|
||||
placeholder="In this workspace, write for the marketing team: short, plain, no jargon."
|
||||
onChange={(value) => setSystemPrompt(value)}
|
||||
/>
|
||||
<CodeEditor
|
||||
mode="markdown"
|
||||
className="h-32"
|
||||
title="Memory here"
|
||||
content={memory}
|
||||
info="What it must remember in this workspace."
|
||||
onChange={(value) => setMemory(value)}
|
||||
/>
|
||||
<AddFormActions submit_label="Save" loading={loading} error={error} onCancel={onCancel} />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { WorkspaceBot } from "@/src/types";
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
import WorkspaceBotForm from "./workspace-bot-form";
|
||||
|
||||
type Props = {
|
||||
workspace_id: number;
|
||||
workspace_name?: string | null;
|
||||
bot_id: number;
|
||||
bot_name?: string | null;
|
||||
/**
|
||||
* What the bot has for this workspace now.
|
||||
*/
|
||||
system_prompt?: string | null;
|
||||
memory?: string | null;
|
||||
onClose: () => void;
|
||||
onSaved: (bots: WorkspaceBot[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* What one bot has for this workspace alone: instructions on top of its
|
||||
* own, and its memory here. The bot rewrites both as it learns; the
|
||||
* coordinator can set the instructions too. Opened from the workspace's
|
||||
* settings and from the bot's.
|
||||
*/
|
||||
export default function WorkspaceBotModal({
|
||||
workspace_id,
|
||||
workspace_name,
|
||||
bot_id,
|
||||
bot_name,
|
||||
system_prompt,
|
||||
memory,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: Props) {
|
||||
const name = bot_name || "This bot";
|
||||
|
||||
return (
|
||||
<AddModalShell
|
||||
open
|
||||
setOpen={(open) => !open && onClose()}
|
||||
title={`${name} in ${workspace_name || "this workspace"}`}
|
||||
description={`${name} keeps its own instructions and memory everywhere. These apply only here, and it updates them itself as it learns how this workspace works.`}
|
||||
>
|
||||
<WorkspaceBotForm
|
||||
workspace_id={workspace_id}
|
||||
bot_id={bot_id}
|
||||
bot_name={bot_name}
|
||||
system_prompt={system_prompt}
|
||||
memory={memory}
|
||||
onCancel={onClose}
|
||||
onSaved={(bots) => {
|
||||
onSaved(bots);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import type { WorkspaceBotRequest } from "@/src/schemas/workspace-schema";
|
||||
import type { WorkspaceBotInstructions } from "@/src/types";
|
||||
|
||||
/**
|
||||
* A bot's instructions and memory for one workspace, for its editor.
|
||||
*/
|
||||
export default async function fetchWorkspaceBotInstructions({ workspace_id, bot_id }: WorkspaceBotRequest) {
|
||||
return await fetchApi<WorkspaceBotRequest, APIResponseObject<WorkspaceBotInstructions>>(
|
||||
`/api/admin/workspaces/workspace-bot`,
|
||||
{ method: "POST", body: { workspace_id, bot_id } },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { WorkspaceBotInstructions } from "@/src/types";
|
||||
import fetchWorkspaceBotInstructions from "../(functions)/fetch-workspace-bot-instructions";
|
||||
|
||||
type Params = {
|
||||
workspace_id: number;
|
||||
bot_id: number;
|
||||
/**
|
||||
* Loads each time this turns true (the editor opens), so it starts from
|
||||
* what was last saved, the bot's own edits included.
|
||||
*/
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bot's instructions and memory for this workspace, loaded when their
|
||||
* editor opens: page props leave them out.
|
||||
*/
|
||||
export default function useWorkspaceBotInstructions({ workspace_id, bot_id, enabled }: Params) {
|
||||
const [instructions, setInstructions] = useState<WorkspaceBotInstructions>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
/** Cleared on close, so the next open never shows an old copy. */
|
||||
setInstructions(undefined);
|
||||
setError(undefined);
|
||||
|
||||
if (!enabled || !workspace_id || !bot_id) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
fetchWorkspaceBotInstructions({ workspace_id, bot_id })
|
||||
.then((res) => {
|
||||
if (cancelled) return;
|
||||
|
||||
if (res.success && res.singleRes) {
|
||||
setInstructions(res.singleRes);
|
||||
} else {
|
||||
setError(res.msg || "Couldn't load the instructions.");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError("Couldn't load the instructions.");
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enabled, workspace_id, bot_id]);
|
||||
|
||||
return { instructions, error };
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export default function BotProfileModal({ bot }: Props) {
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{bot.kind == "coordinator"
|
||||
? "The coordinator keeps its name. Give it a picture, or change its core instructions."
|
||||
: `${name}'s core instructions apply everywhere. What it does in one workspace is set on that workspace's page.`}
|
||||
: `${name}'s core instructions apply everywhere. What it does in one workspace is under Instructions here in the menu of its conversation there.`}
|
||||
</Span>
|
||||
</Stack>
|
||||
{!open ? null : system_prompt != undefined ? (
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Loading from "@moduletrace/twui/elements/Loading";
|
||||
import H2 from "@moduletrace/twui/layout/H2";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { NotebookPen } from "lucide-react";
|
||||
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import WorkspaceBotForm from "@/src/components/general/workspace-bot-form";
|
||||
import useWorkspaceBotInstructions from "../(hooks)/use-workspace-bot-instructions";
|
||||
import HeaderMenuItem from "./header-menu-item";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
workspace: BUN_SQLITE_COBBLE_WORKSPACES;
|
||||
on_open?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The header menu's row, in a workspace conversation, that opens what the
|
||||
* bot has for this workspace alone: its instructions here and its memory
|
||||
* here (crew managers). Both load when it opens, so it shows the bot's
|
||||
* own latest edits.
|
||||
*/
|
||||
export default function BotWorkspaceInstructionsModal({ bot, workspace, on_open }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const name = bot.name || "This bot";
|
||||
const place = workspace.name || "this workspace";
|
||||
const { instructions, error } = useWorkspaceBotInstructions({
|
||||
workspace_id: Number(workspace.id),
|
||||
bot_id: Number(bot.id),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) on_open?.();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
target={
|
||||
<HeaderMenuItem
|
||||
icon={NotebookPen}
|
||||
title="Instructions here"
|
||||
description={`${name}'s instructions and memory in ${place}`}
|
||||
/>
|
||||
}
|
||||
targetWrapperProps={{ className: "w-full" }}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
className="max-w-lg bg-paper p-6"
|
||||
>
|
||||
<Stack className="w-full gap-1! items-stretch">
|
||||
<H2 className="text-lg! leading-6! tracking-normal! font-bold text-ink">{`${name} in ${place}`}</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`On top of ${name}'s core instructions, which apply everywhere. These apply only in ${place}, and ${name} updates them itself as it learns how this workspace works.`}
|
||||
</Span>
|
||||
</Stack>
|
||||
{!open ? null : instructions ? (
|
||||
<WorkspaceBotForm
|
||||
workspace_id={Number(workspace.id)}
|
||||
bot_id={Number(bot.id)}
|
||||
bot_name={bot.name}
|
||||
system_prompt={instructions.system_prompt}
|
||||
memory={instructions.memory}
|
||||
onCancel={() => setOpen(false)}
|
||||
onSaved={() => setOpen(false)}
|
||||
/>
|
||||
) : error ? (
|
||||
<Span className="py-6 text-[13px] leading-[18px] text-ink-secondary">{error}</Span>
|
||||
) : (
|
||||
<Stack className="w-full py-10 items-center">
|
||||
<Loading />
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import BotTasksModal from "./bot-tasks-modal";
|
||||
import BotRoutinesModal from "./bot-routines-modal";
|
||||
import BotModelsModal from "./bot-models-modal";
|
||||
import BotAdminMenuItem from "./bot-admin-menu-item";
|
||||
import BotWorkspaceInstructionsModal from "./bot-workspace-instructions-modal";
|
||||
import HeaderMenuItem from "./header-menu-item";
|
||||
import NewSessionButton from "./new-session-button";
|
||||
import CopyChatMenuItem from "./copy-chat-menu-item";
|
||||
@@ -56,7 +57,8 @@ type Props = {
|
||||
|
||||
/**
|
||||
* The conversation header's menu: the sessions, a new session, copying
|
||||
* the session, the bot's computer, its models, in a workspace making it the admin there, its
|
||||
* the session, the bot's computer, its models, in a workspace its
|
||||
* instructions there and making it the admin there, 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.
|
||||
@@ -77,6 +79,13 @@ export default function ConversationHeaderMenu({
|
||||
bot.kind == "coordinator"
|
||||
? isPassThroughUser({ user_types: pageProps?.user_types })
|
||||
: canManageCrew({ user_types: pageProps?.user_types });
|
||||
/**
|
||||
* A bot's instructions for one workspace, only there, for crew
|
||||
* managers (the coordinator's too: only the super admin sees it).
|
||||
*/
|
||||
const can_edit_here = Boolean(
|
||||
workspace?.id && canManageCrew({ user_types: pageProps?.user_types }),
|
||||
);
|
||||
/**
|
||||
* Admin is per workspace, so only in one, for a worker, and only for
|
||||
* those who hand out roles.
|
||||
@@ -161,6 +170,13 @@ export default function ConversationHeaderMenu({
|
||||
on_open={on_open}
|
||||
onSaved={on_model_saved}
|
||||
/>
|
||||
{can_edit_here && workspace?.id ? (
|
||||
<BotWorkspaceInstructionsModal
|
||||
bot={bot}
|
||||
workspace={workspace}
|
||||
on_open={on_open}
|
||||
/>
|
||||
) : null}
|
||||
{can_set_admin && workspace?.id ? (
|
||||
<BotAdminMenuItem
|
||||
bot_id={Number(bot.id)}
|
||||
|
||||
@@ -7,16 +7,22 @@ import SettingsRowAction from "../../../(partials)/settings-row-action";
|
||||
type Props = {
|
||||
workspace: BotWorkspace;
|
||||
busy?: boolean;
|
||||
/**
|
||||
* Opens what the bot has for this workspace alone.
|
||||
*/
|
||||
onEdit?: () => void;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One workspace the bot works in. Its title opens the workspace's
|
||||
* settings; a worker can be taken out from here.
|
||||
* settings; its instructions and memory there are edited from here, and a
|
||||
* worker can be taken out.
|
||||
*/
|
||||
export default function BotWorkspaceRow({
|
||||
workspace,
|
||||
busy,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}: Props) {
|
||||
const name = workspace.name || `Workspace ${workspace.id}`;
|
||||
@@ -41,6 +47,14 @@ export default function BotWorkspaceRow({
|
||||
: workspace.description || "Uses the workspace's context"
|
||||
}
|
||||
>
|
||||
{onEdit ? (
|
||||
<SettingsRowAction
|
||||
title={`Instructions and memory in ${name}`}
|
||||
label="Instructions"
|
||||
disabled={busy}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
) : null}
|
||||
{onRemove ? (
|
||||
<SettingsRowAction
|
||||
title={`Take ${name} out of this workspace`}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { BotWorkspace, BotWithLastMessage, WorkspaceWithActivity } from "@/src/types";
|
||||
import type { BotWorkspace, BotWithLastMessage, WorkspaceBot, WorkspaceWithActivity } from "@/src/types";
|
||||
import InlineAddSelect from "@/src/components/general/inline-add-select";
|
||||
import WorkspaceBotModal from "@/src/components/general/workspace-bot-modal";
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import SettingsGroup from "../../../(partials)/settings-group";
|
||||
import SettingsList from "../../../(partials)/settings-list";
|
||||
@@ -33,6 +34,7 @@ export default function BotWorkspacesGroup({
|
||||
const [bot_workspaces, setBotWorkspaces] = useState(initial);
|
||||
const [busy_id, setBusyId] = useState<number | "add">();
|
||||
const [status, setStatus] = useState<Status>();
|
||||
const [editing, setEditing] = useState<BotWorkspace>();
|
||||
const is_coordinator = bot.kind == "coordinator";
|
||||
|
||||
const addable = workspaces.filter(
|
||||
@@ -86,6 +88,27 @@ export default function BotWorkspacesGroup({
|
||||
setStatus({ success: true, msg: res.msg || "Saved." });
|
||||
};
|
||||
|
||||
/**
|
||||
* The save answers with the workspace's crew: this bot's row there
|
||||
* carries what it has for the workspace now.
|
||||
*/
|
||||
const saved = ({ workspace_id, bots }: { workspace_id: number; bots: WorkspaceBot[] }) => {
|
||||
const member = bots.find((item) => item.id == bot.id);
|
||||
|
||||
setBotWorkspaces((prev) =>
|
||||
prev.map((workspace) =>
|
||||
Number(workspace.id) == workspace_id
|
||||
? {
|
||||
...workspace,
|
||||
workspace_system_prompt: member?.workspace_system_prompt || null,
|
||||
workspace_memory: member?.workspace_memory || null,
|
||||
}
|
||||
: workspace,
|
||||
),
|
||||
);
|
||||
setStatus({ success: true, msg: "Saved." });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="workspaces"
|
||||
@@ -109,6 +132,7 @@ export default function BotWorkspacesGroup({
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
busy={busy_id == workspace.id}
|
||||
onEdit={() => setEditing(workspace)}
|
||||
onRemove={
|
||||
is_coordinator
|
||||
? undefined
|
||||
@@ -141,6 +165,18 @@ export default function BotWorkspacesGroup({
|
||||
/>
|
||||
)}
|
||||
<SettingsStatus status={status} />
|
||||
{editing ? (
|
||||
<WorkspaceBotModal
|
||||
workspace_id={Number(editing.id)}
|
||||
workspace_name={editing.name}
|
||||
bot_id={Number(bot.id)}
|
||||
bot_name={bot.name}
|
||||
system_prompt={editing.workspace_system_prompt}
|
||||
memory={editing.workspace_memory}
|
||||
onClose={() => setEditing(undefined)}
|
||||
onSaved={(bots) => saved({ workspace_id: Number(editing.id), bots })}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import CodeEditor from "@/src/components/general/code-editor";
|
||||
import type { WorkspaceBot } from "@/src/types";
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
|
||||
type Props = {
|
||||
workspace_id: number;
|
||||
workspace_name: string;
|
||||
bot: WorkspaceBot;
|
||||
onClose: () => void;
|
||||
onSaved: (bots: WorkspaceBot[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* What one bot has for this workspace alone: instructions on top of its
|
||||
* own, and its memory here. The bot rewrites both as it learns; the
|
||||
* coordinator can set the instructions too.
|
||||
*/
|
||||
export default function WorkspaceBotModal({ workspace_id, workspace_name, bot, onClose, onSaved }: Props) {
|
||||
const [system_prompt, setSystemPrompt] = useState(bot.workspace_system_prompt || "");
|
||||
const [memory, setMemory] = useState(bot.workspace_memory || "");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const name = bot.name || "This bot";
|
||||
|
||||
const submit = async () => {
|
||||
if (loading) return;
|
||||
if (!window.confirm(`Save ${bot.name || "this bot"}'s instructions and memory here?`)) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await postAdminApi<WorkspaceBot>({
|
||||
path: "workspaces/update-bot",
|
||||
body: { workspace_id, bot_id: bot.id, system_prompt, memory },
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res.success) {
|
||||
setError(res.msg || "Couldn't save.");
|
||||
return;
|
||||
}
|
||||
|
||||
onSaved(res.payload || []);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<AddModalShell
|
||||
open
|
||||
setOpen={(open) => !open && onClose()}
|
||||
title={`${name} in ${workspace_name}`}
|
||||
description={`${name} keeps its own instructions and memory everywhere. These apply only here, and it updates them itself as it learns how this workspace works.`}
|
||||
>
|
||||
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={submit}>
|
||||
<CodeEditor
|
||||
mode="markdown"
|
||||
className="h-44"
|
||||
title="Instructions here"
|
||||
content={system_prompt}
|
||||
placeholder="In this workspace, write for the marketing team: short, plain, no jargon."
|
||||
onChange={(value) => setSystemPrompt(value)}
|
||||
/>
|
||||
<CodeEditor
|
||||
mode="markdown"
|
||||
className="h-32"
|
||||
title="Memory here"
|
||||
content={memory}
|
||||
info="What it must remember in this workspace."
|
||||
onChange={(value) => setMemory(value)}
|
||||
/>
|
||||
<AddFormActions submit_label="Save" loading={loading} error={error} onCancel={onClose} />
|
||||
</Form>
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
+5
-2
@@ -9,7 +9,7 @@ import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
||||
import SettingsStatus from "@/src/pages/admin/settings/(partials)/settings-status";
|
||||
import InlineAddSelect from "@/src/components/general/inline-add-select";
|
||||
import WorkspaceBotModal from "./workspace-bot-modal";
|
||||
import WorkspaceBotModal from "@/src/components/general/workspace-bot-modal";
|
||||
import WorkspaceBotRow from "./workspace-bot-row";
|
||||
|
||||
type Props = {
|
||||
@@ -148,7 +148,10 @@ export default function WorkspaceBotsGroup({ workspace_id, workspace_name, bots:
|
||||
<WorkspaceBotModal
|
||||
workspace_id={workspace_id}
|
||||
workspace_name={workspace_name}
|
||||
bot={editing}
|
||||
bot_id={Number(editing.id)}
|
||||
bot_name={editing.name}
|
||||
system_prompt={editing.workspace_system_prompt}
|
||||
memory={editing.workspace_memory}
|
||||
onClose={() => setEditing(undefined)}
|
||||
onSaved={setBots}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabWorkspaceBots from "@/src/functions/backend/db/workspaces/grab-workspace-bots";
|
||||
import { WorkspaceBotRequestSchema } from "@/src/schemas/workspace-schema";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type { WorkspaceBotInstructions } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* A bot's instructions and memory for one workspace, for the editor in its
|
||||
* conversation there, which loads them when it opens (page props leave
|
||||
* them out). For super admins and admins; only the super admin reaches
|
||||
* the coordinator.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<
|
||||
APIResponseObject<WorkspaceBotInstructions>
|
||||
> = async ({ req, body }) => {
|
||||
try {
|
||||
if (req.method != "POST") {
|
||||
throw new AppError(`Method not allowed`);
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status || !user.id) {
|
||||
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
||||
}
|
||||
|
||||
if (!canManageCrew({ user_types })) {
|
||||
throw new AppError(`Only an admin can do that.`);
|
||||
}
|
||||
|
||||
const { workspace_id, bot_id } = parseWithSchema({ schema: WorkspaceBotRequestSchema, data: body });
|
||||
const bot = (await grabWorkspaceBots({ user_id: user.id, workspace_id })).find(
|
||||
(member) => member.id == bot_id,
|
||||
);
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`That bot isn't in this workspace.`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
singleRes: {
|
||||
system_prompt: bot.workspace_system_prompt || "",
|
||||
memory: bot.workspace_memory || "",
|
||||
},
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
};
|
||||
@@ -206,6 +206,15 @@ export type WorkspaceBot = BotWithLastMessage & {
|
||||
workspace_memory?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot's instructions and memory for one workspace, as its editor loads
|
||||
* them (`api/admin/workspaces/workspace-bot`).
|
||||
*/
|
||||
export type WorkspaceBotInstructions = {
|
||||
system_prompt: string;
|
||||
memory: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A workspace a bot works in, with what the bot has for that workspace
|
||||
* alone.
|
||||
|
||||
Reference in New Issue
Block a user