143 lines
3.4 KiB
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
|
|
}
|