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]>
This commit is contained in:
@@ -8,7 +8,7 @@ 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 { BotWithLastMessage } from "@/src/types";
|
||||
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";
|
||||
@@ -27,6 +27,12 @@ type Props = {
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,10 +46,11 @@ export default function ConversationHeader({
|
||||
bot,
|
||||
workspace,
|
||||
has_other_unread,
|
||||
model,
|
||||
onModelChange,
|
||||
}: Props) {
|
||||
const { computers } = useContext(BotComputersContext);
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const [model, setModel] = useState(pageProps?.bot_text_model);
|
||||
/**
|
||||
* The menu's toggle shows its change at once; a push (here or from
|
||||
* another tab) brings the live bot's.
|
||||
@@ -143,7 +150,7 @@ export default function ConversationHeader({
|
||||
(h) => h.harness == "text",
|
||||
)?.current;
|
||||
|
||||
setModel(
|
||||
onModelChange(
|
||||
current
|
||||
? {
|
||||
name: current.name,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, type RefObject } from "react";
|
||||
import { useMemo, type ReactNode, type RefObject } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import isAwaitingReply from "../(utils)/is-awaiting-reply";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type {
|
||||
@@ -37,6 +38,11 @@ type Props = {
|
||||
* The conversation went on in another session: no reply is due here.
|
||||
*/
|
||||
moved_on?: boolean;
|
||||
/**
|
||||
* Shown after the messages, or in the middle of an empty thread, e.g.
|
||||
* that the bot has no model to answer with.
|
||||
*/
|
||||
notice?: ReactNode;
|
||||
/**
|
||||
* Quotes text selected in a message in the composer.
|
||||
*/
|
||||
@@ -78,6 +84,7 @@ export default function MessageThread({
|
||||
receiving,
|
||||
has_task,
|
||||
moved_on,
|
||||
notice,
|
||||
onQuote,
|
||||
older,
|
||||
}: Props) {
|
||||
@@ -97,7 +104,10 @@ export default function MessageThread({
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-label={`Conversation with ${bot.name}`}
|
||||
className="w-full max-w-container mx-auto gap-4! items-stretch px-3 md:px-6 pt-6 pb-6"
|
||||
className={twMerge(
|
||||
"w-full max-w-container mx-auto gap-4! items-stretch px-3 md:px-6 pt-6 pb-6",
|
||||
notice && !messages.length && "grow justify-center",
|
||||
)}
|
||||
>
|
||||
{older?.has_more ? (
|
||||
<OlderMessagesLoader
|
||||
@@ -121,6 +131,7 @@ export default function MessageThread({
|
||||
group={group}
|
||||
/>
|
||||
))}
|
||||
{notice}
|
||||
<BotThinkingRow
|
||||
bot={bot}
|
||||
progress={progress}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useContext } 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 { ArrowRight, CpuAi } from "lucide-react";
|
||||
import { SettingsModelsPath, SettingsSetupPath } from "@/src/dict/settings-pages-dict";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import IconTile from "@/src/components/general/icon-tile";
|
||||
import isPassThroughUser from "@/src/utils/rbac/is-pass-through-user";
|
||||
|
||||
type Props = {
|
||||
bot_name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shown in the conversation while the bot has no text model to run on
|
||||
* (nothing would answer a message). A super admin is sent to connect a
|
||||
* provider, or to the guided setup; anyone else is told who can.
|
||||
*/
|
||||
export default function NoModelNotice({ bot_name }: Props) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const can_set_up = isPassThroughUser({ user_types: pageProps?.user_types });
|
||||
|
||||
return (
|
||||
<Stack
|
||||
role="status"
|
||||
className="w-full max-w-[440px] mx-auto gap-3! items-start rounded-default border border-solid border-hairline bg-surface px-5 py-4"
|
||||
>
|
||||
<Row className="gap-3! flex-nowrap items-center">
|
||||
<IconTile Icon={CpuAi} />
|
||||
<Span className="text-sm font-bold leading-5 text-ink">
|
||||
{can_set_up ? "Connect a model provider" : "No model set up yet"}
|
||||
</Span>
|
||||
</Row>
|
||||
<Span className="text-[13px] leading-5 text-ink-secondary">
|
||||
{can_set_up
|
||||
? `${bot_name} has no model to think with, so nothing would answer yet. Add a provider key (OpenRouter has free models) and pick what your bots run on.`
|
||||
: `${bot_name} has no model to think with, so nothing would answer yet. The super admin connects one in Settings.`}
|
||||
</Span>
|
||||
{can_set_up ? (
|
||||
<Row className="w-full gap-2! justify-end">
|
||||
<Button
|
||||
title="Open the guided setup"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
href={SettingsSetupPath}
|
||||
>
|
||||
Guided setup
|
||||
</Button>
|
||||
<Button
|
||||
title="Connect a provider"
|
||||
type="button"
|
||||
size="small"
|
||||
href={SettingsModelsPath}
|
||||
afterIcon={<ArrowRight size={14} aria-hidden />}
|
||||
>
|
||||
Connect a provider
|
||||
</Button>
|
||||
</Row>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import useConversationHistory from "../(hooks)/use-conversation-history";
|
||||
import mergeLoadedHistory from "../(utils)/merge-loaded-history";
|
||||
import LoadingLine from "@/src/components/general/loading-line";
|
||||
import SessionMovedBar from "../(partials)/session-moved-bar";
|
||||
import NoModelNotice from "../(partials)/no-model-notice";
|
||||
import syncSessionQuery from "../(utils)/sync-session-query";
|
||||
import announceShownSession from "@/src/functions/frontend/announce-shown-session";
|
||||
|
||||
@@ -85,6 +86,11 @@ export default function Conversation() {
|
||||
*/
|
||||
const [moved_to_session_id, setMovedToSessionId] = useState<number | null>(null);
|
||||
const [has_other_unread, setHasOtherUnread] = useState(false);
|
||||
/**
|
||||
* The text model the bot runs on here; none shows a notice in the
|
||||
* thread. A save in the models modal updates it.
|
||||
*/
|
||||
const [text_model, setTextModel] = useState(pageProps?.bot_text_model || null);
|
||||
const [reply_to, setReplyTo] = useState<BotMessageWithAttachments | null>(
|
||||
null,
|
||||
);
|
||||
@@ -391,6 +397,8 @@ export default function Conversation() {
|
||||
bot={live_bot}
|
||||
workspace={workspace}
|
||||
has_other_unread={has_other_unread}
|
||||
model={text_model}
|
||||
onModelChange={setTextModel}
|
||||
/>
|
||||
{history.loading ? (
|
||||
<LoadingLine label="Loading the conversation" />
|
||||
@@ -409,6 +417,11 @@ export default function Conversation() {
|
||||
receiving={pending_messages.length > 0}
|
||||
has_task={Boolean(progress.task_id)}
|
||||
moved_on={Boolean(moved_to_session_id)}
|
||||
notice={
|
||||
!text_model && !history.loading ? (
|
||||
<NoModelNotice bot_name={live_bot.name || "This bot"} />
|
||||
) : null
|
||||
}
|
||||
older={older}
|
||||
onQuote={({ text }) =>
|
||||
composer.current?.insertQuote({ text })
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import type { HarnessOverviewPayload } from "@/src/types";
|
||||
import postHarnessApi from "../(functions)/post-harness-api";
|
||||
import type changedHarnessSettings from "../(utils)/changed-harness-settings";
|
||||
|
||||
/**
|
||||
* The providers and harness settings a page edits (the Models settings
|
||||
* and the setup), starting from the page's props and updated from the
|
||||
* fresh data every save returns. `version` remounts the harness form when
|
||||
* the data changes under it; `notice` is the last save's message, kept
|
||||
* across that remount.
|
||||
*/
|
||||
export default function useModelsOverview() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const [overview, setOverview] = useState(pageProps?.harness_overview);
|
||||
const [version, setVersion] = useState(0);
|
||||
const [notice, setNotice] = useState<string>();
|
||||
|
||||
const update = (next: HarnessOverviewPayload) => {
|
||||
setOverview(next);
|
||||
setNotice(undefined);
|
||||
setVersion((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const saveSettings = async ({
|
||||
settings,
|
||||
}: {
|
||||
settings: ReturnType<typeof changedHarnessSettings>;
|
||||
}) => {
|
||||
const res = await postHarnessApi({
|
||||
path: "save-settings",
|
||||
body: { settings },
|
||||
});
|
||||
|
||||
if (res.success && res.singleRes) {
|
||||
update(res.singleRes);
|
||||
setNotice(res.msg || "Saved.");
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
return { overview, version, notice, update, saveSettings };
|
||||
}
|
||||
@@ -1,30 +1,16 @@
|
||||
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";
|
||||
import useModelsOverview from "../(hooks)/use-models-overview";
|
||||
|
||||
/**
|
||||
* 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>();
|
||||
const { overview, version, notice, update, saveSettings } = useModelsOverview();
|
||||
|
||||
if (!overview) return null;
|
||||
|
||||
const update = (next: typeof overview) => {
|
||||
setOverview(next);
|
||||
setNotice(undefined);
|
||||
setVersion((prev) => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProvidersGroup providers={overview.providers} onChanged={update} />
|
||||
@@ -32,19 +18,7 @@ export default function ModelsSettingsSection() {
|
||||
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;
|
||||
}}
|
||||
save={saveSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Bot, Monitor, SlidersHorizontal, Users, Variable } from "lucide-react";
|
||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||
import { SettingsComputersPath, SettingsDefaultPath } from "@/src/dict/settings-pages-dict";
|
||||
import type { SetupLink } from "../(partials)/setup-link-row";
|
||||
|
||||
/**
|
||||
* Where to go once the models are in: the rest is set up from the pages
|
||||
* themselves.
|
||||
*/
|
||||
export const SetupNextLinks: SetupLink[] = [
|
||||
{
|
||||
title: "Meet the coordinator",
|
||||
description: "Ask it to put a crew together, or just say hello.",
|
||||
href: AdminHomePath,
|
||||
Icon: Bot,
|
||||
},
|
||||
{
|
||||
title: "Computers",
|
||||
description: "The main computer starts by itself. Add more, or check it's running.",
|
||||
href: SettingsComputersPath,
|
||||
Icon: Monitor,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
description: "Add the people who'll work with your bots.",
|
||||
href: "/admin/settings/users",
|
||||
Icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
description: "Keys and settings your bots' tools need on their computers.",
|
||||
href: "/admin/settings/environment",
|
||||
Icon: Variable,
|
||||
},
|
||||
{
|
||||
title: "General",
|
||||
description: "The app's name and icon, run limits and how long things are kept.",
|
||||
href: SettingsDefaultPath,
|
||||
Icon: SlidersHorizontal,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* The setup's steps, in order. Every one can be skipped.
|
||||
*/
|
||||
export const SetupSteps = [
|
||||
{ key: "welcome", title: "Welcome" },
|
||||
{ key: "provider", title: "Provider" },
|
||||
{ key: "models", title: "Models" },
|
||||
{ key: "ready", title: "Ready" },
|
||||
] as const;
|
||||
|
||||
export type SetupStepKey = (typeof SetupSteps)[number]["key"];
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import IconTile from "@/src/components/general/icon-tile";
|
||||
import SettingsListRow from "../../(partials)/settings-list-row";
|
||||
|
||||
export type SetupLink = {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
Icon: LucideIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* One place to go on from the setup.
|
||||
*/
|
||||
export default function SetupLinkRow({ title, description, href, Icon }: SetupLink) {
|
||||
return (
|
||||
<SettingsListRow
|
||||
avatar={<IconTile Icon={Icon} />}
|
||||
title={title}
|
||||
href={href}
|
||||
subtitle={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
number: number;
|
||||
current: boolean;
|
||||
done: boolean;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One step in the setup's trail: its number and name. Any step can be
|
||||
* opened, in any order.
|
||||
*/
|
||||
export default function SetupStepItem({ title, number, current, done, onSelect }: Props) {
|
||||
return (
|
||||
<Button
|
||||
title={`Step ${number}: ${title}`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
aria-current={current ? "step" : undefined}
|
||||
className={twMerge(
|
||||
"gap-2! px-2! py-1! rounded-default-sm text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken focus-visible:ring-2 focus-visible:ring-ink",
|
||||
current && "text-ink dark:text-ink font-bold",
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Span
|
||||
className={twMerge(
|
||||
"size-5 shrink-0 flex items-center justify-center rounded-full border border-solid border-hairline-strong text-[11px] leading-none",
|
||||
(current || done) && "bg-ink text-on-ink border-ink",
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-5">{title}</Span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||
|
||||
type Props = {
|
||||
is_first: boolean;
|
||||
is_last: boolean;
|
||||
onBack: () => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Under each step: skip the whole setup, go back, or go on. The last step
|
||||
* goes to the bots.
|
||||
*/
|
||||
export default function SetupStepNav({ is_first, is_last, onBack, onNext }: Props) {
|
||||
return (
|
||||
<Row className="w-full gap-2! items-center justify-between py-6">
|
||||
{is_last ? (
|
||||
<Row />
|
||||
) : (
|
||||
<Button
|
||||
title="Skip the setup"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
href={AdminHomePath}
|
||||
>
|
||||
Skip setup
|
||||
</Button>
|
||||
)}
|
||||
<Row className="gap-2! ml-auto">
|
||||
{is_first ? null : (
|
||||
<Button
|
||||
title="Back"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
beforeIcon={<ArrowLeft size={14} aria-hidden />}
|
||||
onClick={onBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{is_last ? (
|
||||
<Button
|
||||
title="Go to your bots"
|
||||
type="button"
|
||||
size="small"
|
||||
href={AdminHomePath}
|
||||
afterIcon={<ArrowRight size={14} aria-hidden />}
|
||||
>
|
||||
Start chatting
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
title="Next step"
|
||||
type="button"
|
||||
size="small"
|
||||
afterIcon={<ArrowRight size={14} aria-hidden />}
|
||||
onClick={onNext}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import { SetupSteps } from "../(data)/setup-steps";
|
||||
import SetupStepItem from "./setup-step-item";
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
onSelect: (index: number) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Where the setup is: every step, the current one marked.
|
||||
*/
|
||||
export default function SetupStepsTrail({ index, onSelect }: Props) {
|
||||
return (
|
||||
<Row
|
||||
aria-label="Setup steps"
|
||||
className="w-full gap-1! md:gap-2! pt-6 pb-2 border-0 border-b border-solid border-hairline"
|
||||
>
|
||||
{SetupSteps.map((step, i) => (
|
||||
<SetupStepItem
|
||||
key={step.key}
|
||||
title={step.title}
|
||||
number={i + 1}
|
||||
current={i == index}
|
||||
done={i < index}
|
||||
onSelect={() => onSelect(i)}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { CircleCheck, CircleAlert } from "lucide-react";
|
||||
import type { HarnessOverview } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
text?: HarnessOverview;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the bots have a model to think with yet, and which.
|
||||
*/
|
||||
export default function SetupTextModelStatus({ text }: Props) {
|
||||
const current = text?.current;
|
||||
|
||||
return (
|
||||
<Row className="w-full gap-2! flex-nowrap items-start rounded-default border border-solid border-hairline bg-surface px-4 py-3">
|
||||
{current ? (
|
||||
<CircleCheck size={16} aria-hidden className="shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<CircleAlert size={16} aria-hidden className="shrink-0 mt-0.5" />
|
||||
)}
|
||||
<Span className="text-[13px] leading-5 text-ink">
|
||||
{current
|
||||
? `Your bots think with ${current.name || current.slug} on ${current.provider_name}.`
|
||||
: "Your bots have no model to think with yet. Add a key to a provider (OpenRouter has free models) or add another provider."}
|
||||
</Span>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useContext, useState } from "react";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import SettingsList from "../../(partials)/settings-list";
|
||||
import InstallNameForm from "../../general/(partials)/install-name-form";
|
||||
import HarnessSettingsForm from "../../models/(partials)/harness-settings-form";
|
||||
import ProvidersGroup from "../../models/(partials)/providers-group";
|
||||
import useModelsOverview from "../../models/(hooks)/use-models-overview";
|
||||
import { SetupSteps } from "../(data)/setup-steps";
|
||||
import { SetupNextLinks } from "../(data)/setup-next-links";
|
||||
import SetupLinkRow from "../(partials)/setup-link-row";
|
||||
import SetupStepNav from "../(partials)/setup-step-nav";
|
||||
import SetupStepsTrail from "../(partials)/setup-steps-trail";
|
||||
import SetupTextModelStatus from "../(partials)/setup-text-model-status";
|
||||
|
||||
/**
|
||||
* The guided setup a new install opens on after the super admin signs
|
||||
* up, and that stays under Settings to come back to: name the app,
|
||||
* connect a provider, pick the models, then where to go next. Each step
|
||||
* saves on its own and any can be skipped; the provider and model steps
|
||||
* share their data, so a key added shows in the models at once.
|
||||
*/
|
||||
export default function SetupSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const { overview, version, notice, update, saveSettings } = useModelsOverview();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
if (!overview) return null;
|
||||
|
||||
const step = SetupSteps[index]!.key;
|
||||
const text = overview.harnesses.find((harness) => harness.harness == "text");
|
||||
|
||||
const go = (next: number) => {
|
||||
setIndex(Math.max(0, Math.min(SetupSteps.length - 1, next)));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SetupStepsTrail index={index} onSelect={go} />
|
||||
{step == "welcome" ? (
|
||||
<>
|
||||
<SettingsGroup title={`Welcome to ${SiteData["SiteName"]}`}>
|
||||
<Span className="text-sm leading-6 text-ink">
|
||||
{`A few steps get your crew working: connect where models come from, then pick what your bots think, see, hear and speak with. Every step saves on its own, and you can skip any of them. This setup stays under Settings to come back to.`}
|
||||
</Span>
|
||||
</SettingsGroup>
|
||||
<SettingsGroup
|
||||
title="App name"
|
||||
description="The name this install goes by when it's added to a home screen or app list. Optional."
|
||||
>
|
||||
<InstallNameForm install_name={pageProps?.install_name} />
|
||||
</SettingsGroup>
|
||||
</>
|
||||
) : null}
|
||||
{step == "provider" ? (
|
||||
<>
|
||||
<SettingsGroup
|
||||
title="Connect a provider"
|
||||
description="Your bots need at least one model provider. OpenRouter is already added: give it a key and its free models work right away."
|
||||
>
|
||||
<SetupTextModelStatus text={text} />
|
||||
</SettingsGroup>
|
||||
<ProvidersGroup providers={overview.providers} onChanged={update} />
|
||||
</>
|
||||
) : null}
|
||||
{step == "models" ? (
|
||||
<>
|
||||
<SettingsGroup
|
||||
title="Pick models"
|
||||
description="Text picks a free model by itself until you choose one. Transcription, Speech and Embeddings (knowledgebase search) need a model picked before they work."
|
||||
>
|
||||
<SetupTextModelStatus text={text} />
|
||||
</SettingsGroup>
|
||||
<HarnessSettingsForm
|
||||
key={version}
|
||||
harnesses={overview.harnesses}
|
||||
notice={notice}
|
||||
save={saveSettings}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{step == "ready" ? (
|
||||
<>
|
||||
<SettingsGroup title="You're set">
|
||||
<SetupTextModelStatus text={text} />
|
||||
</SettingsGroup>
|
||||
<SettingsGroup
|
||||
title="Next"
|
||||
description="Everything else is set up from its own page, whenever you need it."
|
||||
>
|
||||
<SettingsList>
|
||||
{SetupNextLinks.map((link) => (
|
||||
<SetupLinkRow key={link.href} {...link} />
|
||||
))}
|
||||
</SettingsList>
|
||||
</SettingsGroup>
|
||||
</>
|
||||
) : null}
|
||||
<SetupStepNav
|
||||
is_first={index == 0}
|
||||
is_last={index == SetupSteps.length - 1}
|
||||
onBack={() => go(index - 1)}
|
||||
onNext={() => go(index + 1)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import { server as modelsServer } from "../models/index.server";
|
||||
|
||||
/**
|
||||
* Super admins only, with the same data as the Models page: the setup's
|
||||
* provider and model steps edit it.
|
||||
*/
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) =>
|
||||
await modelsServer(params);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import SetupSection from "./(sections)/setup-section";
|
||||
import SettingsShell from "../(partials)/settings-shell";
|
||||
import settingsPageMeta from "../(functions)/settings-page-meta";
|
||||
|
||||
export default function AdminSettingsSetupPage() {
|
||||
return (
|
||||
<SettingsShell
|
||||
title="Setup"
|
||||
description={`Get ${SiteData["SiteName"]} ready to work, a step at a time.`}
|
||||
>
|
||||
<SetupSection />
|
||||
</SettingsShell>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta = settingsPageMeta({ label: "Setup" });
|
||||
@@ -1,5 +1,5 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||
import { SettingsSetupPath } from "@/src/dict/settings-pages-dict";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams, SignupFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
@@ -42,8 +42,12 @@ export default async function submitSignupForm({
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The first (super admin) account opens on the guided setup, which
|
||||
* can be skipped.
|
||||
*/
|
||||
if (res.success) {
|
||||
window.location.pathname = AdminHomePath;
|
||||
window.location.pathname = SettingsSetupPath;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
|
||||
Reference in New Issue
Block a user