docs: 更新讨论记录和更新日志 - 添加 spinner 动画知识点

This commit is contained in:
2026-04-11 23:57:38 +08:00
parent 13ece24893
commit c1b4f59704
2 changed files with 144 additions and 2 deletions

View File

@@ -20,6 +20,11 @@
- [x] 实现流式 Provider 调用
- [x] 实时打印 token
- [x] 处理非流式 Provider 回退
- [x] 添加加载动画spinner 组件)
- [x] 使用 bubbletea v2 spinner.MiniDot 样式
- [x] 用户输入后显示思考中动画
- [x] 第一个 token 返回后显示思考完成
- [x] 流式输出完成后添加空行分隔
### v0.2.0 (计划)
@@ -48,6 +53,7 @@
- [x] 实现 main.go 入口
- [x] 实现流式输出核心逻辑
- [x] 编译成功,生成 hxclaw 二进制
- [x] 添加 spinner 加载动画组件
---
@@ -239,4 +245,73 @@ os.Stdout.Sync()
- 保留所有历史输出
- 每次刷新缓冲区确保立即显示
**知识点**:最简单的方案就是最有效的方案,不需要额外库
**知识点**:最简单的方案就是最有效的方案,不需要额外库
---
### spinner 组件的 model 更新必须使用返回值
**问题**spinner 动画不动
**现象**:调用 spinner.Update(msg) 后动画不更新
**纠正**spinner model 是值类型,需要使用返回值更新:
```go
// 错误写法
func (s *Spinner) tick() {
msg := s.spinner.Tick()
if msg, ok := msg.(spinner.TickMsg); ok {
s.spinner.Update(msg) // 动画不会动!
}
}
// 正确写法
func (s *Spinner) tick() {
msg := s.spinner.Tick()
if msg, ok := msg.(spinner.TickMsg); ok {
s.spinner, _ = s.spinner.Update(msg) // 必须使用返回值更新
}
}
```
**知识点**bubbletea v2 的组件遵循 TEA 架构模式Update 方法返回更新后的 model需要显式使用返回值。
---
### spinner 和流式输出在同一行的冲突问题
**问题**spinner 使用 `\r` 回到行首刷新,流式输出也在同一行打印,导致内容混在一起
**现象**
```
⠋ 回答中... 好
⠋ 回答中... 注于
```
**纠正**:在第一个 token 时停止 spinner让 spinner 输出 "思考完成." 并换行,然后再开始流式打印:
```go
if firstToken && len(accumulated) > 0 {
spinner.Stop() // 停止 spinner会打印 "思考完成."
firstToken = false
}
```
**知识点**spinner 和流式输出需要分时工作,不能同时占用同一行。
---
### spinner 动画位置和换行策略
**问题**:用户期望动画在前,文字在后,且需要正确的换行
**效果**
```
思考中... ⠋ -> 用户期望 ⠋ 思考中...
思考完成. -> 用户期望 ⠋ 思考完成.
```
**纠正**
- 动画在前,文字在后:`fmt.Printf("\r%s %s", s.spinner.View(), s.text)`
- 换行:"思考完成.\n" + 流式输出后 "fmt.Println()\n"
**知识点**:终端输出需要精确控制位置和换行,否则会导致格式错乱。

View File

@@ -138,4 +138,71 @@ os.Stdout.Sync()
3. 保留所有历史输出
4. 每次刷新缓冲区确保立即显示
这正是 ollama 等工具的流式输出效果。
这正是 ollama 等工具的流式输出效果。
---
### 12. 使用 bubbletea v2 的 spinner 组件实现加载动画
#### 需求分析
用户希望在使用流式输出时,显示加载动画:
- 用户输入后显示 "思考中... ⠋"
- 第一个 token 返回后显示 "思考完成."
- 流式输出完成后添加空行分隔
#### 技术选型
使用 `charm.land/bubbles/v2/spinner` 组件,这是 bubbletea v2 官方提供的 spinner 组件。
#### 实现方案
创建独立的 Spinner 结构体,在独立 goroutine 中运行动画:
```go
type Spinner struct {
text string
state SpinnerState
spinner spinner.Model
stopCh chan struct{}
doneCh chan struct{}
}
```
关键点:
- 使用 `spinner.MiniDot` 动画样式
- 独立 goroutine 使用 ticker 驱动动画帧切换
- 使用 `\r` 回车符在同一行刷新动画
- Stop 时输出 "思考完成."
#### 官方示例参考
```go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
}
```
关键spinner.Tick() 返回的 TickMsg 需要传给 spinner.Update(),并使用返回值更新 spinner model。
#### 注意事项
1. spinner model 更新必须使用返回值:
```go
s.spinner, _ = s.spinner.Update(msg) // 正确
s.spinner.Update(msg) // 错误!动画不会动
```
2. 动画位置:动画在前,文字在后:
```go
fmt.Printf("\r%s %s", s.spinner.View(), s.text) // ⠋ 思考中...
```
3. 换行控制:
- "思考完成." 后需要两个换行符(一个换行 + 一个空行)
- 流式输出完成后也需要空行分隔