summaryrefslogtreecommitdiff
path: root/helpers.go
blob: 97c4d1b9749c5dd6b84c032d56a1f0b571e48dc9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package chatpb

import (
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"
	"go.wit.com/log"
	"google.golang.org/protobuf/proto"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)

func (c *Chats) AddGeminiComment(s string) *Chat {
	chat := new(Chat)

	chat.From = Who_GEMINI
	chat.Content = s
	chat.Ctime = timestamppb.New(time.Now())

	c.AppendNew(chat)

	return chat
}

func (c *Chats) AddUserComment(s string) *Chat {
	chat := new(Chat)

	chat.From = Who_USER
	chat.Content = s

	c.AppendNew(chat)

	return chat
}

func UnmarshalChats(data []byte) (*Chats, error) {
	c := new(Chats)
	err := c.Unmarshal(data)
	return c, err
}

func UnmarshalChatsTEXT(data []byte) (*Chats, error) {
	c := new(Chats)
	err := c.UnmarshalTEXT(data)
	return c, err
}

func (all *Chats) AddFile(filename string) error {
	// Nil checks for safety.
	if all == nil {
		return fmt.Errorf("cannot call AddFile on a nil *Chats object")
	}
	if all.Chats == nil {
		all.Chats = make([]*Chat, 0)
	}

	data, err := os.ReadFile(filename)
	if err != nil {
		log.Fatalf("Error reading file %s: %v", filename, err)
		return err
	}

	logData, err := UnmarshalChatsTEXT(data)
	if err != nil {
		log.Fatalf("Error unmarshaling log file %s: %v", filename, err)
		return err
	}

	// New logic to handle both flat and nested formats
	for _, chatOrGroup := range logData.GetChats() {
		if len(chatOrGroup.GetEntries()) > 0 {
			// This is a new-style Chat group with entries
			for _, entry := range chatOrGroup.GetEntries() {
				// Convert the ChatEntry into a new Chat object for the flat list.
				newChat := convertEntryToChat(entry, filename)
				if newChat != nil {
					newChat.VerifyUuid()
					all.AppendByUuid(newChat)
				}
			}
		} else {
			// This is an old-style flat Chat entry.
			// We still process it to handle its external content file correctly.
			newChat := convertChatToChat(chatOrGroup, filename)
			if newChat != nil {
				newChat.VerifyUuid()
				all.AppendByUuid(newChat)
			}
		}
	}
	return nil
}

// convertChatToChat handles an old-style Chat message. It creates a clean
// copy and resolves its external content file.
func convertChatToChat(chat *Chat, filename string) *Chat {
	if chat == nil {
		return nil
	}

	// Manually create a ChatEntry from the Chat fields to reuse the logic.
	entry := &ChatEntry{
		From:        chat.GetFrom(),
		Ctime:       chat.GetCtime(),
		Content:     chat.GetContent(),
		Table:       chat.GetTable(),
		ToolCalls:   chat.GetToolCalls(),
		ContentFile: chat.GetContentFile(),
		Uuid:        chat.GetUuid(),
		Snippets:    chat.GetSnippets(),
	}
	return convertEntryToChat(entry, filename)
}

// convertEntryToChat creates a new Chat object from a ChatEntry's data
// and resolves its external content file.
func convertEntryToChat(entry *ChatEntry, filename string) *Chat {
	if entry == nil {
		return nil
	}

	// Create a new Chat object and copy the fields.
	newChat := &Chat{
		From:      entry.GetFrom(),
		Ctime:     entry.GetCtime(),
		Table:     entry.GetTable(),
		ToolCalls: entry.GetToolCalls(),
		Snippets:  entry.GetSnippets(),
		Uuid:      entry.GetUuid(),
	}

	// Handle content: prefer content_file, fallback to content.
	var content string
	if contentFile := entry.GetContentFile(); contentFile != "" {
		logDir := filepath.Dir(filename)
		contentPath := filepath.Join(logDir, contentFile)
		contentBytes, err := os.ReadFile(contentPath)
		if err != nil {
			content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err)
		} else {
			content = string(contentBytes)
		}
	} else {
		content = entry.GetContent()
	}
	newChat.Content = content

	return newChat
}

func (chats *Chats) VerifyUuids() bool {
	var changed bool

	all := chats.SortByUuid()
	for all.Scan() {
		chat := all.Next()
		if chat.Uuid == "" {
			chat.Uuid = uuid.New().String()
			changed = true
		}
	}
	return changed
}

func (c *Chat) VerifyUuid() bool {
	if c.Uuid == "" {
		c.Uuid = uuid.New().String()
		return true
	}
	return false
}

func (x *Chats) AppendNew(y *Chat) {
	x.Lock()
	defer x.Unlock()

	var chat *Chat
	chat = proto.Clone(y).(*Chat)

	x.Chats = append(x.Chats, chat)
}