import type { ModelConnectionAccessResponse } from "@opengeni/sdk";
import { useNavigate } from "../../lib/navigation";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
type ComponentProps,
type ReactNode,
} from "react";
import { Field, TextInput } from "../../../ui/field";
import { FormDialog } from "../../../ui/form-dialog";
import { FlushFormPage } from "../../../ui/flush-form-page";
import { userErrorText } from "../../../ui/api-error";
import type { ModelsView } from "../../lib/models-route";
import { returnToSearch, type ReturnTo } from "../../lib/return-to";
import { ModelSettingsPlaceholder } from "./model-settings-placeholder";
/**
* Where a Models page lives: Organization settings > Models, opened through
* `anchorWorkspaceId` (the settings URL's workspace), and the workspace whose
* model page it is or was opened from, if any (`?workspace=`).
*/
export type ModelsScope = {
anchorWorkspaceId: string;
workspaceId?: string | undefined;
};
/* ----------------------------------------------------------------------------
Shared pieces of Organization settings > Models: provider marks, small
menus, the flush form page and the URL state that says which list, account
page or form is showing.
-------------------------------------------------------------------------- */
/**
* The name of the list a sub-page's back link returns to: "Models", or the
* workspace's name for pages opened from a workspace's model page.
*/
const ModelsListLabelContext = createContext("Models");
export const ModelsListLabelProvider = ModelsListLabelContext.Provider;
export function useModelsListLabel(): string {
return useContext(ModelsListLabelContext);
}
// The provider logos live in their own light module so the onboarding model
// step can show them without the Models page graph.
export { ProviderMark, ProviderTile, type ModelProviderId } from "./provider-mark";
/**
* Who an account is for, as the one tag on its row and page: the
* organization's workspaces (all, or the ones it was limited to), this
* workspace, or only the person who connected it.
*/
export interface ModelsScopeLabels {
/** "Shared by Acme": an organization account whose "Available in" this viewer can't read. */
organization: string;
/** "Everyone in Acme": an organization account every workspace can use. */
everyone: string;
/** "Selected workspaces": an organization account limited to some workspaces. */
selected: string;
/** "No workspaces": an organization account that no workspace can use yet. */
none: string;
/** "Design preview only", "This workspace only", or "Personal workspace only". */
workspace: string;
/** "Only you". */
user: string;
}
export function modelsScopeLabels(
organizationName: string,
personal: boolean,
/** Names the workspace in its own accounts' tag ("Design preview only"). */
workspaceName?: string,
): ModelsScopeLabels {
return {
organization: `Shared by ${organizationName}`,
everyone: `Everyone in ${organizationName}`,
selected: "Selected workspaces",
none: "No workspaces",
workspace: personal
? "Personal workspace only"
: workspaceName
? `${workspaceName} only`
: "This workspace only",
user: "Only you",
};
}
/**
* The tag of an organization account, from its "Available in": everyone,
* one named workspace, selected workspaces or people, or none. "Shared by
* Acme" is never guessed while the policy is unknown.
*/
export function organizationReachLabel(
labels: ModelsScopeLabels,
access: ModelConnectionAccessResponse | null | undefined,
): ReactNode {
if (!access) return ;
const { policy, personalWorkspacesSupported } = access;
if (policy.allowedPeople) return policy.allowedPeople.length ? "Selected people" : "No one";
const personal = personalWorkspacesSupported && policy.allowPersonalWorkspaces;
if (policy.allowedWorkspaces === null && (!personalWorkspacesSupported || personal))
return labels.everyone;
// A workspace that connected the account always keeps it.
const reached =
policy.allowedWorkspaces === null
? null
: [...new Set([...policy.allowedWorkspaces, ...(access.localWorkspaceIds ?? [])])];
if (reached !== null && reached.length === 0 && !personal) return labels.none;
if (reached?.length === 1 && !personal) {
const only = access.workspaces.find((workspace) => workspace.id === reached[0]);
if (only) return `${only.name} only`;
}
return labels.selected;
}
/** "Not in use", in the usage column of an account new work here doesn't use. */
export const NOT_IN_USE = Not in use;
/** Who an account in the organization's list reaches, as the list reports it. */
export interface AccountReach {
ownerWorkspaceId: string | null;
everyWorkspace: boolean;
workspaceIds: readonly string[];
personalWorkspaces: boolean;
people: number | null;
}
/**
* The tag of an account in the organization's list, from the reach the list
* reports: "Belongs to Design" for a workspace's own account, else everyone,
* one or two named workspaces, a count of workspaces or of chosen people.
*/
export function accountReachLabel(
labels: ModelsScopeLabels,
reach: AccountReach,
workspaceName: (id: string) => string | undefined,
): string {
if (reach.ownerWorkspaceId !== null) {
const owner = workspaceName(reach.ownerWorkspaceId);
return owner ? `Belongs to ${owner}` : "Belongs to a workspace";
}
if (reach.people !== null)
return reach.people === 0
? "No one"
: reach.people === 1
? "1 person"
: `${reach.people} people`;
if (reach.everyWorkspace) return reach.personalWorkspaces ? labels.everyone : "Shared workspaces";
const names = reach.workspaceIds.map((id) => workspaceName(id) ?? null);
const personal = reach.personalWorkspaces ? ["Personal workspaces"] : [];
if (names.length === 0) return personal[0] ?? labels.none;
if (names.length === 1 && personal.length === 0 && names[0]) return `${names[0]} only`;
if (names.length <= 2 && names.every((name) => name !== null))
return [...(names as string[]), ...personal].join(", ");
return `${names.length} workspaces${personal.length ? " and Personal" : ""}`;
}
/** A full-page form with a back link and a sticky Cancel + primary footer. */
export function ModelsFormPage({
backLabel,
...props
}: Omit, "backLabel"> & {
backLabel?: string | undefined;
}) {
const listLabel = useModelsListLabel();
return ;
}
/**
* Who pays, in one or two words, for the muted part of a model control:
* "GPT-6 Astra" + "Codex". API-key models name their provider.
*/
export function payerShortLabel(row: { billingClass: string; providerLabel: string }): string {
switch (row.billingClass) {
case "codex_subscription":
return "Codex";
case "claude_subscription":
return "Claude";
case "supergrok_subscription":
return "SuperGrok";
case "opengeni_credits":
return "Opengeni credits";
case "byok":
case "organization_byok":
return row.providerLabel;
default:
return row.providerLabel;
}
}
/** Who pays for a model, in product words: "Codex plan", "Opengeni credits". */
export function payerLabel(billingClass: string, fallback?: string): string {
switch (billingClass) {
case "codex_subscription":
return "Codex plan";
case "claude_subscription":
return "Claude plan";
case "supergrok_subscription":
return "SuperGrok plan";
case "opengeni_credits":
return "Opengeni credits";
case "byok":
return "Workspace API key";
case "organization_byok":
return "Organization API key";
default:
return fallback ?? "Provider account";
}
}
/** "3 usage limit resets", or nothing. */
export function resetsLabel(count: number | null | undefined): string | null {
if (typeof count !== "number" || count <= 0) return null;
return count === 1 ? "1 usage limit reset" : `${count} usage limit resets`;
}
/** One line for a failed action: advice for an API error, never its raw message. */
export function errorText(error: unknown, fallback: string): string {
return userErrorText(error, fallback);
}
/* Which page of Organization settings > Models is showing lives in the URL (lib/models-route). */
export interface ModelsNavigation {
account: string | undefined;
view: ModelsView | undefined;
/**
* Where this page was opened from in another scope. It stays in the URL
* while an account and its forms are open; the list drops it.
*/
returnTo: ReturnTo | undefined;
/** Opens an account's page, or the list with `undefined`. `from` crosses scopes. */
openAccount: (account: string | undefined, from?: ReturnTo) => void;
/** Opens a form page, optionally for an account. */
openView: (view: ModelsView | undefined, account?: string) => void;
/** Opens a workspace's model page, or the organization's list with `undefined`. */
openWorkspace: (workspaceId: string | undefined, account?: string, view?: ModelsView) => void;
/** Returns to where a cross-scope link came from. */
goBack: (returnTo: ReturnTo) => void;
}
export function useModelsNavigation(
scope: ModelsScope,
current: {
account?: string | undefined;
view?: ModelsView | undefined;
returnTo?: ReturnTo | undefined;
},
): ModelsNavigation {
const navigate = useNavigate();
const currentReturnTo = current.returnTo;
const go = useCallback(
(search: {
account?: string | undefined;
view?: ModelsView | undefined;
from?: ReturnTo | undefined;
/** Null leaves the workspace page for the organization's list. */
workspace?: string | null | undefined;
}) => {
const from = search.from ?? (search.account ? currentReturnTo : undefined);
const workspace = search.workspace === undefined ? scope.workspaceId : search.workspace;
const next = {
section: "models" as const,
...(workspace ? { workspace } : {}),
...(search.account ? { account: search.account } : {}),
...(search.view ? { view: search.view } : {}),
...returnToSearch(from),
};
void navigate({
to: "/workspaces/$workspaceId/organization",
params: { workspaceId: scope.anchorWorkspaceId },
search: next,
});
},
[navigate, scope.anchorWorkspaceId, scope.workspaceId, currentReturnTo],
);
return {
account: current.account,
view: current.view,
returnTo: currentReturnTo,
openAccount: useCallback((account, from) => go({ account, from }), [go]),
openView: useCallback((view, account) => go({ account, view }), [go]),
openWorkspace: useCallback(
(workspace, account, view) => go({ workspace: workspace ?? null, account, view }),
[go],
),
goBack: useCallback((returnTo) => void navigate({ href: returnTo.path }), [navigate]),
};
}
/** Rename an account: a one-field prompt. `onSave` throws a user-facing error. */
export function RenameAccountDialog({
open,
onOpenChange,
name,
label,
provider,
onSave,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
/** The name shown today. */
name: string;
/** The saved label (empty when the name falls back to the email). */
label: string | null | undefined;
provider: string;
onSave: (label: string) => Promise;
}) {
const [value, setValue] = useState(label ?? "");
useEffect(() => {
if (open) setValue(label ?? "");
}, [open, label]);
const tooLong = value.trim().length > 64;
return (
{
await onSave(value);
}}
onSubmitted={() => onOpenChange(false)}
>
setValue(event.target.value)}
/>
);
}