Files
cobble/src/pages/admin/bots/[bot_id]/(partials)/conversation-header.tsx
T
tbenandClaude Opus 5.5 1e7834ba7f Add a guided setup after signup, and a no-model notice in conversations
The first super admin now lands on Settings > Setup after signing up: a
skippable walk through the app name, connecting a model provider and
picking models, ending with where to go next. It stays under Settings to
come back to.

While a bot has no text model, its conversation shows a notice in the
thread (centred when empty): super admins get Connect a provider and
Guided setup, everyone else is told the super admin sets it up. The
models modal keeps it current.

The Models page's data and save logic move into useModelsOverview, shared
with the setup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:26:20 +01:00

171 lines
7.2 KiB
TypeScript

import { useContext, useEffect, useState } from "react";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import H1 from "@moduletrace/twui/layout/H1";
import Span from "@moduletrace/twui/layout/Span";
import Button from "@moduletrace/twui/layout/Button";
import Dropdown from "@moduletrace/twui/elements/Dropdown";
import useStatus from "@moduletrace/twui/hooks/useStatus";
import { CpuAi, EllipsisVertical, Monitor } from "lucide-react";
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import type { BotTextModel, BotWithLastMessage } from "@/src/types";
import { AppContext } from "@/src/pages/__root";
import WorkspaceAvatar from "@/src/components/general/workspace-avatar";
import RailDrawerButton from "@/src/layouts/bots/(partials)/rail-drawer-button";
import { BotComputersContext } from "../(data)/bot-computers-context";
import BotProfileModal from "./bot-profile-modal";
import ConversationHeaderMenu from "./conversation-header-menu";
import HeaderMetaItem from "./header-meta-item";
import BackButton from "@/src/components/general/back-button";
import AdminShield from "@/src/components/general/admin-shield";
type Props = {
bot: BotWithLastMessage;
/**
* The workspace the conversation is in: its name shows under the
* bot's, and the models modal edits its models.
*/
workspace?: BUN_SQLITE_COBBLE_WORKSPACES | null;
has_other_unread?: boolean;
/**
* The text model the bot runs on, kept by the conversation (which
* shows a notice while there's none).
*/
model?: BotTextModel | null;
onModelChange: (model: BotTextModel | null) => void;
};
/**
* 64px header. Under the name, what the bot runs on: its model and the
* computer it's on. On the right, one menu holding everything the header
* used to show as icons: sessions, a new session, the computer, the models
* and the bot's settings. Kept here so a save or a switch shows under the
* name at once.
*/
export default function ConversationHeader({
bot,
workspace,
has_other_unread,
model,
onModelChange,
}: Props) {
const { computers } = useContext(BotComputersContext);
const { pageProps } = useContext(AppContext);
/**
* The menu's toggle shows its change at once; a push (here or from
* another tab) brings the live bot's.
*/
const [is_admin, setIsAdmin] = useState(Boolean(bot.is_admin));
useEffect(() => {
setIsAdmin(Boolean(bot.is_admin));
}, [bot.is_admin]);
const { open: open_menu, setOpen: set_open_menu } = useStatus();
const computer = computers[0];
const computer_name = computer?.id
? computer.name || `Computer ${computer.id}`
: null;
const is_workspace = pageProps?.url?.pathname.includes(`/workspaces/`);
return (
<Row className="h-16 shrink-0 gap-2! md:gap-3! flex-nowrap items-center px-3 md:px-6 border-0 border-b border-solid border-hairline">
<RailDrawerButton />
{is_workspace ? <BackButton /> : null}
<BotProfileModal bot={bot} />
<Stack className="min-w-0 gap-0!">
<Row className="w-full min-w-0 gap-1.5! flex-nowrap">
<H1 className="min-w-0 text-lg! leading-6! tracking-normal! font-bold text-ink truncate line-clamp-1">
{bot.name}
</H1>
{workspace?.id && is_admin ? (
<AdminShield size={16} label={`Admin of ${workspace.name || "this workspace"}`} />
) : null}
</Row>
<Row className="flex-nowrap w-full">
{workspace?.id ? (
<Row className="min-w-0 gap-2! flex-nowrap">
<WorkspaceAvatar
image_media_id={workspace.image_media_id}
size="xs"
/>
<Span
size="smaller"
className="truncate hidden xl:block"
>
{workspace.name || "Workspace"}
</Span>
</Row>
) : null}
<Row className="min-w-0 gap-x-1! md:gap-x-3! gap-y-0! flex-nowrap grow">
<HeaderMetaItem
icon={CpuAi}
label="Model"
text={
model
? `${model.name} · ${model.provider_name}`
: "No model set up"
}
/>
{computer_name ? (
<HeaderMetaItem
icon={Monitor}
label="Computer"
text={computer_name}
/>
) : null}
</Row>
</Row>
</Stack>
<Row className="ml-auto shrink-0 flex-nowrap justify-end">
<Dropdown
position="bottom-right"
externalOpen={open_menu}
externalSetOpen={set_open_menu}
target={
<Button
title="Bot menu"
aria-label="Bot menu"
aria-haspopup="menu"
type="button"
variant="ghost"
color="text"
className="shrink-0 p-2! 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"
>
<EllipsisVertical size={18} aria-hidden />
</Button>
}
>
<ConversationHeaderMenu
bot={bot}
workspace={workspace}
has_other_unread={has_other_unread}
computer_name={computer_name}
model_name={model?.name}
on_model_saved={(harnesses) => {
const current = harnesses.find(
(h) => h.harness == "text",
)?.current;
onModelChange(
current
? {
name: current.name,
provider_name: current.provider_name,
}
: null,
);
}}
on_open={() => set_open_menu(false)}
is_admin={is_admin}
on_admin_changed={setIsAdmin}
/>
</Dropdown>
</Row>
</Row>
);
}