✨Feat: 构建对话终端
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"learning-assistant/internal/ai"
|
||||
"learning-assistant/internal/ai/generators"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// State TUI状态
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateInput State = "input"
|
||||
StateLoading State = "loading"
|
||||
StateResult State = "result"
|
||||
StateError State = "error"
|
||||
)
|
||||
|
||||
// TabType Tab类型
|
||||
type TabType string
|
||||
|
||||
const (
|
||||
TabGuodegang TabType = "guodegang"
|
||||
TabQuiz TabType = "quiz"
|
||||
TabCard TabType = "card"
|
||||
TabScenario TabType = "scenario"
|
||||
)
|
||||
|
||||
// TabItem Tab项
|
||||
type TabItem struct {
|
||||
Type TabType
|
||||
Label string
|
||||
Icon string
|
||||
}
|
||||
|
||||
// Model TUI模型
|
||||
type Model struct {
|
||||
state State
|
||||
|
||||
// AI组件
|
||||
client *ai.SiliconFlowClient
|
||||
promptManager *ai.PromptManager
|
||||
generator *generators.BaseGenerator
|
||||
|
||||
// 用户输入
|
||||
inputText string
|
||||
|
||||
// 生成结果
|
||||
results map[TabType]string
|
||||
|
||||
// 加载状态
|
||||
loadingStatus map[generators.GeneratorType]bool
|
||||
|
||||
// Tab管理
|
||||
tabs []TabItem
|
||||
activeTab int
|
||||
|
||||
// 错误信息
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
// NewModel 创建模型
|
||||
func NewModel(client *ai.SiliconFlowClient, promptManager *ai.PromptManager) Model {
|
||||
return Model{
|
||||
state: StateResult,
|
||||
client: client,
|
||||
promptManager: promptManager,
|
||||
generator: generators.NewBaseGenerator(client, promptManager),
|
||||
inputText: "",
|
||||
results: make(map[TabType]string),
|
||||
loadingStatus: make(map[generators.GeneratorType]bool),
|
||||
tabs: []TabItem{
|
||||
{Type: TabGuodegang, Label: "讲解", Icon: "🎭"},
|
||||
{Type: TabQuiz, Label: "考核", Icon: "✍️"},
|
||||
{Type: TabCard, Label: "卡片", Icon: "📝"},
|
||||
{Type: TabScenario, Label: "场景", Icon: "🏗️"},
|
||||
},
|
||||
activeTab: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetInput 设置输入
|
||||
func (m *Model) SetInput(input string) {
|
||||
m.inputText = input
|
||||
m.state = StateLoading
|
||||
}
|
||||
|
||||
// Init 初始化
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.handleKeyPress(msg)
|
||||
case contentMsg:
|
||||
return m.handleContentResult(msg)
|
||||
case errorMessage:
|
||||
return m.handleError(msg)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View 视图
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateInput:
|
||||
return m.inputView()
|
||||
case StateLoading:
|
||||
return m.loadingView()
|
||||
case StateResult:
|
||||
return m.resultView()
|
||||
case StateError:
|
||||
return m.errorView()
|
||||
default:
|
||||
return "Unknown state"
|
||||
}
|
||||
}
|
||||
|
||||
// contentMsg 内容生成完成消息
|
||||
type contentMsg struct {
|
||||
Type generators.GeneratorType
|
||||
Content string
|
||||
}
|
||||
|
||||
// errorMessage 错误消息
|
||||
type errorMessage struct {
|
||||
Type generators.GeneratorType
|
||||
Error error
|
||||
}
|
||||
|
||||
// 样式定义
|
||||
var (
|
||||
baseStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FAFAFA"))
|
||||
|
||||
headerStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7D56F4")).
|
||||
Bold(true)
|
||||
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF5555"))
|
||||
)
|
||||
@@ -0,0 +1,160 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"learning-assistant/internal/ai/generators"
|
||||
"learning-assistant/internal/clipboard"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// handleKeyPress 处理按键
|
||||
func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch m.state {
|
||||
case StateInput:
|
||||
return m.handleInputKey(msg)
|
||||
case StateResult:
|
||||
return m.handleResultKey(msg)
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleInputKey 处理输入状态下的按键
|
||||
func (m Model) handleInputKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC:
|
||||
return m, tea.Quit
|
||||
|
||||
case tea.KeyEnter:
|
||||
input := strings.TrimSpace(m.inputText)
|
||||
if input == "" {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// 切换到加载状态
|
||||
m.state = StateLoading
|
||||
|
||||
// 启动5个生成器
|
||||
return m, m.startGeneration()
|
||||
|
||||
case tea.KeyBackspace:
|
||||
// 退格键处理
|
||||
if len(m.inputText) > 0 {
|
||||
m.inputText = m.inputText[:len(m.inputText)-1]
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.KeyRunes:
|
||||
// 普通字符输入
|
||||
m.inputText += string(msg.Runes)
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleResultKey 处理结果状态下的按键
|
||||
func (m Model) handleResultKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyCtrlQ:
|
||||
return m, tea.Quit
|
||||
|
||||
case tea.KeyLeft:
|
||||
m.activeTab = (m.activeTab - 1 + len(m.tabs)) % len(m.tabs)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyRight:
|
||||
m.activeTab = (m.activeTab + 1) % len(m.tabs)
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// startGeneration 开始生成内容
|
||||
func (m Model) startGeneration() tea.Cmd {
|
||||
resultChan := make(chan generators.GenerateResult, 5)
|
||||
|
||||
// 启动5个生成器
|
||||
generatorTypes := []generators.GeneratorType{
|
||||
generators.GeneratorMermaid,
|
||||
generators.GeneratorGuodegang,
|
||||
generators.GeneratorQuiz,
|
||||
generators.GeneratorCard,
|
||||
generators.GeneratorScenario,
|
||||
}
|
||||
|
||||
for _, genType := range generatorTypes {
|
||||
go func(gt generators.GeneratorType) {
|
||||
m.generator.Generate(gt, m.inputText, resultChan)
|
||||
}(genType)
|
||||
}
|
||||
|
||||
// 立即启动一个命令来监听结果
|
||||
return func() tea.Msg {
|
||||
for range generatorTypes {
|
||||
result := <-resultChan
|
||||
if result.Error != nil {
|
||||
return errorMessage{
|
||||
Type: result.Type,
|
||||
Error: result.Error,
|
||||
}
|
||||
}
|
||||
return contentMsg{
|
||||
Type: result.Type,
|
||||
Content: result.Content,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleContentResult 处理内容生成结果
|
||||
func (m Model) handleContentResult(msg contentMsg) (tea.Model, tea.Cmd) {
|
||||
m.loadingStatus[msg.Type] = true
|
||||
|
||||
switch msg.Type {
|
||||
case generators.GeneratorGuodegang:
|
||||
m.results[TabGuodegang] = msg.Content
|
||||
case generators.GeneratorQuiz:
|
||||
m.results[TabQuiz] = msg.Content
|
||||
case generators.GeneratorCard:
|
||||
m.results[TabCard] = msg.Content
|
||||
case generators.GeneratorScenario:
|
||||
m.results[TabScenario] = msg.Content
|
||||
case generators.GeneratorMermaid:
|
||||
// Mermaid复制到剪切板
|
||||
clipboard.WriteSilent(msg.Content)
|
||||
}
|
||||
|
||||
// 检查是否全部完成
|
||||
allDone := true
|
||||
for _, done := range m.loadingStatus {
|
||||
if !done {
|
||||
allDone = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allDone {
|
||||
m.state = StateResult
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// handleError 处理错误
|
||||
func (m Model) handleError(msg errorMessage) (tea.Model, tea.Cmd) {
|
||||
m.state = StateError
|
||||
m.errorMsg = fmt.Sprintf("生成失败: %v", msg.Error)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// handleInput 处理用户输入
|
||||
func (m *Model) handleInput(input string) {
|
||||
m.inputText = strings.TrimSpace(input)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"learning-assistant/internal/ai/generators"
|
||||
)
|
||||
|
||||
// inputView 输入视图
|
||||
func (m Model) inputView() string {
|
||||
title := headerStyle.Render("🎓 学习助手")
|
||||
subtitle := baseStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
inputPrompt := baseStyle.Render("请输入您想学习的知识点,按 Enter 开始生成:\n")
|
||||
|
||||
return fmt.Sprintf(
|
||||
"\n%s\n%s\n\n%s\n\n%s[按 Ctrl+C 退出]\n",
|
||||
title,
|
||||
subtitle,
|
||||
inputPrompt,
|
||||
baseStyle.Render("> "),
|
||||
)
|
||||
}
|
||||
|
||||
// loadingView 加载视图
|
||||
func (m Model) loadingView() string {
|
||||
title := headerStyle.Render("⏳ 正在生成学习内容...")
|
||||
subtitle := baseStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
knowledge := fmt.Sprintf("知识点:%s", m.inputText)
|
||||
knowledgeStyled := baseStyle.Render(knowledge)
|
||||
|
||||
status := "生成状态:\n"
|
||||
|
||||
// Mermaid
|
||||
if m.loadingStatus[generators.GeneratorMermaid] {
|
||||
status += fmt.Sprintf(" ✓ Mermaid 图表\n")
|
||||
} else {
|
||||
status += fmt.Sprintf(" ◯ Mermaid 图表\n")
|
||||
}
|
||||
|
||||
// Guodegang
|
||||
if m.loadingStatus[generators.GeneratorGuodegang] {
|
||||
status += fmt.Sprintf(" ✓ 郭德纲式讲解\n")
|
||||
} else {
|
||||
status += fmt.Sprintf(" ◯ 郭德纲式讲解\n")
|
||||
}
|
||||
|
||||
// Quiz
|
||||
if m.loadingStatus[generators.GeneratorQuiz] {
|
||||
status += fmt.Sprintf(" ✓ 十道考核题\n")
|
||||
} else {
|
||||
status += fmt.Sprintf(" ◯ 十道考核题\n")
|
||||
}
|
||||
|
||||
// Card
|
||||
if m.loadingStatus[generators.GeneratorCard] {
|
||||
status += fmt.Sprintf(" ✓ 记忆卡片\n")
|
||||
} else {
|
||||
status += fmt.Sprintf(" ◯ 记忆卡片\n")
|
||||
}
|
||||
|
||||
// Scenario
|
||||
if m.loadingStatus[generators.GeneratorScenario] {
|
||||
status += fmt.Sprintf(" ✓ 工程场景案例\n")
|
||||
} else {
|
||||
status += fmt.Sprintf(" ◯ 工程场景案例\n")
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"\n%s\n%s\n\n%s\n\n%s %s[按 Ctrl+C 退出]\n",
|
||||
title,
|
||||
subtitle,
|
||||
knowledgeStyled,
|
||||
baseStyle.Render(status),
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// resultView 结果视图
|
||||
func (m Model) resultView() string {
|
||||
title := headerStyle.Render("🎓 学习助手")
|
||||
|
||||
// Tab栏
|
||||
tabBar := m.renderTabs()
|
||||
|
||||
// 分隔线
|
||||
separator := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#586069")).
|
||||
Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
// 当前Tab内容
|
||||
currentTab := m.tabs[m.activeTab]
|
||||
content, ok := m.results[currentTab.Type]
|
||||
if !ok {
|
||||
content = "内容生成中..."
|
||||
}
|
||||
|
||||
// 内容区域(简化渲染,实际应该用viewport)
|
||||
contentView := m.renderContent(content, currentTab)
|
||||
|
||||
// Mermaid复制提示
|
||||
clipboardHint := ""
|
||||
if currentTab.Type == TabGuodegang || currentTab.Type == TabQuiz || currentTab.Type == TabCard || currentTab.Type == TabScenario {
|
||||
clipboardHint = baseStyle.Render("\n✅ Mermaid 图表已自动复制到剪切板")
|
||||
}
|
||||
|
||||
// 底部帮助
|
||||
help := m.renderHelp()
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s\n%s\n\n%s\n\n%s\n%s\n%s\n%s",
|
||||
title,
|
||||
tabBar,
|
||||
separator,
|
||||
contentView,
|
||||
"",
|
||||
clipboardHint,
|
||||
"",
|
||||
help,
|
||||
)
|
||||
}
|
||||
|
||||
// errorView 错误视图
|
||||
func (m Model) errorView() string {
|
||||
title := errorStyle.Render("❌ 生成失败")
|
||||
content := errorStyle.Render(m.errorMsg)
|
||||
help := baseStyle.Render("[按 Ctrl+C 退出,或按 Enter 重新输入]")
|
||||
|
||||
return fmt.Sprintf(
|
||||
"\n%s\n\n%s\n\n%s\n",
|
||||
title,
|
||||
content,
|
||||
help,
|
||||
)
|
||||
}
|
||||
|
||||
// renderTabs 渲染Tab栏
|
||||
func (m Model) renderTabs() string {
|
||||
var tabs []string
|
||||
|
||||
for i, tab := range m.tabs {
|
||||
var style lipgloss.Style
|
||||
if i == m.activeTab {
|
||||
style = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7D56F4")).
|
||||
Bold(true)
|
||||
} else {
|
||||
style = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#586069"))
|
||||
}
|
||||
|
||||
tabs = append(tabs, style.Render(fmt.Sprintf("[%s%s]", tab.Icon, tab.Label)))
|
||||
}
|
||||
|
||||
exitHint := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FAFAFA")).
|
||||
Render("q退出")
|
||||
|
||||
tabs = append(tabs, exitHint)
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
||||
}
|
||||
|
||||
// renderContent 渲染内容
|
||||
func (m Model) renderContent(content string, tab TabItem) string {
|
||||
header := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#04B575")).
|
||||
Bold(true).
|
||||
Render(fmt.Sprintf("%s %s", tab.Icon, tab.Label))
|
||||
|
||||
return fmt.Sprintf("%s\n\n%s", header, baseStyle.Render(content))
|
||||
}
|
||||
|
||||
// renderHelp 渲染帮助信息
|
||||
func (m Model) renderHelp() string {
|
||||
help := baseStyle.Render("←/→ 切换标签页 | ↑/↓ 滚动内容 | q 退出")
|
||||
|
||||
var style lipgloss.Style
|
||||
style = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#586069"))
|
||||
|
||||
return style.Render(help)
|
||||
}
|
||||
Reference in New Issue
Block a user