File size: 8,482 Bytes
4d35814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/**
 * Message branching utilities for conversation tree navigation.
 *
 * Conversation branching allows users to edit messages and create alternate paths
 * while preserving the original conversation flow. Each message has parent/children
 * relationships forming a tree structure.
 *
 * Example tree:
 * root
 *  β”œβ”€β”€ message 1 (user)
 *  β”‚      └── message 2 (assistant)
 *  β”‚             β”œβ”€β”€ message 3 (user)
 *  β”‚             └── message 6 (user) ← new branch
 *  └── message 4 (user)
 *        └── message 5 (assistant)
 */

/**
 * Filters messages to get the conversation path from root to a specific leaf node.
 * If the leafNodeId doesn't exist, returns the path with the latest timestamp.
 *
 * @param messages - All messages in the conversation
 * @param leafNodeId - The target leaf node ID to trace back from
 * @param includeRoot - Whether to include root messages in the result
 * @returns Array of messages from root to leaf, sorted by timestamp
 */
export function filterByLeafNodeId(
	messages: readonly DatabaseMessage[],
	leafNodeId: string,
	includeRoot: boolean = false
): readonly DatabaseMessage[] {
	const result: DatabaseMessage[] = [];
	const nodeMap = new Map<string, DatabaseMessage>();

	// Build node map for quick lookups
	for (const msg of messages) {
		nodeMap.set(msg.id, msg);
	}

	// Find the starting node (leaf node or latest if not found)
	let startNode: DatabaseMessage | undefined = nodeMap.get(leafNodeId);
	if (!startNode) {
		// If leaf node not found, use the message with latest timestamp
		let latestTime = -1;
		for (const msg of messages) {
			if (msg.timestamp > latestTime) {
				startNode = msg;
				latestTime = msg.timestamp;
			}
		}
	}

	// Traverse from leaf to root, collecting messages
	let currentNode: DatabaseMessage | undefined = startNode;
	while (currentNode) {
		// Include message if it's not root, or if we want to include root
		if (currentNode.type !== 'root' || includeRoot) {
			result.push(currentNode);
		}

		// Stop traversal if parent is null (reached root)
		if (currentNode.parent === null) {
			break;
		}
		currentNode = nodeMap.get(currentNode.parent);
	}

	// Sort by timestamp to get chronological order (root to leaf)
	result.sort((a, b) => a.timestamp - b.timestamp);
	return result;
}

/**
 * Finds the leaf node (message with no children) for a given message branch.
 * Traverses down the tree following the last child until reaching a leaf.
 *
 * @param messages - All messages in the conversation
 * @param messageId - Starting message ID to find leaf for
 * @returns The leaf node ID, or the original messageId if no children
 */
export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string {
	const nodeMap = new Map<string, DatabaseMessage>();

	// Build node map for quick lookups
	for (const msg of messages) {
		nodeMap.set(msg.id, msg);
	}

	let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
	while (currentNode && currentNode.children.length > 0) {
		// Follow the last child (most recent branch)
		const lastChildId = currentNode.children[currentNode.children.length - 1];
		currentNode = nodeMap.get(lastChildId);
	}

	return currentNode?.id ?? messageId;
}

/**
 * Finds all descendant messages (children, grandchildren, etc.) of a given message.
 * This is used for cascading deletion to remove all messages in a branch.
 *
 * @param messages - All messages in the conversation
 * @param messageId - The root message ID to find descendants for
 * @returns Array of all descendant message IDs
 */
export function findDescendantMessages(
	messages: readonly DatabaseMessage[],
	messageId: string
): string[] {
	const nodeMap = new Map<string, DatabaseMessage>();

	// Build node map for quick lookups
	for (const msg of messages) {
		nodeMap.set(msg.id, msg);
	}

	const descendants: string[] = [];
	const queue: string[] = [messageId];

	while (queue.length > 0) {
		const currentId = queue.shift()!;
		const currentNode = nodeMap.get(currentId);

		if (currentNode) {
			// Add all children to the queue and descendants list
			for (const childId of currentNode.children) {
				descendants.push(childId);
				queue.push(childId);
			}
		}
	}

	return descendants;
}

