added model switcher
This commit is contained in:
+25
-13
@@ -107,28 +107,40 @@ func defaultConfig() *Config {
|
|||||||
Key: ""},
|
Key: ""},
|
||||||
LLMConfig{Model: ""},
|
LLMConfig{Model: ""},
|
||||||
[]LogConfig{{Level: slog.LevelInfo.Level(), Format: "text"}},
|
[]LogConfig{{Level: slog.LevelInfo.Level(), Format: "text"}},
|
||||||
|
DbConfig{
|
||||||
|
Url: "localhost",
|
||||||
|
User: "psql",
|
||||||
|
Port: "54321",
|
||||||
|
Password: "",
|
||||||
|
DbName: "souvenir",
|
||||||
|
History: HistoryConfig{Enabled: true},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideFromEnv(cfg *Config) {
|
func overrideFromEnv(cfg *Config) {
|
||||||
val := os.Getenv(ENV_PREFIX + "api__url")
|
overrides := []struct {
|
||||||
if val != "" {
|
conf *string
|
||||||
cfg.Api.Url = val
|
key string
|
||||||
|
}{
|
||||||
|
{conf: &cfg.Api.Url, key: ENV_PREFIX + "api__url"},
|
||||||
|
{conf: &cfg.Api.Key, key: ENV_PREFIX + "api__key"},
|
||||||
|
{conf: &cfg.Llm.Model, key: ENV_PREFIX + "llm__model"},
|
||||||
|
{conf: &cfg.Db.DbName, key: ENV_PREFIX + "db__dbName"},
|
||||||
|
{conf: &cfg.Db.Url, key: ENV_PREFIX + "db__url"},
|
||||||
|
{conf: &cfg.Db.User, key: ENV_PREFIX + "db__user"},
|
||||||
|
{conf: &cfg.Db.Password, key: ENV_PREFIX + "db__password"},
|
||||||
|
{conf: &cfg.Db.Port, key: ENV_PREFIX + "db__port"},
|
||||||
}
|
}
|
||||||
val = os.Getenv(ENV_PREFIX + "api__key")
|
for _, override := range(overrides) {
|
||||||
if val != "" {
|
val := os.Getenv(override.key)
|
||||||
cfg.Api.Key = val
|
if val != "" {
|
||||||
}
|
*override.conf = val
|
||||||
val = os.Getenv(ENV_PREFIX + "llm__model")
|
}
|
||||||
if val != "" {
|
|
||||||
cfg.Llm.Model = val
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg Config) validate() error {
|
func (cfg Config) validate() error {
|
||||||
if cfg.Api.Key == "" {
|
|
||||||
return errors.New("No API key configured")
|
|
||||||
}
|
|
||||||
if cfg.Api.Url == "" {
|
if cfg.Api.Url == "" {
|
||||||
return errors.New("No API url configured")
|
return errors.New("No API url configured")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ require (
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|||||||
+61
-15
@@ -13,8 +13,12 @@ import (
|
|||||||
"git.estatecloud.org/radumaco/souvenir/model"
|
"git.estatecloud.org/radumaco/souvenir/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const COMPLETIONS_API = "/v1/chat/completions"
|
||||||
|
const MODELS_API = "/v1/models"
|
||||||
|
|
||||||
type LLMClient struct {
|
type LLMClient struct {
|
||||||
Cfg config.Config
|
Cfg config.Config
|
||||||
|
Logger slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request Types
|
// Request Types
|
||||||
@@ -48,9 +52,49 @@ type Usage struct {
|
|||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client LLMClient) Models() ([]string, error) {
|
||||||
|
resp, err := http.Get(client.Cfg.Api.Url + MODELS_API)
|
||||||
|
if err != nil {
|
||||||
|
client.Logger.Error("There was an error during the network request", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
client.Logger.Error("There was an error reading the response body", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
||||||
|
client.Logger.Error("Call returned non 200 status",
|
||||||
|
"statusCode", resp.StatusCode,
|
||||||
|
"status", resp.Status)
|
||||||
|
return nil,
|
||||||
|
errors.New("Call returned invalid status " +
|
||||||
|
strconv.Itoa(resp.StatusCode) +
|
||||||
|
resp.Status)
|
||||||
|
}
|
||||||
|
client.Logger.Debug("Received data", "data", data)
|
||||||
|
|
||||||
|
var response struct{
|
||||||
|
Data []struct{
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
|
client.Logger.Error("There was an error parsing the response", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client.Logger.Debug("Unmarshal response", "data", response)
|
||||||
|
var ret []string
|
||||||
|
for _, m := range response.Data {
|
||||||
|
ret = append(ret, m.Id)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (client LLMClient) Call(query string, messages []model.Message) ([]model.Message, error) {
|
func (client LLMClient) Call(query string, messages []model.Message) ([]model.Message, error) {
|
||||||
return []model.Message{{Role: "assistant", Content: "message 1"}, {Role: "assistant", Content: "message 2"}}, nil
|
|
||||||
var logger = slog.Default().With("Component", "LLM Client")
|
|
||||||
requestBody := ChatRequest{
|
requestBody := ChatRequest{
|
||||||
Model: client.Cfg.Llm.Model,
|
Model: client.Cfg.Llm.Model,
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
@@ -62,29 +106,31 @@ func (client LLMClient) Call(query string, messages []model.Message) ([]model.Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
body := bytes.NewBuffer(serialized)
|
body := bytes.NewBuffer(serialized)
|
||||||
logger.Debug("Executing llm call", "query", query, "messages", messages, "body", serialized)
|
req, err := http.NewRequest("POST", client.Cfg.Api.Url + COMPLETIONS_API, body)
|
||||||
req, err := http.NewRequest("POST", client.Cfg.Api.Url, body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("There was an error creating the request", "error", err.Error())
|
client.Logger.Error("There was an error creating the request", "error", err.Error())
|
||||||
return []model.Message{}, err
|
return []model.Message{}, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+client.Cfg.Api.Key)
|
if client.Cfg.Api.Key != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.Cfg.Api.Key)
|
||||||
|
}
|
||||||
|
client.Logger.Debug("Executing llm call", "req", req)
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("There was an error during http call", "error", err.Error())
|
client.Logger.Error("There was an error during http call", "error", err.Error())
|
||||||
return []model.Message{}, err
|
return []model.Message{}, err
|
||||||
}
|
}
|
||||||
logger.Debug("LLM Call returned", "response", resp)
|
client.Logger.Debug("LLM Call returned", "response", resp)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("There was an error reading the response body", "error", err.Error())
|
client.Logger.Error("There was an error reading the response body", "error", err.Error())
|
||||||
return []model.Message{}, err
|
return []model.Message{}, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
||||||
logger.Error("Call returned non 200 status",
|
client.Logger.Error("Call returned non 200 status",
|
||||||
"statusCode", resp.StatusCode,
|
"statusCode", resp.StatusCode,
|
||||||
"status", resp.Status)
|
"status", resp.Status)
|
||||||
return []model.Message{},
|
return []model.Message{},
|
||||||
@@ -93,20 +139,20 @@ func (client LLMClient) Call(query string, messages []model.Message) ([]model.Me
|
|||||||
resp.Status)
|
resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Received data", "data", data)
|
client.Logger.Debug("Received data", "data", data)
|
||||||
var response ChatResponse
|
var response ChatResponse
|
||||||
if err := json.Unmarshal(data, &response); err != nil {
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
logger.Error("There was an error parsing the response", "error", err.Error())
|
client.Logger.Error("There was an error parsing the response", "error", err.Error())
|
||||||
return []model.Message{}, err
|
return []model.Message{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []model.Message
|
var result []model.Message
|
||||||
if len(response.Choices) == 0 {
|
if len(response.Choices) == 0 {
|
||||||
logger.Error("The call returned an empty response")
|
client.Logger.Error("The call returned an empty response")
|
||||||
return []model.Message{}, errors.New("The call returned an empty response")
|
return []model.Message{}, errors.New("The call returned an empty response")
|
||||||
}
|
}
|
||||||
for _, choice := range response.Choices {
|
for _, choice := range response.Choices {
|
||||||
logger.Error("Appending response message", "message", choice.Message)
|
client.Logger.Error("Appending response message", "message", choice.Message)
|
||||||
result = append(result, choice.Message)
|
result = append(result, choice.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+91
-27
@@ -15,18 +15,26 @@ import (
|
|||||||
"git.estatecloud.org/radumaco/souvenir/model"
|
"git.estatecloud.org/radumaco/souvenir/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger *slog.Logger
|
type focusState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
focusChat focusState = iota
|
||||||
|
focusOverlay
|
||||||
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
|
logger slog.Logger
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
messages []model.Message
|
messages []model.Message
|
||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
|
focus focusState
|
||||||
|
picker modelPicker
|
||||||
senderStyle lipgloss.Style
|
senderStyle lipgloss.Style
|
||||||
agentStyle lipgloss.Style
|
agentStyle lipgloss.Style
|
||||||
errorStyle lipgloss.Style
|
errorStyle lipgloss.Style
|
||||||
client llm.LLMClient
|
client llm.LLMClient
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
waiting bool
|
waiting bool
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -36,8 +44,12 @@ type agentResponseMessage struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type modelsResponseMessage struct {
|
||||||
|
models []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func InitialModel(config config.Config) uiModel {
|
func InitialModel(config config.Config) uiModel {
|
||||||
logger = slog.Default().With("Component", "UI")
|
|
||||||
ta := textarea.New()
|
ta := textarea.New()
|
||||||
ta.Placeholder = "Send a message..."
|
ta.Placeholder = "Send a message..."
|
||||||
ta.SetVirtualCursor(false)
|
ta.SetVirtualCursor(false)
|
||||||
@@ -62,16 +74,20 @@ func InitialModel(config config.Config) uiModel {
|
|||||||
|
|
||||||
ta.KeyMap.InsertNewline.SetEnabled(false)
|
ta.KeyMap.InsertNewline.SetEnabled(false)
|
||||||
|
|
||||||
return uiModel{
|
model := uiModel{
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
messages: []model.Message{},
|
messages: []model.Message{},
|
||||||
viewport: vp,
|
viewport: vp,
|
||||||
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
|
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
|
||||||
errorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
|
errorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
|
||||||
agentStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("86")),
|
agentStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("86")),
|
||||||
client: llm.LLMClient{Cfg: config},
|
client: llm.LLMClient{Cfg: config, Logger: *slog.Default().With("Component", "LLM")},
|
||||||
|
focus: focusChat,
|
||||||
|
logger: *slog.Default().With("Component", "TUI"),
|
||||||
err: nil,
|
err: nil,
|
||||||
}
|
}
|
||||||
|
model.picker = newModelPicker(&model.focus)
|
||||||
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) Init() tea.Cmd {
|
func (m uiModel) Init() tea.Cmd {
|
||||||
@@ -79,14 +95,16 @@ func (m uiModel) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
m.logger.Debug("Received message", "msg", msg)
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
logger.Debug("Received WindowSizeMsg", "width", msg.Width, "height", msg.Height)
|
|
||||||
m.viewport.SetWidth(msg.Width * 80 / 100)
|
m.viewport.SetWidth(msg.Width * 80 / 100)
|
||||||
m.textarea.SetWidth(msg.Width * 80 / 100)
|
m.textarea.SetWidth(msg.Width * 80 / 100)
|
||||||
m.viewport.SetHeight(msg.Height - lipgloss.Height(m.textarea.View()))
|
m.viewport.SetHeight(msg.Height - lipgloss.Height(m.textarea.View()) - 1)
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.picker.SetSize(msg.Width, msg.Height)
|
||||||
|
|
||||||
var messages = m.renderMessages()
|
var messages = m.renderMessages()
|
||||||
|
|
||||||
@@ -95,16 +113,24 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
} else {
|
} else {
|
||||||
m.viewport.SetContent(renderLanding(m.viewport.Width(), m.viewport.Height()))
|
m.viewport.SetContent(renderLanding(m.viewport.Width(), m.viewport.Height()))
|
||||||
}
|
}
|
||||||
logger.Debug("New sizes",
|
m.logger.Debug("New sizes",
|
||||||
"vp width", m.viewport.Width(), "vp height", m.viewport.Height(),
|
"vp width", m.viewport.Width(), "vp height", m.viewport.Height(),
|
||||||
"ta width", m.textarea.Width(), "ta height", m.textarea.Height())
|
"ta width", m.textarea.Width(), "ta height", m.textarea.Height())
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
|
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
logger.Debug("Received KeyPressMsg", "msg", msg)
|
if m.focus == focusOverlay {
|
||||||
|
picker, cmd := m.picker.Update(msg)
|
||||||
|
m.picker = picker
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "esc":
|
case "esc":
|
||||||
logger.Debug("Received exit sequence. Quiting")
|
if m.focus == focusOverlay {
|
||||||
|
m.focus = focusChat
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case "ctrl+c":
|
||||||
fmt.Println(m.textarea.Value())
|
fmt.Println(m.textarea.Value())
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "shift+enter", "ctrl+j", "alt+enter":
|
case "shift+enter", "ctrl+j", "alt+enter":
|
||||||
@@ -116,13 +142,24 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
Content: input,
|
Content: input,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
logger.Debug("Received enter. Creating new message", "message", input)
|
m.logger.Debug("Received enter. Creating new message", "message", input)
|
||||||
var messages = m.renderMessages()
|
switch input {
|
||||||
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
case "/models":
|
||||||
m.textarea.Reset()
|
m.logger.Debug("Received models sequence")
|
||||||
m.viewport.GotoBottom()
|
m.focus = focusOverlay
|
||||||
m.waiting = true
|
return m, m.getModels()
|
||||||
return m, m.callAgent(input, m.messages)
|
case "/exit":
|
||||||
|
m.logger.Debug("Received exit sequence. Quiting")
|
||||||
|
fmt.Println(m.textarea.Value())
|
||||||
|
return m, tea.Quit
|
||||||
|
default:
|
||||||
|
var messages = m.renderMessages()
|
||||||
|
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
||||||
|
m.textarea.Reset()
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
m.waiting = true
|
||||||
|
return m, m.callAgent(input, m.messages)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.textarea, cmd = m.textarea.Update(msg)
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
@@ -130,17 +167,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cursor.BlinkMsg:
|
case cursor.BlinkMsg:
|
||||||
logger.Debug("Received BlinkMsg")
|
m.logger.Debug("Received BlinkMsg")
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.textarea, cmd = m.textarea.Update(msg)
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case agentResponseMessage:
|
case agentResponseMessage:
|
||||||
logger.Debug("Received agentResponseMessage")
|
m.logger.Debug("Received agentResponseMessage")
|
||||||
m.waiting = false
|
m.waiting = false
|
||||||
resp := msg.response
|
resp := msg.response
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
logger.Error("Message is error", "error", msg.err)
|
m.logger.Error("Message is error", "error", msg.err)
|
||||||
resp = []model.Message{{
|
resp = []model.Message{{
|
||||||
Role: "Error",
|
Role: "Error",
|
||||||
Content: msg.err.Error(),
|
Content: msg.err.Error(),
|
||||||
@@ -148,28 +185,46 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range resp {
|
for _, r := range resp {
|
||||||
logger.Debug("Appending response message", "message", r)
|
m.logger.Debug("Appending response message", "message", r)
|
||||||
m.messages = append(m.messages, r)
|
m.messages = append(m.messages, r)
|
||||||
}
|
}
|
||||||
var messages = m.renderMessages()
|
var messages = m.renderMessages()
|
||||||
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case modelChosenMsg:
|
||||||
|
m.logger.Debug("Received modelChosenMsg", "model", msg.id)
|
||||||
|
m.client.Cfg.Llm.Model = msg.id
|
||||||
|
m.waiting = false
|
||||||
|
m.focus = focusChat
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.focus == focusOverlay {
|
||||||
|
picker, cmd := m.picker.Update(msg)
|
||||||
|
m.picker = picker
|
||||||
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) View() tea.View {
|
func (m uiModel) View() tea.View {
|
||||||
|
if m.focus == focusOverlay {
|
||||||
|
return tea.NewView(lipgloss.Place(m.width, m.height,
|
||||||
|
lipgloss.Center, lipgloss.Center,
|
||||||
|
m.picker.View()))
|
||||||
|
}
|
||||||
ui := lipgloss.JoinVertical(
|
ui := lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
m.viewport.View(),
|
m.viewport.View(),
|
||||||
|
"model: " + m.client.Cfg.Llm.Model + "; url: " + m.client.Cfg.Api.Url,
|
||||||
m.textarea.View())
|
m.textarea.View())
|
||||||
c := m.textarea.Cursor()
|
c := m.textarea.Cursor()
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c.Y += lipgloss.Height(m.viewport.View())
|
c.Y += lipgloss.Height(m.viewport.View()) + 1
|
||||||
gap := m.width - lipgloss.Width(ui)
|
gap := m.width - lipgloss.Width(ui)
|
||||||
c.X += max(gap / 2, 0)
|
c.X += max(gap/2, 0)
|
||||||
}
|
}
|
||||||
view := tea.NewView(lipgloss.PlaceHorizontal(m.width, lipgloss.Center, ui))
|
view := tea.NewView(lipgloss.PlaceHorizontal(m.width, lipgloss.Center, ui))
|
||||||
view.Cursor = c
|
view.Cursor = c
|
||||||
@@ -178,13 +233,22 @@ func (m uiModel) View() tea.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) callAgent(input string, messages []model.Message) tea.Cmd {
|
func (m uiModel) callAgent(input string, messages []model.Message) tea.Cmd {
|
||||||
logger.Debug("Calling agent", "query", input, "messages", messages)
|
m.logger.Debug("Calling agent", "query", input, "messages", messages)
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
resp, err := m.client.Call(input, messages)
|
resp, err := m.client.Call(input, messages)
|
||||||
return agentResponseMessage{response: resp, err: err}
|
return agentResponseMessage{response: resp, err: err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m uiModel) getModels() tea.Cmd {
|
||||||
|
m.logger.Debug("Querying models")
|
||||||
|
return func() tea.Msg {
|
||||||
|
resp, _ := m.client.Models()
|
||||||
|
m.logger.Debug("Received models from llm", "models", resp)
|
||||||
|
return modelsLoadedMsg{models: resp}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m uiModel) renderMessages() string {
|
func (m uiModel) renderMessages() string {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
var width = m.viewport.Width()
|
var width = m.viewport.Width()
|
||||||
@@ -198,7 +262,7 @@ func (m uiModel) renderMessages() string {
|
|||||||
case "assistant":
|
case "assistant":
|
||||||
result.WriteString(m.agentStyle.Render("Agent: "))
|
result.WriteString(m.agentStyle.Render("Agent: "))
|
||||||
result.WriteString(message.Content)
|
result.WriteString(message.Content)
|
||||||
case "error":
|
case "Error":
|
||||||
result.WriteString(m.errorStyle.Render("Error:"))
|
result.WriteString(m.errorStyle.Render("Error:"))
|
||||||
result.WriteString(message.Content)
|
result.WriteString(message.Content)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -38,9 +38,18 @@ const Wordmark = `
|
|||||||
\__ \ (_) | |_| |\ V / __/ | | | | |
|
\__ \ (_) | |_| |\ V / __/ | | | | |
|
||||||
|___/\___/ \__,_| \_/ \___|_| |_|_|_|`
|
|___/\___/ \__,_| \_/ \___|_| |_|_|_|`
|
||||||
|
|
||||||
|
const Commands = `
|
||||||
|
/models
|
||||||
|
/exit
|
||||||
|
`
|
||||||
|
|
||||||
func renderLanding(width int, height int) string {
|
func renderLanding(width int, height int) string {
|
||||||
return lipgloss.PlaceVertical(height, 0.7,
|
return lipgloss.PlaceVertical(height, 0.7,
|
||||||
lipgloss.PlaceHorizontal(width,
|
lipgloss.JoinVertical(lipgloss.Center,
|
||||||
lipgloss.Center,
|
lipgloss.PlaceHorizontal(width,
|
||||||
lipgloss.JoinHorizontal(lipgloss.Center, Logo, Wordmark)))
|
lipgloss.Center,
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Center, Logo, Wordmark)),
|
||||||
|
lipgloss.PlaceHorizontal(width,
|
||||||
|
lipgloss.Center,
|
||||||
|
Commands)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/list"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type modelPicker struct {
|
||||||
|
focus *focusState
|
||||||
|
logger slog.Logger
|
||||||
|
list list.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelsLoadedMsg struct{ models []string }
|
||||||
|
type modelsErrMsg struct{ err error }
|
||||||
|
type modelChosenMsg struct{ id string }
|
||||||
|
|
||||||
|
type modelItem struct{ name string }
|
||||||
|
func (i modelItem) FilterValue() string { return i.name }
|
||||||
|
func (i modelItem) Title() string { return i.name }
|
||||||
|
func (i modelItem) Description() string { return i.name }
|
||||||
|
|
||||||
|
func newModelPicker(focus *focusState) modelPicker {
|
||||||
|
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||||
|
l.Title = "Choose a model"
|
||||||
|
return modelPicker{list: l, logger: *slog.Default().With("Component", "ModelPicker"), focus: focus}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *modelPicker) SetSize(w, h int) {
|
||||||
|
p.list.SetSize(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p modelPicker) Update(msg tea.Msg) (modelPicker, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case modelsLoadedMsg:
|
||||||
|
p.logger.Debug("Received modelsLoadedMsg", "models", msg.models)
|
||||||
|
items := make([]list.Item, len(msg.models))
|
||||||
|
for i, m := range msg.models {
|
||||||
|
items[i] = modelItem{ name: m}
|
||||||
|
}
|
||||||
|
return p, p.list.SetItems(items)
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
if msg.String() == "enter" {
|
||||||
|
p.logger.Debug("Selecting model", "model", p.list.SelectedItem())
|
||||||
|
if it, ok := p.list.SelectedItem().(modelItem); ok {
|
||||||
|
*p.focus = focusChat
|
||||||
|
p.logger.Debug("Sending modelChosenMsg")
|
||||||
|
return p, func() tea.Msg { return modelChosenMsg{it.name} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var cmd tea.Cmd
|
||||||
|
p.list, cmd = p.list.Update(msg)
|
||||||
|
return p, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p modelPicker) View() string {
|
||||||
|
return p.list.View()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user