✨Feat: 构建对话终端
This commit is contained in:
@@ -1,3 +1,204 @@
|
||||
# agent-chain
|
||||
# 学习助手 (Learning Assistant)
|
||||
|
||||
👑 多智能体协同调用
|
||||
一个基于AI知识的交互式学习工具,通过生成多种形式的内容帮助你快速掌握知识点。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎓 **多形式学习内容**:为知识点生成5种不同形式的学习内容
|
||||
- 📊 **Mermaid图表**:知识结构可视化,自动复制到剪切板
|
||||
- 🎭 **郭德纲式讲解**:幽默风趣的知识讲解
|
||||
- ✍️ **十道考核题**:带解析的选择题,检验学习效果
|
||||
- 📝 **记忆卡片**:结构化的关键知识点总结
|
||||
- 🏗️ **工程场景**:实际应用案例和代码示例
|
||||
|
||||
- 🖥️ **交互式TUI界面**:使用Bubble Tea构建的终端用户界面
|
||||
- Tab切换查看不同内容
|
||||
- 键盘导航(←/→翻页,↑/↓滚动)
|
||||
- 流畅的用户体验
|
||||
|
||||
- 🤖 **AI驱动**:基于硅基流动AI服务,使用DeepSeek模型
|
||||
|
||||
## 安装
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Go 1.25.6 或更高版本
|
||||
- 硅基流动API密钥
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆仓库
|
||||
```bash
|
||||
git clone https://github.com/yourusername/learning-assistant.git
|
||||
cd learning-assistant
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. 构建程序
|
||||
```bash
|
||||
go build -o learning.exe cmd/main.go
|
||||
```
|
||||
|
||||
4. 设置API密钥
|
||||
```bash
|
||||
export SILICON_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本使用
|
||||
|
||||
```bash
|
||||
./learning.exe
|
||||
```
|
||||
|
||||
### 操作流程
|
||||
|
||||
1. 程序启动后,输入你想学习的知识点
|
||||
2. 按 Enter 键开始生成
|
||||
3. 等待AI生成完成(后台并发生成5种内容)
|
||||
4. 使用 ←/→ 键切换不同的Tab查看内容
|
||||
5. 使用 ↑/↓ 键滚动长内容
|
||||
6. 按 q 或 Ctrl+C 退出程序
|
||||
|
||||
### 键盘快捷键
|
||||
|
||||
| 按键 | 功能 |
|
||||
|------|------|
|
||||
| Enter | 确认输入/开始生成 |
|
||||
| ← | 切换到上一个Tab |
|
||||
| → | 切换到下一个Tab |
|
||||
| ↑ | 向上滚动内容 |
|
||||
| ↓ | 向下滚动内容 |
|
||||
| q | 退出程序 |
|
||||
| Ctrl+C | 退出程序 |
|
||||
|
||||
## 配置文件
|
||||
|
||||
配置文件位于 `config/config.yaml`:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
endpoint: "https://api.siliconflow.cn/v1"
|
||||
model: "deepseek-ai/DeepSeek-V3"
|
||||
timeout: 120
|
||||
max_retries: 3
|
||||
|
||||
ui:
|
||||
width: 100
|
||||
height: 30
|
||||
colors:
|
||||
primary: "#7D56F4"
|
||||
secondary: "#04B575"
|
||||
text: "#FAFAFA"
|
||||
background: "#232323"
|
||||
border: "#586069"
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
learning-assistant/
|
||||
├── cmd/
|
||||
│ └── main.go # 程序入口
|
||||
├── internal/
|
||||
│ ├── ai/ # AI集成层
|
||||
│ │ ├── client.go # 硅基流动API客户端
|
||||
│ │ ├── prompt.go # 提示词管理
|
||||
│ │ ├── prompts/ # 提示词模板
|
||||
│ │ └── generators/ # 内容生成器
|
||||
│ ├── clipboard/ # 剪切板操作
|
||||
│ └── tui/ # TUI界面层
|
||||
│ ├── model.go # TUI模型
|
||||
│ ├── view.go # 视图渲染
|
||||
│ └── update.go # 事件处理
|
||||
├── config/
|
||||
│ └── config.yaml # 配置文件
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **语言**:Go 1.25.6
|
||||
- **TUI框架**:[Bubble Tea](https://github.com/charmbracelet/bubbletea)
|
||||
- **样式库**:[Lip Gloss](https://github.com/charmbracelet/lipgloss)
|
||||
- **剪切板**:[clipboard](https://github.com/atotto/clipboard)
|
||||
- **AI服务**:硅基流动(DeepSeek模型)
|
||||
|
||||
## 示例
|
||||
|
||||
### 输入示例
|
||||
|
||||
```
|
||||
🎓 学习助手
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
请输入您想学习的知识点: Redis缓存策略
|
||||
```
|
||||
|
||||
### 输出示例
|
||||
|
||||
程序会生成以下内容:
|
||||
|
||||
1. Mermaid图表 - 显示Redis缓存策略的思维导图
|
||||
2. 郭德纲式讲解 - 用幽默的方式讲解缓存策略
|
||||
3. 十道考核题 - 测试对Redis缓存的理解
|
||||
4. 记忆卡片 - 总结核心知识点
|
||||
5. 工程场景 - 展示实际应用案例
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何获取硅基流动API密钥?
|
||||
|
||||
A: 访问 [硅基流动官网](https://cloud.siliconflow.cn) 注册并获取API密钥。
|
||||
|
||||
### Q: 生成失败怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. API密钥是否正确设置
|
||||
2. 网络连接是否正常
|
||||
3. API额度是否充足
|
||||
|
||||
### Q: 如何更改UI主题颜色?
|
||||
|
||||
A: 编辑 `config/config.yaml` 文件中的 `ui.colors` 配置项。
|
||||
|
||||
## 开发
|
||||
|
||||
### 运行开发版本
|
||||
|
||||
```bash
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
go build -o learning.exe cmd/main.go
|
||||
```
|
||||
|
||||
### 依赖管理
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
go mod verify
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - 强大的TUI框架
|
||||
- [硅基流动](https://cloud.siliconflow.cn) - AI服务提供商
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"learning-assistant/internal/ai"
|
||||
"learning-assistant/internal/tui"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
config, err := loadConfig("../config/config.yaml")
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置文件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 获取API密钥
|
||||
// apiKey := getAPIKey()
|
||||
// if apiKey == "" {
|
||||
// fmt.Println("错误: 未设置API密钥")
|
||||
// fmt.Println("请设置环境变量 SILICON_API_KEY 或在配置文件中指定")
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
// 创建AI客户端
|
||||
client := ai.NewClient(config.API)
|
||||
|
||||
// 创建提示词管理器
|
||||
promptManager := ai.NewPromptManager()
|
||||
|
||||
// 读取用户输入
|
||||
fmt.Println("🎓 学习助手")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
fmt.Println()
|
||||
fmt.Print("请输入您想学习的知识点: ")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if !scanner.Scan() {
|
||||
return
|
||||
}
|
||||
|
||||
knowledge := scanner.Text()
|
||||
if knowledge == "" {
|
||||
fmt.Println("输入不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建模型
|
||||
model := tui.NewModel(client, promptManager)
|
||||
model.SetInput(knowledge)
|
||||
|
||||
// 设置信号处理
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// 创建程序
|
||||
p := tea.NewProgram(&model, tea.WithAltScreen())
|
||||
|
||||
// 启动程序
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("启动失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Config 配置
|
||||
type Config struct {
|
||||
API ai.Config
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getAPIKey 获取API密钥
|
||||
func getAPIKey() string {
|
||||
// 优先从环境变量获取
|
||||
if key := os.Getenv("SILICON_API_KEY"); key != "" {
|
||||
return key
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
api:
|
||||
endpoint: "https://api.siliconflow.cn/v1"
|
||||
model: "Pro/deepseek-ai/DeepSeek-V3.2"
|
||||
timeout: 120
|
||||
max_retries: 3
|
||||
api_key: sk-rrizhcztbcpqvzszpsosxbylpuaajjropmzrcdzlqynhopzm
|
||||
|
||||
ui:
|
||||
width: 100
|
||||
height: 30
|
||||
colors:
|
||||
primary: "#7D56F4"
|
||||
secondary: "#04B575"
|
||||
text: "#FAFAFA"
|
||||
background: "#232323"
|
||||
border: "#586069"
|
||||
styles:
|
||||
bold: true
|
||||
rounded: true
|
||||
@@ -0,0 +1,28 @@
|
||||
module learning-assistant
|
||||
|
||||
go 1.25.6
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/charmbracelet/bubbletea v1.3.2
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbletea v1.3.2 h1:nc+gDivH0P8ii8CUcf3zCN/PiUz7LKbp3Iz+vYPScNY=
|
||||
github.com/charmbracelet/bubbletea v1.3.2/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,155 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SiliconFlowClient 硅基流动API客户端
|
||||
type SiliconFlowClient struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
model string
|
||||
timeout time.Duration
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// Config AI配置
|
||||
type Config struct {
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
Model string `yaml:"model"`
|
||||
Timeout int `yaml:"timeout"`
|
||||
MaxRetries int `yaml:"max_retries"`
|
||||
ApiKey string `yaml:"api_key"`
|
||||
}
|
||||
|
||||
// ChatMessage 聊天消息
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// ChatRequest 聊天请求
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []ChatMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
// ChatResponse 聊天响应
|
||||
type ChatResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// Choice 选择项
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message ChatMessage `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
// Usage 使用情况
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// NewClient 创建新的AI客户端
|
||||
func NewClient(config Config) *SiliconFlowClient {
|
||||
return &SiliconFlowClient{
|
||||
apiKey: config.ApiKey,
|
||||
baseURL: config.Endpoint,
|
||||
model: config.Model,
|
||||
timeout: time.Duration(config.Timeout) * time.Second,
|
||||
maxRetries: config.MaxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成内容
|
||||
func (c *SiliconFlowClient) Generate(prompt string) (string, error) {
|
||||
reqBody := ChatRequest{
|
||||
Model: c.model,
|
||||
Messages: []ChatMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: 0.7,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
reqJSON, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < c.maxRetries; attempt++ {
|
||||
result, err := c.sendRequest(reqJSON)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
if attempt < c.maxRetries-1 {
|
||||
time.Sleep(time.Duration(attempt+1) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed after %d attempts: %w", c.maxRetries, lastErr)
|
||||
}
|
||||
|
||||
func (c *SiliconFlowClient) sendRequest(reqJSON []byte) (string, error) {
|
||||
url := c.baseURL + "/chat/completions"
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(reqJSON))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: c.timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API returned status code %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var chatResp ChatResponse
|
||||
if err := json.Unmarshal(body, &chatResp); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if len(chatResp.Choices) == 0 {
|
||||
return "", fmt.Errorf("no choices in response")
|
||||
}
|
||||
|
||||
return chatResp.Choices[0].Message.Content, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package generators
|
||||
|
||||
import (
|
||||
"learning-assistant/internal/ai"
|
||||
)
|
||||
|
||||
// GeneratorType 生成器类型
|
||||
type GeneratorType string
|
||||
|
||||
const (
|
||||
GeneratorMermaid GeneratorType = "mermaid"
|
||||
GeneratorGuodegang GeneratorType = "guodegang"
|
||||
GeneratorQuiz GeneratorType = "quiz"
|
||||
GeneratorCard GeneratorType = "card"
|
||||
GeneratorScenario GeneratorType = "scenario"
|
||||
)
|
||||
|
||||
// GenerateResult 生成结果
|
||||
type GenerateResult struct {
|
||||
Type GeneratorType
|
||||
Content string
|
||||
Error error
|
||||
}
|
||||
|
||||
// BaseGenerator 基础生成器
|
||||
type BaseGenerator struct {
|
||||
client *ai.SiliconFlowClient
|
||||
promptManager *ai.PromptManager
|
||||
}
|
||||
|
||||
// NewBaseGenerator 创建基础生成器
|
||||
func NewBaseGenerator(client *ai.SiliconFlowClient, promptManager *ai.PromptManager) *BaseGenerator {
|
||||
return &BaseGenerator{
|
||||
client: client,
|
||||
promptManager: promptManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成内容
|
||||
func (g *BaseGenerator) Generate(genType GeneratorType, knowledge string, resultChan chan<- GenerateResult) {
|
||||
prompt, err := g.promptManager.GeneratePrompt(string(genType), knowledge)
|
||||
if err != nil {
|
||||
resultChan <- GenerateResult{
|
||||
Type: genType,
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
content, err := g.client.Generate(prompt)
|
||||
if err != nil {
|
||||
resultChan <- GenerateResult{
|
||||
Type: genType,
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- GenerateResult{
|
||||
Type: genType,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed prompts/*.txt
|
||||
var promptFS embed.FS
|
||||
|
||||
// PromptManager 提示词管理器
|
||||
type PromptManager struct {
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
// NewPromptManager 创建提示词管理器
|
||||
func NewPromptManager() *PromptManager {
|
||||
pm := &PromptManager{
|
||||
templates: make(map[string]*template.Template),
|
||||
}
|
||||
|
||||
pm.loadTemplates()
|
||||
return pm
|
||||
}
|
||||
|
||||
func (pm *PromptManager) loadTemplates() {
|
||||
templateFiles := map[string]string{
|
||||
"mermaid": "prompts/mermaid.txt",
|
||||
"guodegang": "prompts/guodegang.txt",
|
||||
"quiz": "prompts/quiz.txt",
|
||||
"card": "prompts/card.txt",
|
||||
"scenario": "prompts/scenario.txt",
|
||||
}
|
||||
|
||||
for name, path := range templateFiles {
|
||||
content, err := promptFS.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpl, err := template.New(name).Parse(string(content))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pm.templates[name] = tmpl
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratePrompt 生成提示词
|
||||
func (pm *PromptManager) GeneratePrompt(templateType string, knowledge string) (string, error) {
|
||||
tmpl, ok := pm.templates[templateType]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("template type %s not found", templateType)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
data := struct {
|
||||
Knowledge string
|
||||
}{
|
||||
Knowledge: knowledge,
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
为"{{.Knowledge}}"制作知识记忆卡片,帮助用户快速掌握关键知识点,要求:
|
||||
1. 结构清晰,重点突出
|
||||
2. 使用emoji图标增强可读性
|
||||
3. 每个部分简洁明了,方便记忆
|
||||
|
||||
输出格式:
|
||||
📌 核心概念:[精确的知识点定义,50字以内]
|
||||
|
||||
🔑 关键要点:
|
||||
- 要点1(具体说明,20字以内)
|
||||
- 要点2(具体说明,20字以内)
|
||||
- 要点3(具体说明,20字以内)
|
||||
- 要点4(具体说明,20字以内)
|
||||
|
||||
⚠️ 常见误区:
|
||||
- 误区1:[说明]
|
||||
- 误区2:[说明]
|
||||
|
||||
✅ 最佳实践:
|
||||
- 实践1:[具体建议]
|
||||
- 实践2:[具体建议]
|
||||
|
||||
🎯 记忆口诀:[朗朗上口的口诀,帮助记忆]
|
||||
@@ -0,0 +1,12 @@
|
||||
用北京话、郭德纲相声演员的口吻讲解"{{.Knowledge}}",要求:
|
||||
1. 开头用"咳咳,咱今儿个来说说这个{{.Knowledge}}..."作为开场
|
||||
2. 中间穿插一些相声评书风格的生动比喻和例子
|
||||
3. 语气轻松诙谐、接地气,但内容要准确专业
|
||||
4. 偶尔使用相声常用语,比如"您听好了"、"这就好比"、"想当年"、"您猜怎么着"
|
||||
5. 每讲完一个要点,可以用"这话怎么说呢?"来过渡
|
||||
6. 最后用"您记住了吗?下次要是有人问起来,您就把这么一通摆出来,准保让他对您刮目相看!"收尾
|
||||
|
||||
注意:
|
||||
- 不要过度夸张,保持内容的准确性和专业性
|
||||
- 语言要通俗易懂,避免过于晦涩的专业术语
|
||||
- 字数控制在300-500字
|
||||
@@ -0,0 +1,19 @@
|
||||
请为"{{.Knowledge}}"生成一个Mermaid思维导图或流程图,要求:
|
||||
1. 使用graph TD或flowchart TD格式
|
||||
2. 清晰展示知识点的结构层次和关系
|
||||
3. 关键节点使用中文标签
|
||||
4. 适当使用不同的节点样式区分概念类型
|
||||
5. 保持代码简洁,便于复制使用
|
||||
|
||||
输出格式(只输出代码,不要其他说明文字):
|
||||
```mermaid
|
||||
graph TD
|
||||
A[{{.Knowledge}}] --> B(核心概念1)
|
||||
A --> C(核心概念2)
|
||||
B --> B1(子概念1.1)
|
||||
B --> B2(子概念1.2)
|
||||
C --> C1(子概念2.1)
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style B fill:#bbf,stroke:#333,stroke-width:2px
|
||||
style C fill:#bbf,stroke:#333,stroke-width:2px
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
关于"{{.Knowledge}}",生成10道单选题,要求:
|
||||
1. 题目类型包括:概念理解、原理分析、应用场景、最佳实践等
|
||||
2. 难度分布:3道基础题、4道中级题、3道进阶题
|
||||
3. 每道题都要提供详细的解析说明
|
||||
4. 选项之间要有明显的区别,避免模棱两可
|
||||
|
||||
必须严格按照以下JSON格式输出(只输出JSON,不要其他说明文字):
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "题目文字,准确描述问题",
|
||||
"options": [
|
||||
"A. 选项内容",
|
||||
"B. 选项内容",
|
||||
"C. 选项内容",
|
||||
"D. 选项内容"
|
||||
],
|
||||
"answer": "A",
|
||||
"explanation": "详细解析为什么选这个答案,以及为什么其他选项不对"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
为"{{.Knowledge}}"提供3个实际的工程应用场景,要求:
|
||||
1. 每个场景都要具体、真实、有代表性
|
||||
2. 包含完整的问题背景、解决方案、代码示例
|
||||
3. 代码使用伪代码或简化的实现,注重逻辑清晰
|
||||
4. 说明使用该知识点解决了什么核心问题
|
||||
5. 强调注意事项和最佳实践
|
||||
|
||||
输出格式:
|
||||
|
||||
场景1:[场景名称,如"高并发缓存更新"]
|
||||
背景:
|
||||
[描述实际业务场景和遇到的问题]
|
||||
|
||||
方案:
|
||||
[说明如何使用{{.Knowledge}}解决问题,分步骤说明]
|
||||
|
||||
代码示例:
|
||||
[使用伪代码或简化代码展示实现方式]
|
||||
|
||||
注意事项:
|
||||
- 关键点1
|
||||
- 关键点2
|
||||
|
||||
场景2:[场景名称]
|
||||
背景:
|
||||
...
|
||||
|
||||
方案:
|
||||
...
|
||||
|
||||
代码示例:
|
||||
...
|
||||
|
||||
注意事项:
|
||||
- ...
|
||||
|
||||
场景3:[场景名称]
|
||||
背景:
|
||||
...
|
||||
|
||||
方案:
|
||||
...
|
||||
|
||||
代码示例:
|
||||
...
|
||||
|
||||
注意事项:
|
||||
- ...
|
||||
@@ -0,0 +1,39 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
)
|
||||
|
||||
var isClipboardAvailable bool
|
||||
|
||||
func init() {
|
||||
// 检测剪切板是否可用
|
||||
// Linux需要xclip或xsel
|
||||
if runtime.GOOS == "linux" {
|
||||
isClipboardAvailable = isXClipAvailable()
|
||||
} else {
|
||||
isClipboardAvailable = true
|
||||
}
|
||||
}
|
||||
|
||||
func isXClipAvailable() bool {
|
||||
// 简单检查,实际使用时会在调用时检测
|
||||
return true
|
||||
}
|
||||
|
||||
// Write 将文本复制到剪切板
|
||||
func Write(text string) error {
|
||||
if !isClipboardAvailable {
|
||||
return fmt.Errorf("clipboard not available on this system")
|
||||
}
|
||||
return clipboard.WriteAll(text)
|
||||
}
|
||||
|
||||
// WriteSilent 静默复制到剪切板,不返回错误
|
||||
func WriteSilent(text string) bool {
|
||||
err := Write(text)
|
||||
return err == nil
|
||||
}
|
||||
@@ -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