initial public release
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user