✨Feat: 构建对话终端

This commit is contained in:
2026-03-20 14:26:24 +08:00
parent 3f5c0bbae1
commit 355645efdf
17 changed files with 1338 additions and 2 deletions
+203 -2
View File
@@ -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
View File
@@ -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 ""
}
+19
View File
@@ -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
+28
View File
@@ -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
)
+43
View File
@@ -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=
+155
View File
@@ -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
}
+63
View File
@@ -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,
}
}
+71
View File
@@ -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
}
+23
View File
@@ -0,0 +1,23 @@
为"{{.Knowledge}}"制作知识记忆卡片,帮助用户快速掌握关键知识点,要求:
1. 结构清晰,重点突出
2. 使用emoji图标增强可读性
3. 每个部分简洁明了,方便记忆
输出格式:
📌 核心概念:[精确的知识点定义,50字以内]
🔑 关键要点:
- 要点1(具体说明,20字以内)
- 要点2(具体说明,20字以内)
- 要点3(具体说明,20字以内)
- 要点4(具体说明,20字以内)
⚠️ 常见误区:
- 误区1:[说明]
- 误区2:[说明]
✅ 最佳实践:
- 实践1:[具体建议]
- 实践2:[具体建议]
🎯 记忆口诀:[朗朗上口的口诀,帮助记忆]
+12
View File
@@ -0,0 +1,12 @@
用北京话、郭德纲相声演员的口吻讲解"{{.Knowledge}}",要求:
1. 开头用"咳咳,咱今儿个来说说这个{{.Knowledge}}..."作为开场
2. 中间穿插一些相声评书风格的生动比喻和例子
3. 语气轻松诙谐、接地气,但内容要准确专业
4. 偶尔使用相声常用语,比如"您听好了"、"这就好比"、"想当年"、"您猜怎么着"
5. 每讲完一个要点,可以用"这话怎么说呢?"来过渡
6. 最后用"您记住了吗?下次要是有人问起来,您就把这么一通摆出来,准保让他对您刮目相看!"收尾
注意:
- 不要过度夸张,保持内容的准确性和专业性
- 语言要通俗易懂,避免过于晦涩的专业术语
- 字数控制在300-500字
+19
View File
@@ -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
```
+22
View File
@@ -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": "详细解析为什么选这个答案,以及为什么其他选项不对"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
为"{{.Knowledge}}"提供3个实际的工程应用场景,要求:
1. 每个场景都要具体、真实、有代表性
2. 包含完整的问题背景、解决方案、代码示例
3. 代码使用伪代码或简化的实现,注重逻辑清晰
4. 说明使用该知识点解决了什么核心问题
5. 强调注意事项和最佳实践
输出格式:
场景1:[场景名称,如"高并发缓存更新"]
背景:
[描述实际业务场景和遇到的问题]
方案:
[说明如何使用{{.Knowledge}}解决问题,分步骤说明]
代码示例:
[使用伪代码或简化代码展示实现方式]
注意事项:
- 关键点1
- 关键点2
场景2:[场景名称]
背景:
...
方案:
...
代码示例:
...
注意事项:
- ...
场景3:[场景名称]
背景:
...
方案:
...
代码示例:
...
注意事项:
- ...
+39
View File
@@ -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
}
+148
View File
@@ -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"))
)
+160
View File
@@ -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)
}
+185
View File
@@ -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)
}