wip chat sessions
This commit is contained in:
21 files changed
+2456
No files matched your search
@@ -0,0 +1,144 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// ConversationItemOptions configure a ConversationItem.
|
||||
type ConversationItemOptions struct {
|
||||
// Today decides the time stamp's form; zero uses the window clock.
|
||||
Today Date
|
||||
Selected bool
|
||||
Disabled bool
|
||||
// Draggable lets the row be dragged onto a ProjectList as a ConversationDrag.
|
||||
Draggable bool
|
||||
}
|
||||
|
||||
// ConversationDrag is the value a dragged conversation carries.
|
||||
type ConversationDrag struct{ ID string }
|
||||
|
||||
// ConversationItemView is a built ConversationItem and what the user asked for this frame.
|
||||
type ConversationItemView struct {
|
||||
Element *ui.Element
|
||||
// Opened, Deleted, Exported and PinToggled are requests for the caller to carry out.
|
||||
Opened, Deleted, Exported, PinToggled bool
|
||||
// Renamed reports a confirmed new title, already in conv.Title.
|
||||
Renamed bool
|
||||
}
|
||||
|
||||
type conversationItemState struct {
|
||||
editing, fresh bool
|
||||
draft string
|
||||
}
|
||||
|
||||
type conversationItemKey struct{}
|
||||
|
||||
// ConversationItem is a conversation row: title, time, and a menu to rename in place, pin, export or delete.
|
||||
func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationItemOptions) ConversationItemView {
|
||||
if conv == nil || conv.ID == "" {
|
||||
panic("mujica: ConversationItem needs a conversation with an ID")
|
||||
}
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
today := opts.Today
|
||||
if today.IsZero() {
|
||||
today = DateOf(c.Now())
|
||||
}
|
||||
row := ui.Row(c).Key(conv.ID).FillWidth().MinHeight(d.ControlHeight()).Gap(d.Unit()).AlignItems(ui.Center).
|
||||
Radius(theme.ControlRadius).Disabled(opts.Disabled)
|
||||
v := ConversationItemView{Element: row}
|
||||
st := ui.Local(row, conversationItemKey{}, func() conversationItemState { return conversationItemState{} })
|
||||
if row.IsDisabled() {
|
||||
st.editing = false
|
||||
}
|
||||
if opts.Selected {
|
||||
row.Background(k.Selection)
|
||||
}
|
||||
title := cmp.Or(conv.Title, msg(c, "conversation_list.untitled"))
|
||||
rename := false
|
||||
row.Children(func() {
|
||||
if st.editing {
|
||||
conversationItemEditor(c, k, conv, st, &v)
|
||||
} else {
|
||||
main := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).
|
||||
Justify(ui.Start).Radius(theme.ControlRadius).Label(title)
|
||||
internal.Ring(main, k.Focus, theme.ControlRadius)
|
||||
if opts.Draggable {
|
||||
main.Drag(ConversationDrag{ID: conv.ID})
|
||||
}
|
||||
if main.Hovered() && !opts.Selected {
|
||||
main.Background(k.SurfaceHover)
|
||||
}
|
||||
main.Transition(colorFade)
|
||||
main.Children(func() {
|
||||
if conv.Pinned {
|
||||
ui.Icon(c, icons.Must("star")).Size(14, 14).Shrink(0).TextColor(k.AccentText).Label(msg(c, "conversation_item.pinned"))
|
||||
}
|
||||
t := ui.Text(c, title).SingleLine().Grow(1).MinWidth(0)
|
||||
if opts.Selected {
|
||||
t.TextColor(k.AccentText)
|
||||
}
|
||||
ui.Text(c, dateSeparatorWhen(c, conv.Updated, today)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0)
|
||||
})
|
||||
if !main.IsDisabled() {
|
||||
v.Opened = main.Clicked()
|
||||
rename = main.Shortcut(0, ui.KeyF2)
|
||||
v.Deleted = main.Shortcut(0, ui.KeyDelete)
|
||||
}
|
||||
}
|
||||
menu := DropdownMenu(c, "", DropdownMenuOptions{Icon: icons.Must("ellipsis"), Label: msg(c, "conversation_item.actions"), Disabled: opts.Disabled}, func(m *PopupMenu) {
|
||||
rename = m.Item(msg(c, "conversation_item.rename"), PopupMenuItemOptions{}) || rename
|
||||
pin := msg(c, "conversation_item.pin")
|
||||
if conv.Pinned {
|
||||
pin = msg(c, "conversation_item.unpin")
|
||||
}
|
||||
v.PinToggled = m.Item(pin, PopupMenuItemOptions{})
|
||||
v.Exported = m.Item(msg(c, "conversation_item.export"), PopupMenuItemOptions{})
|
||||
m.Separator()
|
||||
v.Deleted = m.Item(msg(c, "conversation_item.delete"), PopupMenuItemOptions{}) || v.Deleted
|
||||
})
|
||||
menu.Shrink(0).MinWidth(theme.MinHitSize).MinHeight(theme.MinHitSize)
|
||||
})
|
||||
if rename && !row.IsDisabled() {
|
||||
st.editing, st.fresh, st.draft = true, true, conv.Title
|
||||
}
|
||||
switch {
|
||||
case v.Opened:
|
||||
slog.Debug("mujica: conversation_item open", "id", conv.ID)
|
||||
case v.Deleted, v.Exported, v.PinToggled:
|
||||
slog.Debug("mujica: conversation_item action", "id", conv.ID, "delete", v.Deleted, "export", v.Exported, "pin", v.PinToggled)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// conversationItemEditor is the in-place title field: Enter keeps a non-blank title, Escape cancels.
|
||||
func conversationItemEditor(c *ui.Context, k theme.Tokens, conv *ChatConversation, st *conversationItemState, v *ConversationItemView) {
|
||||
d := Density(c)
|
||||
in := ui.TextInputBase(c, &st.draft).Grow(1).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2).
|
||||
Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.AccentText).
|
||||
Label(msg(c, "conversation_item.rename"))
|
||||
internal.Ring(in, k.Focus, theme.ControlRadius)
|
||||
switch {
|
||||
case st.fresh:
|
||||
st.fresh = false
|
||||
in.Focus()
|
||||
case in.IsDisabled():
|
||||
case in.Submitted():
|
||||
if t := strings.TrimSpace(st.draft); t != "" {
|
||||
v.Renamed = t != conv.Title
|
||||
conv.Title = t
|
||||
}
|
||||
st.editing = false
|
||||
slog.Debug("mujica: conversation_item rename", "id", conv.ID, "changed", v.Renamed)
|
||||
case in.Shortcut(0, ui.KeyEscape):
|
||||
st.editing = false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user