picoclaw/cmd/picoclaw-launcher-tui/ui/models.go

143 lines
3.4 KiB
Go

// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
type modelsAPIResponse struct {
Data []modelEntry `json:"data"`
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
func (a *App) newModelsPage(schemeName, userName, baseURL string) tview.Primitive {
table := tview.NewTable().
SetBorders(false).
SetSelectable(true, false).
SetFixed(0, 0)
table.SetBorder(true).SetTitle(fmt.Sprintf(" Models %s / %s ", schemeName, userName))
var modelIDs []string
status := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetDynamicColors(true).
SetText("[yellow]Fetching models…[-]")
footer := hintBar(" Enter: select ESC: back ")
flex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(status, 1, 0, false).
AddItem(table, 0, 1, false).
AddItem(footer, 1, 0, false)
apiKey := a.resolveKey(schemeName, userName)
go func() {
entries, err := fetchModels(baseURL, apiKey)
a.tapp.QueueUpdateDraw(func() {
if err != nil {
status.SetText(fmt.Sprintf("[red]Error: %s[-]", err.Error()))
table.SetCell(0, 0, tview.NewTableCell("(failed to load models)"))
a.tapp.SetFocus(table)
return
}
if len(entries) == 0 {
status.SetText("[yellow]No models returned[-]")
table.SetCell(0, 0, tview.NewTableCell("(no models available)"))
a.tapp.SetFocus(table)
return
}
status.SetText(fmt.Sprintf("[green]%d model(s) loaded[-]", len(entries)))
for i, m := range entries {
modelIDs = append(modelIDs, m.ID)
table.SetCell(i, 0,
tview.NewTableCell(fmt.Sprintf("%3d", i+1)).
SetAlign(tview.AlignRight).
SetTextColor(tcell.ColorGray).
SetSelectable(false),
)
table.SetCell(i, 1,
tview.NewTableCell(" "+m.ID).
SetAlign(tview.AlignLeft).
SetExpansion(1),
)
}
a.tapp.SetFocus(table)
})
}()
table.SetSelectedFunc(func(row, _ int) {
if row < 0 || row >= len(modelIDs) {
return
}
a.cfg.Provider.Current = tuicfg.ProviderCurrent{
Scheme: schemeName,
User: userName,
Model: modelIDs[row],
}
a.save()
a.goBack()
})
return flex
}
func (a *App) resolveKey(schemeName, userName string) string {
for _, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName && u.Name == userName {
return u.Key
}
}
return ""
}
func fetchModels(baseURL, apiKey string) ([]modelEntry, error) {
url := strings.TrimRight(baseURL, "/") + "/models"
client := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var result modelsAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return result.Data, nil
}