优化代码

This commit is contained in:
hero920103 committed 2026-10-28 08:47:09 +08:00
1 parent 3553790475
commit eda7e8bc96
23 files changed
+3113 -342

No files matched your search

+109 -4
View File
@@ -31,13 +31,29 @@ func colorFor(k bubbleKind) color.Color {
}
}
// borderFor 返回气泡描边色:错误提示用更重的描边,保证一眼能认出。
func borderFor(k bubbleKind) color.Color {
if k == bubbleError {
return colorStrongBorder()
}
return colorBorder()
}
// bubble 是一条消息气泡:圆角背景 + 角色标题 + 正文。
// 流式阶段用 Label 追加文本,结束后换成 RichText 以获得 Markdown 排版。
// 思考过程单独占一块(可折叠),不与正式回答混在一起。
type bubble struct {
root *fyne.Container
bg *canvas.Rectangle
body *fyne.Container
head *widget.Label
kind bubbleKind
thinkBtn *widget.Button
thinkText *widget.RichText
thinkBox *fyne.Container
think strings.Builder
live *widget.Label
text strings.Builder
}
@@ -45,7 +61,7 @@ type bubble struct {
func newBubble(role string, kind bubbleKind) *bubble {
bg := canvas.NewRectangle(colorFor(kind))
bg.CornerRadius = 10
bg.StrokeColor = colorBorder()
bg.StrokeColor = borderFor(kind)
bg.StrokeWidth = 1
head := widget.NewLabelWithStyle(role, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
@@ -57,7 +73,62 @@ func newBubble(role string, kind bubbleKind) *bubble {
body := container.NewVBox(head, live)
root := container.NewStack(bg, container.New(innerPadLayout{}, body))
return &bubble{root: root, bg: bg, body: body, head: head, live: live}
return &bubble{root: root, bg: bg, body: body, head: head, kind: kind, live: live}
}
// retheme 主题切换后重新上色。
func (b *bubble) retheme() {
b.bg.FillColor = colorFor(b.kind)
b.bg.StrokeColor = borderFor(b.kind)
b.bg.Refresh()
}
// ensureThinking 建出思考过程区块,并插在角色标题与正文之间。
func (b *bubble) ensureThinking() {
if b.thinkBox != nil {
return
}
b.thinkText = widget.NewRichText()
b.thinkText.Wrapping = fyne.TextWrapWord
b.thinkBtn = widget.NewButton("▾ 思考过程", func() { b.toggleThinking() })
b.thinkBtn.Importance = widget.LowImportance
b.thinkBtn.Alignment = widget.ButtonAlignLeading
b.thinkBox = container.NewVBox(b.thinkBtn, b.thinkText)
objs := b.body.Objects
next := make([]fyne.CanvasObject, 0, len(objs)+1)
next = append(next, objs[0], b.thinkBox)
next = append(next, objs[1:]...)
b.body.Objects = next
b.body.Refresh()
}
// toggleThinking 折叠/展开思考过程。
func (b *bubble) toggleThinking() {
if b.thinkText == nil {
return
}
if b.thinkText.Visible() {
b.thinkText.Hide()
b.thinkBtn.SetText("▸ 思考过程")
return
}
b.thinkText.Show()
b.thinkBtn.SetText("▾ 思考过程")
}
// appendThinking 追加思考过程文本。
func (b *bubble) appendThinking(delta string) {
b.ensureThinking()
b.think.WriteString(delta)
b.thinkText.Segments = []widget.RichTextSegment{
&widget.TextSegment{
Text: b.think.String(),
Style: widget.RichTextStyle{TextStyle: fyne.TextStyle{Italic: true}},
},
}
b.thinkText.Refresh()
}
// append 追加流式文本。
@@ -76,12 +147,27 @@ func (b *bubble) finish(err error) {
}
text := strings.TrimSpace(b.text.String())
if text == "" {
text = "(没有返回内容)"
if b.think.Len() > 0 {
text = "(模型只给出了思考过程,没有输出回答)"
} else {
text = "(没有返回内容)"
}
}
rt := widget.NewRichTextFromMarkdown(text)
rt.Wrapping = fyne.TextWrapWord
b.body.Objects = []fyne.CanvasObject{b.head, rt}
objs := b.body.Objects
next := make([]fyne.CanvasObject, 0, len(objs))
next = append(next, objs[0])
if b.thinkBox != nil {
next = append(next, b.thinkBox)
}
next = append(next, rt)
b.body.Objects = next
b.body.Refresh()
// 思考过程默认折起来,把正文让到前面;点标题还能再展开。
if b.thinkText != nil && b.thinkText.Visible() {
b.toggleThinking()
}
}
// innerPadLayout 给气泡留内边距。
@@ -113,6 +199,7 @@ type chatView struct {
list *fyne.Container
scroll *container.Scroll
current *bubble
bubbles []*bubble
}
func newChatView() *chatView {
@@ -127,10 +214,18 @@ func newChatView() *chatView {
func (c *chatView) object() fyne.CanvasObject { return c.scroll }
func (c *chatView) appendBubble(b *bubble) {
c.bubbles = append(c.bubbles, b)
c.list.Add(b.root)
c.scroll.ScrollToBottom()
}
// retheme 主题切换后给所有气泡重新上色。
func (c *chatView) retheme() {
for _, b := range c.bubbles {
b.retheme()
}
}
// addUser 添加一条用户消息。
func (c *chatView) addUser(text string, withImage bool) {
if withImage {
@@ -161,6 +256,15 @@ func (c *chatView) appendDelta(delta string) {
c.scroll.ScrollToBottom()
}
// appendThinking 追加思考过程文本。
func (c *chatView) appendThinking(delta string) {
if c.current == nil {
c.beginAssistant(false)
}
c.current.appendThinking(delta)
c.scroll.ScrollToBottom()
}
// endAssistant 结束当前助手消息。
func (c *chatView) endAssistant(err error) {
if c.current == nil {
@@ -182,6 +286,7 @@ func (c *chatView) addNotice(text string) {
// clear 清空消息列表。
func (c *chatView) clear() {
c.list.Objects = nil
c.bubbles = nil
c.current = nil
c.list.Refresh()
}