wip chat sessions
This commit is contained in:
21 files changed
+2456
No files matched your search
@@ -0,0 +1,198 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// ChatConversation is one saved conversation, as the caller stores it.
|
||||
type ChatConversation struct {
|
||||
ID, Title string
|
||||
Updated time.Time
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
// ConversationListOptions configure a ConversationList.
|
||||
type ConversationListOptions struct {
|
||||
// Now dates the groups; zero uses the window clock.
|
||||
Now time.Time
|
||||
// Label names the list for assistive technology.
|
||||
Label string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// ConversationListState is the chosen conversation and the list's place.
|
||||
type ConversationListState struct {
|
||||
// Selected is the chosen conversation's ID; empty for none.
|
||||
Selected string
|
||||
rows ui.ListState
|
||||
cursor int
|
||||
plan conversationListPlan
|
||||
}
|
||||
|
||||
// conversationListPlan is the grouped rows, rebuilt only when the data or day changes.
|
||||
type conversationListPlan struct {
|
||||
src []ChatConversation
|
||||
day Date
|
||||
rows []conversationListRow
|
||||
built bool
|
||||
}
|
||||
|
||||
// conversationListRow is group header g (item < 0) or an index into the items.
|
||||
type conversationListRow struct {
|
||||
group, item int
|
||||
}
|
||||
|
||||
// ConversationListView is a built ConversationList and this frame's events.
|
||||
type ConversationListView struct {
|
||||
Element *ui.Element
|
||||
changed bool
|
||||
submitted bool
|
||||
}
|
||||
|
||||
// Changed reports a newly chosen conversation, in State.Selected.
|
||||
func (v ConversationListView) Changed() bool { return v.changed }
|
||||
|
||||
// Submitted reports Enter or a double click on the chosen conversation.
|
||||
func (v ConversationListView) Submitted() bool { return v.submitted }
|
||||
|
||||
// ConversationList groups conversations into pinned, today, yesterday, past 7 days and older, building only rows in view.
|
||||
func ConversationList(c *ui.Context, s *ConversationListState, items []ChatConversation, opts ConversationListOptions, row func(conv ChatConversation)) ConversationListView {
|
||||
if s == nil {
|
||||
panic("mujica: ConversationList needs a state")
|
||||
}
|
||||
now := opts.Now
|
||||
if now.IsZero() {
|
||||
now = c.Now()
|
||||
}
|
||||
conversationListPlanRows(&s.plan, items, DateOf(now))
|
||||
rows := s.plan.rows
|
||||
s.cursor = slices.IndexFunc(rows, func(r conversationListRow) bool { return r.item >= 0 && items[r.item].ID == s.Selected })
|
||||
before := s.cursor
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
cursor := &s.cursor
|
||||
if opts.Disabled {
|
||||
// A disabled list moves a copy of the cursor.
|
||||
cp := s.cursor
|
||||
cursor = &cp
|
||||
}
|
||||
s.rows.Key = func(i int) any {
|
||||
if r := rows[i]; r.item >= 0 {
|
||||
return items[r.item].ID
|
||||
}
|
||||
return conversationListGroups[rows[i].group]
|
||||
}
|
||||
s.rows.Header = func(i int) bool { return rows[i].item < 0 }
|
||||
s.rows.Label = func(i int) string { return conversationListTitle(c, items, rows[i]) }
|
||||
s.rows.Selected = cursor
|
||||
var list *ui.Element
|
||||
wrap := ui.Column(c).Grow(1).MinHeight(0).Disabled(opts.Disabled)
|
||||
wrap.Children(func() {
|
||||
restore := dataSelectTheme(c, 0)
|
||||
list = ui.List(c, &s.rows, len(rows), func(i int) {
|
||||
r := rows[i]
|
||||
if r.item < 0 {
|
||||
conversationListHeader(c, k, msg(c, conversationListGroups[r.group]))
|
||||
return
|
||||
}
|
||||
conv := items[r.item]
|
||||
ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
||||
ui.Box(c).Size(8, 8).Shrink(0).Draw(func(p *ui.Painter, b ui.Rect) {
|
||||
if conv.ID == s.Selected {
|
||||
p.FillPath(internal.Diamond(b.X+b.W/2, b.Y+b.H/2, 8), k.AccentText)
|
||||
}
|
||||
})
|
||||
if row != nil {
|
||||
ui.Row(c).Grow(1).MinWidth(0).Children(func() { row(conv) })
|
||||
return
|
||||
}
|
||||
ui.Text(c, conversationListTitle(c, items, r)).SingleLine().Grow(1).MinWidth(0)
|
||||
})
|
||||
}).Grow(1).MinHeight(0).Padding(d.Unit()).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
||||
Radius(theme.ControlRadius).Label(opts.Label)
|
||||
restore()
|
||||
internal.Ring(list, k.Focus, theme.ControlRadius)
|
||||
})
|
||||
v := ConversationListView{Element: wrap}
|
||||
if !wrap.IsDisabled() {
|
||||
if s.cursor != before && s.cursor >= 0 && rows[s.cursor].item >= 0 {
|
||||
s.Selected, v.changed = items[rows[s.cursor].item].ID, true
|
||||
slog.Debug("mujica: conversation_list select", "id", s.Selected)
|
||||
}
|
||||
v.submitted = list.Submitted() && s.Selected != ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func conversationListTitle(c *ui.Context, items []ChatConversation, r conversationListRow) string {
|
||||
if r.item < 0 {
|
||||
return msg(c, conversationListGroups[r.group])
|
||||
}
|
||||
return cmp.Or(items[r.item].Title, msg(c, "conversation_list.untitled"))
|
||||
}
|
||||
|
||||
func conversationListHeader(c *ui.Context, k theme.Tokens, title string) {
|
||||
d := Density(c)
|
||||
ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).AlignItems(ui.Center).Background(k.Surface).Children(func() {
|
||||
ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).SingleLine()
|
||||
})
|
||||
}
|
||||
|
||||
// conversationListPlanRows regroups items when the slice or the day changed.
|
||||
func conversationListPlanRows(p *conversationListPlan, items []ChatConversation, today Date) {
|
||||
same := p.built && p.day == today && len(p.src) == len(items) && (len(items) == 0 || &p.src[0] == &items[0])
|
||||
if same {
|
||||
return
|
||||
}
|
||||
p.src, p.day, p.built = items, today, true
|
||||
order := make([]int, len(items))
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
slices.SortStableFunc(order, func(a, b int) int { return items[b].Updated.Compare(items[a].Updated) })
|
||||
groups := [5][]int{}
|
||||
for _, i := range order {
|
||||
g := conversationListGroup(items[i], today)
|
||||
groups[g] = append(groups[g], i)
|
||||
}
|
||||
p.rows = p.rows[:0]
|
||||
for g, idx := range groups {
|
||||
if len(idx) == 0 {
|
||||
continue
|
||||
}
|
||||
p.rows = append(p.rows, conversationListRow{group: g, item: -1})
|
||||
for _, i := range idx {
|
||||
p.rows = append(p.rows, conversationListRow{item: i})
|
||||
}
|
||||
}
|
||||
slog.Debug("mujica: conversation_list regroup", "count", len(items))
|
||||
}
|
||||
|
||||
var conversationListGroups = [5]string{
|
||||
"conversation_list.pinned", "conversation_list.today", "conversation_list.yesterday",
|
||||
"conversation_list.week", "conversation_list.older",
|
||||
}
|
||||
|
||||
// conversationListGroup is the group index of conv: pinned, today, yesterday, 7 days, older.
|
||||
func conversationListGroup(conv ChatConversation, today Date) int {
|
||||
day := DateOf(conv.Updated)
|
||||
switch {
|
||||
case conv.Pinned:
|
||||
return 0
|
||||
case day.Compare(today) >= 0:
|
||||
return 1
|
||||
case day == today.AddDays(-1):
|
||||
return 2
|
||||
case day.Compare(today.AddDays(-7)) >= 0:
|
||||
return 3
|
||||
}
|
||||
return 4
|
||||
}
|
||||
Reference in new issue
Block a user