feat: support tui
This commit is contained in:
187
tui/panels.go
Normal file
187
tui/panels.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/voocel/ainovel-cli/app"
|
||||
)
|
||||
|
||||
// renderTopBar 渲染顶部状态栏。
|
||||
func renderTopBar(snap app.UISnapshot, width int, spinnerFrame string) string {
|
||||
left := lipgloss.NewStyle().Foreground(colorText).Bold(true).Render(snap.NovelName)
|
||||
if snap.Style != "" && snap.Style != "default" {
|
||||
left += " " + lipgloss.NewStyle().Foreground(colorDim).Render(snap.Style)
|
||||
}
|
||||
left += " " + lipgloss.NewStyle().Foreground(colorDim).Render(snap.ModelName)
|
||||
|
||||
// 状态胶囊
|
||||
label := snap.StatusLabel
|
||||
if label == "" {
|
||||
label = "READY"
|
||||
}
|
||||
color, ok := statusColors[label]
|
||||
if !ok {
|
||||
color = colorDim
|
||||
}
|
||||
capsule := statusCapsule.Foreground(lipgloss.Color("#1a1a2e")).Background(color).Render(label)
|
||||
|
||||
// Spinner(运行中显示)
|
||||
if snap.IsRunning && spinnerFrame != "" {
|
||||
capsule = lipgloss.NewStyle().Foreground(colorAccent).Render(spinnerFrame) + " " + capsule
|
||||
}
|
||||
|
||||
// 左右填充
|
||||
gap := width - lipgloss.Width(left) - lipgloss.Width(capsule) - 2
|
||||
if gap < 1 {
|
||||
gap = 1
|
||||
}
|
||||
|
||||
return topBarStyle.Width(width).Render(left + strings.Repeat(" ", gap) + capsule)
|
||||
}
|
||||
|
||||
// renderStatePanel 渲染左侧状态面板。
|
||||
func renderStatePanel(snap app.UISnapshot, width, height int) string {
|
||||
var b strings.Builder
|
||||
|
||||
if snap.RecoveryLabel != "" {
|
||||
b.WriteString(highlightValueStyle.Render("恢复: " + truncate(snap.RecoveryLabel, width-4)))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
b.WriteString(panelTitleStyle.Render("状态"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(renderField("Phase", snap.Phase))
|
||||
b.WriteString(renderFlowField(snap.Flow))
|
||||
b.WriteString(renderField("Chapter", fmt.Sprintf("%d / %d", snap.CompletedCount, snap.TotalChapters)))
|
||||
b.WriteString(renderField("Words", formatNumber(snap.TotalWordCount)))
|
||||
|
||||
if snap.InProgressChapter > 0 {
|
||||
b.WriteString(renderField("Writing", fmt.Sprintf("第%d章 场景%d", snap.InProgressChapter, snap.CompletedScenes)))
|
||||
}
|
||||
|
||||
if len(snap.PendingRewrites) > 0 {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(panelTitleStyle.Render("返工"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(renderHighlightField("Pending", fmt.Sprintf("%v", snap.PendingRewrites)))
|
||||
if snap.RewriteReason != "" {
|
||||
b.WriteString(renderField("Reason", truncate(snap.RewriteReason, width-12)))
|
||||
}
|
||||
}
|
||||
|
||||
if snap.PendingSteer != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(panelTitleStyle.Render("干预"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(renderHighlightField("Steer", truncate(snap.PendingSteer, width-12)))
|
||||
}
|
||||
|
||||
style := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
Border(baseBorder, false, true, false, false).
|
||||
BorderForeground(colorDim).
|
||||
Padding(0, 1)
|
||||
|
||||
return style.Render(b.String())
|
||||
}
|
||||
|
||||
// renderEventContent 将事件列表渲染为纯文本(供 viewport 使用)。
|
||||
func renderEventContent(events []app.UIEvent, width int) string {
|
||||
var b strings.Builder
|
||||
for i, ev := range events {
|
||||
ts := ev.Time.Format("15:04:05")
|
||||
cat := ev.Category
|
||||
|
||||
color, ok := categoryColors[cat]
|
||||
if !ok {
|
||||
color = colorText
|
||||
}
|
||||
|
||||
catStyle := lipgloss.NewStyle().Foreground(color).Width(7)
|
||||
tsStyle := lipgloss.NewStyle().Foreground(colorDim)
|
||||
sumStyle := lipgloss.NewStyle().Foreground(color)
|
||||
|
||||
line := tsStyle.Render(ts) + " " + catStyle.Render(cat) + " " + sumStyle.Render(truncate(ev.Summary, width-20))
|
||||
b.WriteString(line)
|
||||
if i < len(events)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderEventFlowViewport 用 viewport 包装渲染事件流面板。
|
||||
func renderEventFlowViewport(vp viewport.Model, width, height int) string {
|
||||
style := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
Padding(0, 1)
|
||||
|
||||
return style.Render(vp.View())
|
||||
}
|
||||
|
||||
// renderDetailPanel 渲染右侧详情面板。
|
||||
func renderDetailPanel(snap app.UISnapshot, width, height int) string {
|
||||
var b strings.Builder
|
||||
|
||||
if snap.LastCommitSummary != "" {
|
||||
b.WriteString(cardTitleStyle.Render("─ 最近提交 ─"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(cardContentStyle.Render(snap.LastCommitSummary))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if snap.LastReviewSummary != "" {
|
||||
b.WriteString(cardTitleStyle.Render("─ 最近审阅 ─"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(cardContentStyle.Render(snap.LastReviewSummary))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if snap.LastCheckpointName != "" {
|
||||
b.WriteString(cardTitleStyle.Render("─ 检查点 ─"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(cardContentStyle.Render(snap.LastCheckpointName))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(snap.RecentSummaries) > 0 {
|
||||
b.WriteString(cardTitleStyle.Render("─ 摘要 ─"))
|
||||
b.WriteString("\n")
|
||||
for _, s := range snap.RecentSummaries {
|
||||
b.WriteString(cardContentStyle.Render(s))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
style := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
Border(baseBorder, false, false, false, true).
|
||||
BorderForeground(colorDim).
|
||||
Padding(0, 1)
|
||||
|
||||
return style.Render(b.String())
|
||||
}
|
||||
|
||||
// renderWelcome 渲染新建态首屏。
|
||||
func renderWelcome(width, height int, errMsg string) string {
|
||||
content := lipgloss.NewStyle().Foreground(colorText).Render("还没有开始创作。") + "\n\n" +
|
||||
lipgloss.NewStyle().Foreground(colorDim).Render("请输入你的小说需求,系统会先进入设定与大纲阶段。") + "\n\n" +
|
||||
lipgloss.NewStyle().Foreground(colorAccent).Render("示例:写一部 12 章都市悬疑小说,主角是一名女法医")
|
||||
|
||||
if errMsg != "" {
|
||||
content += "\n\n" + lipgloss.NewStyle().Foreground(colorError).Bold(true).Render("错误: "+errMsg)
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(content)
|
||||
}
|
||||
Reference in New Issue
Block a user