feat: 添加流式输出动画效果
- 添加 spinner 组件,使用 bubbletea v2 的 MiniDot 动画 - 用户输入后显示思考中动画 - 第一个 token 返回后显示思考完成 - 流式输出完成后添加空行分隔
This commit is contained in:
90
cmd/hxclaw/internal/spinner.go
Normal file
90
cmd/hxclaw/internal/spinner.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
type SpinnerState int
|
||||
|
||||
const (
|
||||
StateThinking SpinnerState = iota
|
||||
StateAnswering
|
||||
StateDone
|
||||
)
|
||||
|
||||
type Spinner struct {
|
||||
text string
|
||||
state SpinnerState
|
||||
spinner spinner.Model
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
}
|
||||
|
||||
func NewSpinner(text string) *Spinner {
|
||||
s := spinner.New(
|
||||
spinner.WithSpinner(spinner.MiniDot),
|
||||
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("243"))),
|
||||
)
|
||||
return &Spinner{
|
||||
text: text,
|
||||
state: StateThinking,
|
||||
spinner: s,
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) Start() {
|
||||
go s.run()
|
||||
s.tick()
|
||||
}
|
||||
|
||||
func (s *Spinner) Update(text string) {
|
||||
s.text = text
|
||||
s.state = StateAnswering
|
||||
}
|
||||
|
||||
func (s *Spinner) Stop() {
|
||||
close(s.stopCh)
|
||||
<-s.doneCh
|
||||
}
|
||||
|
||||
func (s *Spinner) tick() {
|
||||
msg := s.spinner.Tick()
|
||||
if msg, ok := msg.(spinner.TickMsg); ok {
|
||||
s.spinner, _ = s.spinner.Update(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) run() {
|
||||
defer close(s.doneCh)
|
||||
|
||||
ticker := time.NewTicker(time.Second / 12)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.tick()
|
||||
s.render()
|
||||
case <-s.stopCh:
|
||||
s.clear()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) render() {
|
||||
fmt.Printf("\r%s %s", s.spinner.View(), s.text)
|
||||
os.Stdout.Sync()
|
||||
}
|
||||
|
||||
func (s *Spinner) clear() {
|
||||
fmt.Printf("\r%s 思考完成.\n", s.spinner.View())
|
||||
os.Stdout.Sync()
|
||||
}
|
||||
@@ -151,10 +151,20 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||
// 获取工具定义
|
||||
toolDefs := agentInstance.Tools.ToProviderDefs()
|
||||
|
||||
// 启动 spinner,显示 "思考中..."
|
||||
spinner := internal.NewSpinner("思考中...")
|
||||
spinner.Start()
|
||||
|
||||
fmt.Print("\n")
|
||||
var result strings.Builder
|
||||
var printedLen int
|
||||
firstToken := true
|
||||
_, err := sp.ChatStream(ctx, messages, toolDefs, agentInstance.Model, nil, func(accumulated string) {
|
||||
// 检测到第一个 token 时,停止 spinner
|
||||
if firstToken && len(accumulated) > 0 {
|
||||
spinner.Stop()
|
||||
firstToken = false
|
||||
}
|
||||
if len(accumulated) > printedLen {
|
||||
fmt.Print(accumulated[printedLen:])
|
||||
os.Stdout.Sync()
|
||||
@@ -162,13 +172,15 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||
printedLen = len(accumulated)
|
||||
}
|
||||
})
|
||||
fmt.Println()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("流式调用错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
|
||||
// 将用户消息和回复保存到 session
|
||||
if result.Len() > 0 {
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
|
||||
|
||||
Reference in New Issue
Block a user