package main import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" llm "gitea.stevedudenhoeffer.com/steve/go-llm" ) // View renders the current state func (m Model) View() string { switch m.state { case StateProviderSelect: return m.renderProviderSelect() case StateModelSelect: return m.renderModelSelect() case StateImageInput: return m.renderImageInput() case StateToolsPanel: return m.renderToolsPanel() case StateSettings: return m.renderSettings() case StateAPIKeyInput: return m.renderAPIKeyInput() default: return m.renderChat() } } // renderChat renders the main chat view func (m Model) renderChat() string { var b strings.Builder // Header provider := m.providerName if provider == "" { provider = "None" } model := m.modelName if model == "" { model = "None" } header := headerStyle.Render(fmt.Sprintf("go-llm CLI %s", providerBadgeStyle.Render(fmt.Sprintf("%s/%s", provider, model)))) b.WriteString(header) b.WriteString("\n") // Messages viewport if m.viewportReady { b.WriteString(m.viewport.View()) b.WriteString("\n") } // Image indicator if len(m.pendingImages) > 0 { b.WriteString(imageIndicatorStyle.Render(fmt.Sprintf(" [%d image(s) attached]", len(m.pendingImages)))) b.WriteString("\n") } // Error if m.err != nil { b.WriteString(errorStyle.Render(" Error: " + m.err.Error())) b.WriteString("\n") } // Loading if m.loading { b.WriteString(loadingStyle.Render(" Thinking...")) b.WriteString("\n") } // Input inputBox := inputStyle.Render(m.input.View()) b.WriteString(inputBox) b.WriteString("\n") // Help help := inputHelpStyle.Render("Enter: send | Ctrl+I: image | Ctrl+T: tools | Ctrl+P: provider | Ctrl+M: model | Ctrl+S: settings | Ctrl+N: new | Esc: quit") b.WriteString(help) return appStyle.Render(b.String()) } // renderMessages renders all messages for the viewport func (m Model) renderMessages() string { var b strings.Builder if len(m.messages) == 0 { b.WriteString(systemMsgStyle.Render("[System] " + m.systemPrompt)) b.WriteString("\n\n") b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Start a conversation by typing a message below.")) return b.String() } b.WriteString(systemMsgStyle.Render("[System] " + m.systemPrompt)) b.WriteString("\n\n") for _, msg := range m.messages { var content string var style lipgloss.Style switch msg.Role { case llm.RoleUser: style = userMsgStyle label := roleLabelStyle.Foreground(secondaryColor).Render("[User]") content = label + " " + msg.Content if msg.Images > 0 { content += imageIndicatorStyle.Render(fmt.Sprintf(" [%d image(s)]", msg.Images)) } case llm.RoleAssistant: style = assistantMsgStyle label := roleLabelStyle.Foreground(lipgloss.Color("255")).Render("[Assistant]") content = label + " " + msg.Content case llm.Role("tool_call"): style = toolCallStyle content = " -> Calling: " + msg.Content case llm.Role("tool_result"): style = toolResultStyle content = " <- Result: " + msg.Content default: style = assistantMsgStyle content = msg.Content } b.WriteString(style.Render(content)) b.WriteString("\n\n") } return b.String() } // renderProviderSelect renders the provider selection view func (m Model) renderProviderSelect() string { var b strings.Builder b.WriteString(headerStyle.Render("Select Provider")) b.WriteString("\n\n") for i, item := range m.listItems { cursor := " " style := normalItemStyle if i == m.listIndex { cursor = "> " style = selectedItemStyle } b.WriteString(style.Render(cursor + item)) b.WriteString("\n") } b.WriteString("\n") b.WriteString(helpStyle.Render("Use arrow keys or j/k to navigate, Enter to select, Esc to cancel")) return appStyle.Render(b.String()) } // renderAPIKeyInput renders the API key input view func (m Model) renderAPIKeyInput() string { var b strings.Builder provider := m.providers[m.listIndex] b.WriteString(headerStyle.Render(fmt.Sprintf("Enter API Key for %s", provider.Name))) b.WriteString("\n\n") b.WriteString(fmt.Sprintf("Environment variable: %s\n\n", provider.EnvVar)) b.WriteString("Enter your API key below (it will be hidden):\n\n") inputBox := inputStyle.Render(m.apiKeyInput.View()) b.WriteString(inputBox) b.WriteString("\n\n") b.WriteString(helpStyle.Render("Enter to confirm, Esc to cancel")) return appStyle.Render(b.String()) } // renderModelSelect renders the model selection view func (m Model) renderModelSelect() string { var b strings.Builder b.WriteString(headerStyle.Render(fmt.Sprintf("Select Model (%s)", m.providerName))) b.WriteString("\n\n") for i, item := range m.listItems { cursor := " " style := normalItemStyle if i == m.listIndex { cursor = "> " style = selectedItemStyle } if item == m.modelName { item += " (current)" } b.WriteString(style.Render(cursor + item)) b.WriteString("\n") } b.WriteString("\n") b.WriteString(helpStyle.Render("Use arrow keys or j/k to navigate, Enter to select, Esc to cancel")) return appStyle.Render(b.String()) } // renderImageInput renders the image input view func (m Model) renderImageInput() string { var b strings.Builder b.WriteString(headerStyle.Render("Add Image")) b.WriteString("\n\n") b.WriteString("Enter an image source:\n") b.WriteString(" - File path (e.g., /path/to/image.png)\n") b.WriteString(" - URL (e.g., https://example.com/image.jpg)\n") b.WriteString(" - Base64 data or data URL\n\n") if len(m.pendingImages) > 0 { b.WriteString(imageIndicatorStyle.Render(fmt.Sprintf("Currently attached: %d image(s)\n\n", len(m.pendingImages)))) } inputBox := inputStyle.Render(m.input.View()) b.WriteString(inputBox) b.WriteString("\n\n") b.WriteString(helpStyle.Render("Enter to add image, Esc to cancel")) return appStyle.Render(b.String()) } // renderToolsPanel renders the tools panel func (m Model) renderToolsPanel() string { var b strings.Builder b.WriteString(headerStyle.Render("Tools / Function Calling")) b.WriteString("\n\n") status := "DISABLED" statusStyle := errorStyle if m.toolsEnabled { status = "ENABLED" statusStyle = lipgloss.NewStyle().Foreground(successColor).Bold(true) } b.WriteString(settingLabelStyle.Render("Tools Status:")) b.WriteString(statusStyle.Render(status)) b.WriteString("\n\n") b.WriteString("Available tools:\n") for _, fn := range m.toolbox.Functions() { b.WriteString(fmt.Sprintf(" - %s: %s\n", selectedItemStyle.Render(fn.Name), fn.Description)) } b.WriteString("\n") b.WriteString(helpStyle.Render("Press 't' to toggle tools, Enter or 'q' to close")) return appStyle.Render(b.String()) } // renderSettings renders the settings view func (m Model) renderSettings() string { var b strings.Builder b.WriteString(headerStyle.Render("Settings")) b.WriteString("\n\n") // Temperature tempStr := "default" if m.temperature != nil { tempStr = fmt.Sprintf("%.1f", *m.temperature) } b.WriteString(settingLabelStyle.Render("Temperature:")) b.WriteString(settingValueStyle.Render(tempStr)) b.WriteString("\n\n") b.WriteString("Press a key to set temperature:\n") b.WriteString(" 1 - Default (model decides)\n") b.WriteString(" 2 - 0.0 (deterministic)\n") b.WriteString(" 3 - 0.5 (balanced)\n") b.WriteString(" 4 - 0.7 (creative)\n") b.WriteString(" 5 - 1.0 (very creative)\n") b.WriteString("\n") // System prompt b.WriteString(settingLabelStyle.Render("System Prompt:")) b.WriteString("\n") b.WriteString(settingValueStyle.Render(" " + m.systemPrompt)) b.WriteString("\n\n") b.WriteString(helpStyle.Render("Enter or 'q' to close")) return appStyle.Render(b.String()) }