优化代码
This commit is contained in:
1 parent
3553790475
commit
eda7e8bc96
23 files changed
+3113
-342
No files matched your search
+109
-4
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user