package tui import ( "fmt" "time" "github.com/cobr-ai/apex/internal/intake" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // IntakeModel represents the Intake tab state machine type IntakeModel struct { session *intake.IntakeSession manager *intake.Manager currentView IntakeView formInputs []textinput.Model focusedInput int errorMsg string } // IntakeView represents which view is being shown type IntakeView int const ( ViewWelcome IntakeView = iota ViewIdentity ViewEducation ViewRoles ViewCertifications ViewSkills ViewProjects ViewPreferences ViewVoice ViewReview ViewComplete ) // NewIntakeModel creates a new intake model func NewIntakeModel(manager *intake.Manager) *IntakeModel { return &IntakeModel{ manager: manager, currentView: ViewWelcome, formInputs: make([]textinput.Model, 0), } } // Init initializes the intake model func (m *IntakeModel) Init() tea.Cmd { session, _ := m.manager.GetCurrentSession() if session != nil { m.session = session m.currentView = ViewWelcome + IntakeView(session.CurrentPhase) if m.currentView >= ViewComplete { m.currentView = ViewComplete } } return nil } // Update handles user input func (m *IntakeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "tab": if len(m.formInputs) > 0 { m.focusedInput = (m.focusedInput + 1) % len(m.formInputs) m.updateFocus() } case "shift+tab": if len(m.formInputs) > 0 { m.focusedInput = (m.focusedInput - 1 + len(m.formInputs)) % len(m.formInputs) m.updateFocus() } case "enter": return m.handleSubmit() case "n": if m.currentView == ViewWelcome { m.currentView = ViewIdentity m.setupFormForPhase(intake.PhaseIdentity) } } } if len(m.formInputs) > 0 && m.focusedInput < len(m.formInputs) { var cmd tea.Cmd m.formInputs[m.focusedInput], cmd = m.formInputs[m.focusedInput].Update(msg) return m, cmd } return m, nil } // View renders the intake form func (m *IntakeModel) View() string { switch m.currentView { case ViewWelcome: return m.renderWelcome() case ViewIdentity: return m.renderIdentityForm() case ViewEducation, ViewRoles, ViewCertifications, ViewProjects: return m.renderMultiItemForm() case ViewSkills, ViewPreferences, ViewVoice: return m.renderForm() case ViewReview: return m.renderReview() case ViewComplete: return m.renderComplete() default: return "Unknown view" } } func (m *IntakeModel) renderWelcome() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")).Render("Welcome to Apex Intake") options := "[n] Start New | [i] Import DOCX | [r] Resume | [q] Quit" return lipgloss.JoinVertical(lipgloss.Top, title, options) } func (m *IntakeModel) renderIdentityForm() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")).Render("Phase 1: Identity") inputs := lipgloss.JoinVertical(lipgloss.Top, m.renderFormInputs()...) errorLine := "" if m.errorMsg != "" { errorLine = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("Error: " + m.errorMsg) } return lipgloss.JoinVertical(lipgloss.Top, title, "", inputs, errorLine) } func (m *IntakeModel) renderForm() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")).Render("Intake Form") inputs := lipgloss.JoinVertical(lipgloss.Top, m.renderFormInputs()...) errorLine := "" if m.errorMsg != "" { errorLine = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("Error: " + m.errorMsg) } return lipgloss.JoinVertical(lipgloss.Top, title, "", inputs, errorLine) } func (m *IntakeModel) renderMultiItemForm() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")).Render("Multi-Item Phase") inputs := lipgloss.JoinVertical(lipgloss.Top, m.renderFormInputs()...) errorLine := "" if m.errorMsg != "" { errorLine = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("Error: " + m.errorMsg) } return lipgloss.JoinVertical(lipgloss.Top, title, "", inputs, errorLine) } func (m *IntakeModel) renderFormInputs() []string { var lines []string for i, input := range m.formInputs { style := lipgloss.NewStyle().Width(60) if i == m.focusedInput { style = style.Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("4")) } lines = append(lines, style.Render(input.View())) } return lines } func (m *IntakeModel) renderReview() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")).Render("Review") return title } func (m *IntakeModel) renderComplete() string { title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2")).Render("Intake Complete!") return title } func (m *IntakeModel) updateFocus() { for i := range m.formInputs { if i == m.focusedInput { m.formInputs[i].Focus() } else { m.formInputs[i].Blur() } } } func (m *IntakeModel) handleSubmit() (tea.Model, tea.Cmd) { if m.session == nil { m.errorMsg = "No session loaded" return m, nil } // Collect form input into answer structure answer := intake.FormAnswer{ Phase: m.session.CurrentPhase, Fields: make(map[string]string), NestedItems: []map[string]string{}, SubmittedAt: time.Now(), } // Map form inputs to fields based on current phase switch m.session.CurrentPhase { case intake.PhaseIdentity: if len(m.formInputs) >= 4 { answer.Fields["name"] = m.formInputs[0].Value() answer.Fields["email"] = m.formInputs[1].Value() answer.Fields["location"] = m.formInputs[2].Value() answer.Fields["timezone"] = m.formInputs[3].Value() } case intake.PhaseEducation: // Multi-item: accumulate from form inputs for _, input := range m.formInputs { if input.Value() != "" { answer.NestedItems = append(answer.NestedItems, map[string]string{ "school": input.Value(), }) } } case intake.PhaseRoles: for _, input := range m.formInputs { if input.Value() != "" { answer.NestedItems = append(answer.NestedItems, map[string]string{ "title": input.Value(), }) } } case intake.PhaseCertifications, intake.PhaseProjects: for _, input := range m.formInputs { if input.Value() != "" { answer.NestedItems = append(answer.NestedItems, map[string]string{ "name": input.Value(), }) } } default: // Single-item phases for i, input := range m.formInputs { answer.Fields[fmt.Sprintf("field_%d", i)] = input.Value() } } // Validate required fields if !m.validateAnswer(answer) { m.errorMsg = "Please fill all required fields" return m, nil } // Save answer to database if err := m.manager.SubmitPhaseAnswer(m.session, answer); err != nil { m.errorMsg = fmt.Sprintf("Error saving: %v", err) return m, nil } // Advance to next phase nextPhase := m.manager.AdvanceToNextPhase(m.session) // Clear error and setup next view m.errorMsg = "" m.currentView = ViewIdentity + IntakeView(nextPhase) if nextPhase == intake.PhaseComplete { m.manager.CompleteIntake(m.session) m.currentView = ViewComplete } else { m.setupFormForPhase(nextPhase) } return m, nil } func (m *IntakeModel) validateAnswer(answer intake.FormAnswer) bool { switch answer.Phase { case intake.PhaseIdentity: required := []string{"name", "email", "location", "timezone"} for _, field := range required { if answer.Fields[field] == "" { return false } } return true case intake.PhaseEducation, intake.PhaseRoles, intake.PhaseCertifications, intake.PhaseProjects: return len(answer.NestedItems) > 0 case intake.PhaseSkills, intake.PhasePreferences, intake.PhaseVoice: return len(answer.Fields) > 0 || len(answer.NestedItems) > 0 } return true } func (m *IntakeModel) setupFormForPhase(phase intake.Phase) { m.formInputs = make([]textinput.Model, 0) switch phase { case intake.PhaseIdentity: m.formInputs = make([]textinput.Model, 4) labels := []string{"Name", "Email", "Location", "Timezone"} for i := range m.formInputs { m.formInputs[i] = textinput.New() m.formInputs[i].Placeholder = labels[i] if i == 0 { m.formInputs[i].Focus() } } case intake.PhaseEducation, intake.PhaseRoles, intake.PhaseCertifications, intake.PhaseProjects: m.formInputs = make([]textinput.Model, 3) // Allow multiple items for i := range m.formInputs { m.formInputs[i] = textinput.New() if i == 0 { m.formInputs[i].Focus() } } default: m.formInputs = make([]textinput.Model, 2) for i := range m.formInputs { m.formInputs[i] = textinput.New() if i == 0 { m.formInputs[i].Focus() } } } m.focusedInput = 0 }