Files
llmgw/internal/cli/tui_presets.go
T
Ilker ManapandClaude Opus 5.5 848db08469
CI / test (push) Failing after 6m27s
The cluster page and the console say which picture and 3D models are here, and add them
A site whose catalog had no flux-schnell or triposr showed no Images or 3D
page, and nothing said they were one click away. The cluster page now has
a Pictures and 3D models card under the models: each preset, what it
makes, the classes of card here it fits, whether and by whom it was
added, and why the leader has not added it by itself; an Add button, which
says first when it would install ComfyUI on the GPU nodes and restart the
control planes; and the switch for presets.auto. The console's models
screen shows the same under its table: A adds one, asking first in the
same case, and P turns automatic adding on or off.

`ctl model add PRESET` and `ctl model presets` go through the presets API,
so the control plane keeps who added one, and `ctl model presets auto
on|off` flips the setting; against a control plane from before, add sends
the entry as v0.14.4 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:48:40 +02:00

129 lines
4.6 KiB
Go

package cli
import (
"encoding/json"
"net/http"
"net/url"
"strings"
tea "github.com/charmbracelet/bubbletea"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
)
// presetsBlock is the models that come with llmgw, under the models table:
// whether each fits a card here and whether it is in the catalog, and
// whether the leader adds them by itself. Empty from a control plane that
// cannot say.
func presetsBlock(v *adminapi.PresetsView, width int) string {
if v == nil || len(v.Presets) == 0 {
return ""
}
var rows [][]string
var why []string
for _, p := range v.Presets {
rows = append(rows, []string{p.Name, p.Makes, orDash(strings.Join(p.FitsClasses, ",")), presetState(p)})
// Why the leader leaves it, under the table: a reason is longer than
// any column the panel has room for. Automatic adding being off is
// said once, below.
if p.Blocked != "" && !strings.HasPrefix(p.Blocked, "presets.auto") && !p.Declined {
why = append(why, " "+wrap(p.Name+": "+p.Blocked, width-4, " "))
}
}
auto := "Added by themselves where a card fits · P stops that"
if !v.Auto {
auto = "Not added by themselves · P starts that · A adds one"
}
out := "\n\n Pictures and 3D models that come with llmgw\n" +
table([]string{"PRESET", "MAKES", "FITS", "HERE"}, rows, -1, width, false)
if len(why) > 0 {
out += "\n" + sDim.Render(strings.Join(why, "\n"))
}
return out + "\n " + sDim.Render(auto)
}
// presetChoice is how a preset is offered in the add dialog: its name, and
// where it stands when that is worth knowing before choosing it.
func presetChoice(v *adminapi.PresetsView, name string) string {
if v == nil {
return name
}
for _, p := range v.Presets {
if p.Name != name {
continue
}
switch {
case p.InCatalog:
return name + " (already here)"
case presetAddsComfyUI(v, p):
return name + " (installs ComfyUI too)"
case !p.Fits:
return name + " (no card here fits it)"
case p.Declined:
return name + " (removed before)"
}
}
return name
}
// presetAddsComfyUI is whether adding a preset by hand also sets
// engines.comfyui, which has the GPU nodes install it and the control planes
// restart on the setting.
func presetAddsComfyUI(v *adminapi.PresetsView, p adminapi.PresetView) bool {
return v != nil && !p.InCatalog && v.ComfyUI == ""
}
// addPreset adds a preset through the control plane, or, from one from
// before it could, the way the release that brought presets did. One whose
// adding installs ComfyUI and restarts the control planes is asked about
// first.
func (m *consoleModel) addPreset(name string) tea.Cmd {
if m.data.presets != nil {
for _, p := range m.data.presets.Presets {
if p.Name == name && presetAddsComfyUI(m.data.presets, p) {
m.confirmD = &confirmDialog{title: "Add " + name + " and ComfyUI",
text: "The cluster installs no ComfyUI yet. Adding " + name + " sets engines.comfyui:\n" +
"every GPU node installs it, about 10 GB each, and the control planes restart\n" +
"on the setting, standbys first. Engines keep serving.",
yes: func(m *consoleModel) tea.Cmd { return m.postPreset(name) }}
return nil
}
}
}
return m.postPreset(name)
}
func (m *consoleModel) postPreset(name string) tea.Cmd {
if m.data.presets == nil {
return m.runCLI("Add "+name, "ctl", "--server="+m.client.base, "model", "add", name)
}
return m.call("Add "+name, http.MethodPost, "/api/v1/presets/"+url.PathEscape(name), map[string]string{},
func(_ json.RawMessage) string {
return name + " is added, and ComfyUI with it where the cluster had none.\n" +
"It is placed on the next pass, and loads when first asked for."
})
}
// autoPresetsConfirm turns the adding of presets by themselves on or off,
// after saying what that does.
func (m *consoleModel) autoPresetsConfirm() {
on := !m.data.presets.Auto
title, text := "Stop adding pictures and 3D models by themselves",
"Presets already added stay. A preset is added only when somebody adds it."
if on {
title, text = "Add pictures and 3D models by themselves",
"Where a card fits one and the catalog has nothing that makes what it makes, the leader adds it.\n"+
"That also has every GPU node install ComfyUI, about 10 GB each, and the mirror fetch its weights.\n"+
"A preset somebody removed is not added again."
}
m.confirmD = &confirmDialog{title: title, text: text, yes: func(m *consoleModel) tea.Cmd {
return m.call(title, http.MethodPut, "/api/v1/presets/auto", adminapi.PresetsAutoRequest{On: on},
func(_ json.RawMessage) string {
if on {
return "Presets are added by themselves where a card fits."
}
return "Presets are no longer added by themselves."
})
}}
}