323 lines
8.6 KiB
Go
323 lines
8.6 KiB
Go
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
|
|
}
|