init.go 6.7 KB

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