- Add safety checks to Zig build - Add TUI with job management and narrative views - Add WebSocket support and export services - Add smart configuration defaults - Update API routes with security headers - Update SECURITY.md with comprehensive policy - Add Makefile security scanning targets
147 lines
3.3 KiB
Go
147 lines
3.3 KiB
Go
// 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")
|
|
}
|