ludmila-ai-tui/session.go
2026-06-03 15:05:38 +07:00

217 lines
4.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"charm.land/bubbles/v2/list"
)
type SessionData struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Title string `json:"title"`
Provider string `json:"provider"`
Model string `json:"model"`
Messages []message `json:"messages"`
History []string `json:"history"`
}
type sessionFile struct {
Session SessionData `json:"session"`
Deleted bool `json:"-"`
}
func currentSession(m *chatbot) *SessionData {
if m.sessionIdx < 0 || m.sessionIdx >= len(m.sessions) {
return nil
}
return &m.sessions[m.sessionIdx]
}
func dataDir() string {
dir := os.Getenv("XDG_DATA_HOME")
if dir == "" {
home, _ := os.UserHomeDir()
dir = filepath.Join(home, ".local", "share")
}
return filepath.Join(dir, "ludmila-ai-tui")
}
func sessionsDir() string {
return filepath.Join(dataDir(), "sessions")
}
func sessionPath(id string) string {
return filepath.Join(sessionsDir(), id+".json")
}
func ListSessions() ([]SessionData, error) {
dir := sessionsDir()
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read sessions dir: %w", err)
}
var sessions []SessionData
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
var file sessionFile
if err := json.Unmarshal(data, &file); err != nil {
continue
}
if file.Deleted {
continue
}
sessions = append(sessions, file.Session)
}
sort.Slice(sessions, func(i, j int) bool {
return sessions[i].UpdatedAt.After(sessions[j].UpdatedAt)
})
return sessions, nil
}
func SaveSession(s SessionData) error {
dir := sessionsDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create sessions dir: %w", err)
}
s.UpdatedAt = time.Now()
file := sessionFile{Session: s}
data, err := json.MarshalIndent(file, "", " ")
if err != nil {
return fmt.Errorf("marshal session: %w", err)
}
if err := os.WriteFile(sessionPath(s.ID), data, 0o644); err != nil {
return fmt.Errorf("write session: %w", err)
}
return nil
}
func DeleteSession(id string) error {
data, err := os.ReadFile(sessionPath(id))
if err != nil {
return fmt.Errorf("read session: %w", err)
}
var file sessionFile
if err := json.Unmarshal(data, &file); err != nil {
return fmt.Errorf("unmarshal session: %w", err)
}
file.Deleted = true
updatedData, err := json.MarshalIndent(file, "", " ")
if err != nil {
return fmt.Errorf("marshal deleted session: %w", err)
}
return os.WriteFile(sessionPath(id), updatedData, 0o644)
}
func NewSessionData(provider, model string) SessionData {
now := time.Now()
return SessionData{
ID: now.Format("20060102_150405"),
CreatedAt: now,
UpdatedAt: now,
Title: "New session",
Provider: provider,
Model: model,
}
}
func sessionTitle(s SessionData) string {
if s.Title != "" && s.Title != "New session" {
return s.Title
}
return "New session"
}
func (m *chatbot) saveCurrentSession() {
if cs := currentSession(m); cs != nil {
cs.UpdatedAt = time.Now()
cs.Messages = m.messages
cs.History = m.history
cs.Provider = m.currentProvider
cs.Model = m.currentModel
if len(cs.Messages) > 0 && (cs.Title == "" || cs.Title == "New session") {
cs.Title = sessionTitleFromMessages(cs.Messages)
}
m.sessions[m.sessionIdx] = *cs
_ = SaveSession(*cs)
}
}
func sessionTitleFromMessages(msgs []message) string {
if len(msgs) == 0 {
return "New session"
}
title := strings.TrimSpace(msgs[0].Content)
if idx := strings.IndexAny(title, ".\n"); idx > 0 {
title = title[:idx]
}
runes := []rune(title)
if len(runes) > 40 {
title = string(runes[:37]) + "..."
}
return title
}
type sessionItem struct {
session SessionData
active bool
}
func (s sessionItem) Title() string {
prefix := " "
if s.active {
prefix = "* "
}
return prefix + sessionTitle(s.session)
}
func (s sessionItem) Description() string {
parts := []string{
s.session.Provider + "/" + s.session.Model,
"(" + fmt.Sprintf("%d msgs", len(s.session.Messages)) + ")",
s.session.UpdatedAt.Format("Jan 02 15:04"),
}
return strings.Join(parts, " · ")
}
func (s sessionItem) FilterValue() string {
return s.session.Title + " " + s.session.Provider + " " + s.session.Model
}
func buildSessionItems(sessions []SessionData, activeIdx int) []list.Item {
items := make([]list.Item, len(sessions))
for i, s := range sessions {
items[i] = sessionItem{session: s, active: i == activeIdx}
}
return items
}