init.go

  1package page
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"strconv"
  8
  9	"github.com/charmbracelet/bubbles/key"
 10	tea "github.com/charmbracelet/bubbletea"
 11	"github.com/charmbracelet/huh"
 12	"github.com/charmbracelet/lipgloss"
 13	"github.com/kujtimiihoxha/termai/internal/llm/models"
 14	"github.com/kujtimiihoxha/termai/internal/tui/layout"
 15	"github.com/kujtimiihoxha/termai/internal/tui/styles"
 16	"github.com/kujtimiihoxha/termai/internal/tui/util"
 17	"github.com/spf13/viper"
 18)
 19
 20var InitPage PageID = "init"
 21
 22type configSaved struct{}
 23
 24type initPage struct {
 25	form         *huh.Form
 26	width        int
 27	height       int
 28	saved        bool
 29	errorMsg     string
 30	statusMsg    string
 31	modelOpts    []huh.Option[string]
 32	bigModel     string
 33	smallModel   string
 34	openAIKey    string
 35	anthropicKey string
 36	groqKey      string
 37	maxTokens    string
 38	dataDir      string
 39	agent        string
 40}
 41
 42func (i *initPage) Init() tea.Cmd {
 43	return i.form.Init()
 44}
 45
 46func (i *initPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 47	var cmds []tea.Cmd
 48
 49	switch msg := msg.(type) {
 50	case tea.WindowSizeMsg:
 51		i.width = msg.Width - 4 // Account for border
 52		i.height = msg.Height - 4
 53		i.form = i.form.WithWidth(i.width).WithHeight(i.height)
 54		return i, nil
 55
 56	case configSaved:
 57		i.saved = true
 58		i.statusMsg = "Configuration saved successfully. Press any key to continue."
 59		return i, nil
 60	}
 61
 62	if i.saved {
 63		switch msg.(type) {
 64		case tea.KeyMsg:
 65			return i, util.CmdHandler(PageChangeMsg{ID: ReplPage})
 66		}
 67		return i, nil
 68	}
 69
 70	// Process the form
 71	form, cmd := i.form.Update(msg)
 72	if f, ok := form.(*huh.Form); ok {
 73		i.form = f
 74		cmds = append(cmds, cmd)
 75	}
 76
 77	if i.form.State == huh.StateCompleted {
 78		// Save configuration to file
 79		configPath := filepath.Join(os.Getenv("HOME"), ".termai.yaml")
 80		maxTokens, _ := strconv.Atoi(i.maxTokens)
 81		config := map[string]any{
 82			"models": map[string]string{
 83				"big":   i.bigModel,
 84				"small": i.smallModel,
 85			},
 86			"providers": map[string]any{
 87				"openai": map[string]string{
 88					"key": i.openAIKey,
 89				},
 90				"anthropic": map[string]string{
 91					"key": i.anthropicKey,
 92				},
 93				"groq": map[string]string{
 94					"key": i.groqKey,
 95				},
 96				"common": map[string]int{
 97					"max_tokens": maxTokens,
 98				},
 99			},
100			"data": map[string]string{
101				"dir": i.dataDir,
102			},
103			"agents": map[string]string{
104				"default": i.agent,
105			},
106			"log": map[string]string{
107				"level": "info",
108			},
109		}
110
111		// Write config to viper
112		for k, v := range config {
113			viper.Set(k, v)
114		}
115
116		// Save configuration
117		err := viper.WriteConfigAs(configPath)
118		if err != nil {
119			i.errorMsg = fmt.Sprintf("Failed to save configuration: %s", err)
120			return i, nil
121		}
122
123		// Return to main page
124		return i, util.CmdHandler(configSaved{})
125	}
126
127	return i, tea.Batch(cmds...)
128}
129
130func (i *initPage) View() string {
131	if i.saved {
132		return lipgloss.NewStyle().
133			Width(i.width).
134			Height(i.height).
135			Align(lipgloss.Center, lipgloss.Center).
136			Render(lipgloss.JoinVertical(
137				lipgloss.Center,
138				lipgloss.NewStyle().Foreground(styles.Green).Render("✓ Configuration Saved"),
139				"",
140				lipgloss.NewStyle().Foreground(styles.Blue).Render(i.statusMsg),
141			))
142	}
143
144	view := i.form.View()
145	if i.errorMsg != "" {
146		errorBox := lipgloss.NewStyle().
147			Padding(1).
148			Border(lipgloss.RoundedBorder()).
149			BorderForeground(styles.Red).
150			Width(i.width - 4).
151			Render(i.errorMsg)
152		view = lipgloss.JoinVertical(lipgloss.Left, errorBox, view)
153	}
154	return view
155}
156
157func (i *initPage) GetSize() (int, int) {
158	return i.width, i.height
159}
160
161func (i *initPage) SetSize(width int, height int) {
162	i.width = width
163	i.height = height
164	i.form = i.form.WithWidth(width).WithHeight(height)
165}
166
167func (i *initPage) BindingKeys() []key.Binding {
168	if i.saved {
169		return []key.Binding{
170			key.NewBinding(
171				key.WithKeys("enter", "space", "esc"),
172				key.WithHelp("any key", "continue"),
173			),
174		}
175	}
176	return i.form.KeyBinds()
177}
178
179func NewInitPage() tea.Model {
180	// Create model options
181	var modelOpts []huh.Option[string]
182	for id, model := range models.SupportedModels {
183		modelOpts = append(modelOpts, huh.NewOption(model.Name, string(id)))
184	}
185
186	// Create agent options
187	agentOpts := []huh.Option[string]{
188		huh.NewOption("Coder", "coder"),
189		huh.NewOption("Assistant", "assistant"),
190	}
191
192	// Init page with form
193	initModel := &initPage{
194		modelOpts:  modelOpts,
195		bigModel:   string(models.Claude37Sonnet),
196		smallModel: string(models.Claude37Sonnet),
197		maxTokens:  "4000",
198		dataDir:    ".termai",
199		agent:      "coder",
200	}
201
202	// API Keys group
203	apiKeysGroup := huh.NewGroup(
204		huh.NewNote().
205			Title("API Keys").
206			Description("You need to provide at least one API key to use termai"),
207
208		huh.NewInput().
209			Title("OpenAI API Key").
210			Placeholder("sk-...").
211			Key("openai_key").
212			Value(&initModel.openAIKey),
213
214		huh.NewInput().
215			Title("Anthropic API Key").
216			Placeholder("sk-ant-...").
217			Key("anthropic_key").
218			Value(&initModel.anthropicKey),
219
220		huh.NewInput().
221			Title("Groq API Key").
222			Placeholder("gsk_...").
223			Key("groq_key").
224			Value(&initModel.groqKey),
225	)
226
227	// Model configuration group
228	modelsGroup := huh.NewGroup(
229		huh.NewNote().
230			Title("Model Configuration").
231			Description("Select which models to use"),
232
233		huh.NewSelect[string]().
234			Title("Big Model").
235			Options(modelOpts...).
236			Key("big_model").
237			Value(&initModel.bigModel),
238
239		huh.NewSelect[string]().
240			Title("Small Model").
241			Options(modelOpts...).
242			Key("small_model").
243			Value(&initModel.smallModel),
244
245		huh.NewInput().
246			Title("Max Tokens").
247			Placeholder("4000").
248			Key("max_tokens").
249			CharLimit(5).
250			Validate(func(s string) error {
251				var n int
252				_, err := fmt.Sscanf(s, "%d", &n)
253				if err != nil || n <= 0 {
254					return fmt.Errorf("must be a positive number")
255				}
256				initModel.maxTokens = s
257				return nil
258			}).
259			Value(&initModel.maxTokens),
260	)
261
262	// General settings group
263	generalGroup := huh.NewGroup(
264		huh.NewNote().
265			Title("General Settings").
266			Description("Configure general termai settings"),
267
268		huh.NewInput().
269			Title("Data Directory").
270			Placeholder(".termai").
271			Key("data_dir").
272			Value(&initModel.dataDir),
273
274		huh.NewSelect[string]().
275			Title("Default Agent").
276			Options(agentOpts...).
277			Key("agent").
278			Value(&initModel.agent),
279
280		huh.NewConfirm().
281			Title("Save Configuration").
282			Affirmative("Save").
283			Negative("Cancel"),
284	)
285
286	// Create form with theme
287	form := huh.NewForm(
288		apiKeysGroup,
289		modelsGroup,
290		generalGroup,
291	).WithTheme(styles.HuhTheme()).
292		WithShowHelp(true).
293		WithShowErrors(true)
294
295	// Set the form in the model
296	initModel.form = form
297
298	return layout.NewSinglePane(
299		initModel,
300		layout.WithSinglePaneFocusable(true),
301		layout.WithSinglePaneBordered(true),
302		layout.WithSinglePaneBorderText(
303			map[layout.BorderPosition]string{
304				layout.TopMiddleBorder: "Welcome to termai - Initial Setup",
305			},
306		),
307	)
308}