/**
 * Gets sibling information for a message, including all sibling IDs and current position.
 * Siblings are messages that share the same parent.
 *
 * @param messages - All messages in the conversation
 * @param messageId - The message to get sibling info for
 * @returns Sibling information including leaf node IDs for navigation
 */
export function getMessageSiblings(
	messages: readonly DatabaseMessage[],
	messageId: string
): ChatMessageSiblingInfo | null {
	const nodeMap = new Map<string, DatabaseMessage>();

	// Build node map for quick lookups
	for (const msg of messages) {
		nodeMap.set(msg.id, msg);
	}

	const message = nodeMap.get(messageId);
	if (!message) {
		return null;
	}

	// Handle null parent (root message) case
	if (message.parent === null) {
		// No parent means this is likely a root node with no siblings
		return {
			message,
			siblingIds: [messageId],
			currentIndex: 0,
			totalSiblings: 1
		};
	}

	const parentNode = nodeMap.get(message.parent);
	if (!parentNode) {
		// Parent not found - treat as single message
		return {
			message,
			siblingIds: [messageId],
			currentIndex: 0,
			totalSiblings: 1
		};
	}

	// Get all sibling IDs (including self)
	const siblingIds = parentNode.children;

	// Convert sibling message IDs to their corresponding leaf node IDs
	// This allows navigation between different conversation branches
	const siblingLeafIds = siblingIds.map((siblingId: string) => findLeafNode(messages, siblingId));

	// Find current message's position among siblings
	const currentIndex = siblingIds.indexOf(messageId);

	return {
		message,
		siblingIds: siblingLeafIds,
		currentIndex,
		totalSiblings: siblingIds.length
	};
}

/**
 * Creates a display-ready list of messages with sibling information for UI rendering.
 * This is the main function used by chat components to render conversation branches.
 *
 * @param messages - All messages in the conversation
 * @param leafNodeId - Current leaf node being viewed
 * @returns Array of messages with sibling navigation info
 */
export function getMessageDisplayList(
	messages: readonly DatabaseMessage[],
	leafNodeId: string
): ChatMessageSiblingInfo[] {
	// Get the current conversation path
	const currentPath = filterByLeafNodeId(messages, leafNodeId, true);
	const result: ChatMessageSiblingInfo[] = [];

	// Add sibling info for each message in the current path
	for (const message of currentPath) {
		if (message.type === 'root') {
			continue; // Skip root messages in display
		}

		const siblingInfo = getMessageSiblings(messages, message.id);
		if (siblingInfo) {
			result.push(siblingInfo);
		}
	}

	return result;
}

/**
 * Checks if a message has multiple siblings (indicating branching at that point).
 *
 * @param messages - All messages in the conversation
 * @param messageId - The message to check
 * @returns True if the message has siblings
 */
export function hasMessageSiblings(
	messages: readonly DatabaseMessage[],
	messageId: string
): boolean {
	const siblingInfo = getMessageSiblings(messages, messageId);
	return siblingInfo ? siblingInfo.totalSiblings > 1 : false;
}

/**
 * Gets the next sibling message ID for navigation.
 *
 * @param messages - All messages in the conversation
 * @param messageId - Current message ID
 * @returns Next sibling's leaf node ID, or null if at the end
 */
export function getNextSibling(
	messages: readonly DatabaseMessage[],
	messageId: string
): string | null {
	const siblingInfo = getMessageSiblings(messages, messageId);
	if (!siblingInfo || siblingInfo.currentIndex >= siblingInfo.totalSiblings - 1) {
		return null;
	}

	return siblingInfo.siblingIds[siblingInfo.currentIndex + 1];
}

/**
 * Gets the previous sibling message ID for navigation.
 *
 * @param messages - All messages in the conversation
 * @param messageId - Current message ID
 * @returns Previous sibling's leaf node ID, or null if at the beginning
 */
export function getPreviousSibling(
	messages: readonly DatabaseMessage[],
	messageId: string
): string | null {
	const siblingInfo = getMessageSiblings(messages, messageId);
	if (!siblingInfo || siblingInfo.currentIndex <= 0) {
		return null;
	}

	return siblingInfo.siblingIds[siblingInfo.currentIndex - 1];
}