2026-04-23 20:11:22 +08:00
|
|
|
// Package internal 包含 hxclaw 的内部工具模块
|
|
|
|
|
// 提供配置管理、Markdown 渲染、输入读取等功能
|
2026-04-11 23:55:43 +08:00
|
|
|
package internal
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"charm.land/bubbles/v2/spinner"
|
|
|
|
|
"charm.land/lipgloss/v2"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-23 20:11:22 +08:00
|
|
|
// SpinnerState 表示加载动画的状态
|
2026-04-11 23:55:43 +08:00
|
|
|
type SpinnerState int
|
|
|
|
|
|
|
|
|
|
const (
|
2026-04-23 20:11:22 +08:00
|
|
|
StateThinking SpinnerState = iota // 思考中状态
|
|
|
|
|
StateAnswering // 回答中状态
|
|
|
|
|
StateDone // 完成状态
|
2026-04-11 23:55:43 +08:00
|
|
|
)
|
|
|
|
|
|
2026-04-23 20:11:22 +08:00
|
|
|
// Spinner 加载动画组件,用于显示思考状态
|
2026-04-11 23:55:43 +08:00
|
|
|
type Spinner struct {
|
2026-04-23 20:11:22 +08:00
|
|
|
text string // 显示的文本内容
|
|
|
|
|
state SpinnerState // 当前状态
|
|
|
|
|
spinner spinner.Model // 底层动画模型
|
|
|
|
|
stopCh chan struct{} // 停止信号通道
|
|
|
|
|
doneCh chan struct{} // 完成信号通道
|
2026-04-11 23:55:43 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 20:11:22 +08:00
|
|
|
// NewSpinner 创建一个新的加载动画实例
|
|
|
|
|
// text: 初始显示的文本内容
|
2026-04-11 23:55:43 +08:00
|
|
|
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() {
|
2026-04-15 06:13:52 +08:00
|
|
|
fmt.Printf("\r %s %s", s.spinner.View(), s.text)
|
2026-04-11 23:55:43 +08:00
|
|
|
os.Stdout.Sync()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Spinner) clear() {
|
2026-04-15 06:13:52 +08:00
|
|
|
fmt.Printf("\r %s 思考完成.\n", s.spinner.View())
|
2026-04-11 23:55:43 +08:00
|
|
|
os.Stdout.Sync()
|
|
|
|
|
}
|