// Package view provides TUI rendering functionality package view import ( "strings" "github.com/charmbracelet/lipgloss" "github.com/jfraeys/fetch_ml/cmd/tui/internal/model" ) // NarrativeView displays job research context and outcome type NarrativeView struct { Width int Height int } // View renders the narrative/outcome for a job func (v *NarrativeView) View(job model.Job) string { if job.Hypothesis == "" && job.Context == "" && job.Intent == "" { return v.renderEmpty() } var sections []string // Title titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#ff9e64")). MarginBottom(1) sections = append(sections, titleStyle.Render("๐Ÿ“Š Research Context")) // Hypothesis if job.Hypothesis != "" { sections = append(sections, v.renderSection("๐Ÿงช Hypothesis", job.Hypothesis)) } // Context if job.Context != "" { sections = append(sections, v.renderSection("๐Ÿ“š Context", job.Context)) } // Intent if job.Intent != "" { sections = append(sections, v.renderSection("๐ŸŽฏ Intent", job.Intent)) } // Expected Outcome if job.ExpectedOutcome != "" { sections = append(sections, v.renderSection("๐Ÿ“ˆ Expected Outcome", job.ExpectedOutcome)) } // Actual Outcome (if available) if job.ActualOutcome != "" { statusIcon := "โ“" switch job.OutcomeStatus { case "validated": statusIcon = "โœ…" case "invalidated": statusIcon = "โŒ" case "inconclusive": statusIcon = "โš–๏ธ" case "partial": statusIcon = "๐Ÿ“" } sections = append(sections, v.renderSection(statusIcon+" Actual Outcome", job.ActualOutcome)) } // Combine all sections content := lipgloss.JoinVertical(lipgloss.Left, sections...) // Apply border and padding style := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#7aa2f7")). Padding(1, 2). Width(v.Width - 4) return style.Render(content) } func (v *NarrativeView) renderSection(title, content string) string { titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#7aa2f7")) contentStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("#d8dee9")). MarginLeft(2) // Wrap long content wrapped := wrapText(content, v.Width-12) return lipgloss.JoinVertical(lipgloss.Left, titleStyle.Render(title), contentStyle.Render(wrapped), "", ) } func (v *NarrativeView) renderEmpty() string { style := lipgloss.NewStyle(). Foreground(lipgloss.Color("#88c0d0")). Italic(true). Padding(2) return style.Render("No narrative data available for this job.\n\n" + "Use --hypothesis, --context, --intent flags when queuing jobs.") } // wrapText wraps text to maxWidth, preserving newlines func wrapText(text string, maxWidth int) string { if maxWidth <= 0 { return text } lines := strings.Split(text, "\n") var result []string for _, line := range lines { if len(line) <= maxWidth { result = append(result, line) continue } // Simple word wrap words := strings.Fields(line) var currentLine string for _, word := range words { if len(currentLine)+len(word)+1 > maxWidth { if currentLine != "" { result = append(result, currentLine) } currentLine = word } else { if currentLine != "" { currentLine += " " } currentLine += word } } if currentLine != "" { result = append(result, currentLine) } } return strings.Join(result, "\n") }