Add the harness, envs, per-bot models and chat attachments

- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 03:38:38 +01:00
co-authored by Claude Opus 5.5
parent d49f524fe8
commit 6a2665dae9
223 changed files with 12342 additions and 162 deletions
@@ -0,0 +1,22 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { EnvsOverviewPayload } from "@/src/types";
type Params = {
/**
* Under `/api/admin/envs/`.
*/
path: "save-env" | "delete-env";
body: Record<string, unknown>;
};
/**
* Posts to an env API route. Every route answers with the fresh page data
* in `singleRes`, so the page updates in place.
*/
export default async function postEnvApi({ path, body }: Params) {
return await fetchApi<Record<string, unknown>, APIResponseObject<EnvsOverviewPayload>>(
`/api/admin/envs/${path}`,
{ method: "POST", body },
);
}
@@ -0,0 +1,152 @@
import { useState } from "react";
import Form from "@moduletrace/twui/form/Form";
import Input from "@moduletrace/twui/form/Input";
import Checkbox from "@moduletrace/twui/form/Checkbox";
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import type { EnvScope, EnvsOverviewPayload, KnownEnvStatus, PublicEnv } from "@/src/types";
import SettingsStatus from "../../(partials)/settings-status";
import postEnvApi from "../(functions)/post-env-api";
type Props = {
scope: EnvScope;
/**
* Edits this env; adds one when not given.
*/
env?: PublicEnv;
/**
* Sets this app setting: its key, description and flags are fixed.
*/
known?: KnownEnvStatus;
onSaved: (overview: EnvsOverviewPayload, msg: string) => void;
onCancel: () => void;
};
/**
* Adds or edits an env in one scope. A saved secret is never shown; typing
* a new value replaces it, leaving it empty keeps it.
*/
export default function EnvForm({ scope, env, known, onSaved, onCancel }: Props) {
const is_known = Boolean(known || env?.known);
const [key, setKey] = useState(env?.key || known?.key || "");
const [value, setValue] = useState(env && !env.secret ? env.value || "" : "");
const [description, setDescription] = useState(env?.description || "");
const [secret, setSecret] = useState(env?.secret ?? known?.secret ?? true);
const [expose, setExpose] = useState(env?.expose_to_computers ?? true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const id = `env-${env?.id || known?.key || "new"}`;
const keeps_secret = Boolean(env?.secret);
const submit = async () => {
if (loading) return;
setLoading(true);
setError(undefined);
const res = await postEnvApi({
path: "save-env",
body: {
env_id: env?.id,
key,
value: keeps_secret && !value ? undefined : value,
description: is_known ? undefined : description,
paradigm: scope.paradigm,
workspace_id: scope.paradigm == "workspace" ? scope.id : null,
computer_id: scope.paradigm == "computer" ? scope.id : null,
secret,
expose_to_computers: expose,
},
});
setLoading(false);
if (!res.success || !res.singleRes) {
setError(res.msg || "Couldn't save the env.");
return;
}
onSaved(res.singleRes, res.msg || "Saved.");
};
return (
<Form className="w-full gap-4! items-stretch py-4" onSubmit={submit}>
<Input
id={`${id}-key`}
title="Key"
label="Key"
showLabel
placeholder="GITHUB_TOKEN"
defaultValue={key}
disabled={Boolean(env) || is_known}
autoComplete="off"
onChange={(e) => setKey(e.target.value)}
/>
<Input
id={`${id}-value`}
title="Value"
label="Value"
showLabel
type={secret ? "password" : "text"}
autoComplete="off"
defaultValue={value}
placeholder={keeps_secret ? "Saved. Type a new value to replace it." : ""}
info="Encrypted before it's stored."
onChange={(e) => setValue(e.target.value)}
/>
{is_known ? null : (
<>
<Input
id={`${id}-description`}
title="Description"
label="Description"
showLabel
placeholder="A token for the company GitHub"
defaultValue={description}
info="Bots see the key and this description, never the value."
onChange={(e) => setDescription(e.target.value)}
/>
<Checkbox
title="Secret"
label="Secret: hidden once saved, and blanked out of everything bots read"
checked={secret}
changeHandler={setSecret}
/>
<Checkbox
title="Set on computers"
label="Set on computers, for bots' commands and code"
checked={expose}
changeHandler={setExpose}
/>
</>
)}
<Stack className="gap-3! items-stretch">
<SettingsStatus status={error ? { error: true, msg: error } : undefined} />
<Row className="gap-2! flex-nowrap justify-end">
<Button
title="Cancel"
type="button"
variant="ghost"
color="gray"
size="small"
disabled={loading}
onClick={onCancel}
>
Cancel
</Button>
<Button
title={env ? "Save env" : "Add env"}
type="submit"
size="small"
loading={loading}
disabled={loading}
>
{env ? "Save env" : "Add env"}
</Button>
</Row>
</Stack>
</Form>
);
}
@@ -0,0 +1,49 @@
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import { Pencil, Plus, Trash2 } from "lucide-react";
type Props = {
name: string;
/**
* "set" for an app setting that isn't saved yet.
*/
mode: "edit" | "set";
busy?: boolean;
onEdit: () => void;
onRemove?: () => void;
};
/**
* Edit (or Set) and Remove for one env row.
*/
export default function EnvRowActions({ name, mode, busy, onEdit, onRemove }: Props) {
return (
<Row className="gap-1! flex-nowrap shrink-0">
<Button
title={mode == "set" ? `Set ${name}` : `Edit ${name}`}
type="button"
variant="ghost"
color="gray"
size="small"
disabled={busy}
onClick={onEdit}
>
{mode == "set" ? <Plus size={16} aria-hidden /> : <Pencil size={16} aria-hidden />}
</Button>
{onRemove ? (
<Button
title={`Remove ${name}`}
type="button"
variant="ghost"
color="gray"
size="small"
loading={busy}
disabled={busy}
onClick={onRemove}
>
<Trash2 size={16} aria-hidden />
</Button>
) : null}
</Row>
);
}
@@ -0,0 +1,92 @@
import { useState } from "react";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import type { EnvScope, EnvsOverviewPayload, KnownEnvStatus, PublicEnv } from "@/src/types";
import postEnvApi from "../(functions)/post-env-api";
import EnvForm from "./env-form";
import EnvRowActions from "./env-row-actions";
type Props = {
scope: EnvScope;
/**
* The saved env, if any. An app setting may have none yet.
*/
env?: PublicEnv;
known?: KnownEnvStatus;
onChanged: (overview: EnvsOverviewPayload, msg: string) => void;
onError: (msg: string) => void;
};
/**
* One env: its key, description and value (or "secret"), with Edit and
* Remove. Edit opens the form in place.
*/
export default function EnvRow({ scope, env, known, onChanged, onError }: Props) {
const [editing, setEditing] = useState(false);
const [busy, setBusy] = useState(false);
const key = env?.key || known?.key || "";
const remove = async () => {
if (!env || busy) return;
if (!window.confirm(`Remove ${key}? Computers drop it on their next run.`)) return;
setBusy(true);
const res = await postEnvApi({ path: "delete-env", body: { env_id: env.id } });
setBusy(false);
if (res.success && res.singleRes) onChanged(res.singleRes, res.msg || "Removed.");
else onError(res.msg || "Couldn't remove the env.");
};
if (editing) {
return (
<Stack className="w-full gap-0! items-stretch border-0 border-b border-solid border-hairline last:border-b-0">
<EnvForm
scope={scope}
env={env}
known={known}
onSaved={(overview, msg) => {
setEditing(false);
onChanged(overview, msg);
}}
onCancel={() => setEditing(false)}
/>
</Stack>
);
}
const status = env
? env.secret
? "Secret, saved"
: env.value || "Empty"
: known?.from_process
? "From the server's environment"
: "Not set";
const flags = env && !env.known && !env.expose_to_computers ? " · not on computers" : "";
return (
<Row className="w-full gap-3! flex-nowrap items-start justify-between py-3 border-0 border-b border-solid border-hairline last:border-b-0">
<Stack className="gap-0.5! min-w-0">
<Span className="text-sm font-bold leading-5 text-ink truncate">{key}</Span>
{env?.description || known?.description ? (
<Span className="text-[13px] leading-[18px] text-ink-secondary break-words">
{known?.description || env?.description}
</Span>
) : null}
<Span className="text-[13px] leading-[18px] text-ink-secondary break-all">
{status}
{flags}
</Span>
</Stack>
<EnvRowActions
name={key}
mode={env ? "edit" : "set"}
busy={busy}
onEdit={() => setEditing(true)}
onRemove={env ? remove : undefined}
/>
</Row>
);
}
@@ -0,0 +1,39 @@
import Select from "@moduletrace/twui/form/Select";
import type { EnvScope } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import SettingsStatus from "../../(partials)/settings-status";
import scopeValue from "../(utils)/scope-value";
type Props = {
scopes: EnvScope[];
value: string;
onChange: (value: string) => void;
status?: { error?: boolean; success?: boolean; msg?: string };
};
/**
* Where the envs below apply: general, a workspace or a computer. Also
* shows the last change's status.
*/
export default function EnvScopeSelect({ scopes, value, onChange, status }: Props) {
return (
<SettingsGroup
title="Where"
description="A computer's own envs beat its workspace's, which beat general ones."
>
<Select
id="env-scope"
title="Where"
label="Where"
showLabel
options={scopes.map((scope) => ({
value: scopeValue({ scope }),
title: scope.title,
default: scopeValue({ scope }) == value,
}))}
changeHandler={(next) => onChange(String(next))}
/>
<SettingsStatus status={status} />
</SettingsGroup>
);
}
@@ -0,0 +1,78 @@
import { useState } from "react";
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 { Plus } from "lucide-react";
import type { EnvScope, EnvsOverviewPayload, PublicEnv } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import EnvForm from "./env-form";
import EnvRow from "./env-row";
type Props = {
scope: EnvScope;
/**
* The scope's own envs (app settings left out).
*/
envs: PublicEnv[];
onChanged: (overview: EnvsOverviewPayload, msg: string) => void;
onError: (msg: string) => void;
};
const Descriptions: Record<EnvScope["paradigm"], string> = {
general: "Set on every computer. A workspace or computer can set the same key to override it.",
workspace: "Set on this workspace's computers when its bots use them. Beats general, loses to a computer's own.",
computer: "Set on this computer only. Beats workspace and general.",
};
/**
* The envs bots and computers use, in one scope, with an add form.
*/
export default function EnvsGroup({ scope, envs, onChanged, onError }: Props) {
const [adding, setAdding] = useState(false);
return (
<SettingsGroup title="Envs" description={Descriptions[scope.paradigm]}>
<Stack className="w-full gap-0! items-stretch">
{envs.map((env) => (
<EnvRow
key={env.id}
scope={scope}
env={env}
onChanged={onChanged}
onError={onError}
/>
))}
{envs.length ? null : (
<Span className="text-[13px] leading-5 text-ink-secondary py-3">
Nothing set here yet.
</Span>
)}
</Stack>
{adding ? (
<EnvForm
scope={scope}
onSaved={(overview, msg) => {
setAdding(false);
onChanged(overview, msg);
}}
onCancel={() => setAdding(false)}
/>
) : (
<Row className="gap-3! items-center justify-end">
<Button
title="Add an env"
type="button"
variant="outlined"
color="gray"
size="small"
onClick={() => setAdding(true)}
>
<Plus size={16} aria-hidden />
Add an env
</Button>
</Row>
)}
</SettingsGroup>
);
}
@@ -0,0 +1,41 @@
import Stack from "@moduletrace/twui/layout/Stack";
import type { EnvScope, EnvsOverviewPayload, PublicEnv } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import EnvRow from "./env-row";
type Props = {
scope: EnvScope;
overview: EnvsOverviewPayload;
onChanged: (overview: EnvsOverviewPayload, msg: string) => void;
onError: (msg: string) => void;
};
/**
* The app settings: envs the app itself reads. General only, never on
* computers.
*/
export default function KnownEnvsGroup({ scope, overview, onChanged, onError }: Props) {
return (
<SettingsGroup
title="App settings"
description="What the app itself reads. A value here wins over the server's environment. These never reach computers or bots."
>
<Stack className="w-full gap-0! items-stretch">
{overview.known.map((known) => (
<EnvRow
key={known.key}
scope={scope}
known={known}
env={findGeneral({ envs: overview.envs, key: known.key })}
onChanged={onChanged}
onError={onError}
/>
))}
</Stack>
</SettingsGroup>
);
}
function findGeneral({ envs, key }: { envs: PublicEnv[]; key: string }) {
return envs.find((env) => env.paradigm == "general" && env.key == key);
}
@@ -0,0 +1,65 @@
import { useContext, useState } from "react";
import { AppContext } from "@/src/pages/__root";
import type { EnvsOverviewPayload } from "@/src/types";
import EnvScopeSelect from "../(partials)/env-scope-select";
import EnvsGroup from "../(partials)/envs-group";
import KnownEnvsGroup from "../(partials)/known-envs-group";
import scopeValue, { envInScope } from "../(utils)/scope-value";
/**
* Pick where (general, a workspace, a computer), then that scope's envs.
* General also lists the app settings. Every change saves at once and
* refreshes the page data.
*/
export default function EnvironmentSettingsSection() {
const { pageProps } = useContext(AppContext);
const [overview, setOverview] = useState(pageProps?.envs_overview);
const [selected, setSelected] = useState("general");
const [status, setStatus] = useState<{ error?: boolean; success?: boolean; msg?: string }>();
/**
* Remounts the lists when the data changes under them.
*/
const [version, setVersion] = useState(0);
if (!overview) return null;
const scope =
overview.scopes.find((s) => scopeValue({ scope: s }) == selected) || overview.scopes[0]!;
const changed = (next: EnvsOverviewPayload, msg: string) => {
setOverview(next);
setStatus({ success: true, msg });
setVersion((prev) => prev + 1);
};
const failed = (msg: string) => setStatus({ error: true, msg });
return (
<>
<EnvScopeSelect
scopes={overview.scopes}
value={scopeValue({ scope })}
status={status}
onChange={(next) => {
setSelected(next);
setStatus(undefined);
}}
/>
{scope.paradigm == "general" ? (
<KnownEnvsGroup
key={`known-${version}`}
scope={scope}
overview={overview}
onChanged={changed}
onError={failed}
/>
) : null}
<EnvsGroup
key={`${scopeValue({ scope })}-${version}`}
scope={scope}
envs={overview.envs.filter((env) => !env.known && envInScope({ env, scope }))}
onChanged={changed}
onError={failed}
/>
</>
);
}
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test";
import type { PublicEnv } from "@/src/types";
import scopeValue, { envInScope } from "./scope-value";
const env = (fields: Partial<PublicEnv>): PublicEnv => ({
id: 1,
key: "K",
description: "",
paradigm: "general",
workspace_id: null,
computer_id: null,
secret: true,
expose_to_computers: true,
known: false,
...fields,
});
describe("scopeValue", () => {
test("names each scope", () => {
expect(scopeValue({ scope: { paradigm: "general", id: null } })).toBe("general");
expect(scopeValue({ scope: { paradigm: "computer", id: 4 } })).toBe("computer:4");
});
});
describe("envInScope", () => {
test("matches the paradigm and its id", () => {
const scope = { paradigm: "workspace" as const, id: 2, title: "" };
expect(envInScope({ env: env({ paradigm: "workspace", workspace_id: 2 }), scope })).toBe(true);
expect(envInScope({ env: env({ paradigm: "workspace", workspace_id: 3 }), scope })).toBe(false);
expect(envInScope({ env: env({}), scope })).toBe(false);
});
});
@@ -0,0 +1,18 @@
import type { EnvScope, PublicEnv } from "@/src/types";
/**
* A scope as one select value: `general`, `workspace:3`, `computer:1`.
*/
export default function scopeValue({ scope }: { scope: Pick<EnvScope, "paradigm" | "id"> }) {
return scope.paradigm == "general" ? "general" : `${scope.paradigm}:${scope.id}`;
}
/**
* Whether an env is set in this scope.
*/
export function envInScope({ env, scope }: { env: PublicEnv; scope: EnvScope }) {
if (env.paradigm != scope.paradigm) return false;
if (scope.paradigm == "workspace") return env.workspace_id == scope.id;
if (scope.paradigm == "computer") return env.computer_id == scope.id;
return true;
}
@@ -0,0 +1,33 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import { SettingsDefaultPath } from "@/src/dict/settings-pages-dict";
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import grabEnvsOverview from "@/src/functions/backend/db/envs/grab-envs-overview";
import grabSettingsServerProps from "../(functions)/grab-settings-server-props";
/**
* Super admins only; everyone else lands on the general settings.
*/
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
const settings = await grabSettingsServerProps(params);
if (!settings.props) return settings;
const user = await grabSuperAdminUser({ req: params.req });
if (!user?.id) {
return {
redirect: {
destination: SettingsDefaultPath,
permanent: false,
},
};
}
return {
props: {
...settings.props,
envs_overview: await grabEnvsOverview({ user_id: Number(user.id) }),
},
};
};
@@ -0,0 +1,16 @@
import EnvironmentSettingsSection from "./(sections)/environment-settings-section";
import SettingsShell from "../(partials)/settings-shell";
import settingsPageMeta from "../(functions)/settings-page-meta";
export default function AdminSettingsEnvironmentPage() {
return (
<SettingsShell
title="Environment"
description="Keys and settings your bots and computers use."
>
<EnvironmentSettingsSection />
</SettingsShell>
);
}
export const meta = settingsPageMeta({ label: "Environment" });
@@ -0,0 +1,25 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { HarnessOverviewPayload } from "@/src/types";
type Params = {
/**
* Under `/api/admin/harness/`, e.g. `save-provider`.
*/
path: "save-provider" | "sync-provider" | "delete-provider" | "save-settings";
body: Record<string, unknown>;
};
/**
* Posts to a harness API route. Every route answers with the fresh page
* data in `singleRes`, so the page updates in place.
*/
export default async function postHarnessApi({ path, body }: Params) {
return await fetchApi<
Record<string, unknown>,
APIResponseObject<HarnessOverviewPayload>
>(`/api/admin/harness/${path}`, {
method: "POST",
body,
});
}
@@ -0,0 +1,31 @@
import Span from "@moduletrace/twui/layout/Span";
import type { HarnessOverview } from "@/src/types";
type Props = {
harness: HarnessOverview;
};
/**
* What the harness runs on right now, as saved, in one line.
*/
export default function HarnessCurrentModel({ harness }: Props) {
const text = !harness.enabled
? "Turned off."
: harness.current
? `Now using ${harness.current.name} on ${harness.current.provider_name}${
harness.current.source == "auto" ? ", picked automatically" : ""
}.${
harness.candidate_count > 1
? ` ${harness.candidate_count - 1} more to fall back on.`
: ""
}`
: harness.required
? "Nothing to run on yet: no provider has a usable key."
: "No free model can do this right now. Pick one to turn it on.";
return (
<Span className="text-[13px] leading-[18px] text-ink-secondary">
{text}
</Span>
);
}
@@ -0,0 +1,44 @@
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
import type { HarnessModelOption } from "@/src/types";
import harnessModelSelectOptions, {
AutoModelValue,
} from "../(utils)/harness-model-select-options";
type Props = {
id: string;
/**
* Already narrowed to the picked provider.
*/
models: HarnessModelOption[];
model_id: number | null;
provider_name?: string;
onChange: (model_id: number | null) => void;
};
/**
* Searchable model picker: automatic, or any model that can do the job.
*/
export default function HarnessModelSelect({
id,
models,
model_id,
provider_name,
onChange,
}: Props) {
return (
<SearchSelect
id={id}
title="Model"
label="Model"
showLabel
placeholder="Search models"
options={harnessModelSelectOptions({ models, model_id, provider_name })}
defaultValues={[model_id ? String(model_id) : AutoModelValue]}
changeHandler={(value) => {
const picked = Array.isArray(value) ? value[0] : value;
onChange(!picked || picked == AutoModelValue ? null : Number(picked));
}}
/>
);
}
@@ -0,0 +1,63 @@
import Select from "@moduletrace/twui/form/Select";
import type { HarnessProviderChoice } from "@/src/types";
export const InheritProviderValue = "inherit";
export const AnyProviderValue = "any";
type Props = {
id: string;
providers: HarnessProviderChoice[];
provider_id: number | null;
/**
* On a bot's page: offer the default, titled this.
*/
inherit_title?: string;
inherit?: boolean;
onChange: (value: { provider_id: number | null; inherit: boolean }) => void;
};
/**
* Where a harness gets its models: any provider, or one of them. On a
* bot's page it can also use the default setting instead.
*/
export default function HarnessProviderSelect({
id,
providers,
provider_id,
inherit_title,
inherit,
onChange,
}: Props) {
const value = inherit
? InheritProviderValue
: provider_id
? String(provider_id)
: AnyProviderValue;
return (
<Select
id={id}
title="Provider"
label="Provider"
showLabel
value={value}
options={[
...(inherit_title ? [{ value: InheritProviderValue, title: inherit_title }] : []),
{ value: AnyProviderValue, title: "Any provider" },
...providers.map((provider) => ({
value: String(provider.id),
title: provider.name,
})),
]}
changeHandler={(picked) =>
onChange({
inherit: picked == InheritProviderValue,
provider_id:
picked == InheritProviderValue || picked == AnyProviderValue
? null
: Number(picked),
})
}
/>
);
}
@@ -0,0 +1,97 @@
import Checkbox from "@moduletrace/twui/form/Checkbox";
import type { HarnessFormValue, HarnessOverview } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import HarnessCurrentModel from "./harness-current-model";
import HarnessModelSelect from "./harness-model-select";
import HarnessProviderSelect from "./harness-provider-select";
import HarnessVoiceSelect from "./harness-voice-select";
type Props = {
harness: HarnessOverview;
value: HarnessFormValue;
onChange: (value: HarnessFormValue) => void;
};
/**
* One harness: its provider, then its model (narrowed to that provider),
* whether it falls back to the automatic picks, whether it's on, and its
* voice for speech. On a bot's page the provider select can also use the
* default, which hides the rest.
*/
export default function HarnessSettingGroup({ harness, value, onChange }: Props) {
const id = `harness-${harness.harness}`;
const provider = harness.providers.find((p) => p.id == value.provider_id);
const models = value.provider_id
? harness.models.filter((model) => model.provider_id == value.provider_id)
: harness.models;
const picked = models.find((model) => model.id == value.model_id);
const voices = (picked || harness.current)?.voices;
const on_bot_page = harness.default !== undefined;
const default_name = harness.default?.enabled === false
? "turned off"
: harness.default?.current
? `${harness.default.current.name} on ${harness.default.current.provider_name}`
: "nothing available";
return (
<SettingsGroup title={harness.title} description={harness.description}>
<HarnessProviderSelect
id={`${id}-provider`}
providers={harness.providers}
provider_id={value.provider_id}
inherit={value.inherit}
inherit_title={on_bot_page ? `Same as the default (now ${default_name})` : undefined}
onChange={(next) =>
onChange({
...value,
...next,
model_id:
next.provider_id && picked?.provider_id != next.provider_id
? null
: value.model_id,
voice: undefined,
})
}
/>
{value.inherit ? null : (
<HarnessModelSelect
key={value.provider_id || "any"}
id={`${id}-model`}
models={models}
model_id={value.model_id}
provider_name={provider?.name}
onChange={(model_id) => onChange({ ...value, model_id, voice: undefined })}
/>
)}
{value.inherit && harness.default ? (
<HarnessCurrentModel harness={{ ...harness, ...harness.default }} />
) : (
<HarnessCurrentModel harness={harness} />
)}
{!value.inherit && value.model_id ? (
<Checkbox
title="Fall back to the automatic picks if this model fails"
label={`Fall back to the automatic picks${provider ? ` on ${provider.name}` : ""} if this model fails`}
checked={value.fallback}
changeHandler={(fallback) => onChange({ ...value, fallback })}
/>
) : null}
{!value.inherit && harness.harness == "speech" && voices?.length ? (
<HarnessVoiceSelect
id={`${id}-voice`}
voices={voices}
voice={value.voice}
onChange={(voice) => onChange({ ...value, voice: voice || undefined })}
/>
) : null}
{value.inherit || harness.required ? null : (
<Checkbox
title={`Turn ${harness.title.toLowerCase()} on`}
label="Turned on"
checked={value.enabled}
changeHandler={(enabled) => onChange({ ...value, enabled })}
/>
)}
</SettingsGroup>
);
}
@@ -0,0 +1,103 @@
import { useState } from "react";
import Form from "@moduletrace/twui/form/Form";
import useFormInit from "@/src/hooks/use-form-init";
import type { HarnessOverview, HarnessSettingsFormObject } from "@/src/types";
import SettingsSaveBar from "../../(partials)/settings-save-bar";
import changedHarnessSettings from "../(utils)/changed-harness-settings";
import toHarnessForm from "../(utils)/to-harness-form";
import HarnessSettingGroup from "./harness-setting-group";
type Props = {
harnesses: HarnessOverview[];
/**
* Sends the changed settings. The page refreshes its own data on
* success (which remounts this form).
*/
save: (params: {
settings: ReturnType<typeof changedHarnessSettings>;
}) => Promise<{ success: boolean; msg?: string }>;
/**
* The last save's message, kept by the page across the remount.
*/
notice?: string;
};
/**
* Which model each harness uses, saved together from one save bar. Used
* for the defaults and, with "same as the default", for a worker bot.
*/
export default function HarnessSettingsForm({
harnesses,
save,
notice: saved_notice,
}: Props) {
const saved = toHarnessForm({ harnesses });
/**
* Inputs keep their own state; bumping this remounts them on Discard.
*/
const [form_key, setFormKey] = useState(0);
/**
* Kept here: `useFormInit` clears `status` whenever the form changes.
*/
const [notice, setNotice] = useState<string | undefined>(saved_notice);
const { form, setForm, loading, setLoading, status, setStatus } =
useFormInit<HarnessSettingsFormObject>({ existing: saved });
const changed = changedHarnessSettings({ form, saved });
const dirty = changed.length > 0;
const discard = () => {
setForm(saved);
setStatus({});
setNotice(undefined);
setFormKey((prev) => prev + 1);
};
const submit = async () => {
if (!dirty || loading) return;
setLoading(true);
setStatus({});
const res = await save({ settings: changed });
setLoading(false);
if (!res.success) {
setStatus({ error: true, msg: res.msg || "Couldn't save the model settings." });
return;
}
setNotice(res.msg || "Saved.");
};
return (
<Form key={form_key} className="w-full gap-0! items-stretch" onSubmit={submit}>
{harnesses.map((harness) => (
<HarnessSettingGroup
key={harness.harness}
harness={harness}
value={form[harness.harness] || saved[harness.harness]!}
onChange={(value) => {
setNotice(undefined);
setForm((prev) => ({ ...prev, [harness.harness]: value }));
}}
/>
))}
<SettingsSaveBar
dirty={dirty}
loading={loading}
status={
status?.msg
? status
: !dirty && notice
? { success: true, msg: notice }
: undefined
}
onDiscard={discard}
/>
</Form>
);
}
@@ -0,0 +1,31 @@
import Select from "@moduletrace/twui/form/Select";
type Props = {
id: string;
voices: string[];
voice?: string;
onChange: (voice: string) => void;
};
/**
* Voice for the speech harness, from the model's own voices.
*/
export default function HarnessVoiceSelect({ id, voices, voice, onChange }: Props) {
return (
<Select
id={id}
title="Voice"
label="Voice"
showLabel
options={[
{ value: "", title: "The model's default", default: !voice },
...voices.map((name) => ({
value: name,
title: name,
default: name == voice,
})),
]}
changeHandler={(value) => onChange(value)}
/>
);
}
@@ -0,0 +1,169 @@
import { useState } from "react";
import Form from "@moduletrace/twui/form/Form";
import Input from "@moduletrace/twui/form/Input";
import Select from "@moduletrace/twui/form/Select";
import Checkbox from "@moduletrace/twui/form/Checkbox";
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import {
HarnessProviderKinds,
type HarnessProviderKind,
} from "@/src/dict/harness-provider-dict";
import type { HarnessOverviewPayload, PublicHarnessProvider } from "@/src/types";
import SettingsStatus from "../../(partials)/settings-status";
import postHarnessApi from "../(functions)/post-harness-api";
type Props = {
/**
* Edits this provider; adds one when not given.
*/
provider?: PublicHarnessProvider;
onSaved: (overview: HarnessOverviewPayload, msg: string) => void;
onCancel: () => void;
};
/**
* Adds or edits a provider. A saved key is never shown; typing a new one
* replaces it.
*/
export default function ProviderForm({ provider, onSaved, onCancel }: Props) {
const [kind, setKind] = useState<HarnessProviderKind>(
provider?.kind || "openrouter",
);
const preset = HarnessProviderKinds.find((p) => p.value == kind)!;
const [name, setName] = useState(provider?.name || "");
const [base_url, setBaseUrl] = useState(provider?.base_url || "");
const [api_key, setApiKey] = useState("");
const [clear_key, setClearKey] = useState(false);
const [enabled, setEnabled] = useState(provider?.enabled ?? true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const id = `provider-${provider?.id || "new"}`;
const submit = async () => {
if (loading) return;
setLoading(true);
setError(undefined);
const res = await postHarnessApi({
path: "save-provider",
body: {
provider_id: provider?.id,
kind,
name,
base_url,
api_key: api_key || undefined,
clear_key: clear_key || undefined,
enabled,
},
});
setLoading(false);
if (!res.success || !res.singleRes) {
setError(res.msg || "Couldn't save the provider.");
return;
}
onSaved(res.singleRes, res.msg || "Saved.");
};
return (
<Form className="w-full gap-4! items-stretch" onSubmit={submit}>
{provider ? null : (
<Select
id={`${id}-kind`}
title="Provider"
label="Provider"
showLabel
options={HarnessProviderKinds.map((option) => ({
value: option.value,
title: option.title,
default: option.value == kind,
}))}
changeHandler={(value) => {
setKind(value as HarnessProviderKind);
setBaseUrl("");
}}
/>
)}
<Input
id={`${id}-name`}
title="Name"
label="Name"
showLabel
placeholder={preset.title}
defaultValue={name}
onChange={(e) => setName(e.target.value)}
/>
<Input
id={`${id}-base-url`}
key={`${id}-base-url-${kind}`}
title="API address"
label="API address"
showLabel
placeholder={preset.base_url || "http://localhost:8000/v1"}
defaultValue={base_url}
info={preset.base_url ? `Leave empty for ${preset.base_url}` : "Required: the server's OpenAI-compatible base URL."}
onChange={(e) => setBaseUrl(e.target.value)}
/>
<Input
id={`${id}-key`}
title="API key"
label="API key"
showLabel
type="password"
autoComplete="off"
placeholder={provider?.has_key ? "Saved. Type a new key to replace it." : "Paste the key"}
info={
preset.key_env
? `Encrypted before it's stored. If empty, ${preset.key_env} from the environment is used.`
: "Encrypted before it's stored. Optional for this provider."
}
onChange={(e) => setApiKey(e.target.value)}
/>
{provider?.has_key ? (
<Checkbox
title="Remove the saved key"
label="Remove the saved key"
checked={clear_key}
changeHandler={setClearKey}
/>
) : null}
<Checkbox
title="Turn this provider on"
label="Turned on"
checked={enabled}
changeHandler={setEnabled}
/>
<Stack className="gap-3! items-stretch">
<SettingsStatus status={error ? { error: true, msg: error } : undefined} />
<Row className="gap-2! flex-nowrap justify-end">
<Button
title="Cancel"
type="button"
variant="ghost"
color="gray"
size="small"
disabled={loading}
onClick={onCancel}
>
Cancel
</Button>
<Button
title={provider ? "Save provider" : "Add provider"}
type="submit"
size="small"
loading={loading}
disabled={loading}
>
{provider ? "Save provider" : "Add provider"}
</Button>
</Row>
</Stack>
</Form>
);
}
@@ -0,0 +1,123 @@
import { useState } from "react";
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 { Pencil, RefreshCw, Trash2 } from "lucide-react";
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
import type { HarnessOverviewPayload, PublicHarnessProvider } from "@/src/types";
import postHarnessApi from "../(functions)/post-harness-api";
import ProviderForm from "./provider-form";
import ProviderStatus from "./provider-status";
type Props = {
provider: PublicHarnessProvider;
onChanged: (overview: HarnessOverviewPayload, msg: string) => void;
onError: (msg: string) => void;
};
/**
* One provider: name, key and sync status, and its actions. Edit opens the
* form in place.
*/
export default function ProviderRow({ provider, onChanged, onError }: Props) {
const [editing, setEditing] = useState(false);
const [busy, setBusy] = useState<"sync" | "delete">();
const act = async (action: "sync" | "delete") => {
if (busy) return;
if (
action == "delete" &&
!window.confirm(`Remove ${provider.name}? Harnesses set to its models go back to automatic.`)
) {
return;
}
setBusy(action);
const res = await postHarnessApi({
path: action == "sync" ? "sync-provider" : "delete-provider",
body: { provider_id: provider.id },
});
setBusy(undefined);
if (res.singleRes) {
onChanged(res.singleRes, res.msg || "Done.");
}
if (!res.success) {
onError(res.msg || "That didn't work.");
}
};
if (editing) {
return (
<Stack className="w-full gap-0! items-stretch py-4 border-0 border-b border-solid border-hairline last:border-b-0">
<ProviderForm
provider={provider}
onSaved={(overview, msg) => {
setEditing(false);
onChanged(overview, msg);
}}
onCancel={() => setEditing(false)}
/>
</Stack>
);
}
return (
<Row className="w-full gap-3! flex-nowrap items-start justify-between py-3 border-0 border-b border-solid border-hairline last:border-b-0">
<Stack className="gap-0.5! min-w-0">
<Span className="text-sm font-bold leading-5 text-ink truncate">
{provider.name}
{provider.name != grabProviderPreset({ kind: provider.kind }).title ? (
<Span className="font-normal text-ink-secondary">
{" "}
({grabProviderPreset({ kind: provider.kind }).title})
</Span>
) : null}
</Span>
<ProviderStatus provider={provider} />
</Stack>
<Row className="gap-1! flex-nowrap shrink-0">
<Button
title={`Sync ${provider.name} models now`}
type="button"
variant="ghost"
color="gray"
size="small"
loading={busy == "sync"}
disabled={Boolean(busy) || !provider.enabled}
onClick={() => act("sync")}
>
<RefreshCw size={16} aria-hidden />
</Button>
<Button
title={`Edit ${provider.name}`}
type="button"
variant="ghost"
color="gray"
size="small"
disabled={Boolean(busy)}
onClick={() => setEditing(true)}
>
<Pencil size={16} aria-hidden />
</Button>
<Button
title={`Remove ${provider.name}`}
type="button"
variant="ghost"
color="gray"
size="small"
loading={busy == "delete"}
disabled={Boolean(busy)}
onClick={() => act("delete")}
>
<Trash2 size={16} aria-hidden />
</Button>
</Row>
</Row>
);
}
@@ -0,0 +1,34 @@
import Span from "@moduletrace/twui/layout/Span";
import type { PublicHarnessProvider } from "@/src/types";
import relativeTime from "@/src/utils/relative-time";
type Props = {
provider: PublicHarnessProvider;
};
/**
* Where the provider's key comes from and how its last model sync went.
*/
export default function ProviderStatus({ provider }: Props) {
const key = provider.has_key
? "Key saved"
: provider.has_env_key
? `Key from ${provider.key_env}`
: provider.needs_key
? "Needs a key"
: "No key needed";
const sync = !provider.enabled
? "Turned off"
: provider.sync_error
? `Last sync failed: ${provider.sync_error}`
: provider.last_synced_at
? `Models synced ${relativeTime({ at: provider.last_synced_at }) == "now" ? "just now" : `${relativeTime({ at: provider.last_synced_at })} ago`}`
: "Models not synced yet";
return (
<Span className="text-[13px] leading-[18px] text-ink-secondary break-words">
{key} · {sync}
</Span>
);
}
@@ -0,0 +1,75 @@
import { useState } from "react";
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import { Plus } from "lucide-react";
import type { HarnessOverviewPayload, PublicHarnessProvider } from "@/src/types";
import SettingsGroup from "../../(partials)/settings-group";
import SettingsStatus from "../../(partials)/settings-status";
import ProviderForm from "./provider-form";
import ProviderRow from "./provider-row";
type Props = {
providers: PublicHarnessProvider[];
onChanged: (overview: HarnessOverviewPayload) => void;
};
/**
* Where models come from. Each change saves right away and refreshes the
* whole page's data.
*/
export default function ProvidersGroup({ providers, onChanged }: Props) {
const [adding, setAdding] = useState(false);
const [status, setStatus] = useState<{ error?: boolean; success?: boolean; msg?: string }>();
const changed = (overview: HarnessOverviewPayload, msg: string) => {
setStatus({ success: true, msg });
onChanged(overview);
};
return (
<SettingsGroup
title="Providers"
description="Where models come from. Keys are encrypted and never shown again. Model lists refresh every hour."
>
<Stack className="w-full gap-0! items-stretch">
{providers.map((provider) => (
<ProviderRow
key={provider.id}
provider={provider}
onChanged={changed}
onError={(msg) => setStatus({ error: true, msg })}
/>
))}
</Stack>
{adding ? (
<ProviderForm
onSaved={(overview, msg) => {
setAdding(false);
changed(overview, msg);
}}
onCancel={() => setAdding(false)}
/>
) : (
<Row className="gap-3! items-center justify-between">
<SettingsStatus status={status} />
<Button
title="Add a provider"
type="button"
variant="outlined"
color="gray"
size="small"
className="ml-auto"
onClick={() => {
setStatus(undefined);
setAdding(true);
}}
>
<Plus size={16} aria-hidden />
Add a provider
</Button>
</Row>
)}
</SettingsGroup>
);
}
@@ -0,0 +1,51 @@
import { useContext, useState } from "react";
import { AppContext } from "@/src/pages/__root";
import HarnessSettingsForm from "../(partials)/harness-settings-form";
import ProvidersGroup from "../(partials)/providers-group";
import postHarnessApi from "../(functions)/post-harness-api";
/**
* Providers first (where models come from), then what each harness uses.
* Both update from the fresh data every save returns.
*/
export default function ModelsSettingsSection() {
const { pageProps } = useContext(AppContext);
const [overview, setOverview] = useState(pageProps?.harness_overview);
/**
* Remounts the harness form when the data changes under it.
*/
const [version, setVersion] = useState(0);
const [notice, setNotice] = useState<string>();
if (!overview) return null;
const update = (next: typeof overview) => {
setOverview(next);
setNotice(undefined);
setVersion((prev) => prev + 1);
};
return (
<>
<ProvidersGroup providers={overview.providers} onChanged={update} />
<HarnessSettingsForm
key={version}
harnesses={overview.harnesses}
notice={notice}
save={async ({ settings }) => {
const res = await postHarnessApi({
path: "save-settings",
body: { settings },
});
if (res.success && res.singleRes) {
update(res.singleRes);
setNotice(res.msg || "Saved.");
}
return res;
}}
/>
</>
);
}
@@ -0,0 +1,54 @@
import { describe, expect, test } from "bun:test";
import changedHarnessSettings from "./changed-harness-settings";
const saved = {
text: { model_id: null, provider_id: null, fallback: true, enabled: true },
speech: { model_id: 4, provider_id: null, fallback: true, enabled: true, voice: "george" },
};
describe("changedHarnessSettings", () => {
test("is empty when nothing changed", () => {
expect(
changedHarnessSettings({ form: structuredClone(saved), saved }),
).toEqual([]);
});
test("lists only the harnesses that changed", () => {
expect(
changedHarnessSettings({
form: {
...saved,
speech: { ...saved.speech, voice: "sarah" },
},
saved,
}),
).toEqual([
{
harness: "speech",
model_id: 4,
provider_id: null,
fallback: true,
enabled: true,
voice: "sarah",
},
]);
});
test("lists a provider change", () => {
expect(
changedHarnessSettings({
form: { ...saved, text: { ...saved.text, provider_id: 2 } },
saved,
}),
).toEqual([{ harness: "text", model_id: null, provider_id: 2, fallback: true, enabled: true }]);
});
test("treats an empty voice as no voice", () => {
expect(
changedHarnessSettings({
form: { text: { ...saved.text, voice: "" } },
saved,
}),
).toEqual([]);
});
});
@@ -0,0 +1,35 @@
import type { HarnessKind } from "@/src/dict/harness-dict";
import type {
HarnessFormValue,
HarnessSettingsFormObject,
} from "@/src/types";
type Params = {
form: HarnessSettingsFormObject;
saved: HarnessSettingsFormObject;
};
/**
* The harness settings that differ from what is saved, ready to send.
*/
export default function changedHarnessSettings({ form, saved }: Params) {
return (Object.keys(form) as HarnessKind[]).flatMap((harness) => {
const value = form[harness];
if (!value || isSame({ a: value, b: saved[harness] })) return [];
return [{ harness, ...value }];
});
}
function isSame({ a, b }: { a: HarnessFormValue; b?: HarnessFormValue }) {
return (
Boolean(b) &&
Boolean(a.inherit) == Boolean(b!.inherit) &&
a.model_id == b!.model_id &&
(a.provider_id ?? null) == (b!.provider_id ?? null) &&
a.fallback == b!.fallback &&
a.enabled == b!.enabled &&
(a.voice || "") == (b!.voice || "")
);
}
@@ -0,0 +1,46 @@
import type { TWUISelectOptionObject } from "@moduletrace/twui/form/Select";
import type { HarnessModelOption } from "@/src/types";
export const AutoModelValue = "auto";
type Params = {
/**
* The models to offer, already narrowed to the picked provider.
*/
models: HarnessModelOption[];
model_id: number | null;
/**
* The picked provider's name; undefined for any provider.
*/
provider_name?: string;
};
/**
* The model picker's options: automatic (the best free model, on the
* picked provider if there is one), then every model that can do the job
* (free ones first, as the server sorts them).
*/
export default function harnessModelSelectOptions({
models,
model_id,
provider_name,
}: Params): TWUISelectOptionObject[] {
const has_free = models.some((model) => model.free);
return [
{
value: AutoModelValue,
title: provider_name
? has_free
? `Automatic (best free model on ${provider_name})`
: `Automatic (no free models on ${provider_name}: pick one)`
: "Automatic (best free model)",
default: !model_id,
},
...models.map((model) => ({
value: String(model.id),
title: `${model.name}${provider_name ? "" : ` · ${model.provider_name}`}${model.free && !/free/i.test(model.name) ? " · free" : ""}`,
default: model.id == model_id,
})),
];
}
@@ -0,0 +1,32 @@
import type {
HarnessOverview,
HarnessSettingsFormObject,
} from "@/src/types";
type Params = {
harnesses: HarnessOverview[];
};
/**
* The saved harness settings as form values.
*/
export default function toHarnessForm({
harnesses,
}: Params): HarnessSettingsFormObject {
return Object.fromEntries(
harnesses.map((harness) => [
harness.harness,
{
inherit: harness.inherit,
model_id: harness.model_id ?? null,
provider_id: harness.provider_id ?? null,
fallback: harness.fallback,
enabled: harness.enabled,
voice:
typeof harness.options.voice == "string"
? harness.options.voice
: undefined,
},
]),
);
}
@@ -0,0 +1,34 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import { SettingsDefaultPath } from "@/src/dict/settings-pages-dict";
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import ensureDefaultHarnessProvider from "@/src/functions/backend/db/harness/ensure-default-harness-provider";
import grabHarnessOverview from "@/src/functions/backend/db/harness/grab-harness-overview";
import grabSettingsServerProps from "../(functions)/grab-settings-server-props";
/**
* Super admins only; everyone else lands on the general settings.
*/
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
const settings = await grabSettingsServerProps(params);
if (!settings.props) return settings;
if (!(await grabSuperAdminUser({ req: params.req }))) {
return {
redirect: {
destination: SettingsDefaultPath,
permanent: false,
},
};
}
await ensureDefaultHarnessProvider();
return {
props: {
...settings.props,
harness_overview: await grabHarnessOverview(),
},
};
};
+16
View File
@@ -0,0 +1,16 @@
import ModelsSettingsSection from "./(sections)/models-settings-section";
import SettingsShell from "../(partials)/settings-shell";
import settingsPageMeta from "../(functions)/settings-page-meta";
export default function AdminSettingsModelsPage() {
return (
<SettingsShell
title="Models"
description="What your bots think, read, hear and speak with."
>
<ModelsSettingsSection />
</SettingsShell>
);
}
export const meta = settingsPageMeta({ label: "Models" });