Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217import { noop, useMutation } from '@tanstack/react-query'import { Link } from '@tanstack/react-router'import { api, json, read } from '../../../shared/api.ts'import { cn } from '../../../shared/cn.ts'import { lastError } from '../../../shared/errors.ts'import { useAppForm } from '../../../shared/form.tsx'import { ErrorAlert } from '../../../shared/ui/Alert.tsx'import { Badge } from '../../../shared/ui/Badge.tsx'import { Button } from '../../../shared/ui/Button.tsx'import { Code } from '../../../shared/ui/Code.tsx'import { undivided } from '../../../shared/ui/divided-rows.ts'import * as ItemList from '../../../shared/ui/ItemList.tsx'import { inlineForm, SettingRow, SettingsEmpty, SettingsGroup,} from '../../../shared/ui/SettingsPage.tsx'import { ToggleChip } from '../../../shared/ui/ToggleChip.tsx'import { type Capabilities, capabilities } from '../../models/capabilities.ts'import { ModelTable } from '../../models/components/ModelTable.tsx'import { useModelsChanged, usePluginsChanged } from '../hooks/changes.ts'import type { AdminModel } from '../lib/models.ts'import type { AdminPlugin } from '../queries.ts'
type Provider = AdminPlugin['providers'][number]
interface Props { instance: AdminPlugin /** The plugin's options as they are in its form, saved or not, for listing models. */ options: Record<string, unknown> provider: Provider models: AdminModel[]}
/** A provider's admin models, each opening its page, and the ways to add more. */export function ProviderModels(props: Props) { const modelsChanged = useModelsChanged() const add = useMutation({ mutationFn: (model: AdminModel) => read(api.admin.models.$post({}, json(model))), onSuccess: modelsChanged, }) const list = useMutation({ mutationFn: () => read( api.admin.plugins['list-models'].$post( {}, json({ package: props.instance.package, instanceId: props.instance.id, options: props.options, }), ), ), }) const listed = (list.data?.models ?? []).filter((model) => model.provider === props.provider.id) // A new model starts with the user role and the provider's default capabilities. const offer = (model: { id: string; name: string; capabilities: Capabilities }) => add.mutateAsync({ provider: props.provider.id, ...model, roles: ['user'], default: false }) /** Whether that model is on its way in, so a second tick doesn't add it twice. */ const adding = (id: string) => add.isPending && add.variables?.id === id const offered = new Set(props.models.map((model) => model.id)) const form = useAppForm({ defaultValues: { id: '' }, // Returning the add keeps Add model busy until it lands. A failure shows under the form. onSubmit: ({ value, formApi }) => { const id = value.id.trim() return offer({ id, name: id, capabilities: props.provider.defaultCapabilities }).then( () => formApi.reset(), noop, ) }, }) const name = props.provider.name return ( <> <SettingsGroup title={`Models for ${name}`} description="Each opens its page, where you set its name, capabilities, and roles." > <ModelList pluginId={props.instance.id} models={props.models} /> </SettingsGroup> <SettingsGroup title={`Add ${name} models`} description={addHint(props.provider)}> <ModelDefaults provider={props.provider} /> {listed.length > 0 && ( // Models the provider lists, to tick the ones to offer, over the field to type one. <ModelTable aria-label="Models to offer" models={listed} checkedIds={new Set(add.isPending && add.variables ? [add.variables.id] : [])} addedIds={offered} disabled={!props.provider.hasAdminKey} onToggle={(model) => { if (adding(model.id)) return void offer({ id: model.id, name: model.name, capabilities: model.capabilities, }).catch(noop) }} /> )} <form.AppForm> <form.Form className={cn(inlineForm, listed.length > 0 && undivided)}> <form.AppField name="id"> {(field) => ( <field.TextField aria-label={`Model ID for ${name}`} placeholder="Model ID" literal required /> )} </form.AppField> <form.SubmitButton disabled={!props.provider.hasAdminKey}>Add model</form.SubmitButton> {props.provider.listsModels && ( <Button tone="quiet" busy={list.isPending} onClick={() => list.mutate()}> Refresh model list </Button> )} </form.Form> </form.AppForm> <ErrorAlert error={lastError(add, list)} variant="card" /> </SettingsGroup> </> )}
interface ModelDefaultsProps { provider: Provider}
/** The provider's default capabilities field. */function ModelDefaults(props: ModelDefaultsProps) { const pluginsChanged = usePluginsChanged() const save = useMutation({ mutationFn: (defaults: Capabilities) => read( api.admin.providers[':id'].defaults.$put( { param: { id: props.provider.id } }, json(defaults), ), ), onSuccess: pluginsChanged, }) const defaults = (save.isPending && save.variables) || props.provider.defaultCapabilities return ( <SettingRow label="Default capabilities" description="Select the model capabilities that should be enabled by default for all models under this provider." issues={<ErrorAlert error={save.error} variant="field" />} > <fieldset aria-label="Default capabilities" className="flex gap-1.5"> {capabilities.map((capability) => ( <ToggleChip key={capability.key} pressed={defaults[capability.key]} onPressedChange={(pressed) => save.mutate({ ...defaults, [capability.key]: pressed })} > {capability.label} </ToggleChip> ))} </fieldset> </SettingRow> )}
/** What adding a provider's models takes, which depends on its key and whether it lists them. */function addHint(provider: Provider) { if (!provider.hasAdminKey) return "Add an admin key to offer this provider's models." if (provider.listsModels) return 'Pick from the models the provider lists, using the settings above as they are now, or type a model ID.' return 'Type the ID of a model to offer.'}
interface ModelListProps { pluginId: string models: AdminModel[]}
/** The provider's admin models, each opening its page, with what it can do and who gets it. */function ModelList(props: ModelListProps) { if (props.models.length === 0) return <SettingsEmpty>No models yet.</SettingsEmpty> return ( <ItemList.Root> {props.models.map((model) => { const can = capabilities .filter((capability) => model.capabilities[capability.key]) .map((capability) => capability.label) return ( <ItemList.LinkItem key={model.id}> <ItemList.Names title={ <> <Link to="/admin/plugins/$id/model" params={{ id: props.pluginId }} search={{ provider: model.provider, id: model.id }} > {model.name} </Link>{' '} {model.default && <Badge tone="good">Default</Badge>} </> } details={[ <Code key="id">{model.id}</Code>, can.length > 0 && can.join(', '), model.roles.length ? `For ${model.roles.join(', ')}` : 'No roles', ]} /> </ItemList.LinkItem> ) })} </ItemList.Root> )}