Model Selector
ngx-prompt-kit original — not part of ibelick/prompt-kit
A composer's model switcher: a compact trigger pill that opens a wide panel with a spartan input-group search, a maker rail, admin-curated sections, price tiers and credit estimates. On phones it becomes a bottom sheet with maker chips, which closes when dragged down by its handle or title.
Examples
In a composer
The panel opens upward, aligned to and as wide as the composer (via [anchor]). Try typing “code”, the arrow keys and Enter, or resize below 768px for the bottom sheet.
<div #composer class="rounded-3xl border p-3">
<textarea …></textarea>
<div class="flex items-center gap-1.5">
<div class="grow"></div>
<pk-model-selector
[models]="models"
[sections]="sections"
[(value)]="modelId"
[anchor]="composer"
(selected)="onSelected($event)"
>
<div pkModelSelectorFooter>
<span>Credits are an estimate for a typical reply. Longer answers cost more.</span>
<span class="text-foreground font-medium">8,406 credits left</span>
</div>
</pk-model-selector>
</div>
</div>
// models: map your API type onto SelectorModel
models = api.map((m) => ({
id: m.id,
name: m.name,
maker: m.maker,
iconUrl: providerIconUrl({ id: m.id }),
makerIconUrl: makerIconUrl({ id: m.id }),
description: m.description,
capabilities: m.capabilities,
priceTier: priceTier(m.inputPricePer1M, m.outputPricePer1M),
costLabel: `≈ ${m.credits} credits`,
}));Inline, below a centred composer
With [inline], the desktop panel opens in the page's flow instead of floating: the page renders inlinePortal through a cdkPortalOutlet where the panel belongs, here in place of the suggestions under an empty chat's composer. Escape and a click outside close it. Phones still get the bottom sheet.
<div #composer class="rounded-3xl border p-3">
<textarea …></textarea>
<pk-model-selector
#selector
[models]="models"
[(value)]="modelId"
[anchor]="composer"
[inline]="true"
/>
</div>
<!-- The panel opens here, in the page's flow, while it is open. -->
@if (selector.inlinePortal(); as portal) {
<ng-template [cdkPortalOutlet]="portal" />
} @else {
<suggestions />
}Installation
Add the model-selector component (and the cn() utility) to your project.
ng generate ngx-prompt-kit:model-selector It is built on spartan/ui, so add these helm components too: button, input-group, sheet.
ng g @spartan-ng/cli:ui button input-group sheetComponent API
PkModelSelector
| Prop | Type | Default | Description |
|---|---|---|---|
| models | readonly SelectorModel[] | — | All selectable models (required). Rail order follows first appearance of each maker. |
| sections | readonly SelectorSection[] | [] | Curated groups shown under one rail entry above the makers. When present, that entry is selected on open. |
| value | string | null | null | Selected model id. Two-way bindable via [(value)]. |
| anchor | ElementRef | HTMLElement | null | null | Element the desktop panel aligns to and matches the width of (e.g. the composer). Without it the panel is 784px wide, aligned to the trigger’s end. |
| inline | boolean | false | On desktop, open in the page’s flow instead of a floating panel. The page renders inlinePortal where the panel belongs. Escape and a pointer down outside the panel and trigger close it. |
| placeholder | string | "Select model" | Trigger text when nothing is selected. |
| title | string | "Choose a model" | Sheet title and the panel’s accessible name. |
| sectionsLabel | string | "Recommended" | Rail entry / chip label for the curated sections. |
| searchPlaceholder | string | "Search by name or what you need, …" | Desktop search placeholder. |
| mobileSearchPlaceholder | string | "Search models or what you need" | Bottom-sheet search placeholder. |
| searchLabel | string | "Search models" | Accessible label of the search field. |
| railLabel | string | "Model makers" | Accessible label of the maker rail / chip row. |
| noResultsText | string | "No models match “{query}”." | Empty search text; {query} is substituted. |
| disabled | boolean | false | Disables the trigger. |
| class | string | — | Extra classes for the host. |
Outputs & methods
| Prop | Type | Default | Description |
|---|---|---|---|
| selected | (id: string) => void | — | Fires when a row is picked. The panel closes and focus returns to the trigger. |
| openChange | (open: boolean) => void | — | Fires when the panel or sheet opens or closes. |
| inlinePortal | Signal<TemplatePortal | null> | — | With [inline], the open panel for a cdkPortalOutlet in the page; null while closed. |
| open() / close() | void | — | Imperative control, e.g. from a keyboard shortcut. |
Content projection
| Prop | Type | Default | Description |
|---|---|---|---|
| [pkModelSelectorFooter] | element | — | Footer strip content (note + balance). Laid out as a space-between row; use md:hidden / hidden md:inline for phone-specific text. |
CSS variables
| Prop | Type | Default | Description |
|---|---|---|---|
| --pk-model-selector-rail-fill | color | var(--muted) at 40% | Fill of the desktop maker rail and footer, e.g. var(--sidebar) to match a sidebar layout. Set it where the overlay can inherit it, such as :root. |
SelectorModel interface
| Prop | Type | Default | Description |
|---|---|---|---|
| id / name | string | — | Identifier and display name (required). |
| maker | string | — | Grouping key and rail label, e.g. "Anthropic" (required). |
| shortName | string? | — | Shorter trigger label on phones. |
| iconUrl | string? | — | Brand icon; providerIconUrl() from model-icon resolves one from the bundled set. |
| makerIconUrl | string? | — | The maker’s own icon for the rail, when a model’s icon is a sub-brand’s (Gemma under Google). Falls back to the first model’s iconUrl. |
| description | string? | — | One truncated line under the name. Searched. |
| capabilities | readonly string[]? | — | Chip labels next to the name. Searched. |
| priceTier | 1 | 2 | 3 | — | Renders $, $$ or $$$. |
| costLabel | string? | — | Hint under the price tier, e.g. "≈ 14 credits". |
| disabled | boolean? | — | Shown dimmed and not selectable. |
SelectorSection interface
| Prop | Type | Default | Description |
|---|---|---|---|
| id / label | string | — | Identifier and group heading. |
| description | string? | — | Muted text next to the heading. |
| modelIds | readonly string[] | — | Models in display order; unknown ids are skipped. |
priceTier(input, output, thresholds?)
| Prop | Type | Default | Description |
|---|---|---|---|
| returns | 1 | 2 | 3 | — | Blended price = (3 × input + output) / 4 per 1M tokens; < low → 1, < high → 2, else 3. |
| thresholds | Partial<{ low; high }> | { low: 2.5, high: 10 } | Boundaries in your pricing currency. Throws RangeError on negative / non-finite prices. |