@aleph-alpha/chat-kit 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DmcLXAgB.js → CkInput.vue_vue_type_script_setup_true_lang-DpYyyCgU.js} +481 -79
- package/dist/chat-kit.css +9 -14
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +20 -0
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/index.d.ts +1 -0
- package/dist/components/base/CkAgentMessage/index.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/types.d.ts +12 -0
- package/dist/components/base/CkAgentMessage/types.d.ts.map +1 -0
- package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts +13 -0
- package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts.map +1 -1
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
- package/dist/components/base/CkConversationLayout/types.d.ts +9 -2
- package/dist/components/base/CkConversationLayout/types.d.ts.map +1 -1
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts +8 -0
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +1 -1
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts +3 -15
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkCopyMessageActionButton/index.d.ts +1 -0
- package/dist/components/base/CkCopyMessageActionButton/index.d.ts.map +1 -1
- package/dist/components/base/CkCopyMessageActionButton/types.d.ts +45 -0
- package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +1 -0
- package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts +31 -0
- package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts.map +1 -0
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +31 -0
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -0
- package/dist/components/base/CkHistoryItem/activeIndicator.d.ts +15 -0
- package/dist/components/base/CkHistoryItem/activeIndicator.d.ts.map +1 -0
- package/dist/components/base/CkHistoryItem/index.d.ts +3 -0
- package/dist/components/base/CkHistoryItem/index.d.ts.map +1 -0
- package/dist/components/base/CkHistoryItem/types.d.ts +96 -0
- package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -0
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts +16 -0
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts.map +1 -0
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts +17 -0
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts.map +1 -0
- package/dist/components/base/CkHistoryWrapper/index.d.ts +3 -0
- package/dist/components/base/CkHistoryWrapper/index.d.ts.map +1 -0
- package/dist/components/base/CkHistoryWrapper/types.d.ts +17 -0
- package/dist/components/base/CkHistoryWrapper/types.d.ts.map +1 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +28 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/types.d.ts +16 -0
- package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts +4 -0
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +1 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts +2 -5
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkMessageActionButton/index.d.ts +1 -0
- package/dist/components/base/CkMessageActionButton/index.d.ts.map +1 -1
- package/dist/components/base/CkMessageActionButton/types.d.ts +22 -0
- package/dist/components/base/CkMessageActionButton/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts +12 -0
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts.map +1 -1
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +2 -7
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptActionButton/index.d.ts +1 -0
- package/dist/components/base/CkPromptActionButton/index.d.ts.map +1 -1
- package/dist/components/base/CkPromptActionButton/types.d.ts +33 -0
- package/dist/components/base/CkPromptActionButton/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +17 -0
- package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts +3 -9
- package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/index.d.ts +1 -0
- package/dist/components/base/CkPromptInput/index.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/types.d.ts +39 -0
- package/dist/components/base/CkPromptInput/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.d.ts +12 -0
- package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.d.ts.map +1 -1
- package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts +2 -4
- package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptInputDisclaimer/index.d.ts +1 -0
- package/dist/components/base/CkPromptInputDisclaimer/index.d.ts.map +1 -1
- package/dist/components/base/CkPromptInputDisclaimer/types.d.ts +18 -0
- package/dist/components/base/CkPromptInputDisclaimer/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts +8 -0
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts +3 -6
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/index.d.ts +1 -0
- package/dist/components/base/CkPromptStopButton/index.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/types.d.ts +28 -0
- package/dist/components/base/CkPromptStopButton/types.d.ts.map +1 -0
- package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts +15 -0
- package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
- package/dist/components/base/CkUserMessage/types.d.ts +9 -2
- package/dist/components/base/CkUserMessage/types.d.ts.map +1 -1
- package/dist/components/base/icons/CkStopIcon.vue.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +5 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +55 -0
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/types.d.ts +25 -6
- package/dist/components/composed/CkConversation/types.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/CkHistory.stories.d.ts +33 -0
- package/dist/components/composed/CkHistory/CkHistory.stories.d.ts.map +1 -0
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +50 -0
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts.map +1 -0
- package/dist/components/composed/CkHistory/index.d.ts +3 -0
- package/dist/components/composed/CkHistory/index.d.ts.map +1 -0
- package/dist/components/composed/CkHistory/types.d.ts +71 -0
- package/dist/components/composed/CkHistory/types.d.ts.map +1 -0
- package/dist/components/composed/CkInput/CkInput.stories.d.ts +31 -1
- package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +1 -1
- package/dist/components/composed/CkInput/CkInput.vue.d.ts +3 -12
- package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +1 -1
- package/dist/components/composed/CkInput/index.d.ts +1 -0
- package/dist/components/composed/CkInput/index.d.ts.map +1 -1
- package/dist/components/composed/CkInput/types.d.ts +60 -0
- package/dist/components/composed/CkInput/types.d.ts.map +1 -0
- package/dist/components/composed/index.d.ts +2 -0
- package/dist/components/composed/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -4
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +14 -11
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +3 -3
- package/dist/composables/useConversations.d.ts +72 -0
- package/dist/composables/useConversations.d.ts.map +1 -0
- package/dist/generateId-BHf0W4Rq.js +6 -0
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/useChatKitLabels.d.ts +8 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/secondsToDate.d.ts +6 -0
- package/dist/helpers/secondsToDate.d.ts.map +1 -0
- package/dist/helpers/toError.d.ts +7 -0
- package/dist/helpers/toError.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -16
- package/dist/services/ChatService.d.ts.map +1 -1
- package/dist/services/MockChatService.d.ts.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +4 -1
- package/dist/services/types.d.ts +53 -0
- package/dist/services/types.d.ts.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/{useChatKitLabels-DNwoZ1x6.js → useChatKitLabels-Dvi2FmjE.js} +8 -2
- package/dist/useConversations-Ca242D2P.js +195 -0
- package/dist/{useMessageHandler-D7OdrrcB.js → useMessageHandler-uLHNac39.js} +1 -3
- package/package.json +5 -4
- package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
- package/src/components/base/CkAgentMessage/index.ts +2 -0
- package/src/components/base/CkAgentMessage/types.ts +11 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
- package/src/components/base/CkConversationLayout/index.ts +2 -0
- package/src/components/base/CkConversationLayout/types.ts +18 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
- package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
- package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
- package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
- package/src/components/base/CkHistoryItem/index.ts +2 -0
- package/src/components/base/CkHistoryItem/types.ts +111 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
- package/src/components/base/CkHistoryWrapper/index.ts +2 -0
- package/src/components/base/CkHistoryWrapper/types.ts +16 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
- package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
- package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
- package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
- package/src/components/base/CkMessageActionButton/index.ts +2 -0
- package/src/components/base/CkMessageActionButton/types.ts +21 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
- package/src/components/base/CkPromptActionButton/index.ts +2 -0
- package/src/components/base/CkPromptActionButton/types.ts +32 -0
- package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
- package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
- package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
- package/src/components/base/CkPromptInput/index.ts +2 -0
- package/src/components/base/CkPromptInput/types.ts +39 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
- package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
- package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
- package/src/components/base/CkPromptStopButton/index.ts +2 -0
- package/src/components/base/CkPromptStopButton/types.ts +28 -0
- package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
- package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
- package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
- package/src/components/base/CkUserMessage/index.ts +2 -0
- package/src/components/base/CkUserMessage/types.ts +14 -0
- package/src/components/base/icons/CkSendIcon.vue +22 -0
- package/src/components/base/icons/CkStopIcon.vue +18 -0
- package/src/components/base/icons/index.ts +2 -0
- package/src/components/base/index.ts +21 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
- package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
- package/src/components/composed/CkConversation/CkConversation.vue +146 -0
- package/src/components/composed/CkConversation/index.ts +2 -0
- package/src/components/composed/CkConversation/types.ts +37 -0
- package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
- package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
- package/src/components/composed/CkHistory/CkHistory.vue +126 -0
- package/src/components/composed/CkHistory/index.ts +2 -0
- package/src/components/composed/CkHistory/types.ts +72 -0
- package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
- package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
- package/src/components/composed/CkInput/CkInput.vue +104 -0
- package/src/components/composed/CkInput/index.ts +2 -0
- package/src/components/composed/CkInput/types.ts +60 -0
- package/src/components/composed/index.ts +5 -0
- package/src/components/index.ts +34 -0
- package/src/composables/index.ts +9 -0
- package/src/composables/types.ts +36 -0
- package/src/composables/useChat.ts +18 -0
- package/src/composables/useConversations.spec.ts +557 -0
- package/src/composables/useConversations.ts +321 -0
- package/src/handlers/index.ts +9 -0
- package/src/handlers/useChatKitLabels.spec.ts +290 -0
- package/src/handlers/useChatKitLabels.ts +96 -0
- package/src/handlers/useMarkdownParser.spec.ts +112 -0
- package/src/handlers/useMarkdownParser.ts +47 -0
- package/src/handlers/useMessageHandler.spec.ts +535 -0
- package/src/handlers/useMessageHandler.ts +138 -0
- package/src/helpers/conversationTree.spec.ts +423 -0
- package/src/helpers/conversationTree.ts +117 -0
- package/src/helpers/generateId.ts +5 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/secondsToDate.ts +7 -0
- package/src/helpers/toError.ts +8 -0
- package/src/index.ts +79 -0
- package/src/services/ChatService.ts +60 -0
- package/src/services/MockChatService.ts +47 -0
- package/src/services/index.ts +12 -0
- package/src/services/types.ts +92 -0
- package/src/test-setup.ts +12 -0
- package/src/test-utils/createTextPlugin.ts +9 -0
- package/src/types.ts +47 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateId,
|
|
3
|
+
addNode as treeAddNode,
|
|
4
|
+
createEmptyTree,
|
|
5
|
+
getLinearPath,
|
|
6
|
+
navigate as treeNavigate,
|
|
7
|
+
updateNode,
|
|
8
|
+
updateNodeId,
|
|
9
|
+
} from '../helpers';
|
|
10
|
+
import type {
|
|
11
|
+
BaseMessage,
|
|
12
|
+
ConversationTree,
|
|
13
|
+
MessageRole,
|
|
14
|
+
MessageStatus,
|
|
15
|
+
TreeMessage,
|
|
16
|
+
} from '../types';
|
|
17
|
+
import { computed, reactive, readonly, ref, type Ref } from 'vue';
|
|
18
|
+
|
|
19
|
+
export const useMessageHandler = () => {
|
|
20
|
+
const tree = reactive<ConversationTree>(createEmptyTree());
|
|
21
|
+
const currentUserMessage = ref('');
|
|
22
|
+
|
|
23
|
+
const messages = computed<BaseMessage[]>(() => getLinearPath(tree));
|
|
24
|
+
|
|
25
|
+
const currentLeafId = computed<string | null>(() => {
|
|
26
|
+
const list = messages.value;
|
|
27
|
+
return list.length > 0 ? list[list.length - 1].id : null;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function setCurrentUserMessage(text: string) {
|
|
31
|
+
currentUserMessage.value = text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clearCurrentUserMessage() {
|
|
35
|
+
currentUserMessage.value = '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commitUserMessage(): BaseMessage {
|
|
39
|
+
const id = generateId();
|
|
40
|
+
const text = currentUserMessage.value.trim();
|
|
41
|
+
if (!text) throw new Error('No message');
|
|
42
|
+
|
|
43
|
+
const node: TreeMessage = {
|
|
44
|
+
id,
|
|
45
|
+
parentId: currentLeafId.value,
|
|
46
|
+
childrenIds: [],
|
|
47
|
+
activeChildIndex: 0,
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: text,
|
|
50
|
+
status: 'completed',
|
|
51
|
+
createdAt: new Date(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
treeAddNode(tree, node);
|
|
55
|
+
clearCurrentUserMessage();
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: node.id,
|
|
59
|
+
role: node.role,
|
|
60
|
+
content: node.content,
|
|
61
|
+
createdAt: node.createdAt,
|
|
62
|
+
status: node.status,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function updateMessage(message: {
|
|
67
|
+
id: string;
|
|
68
|
+
content: string;
|
|
69
|
+
role: MessageRole;
|
|
70
|
+
status: MessageStatus;
|
|
71
|
+
}) {
|
|
72
|
+
const existing = tree.nodes[message.id];
|
|
73
|
+
|
|
74
|
+
if (existing) {
|
|
75
|
+
updateNode(tree, message.id, {
|
|
76
|
+
content: message.content,
|
|
77
|
+
role: message.role,
|
|
78
|
+
status: message.status,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const node: TreeMessage = {
|
|
82
|
+
id: message.id,
|
|
83
|
+
parentId: currentLeafId.value,
|
|
84
|
+
childrenIds: [],
|
|
85
|
+
activeChildIndex: 0,
|
|
86
|
+
role: message.role,
|
|
87
|
+
content: message.content,
|
|
88
|
+
status: message.status,
|
|
89
|
+
createdAt: new Date(),
|
|
90
|
+
};
|
|
91
|
+
treeAddNode(tree, node);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getMessageById(id: string): BaseMessage | undefined {
|
|
96
|
+
const node = tree.nodes[id];
|
|
97
|
+
if (!node) return undefined;
|
|
98
|
+
return {
|
|
99
|
+
id: node.id,
|
|
100
|
+
role: node.role,
|
|
101
|
+
content: node.content,
|
|
102
|
+
createdAt: node.createdAt,
|
|
103
|
+
status: node.status,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateMessageId(oldId: string, newId: string) {
|
|
108
|
+
updateNodeId(tree, oldId, newId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clearMessages() {
|
|
112
|
+
Object.assign(tree, createEmptyTree());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setTree(incoming: ConversationTree) {
|
|
116
|
+
tree.nodes = incoming.nodes;
|
|
117
|
+
tree.rootId = incoming.rootId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function navigate(targetId: string) {
|
|
121
|
+
treeNavigate(tree, targetId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
messages: messages as Ref<BaseMessage[]>,
|
|
126
|
+
currentUserMessage: readonly(currentUserMessage),
|
|
127
|
+
currentLeafId,
|
|
128
|
+
updateMessage,
|
|
129
|
+
setCurrentUserMessage,
|
|
130
|
+
getMessageById,
|
|
131
|
+
clearCurrentUserMessage,
|
|
132
|
+
commitUserMessage,
|
|
133
|
+
updateMessageId,
|
|
134
|
+
clearMessages,
|
|
135
|
+
setTree,
|
|
136
|
+
navigate,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import type { ConversationTree, TreeMessage } from '../types';
|
|
2
|
+
import {
|
|
3
|
+
addNode,
|
|
4
|
+
createEmptyTree,
|
|
5
|
+
getLinearPath,
|
|
6
|
+
navigate,
|
|
7
|
+
updateNode,
|
|
8
|
+
updateNodeId,
|
|
9
|
+
} from './conversationTree';
|
|
10
|
+
|
|
11
|
+
function makeTreeMessage(
|
|
12
|
+
overrides: Partial<TreeMessage> & { id: string },
|
|
13
|
+
): TreeMessage {
|
|
14
|
+
return {
|
|
15
|
+
parentId: null,
|
|
16
|
+
childrenIds: [],
|
|
17
|
+
activeChildIndex: 0,
|
|
18
|
+
role: 'user',
|
|
19
|
+
content: '',
|
|
20
|
+
status: 'completed',
|
|
21
|
+
createdAt: new Date('2025-01-01'),
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeTree(
|
|
27
|
+
nodes: Record<string, TreeMessage>,
|
|
28
|
+
rootId: string,
|
|
29
|
+
): ConversationTree {
|
|
30
|
+
for (const node of Object.values(nodes)) {
|
|
31
|
+
node.childrenIds = [];
|
|
32
|
+
}
|
|
33
|
+
for (const node of Object.values(nodes)) {
|
|
34
|
+
if (node.parentId !== null) {
|
|
35
|
+
const parent = nodes[node.parentId];
|
|
36
|
+
if (parent) parent.childrenIds.push(node.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const node of Object.values(nodes)) {
|
|
40
|
+
node.activeChildIndex = 0;
|
|
41
|
+
}
|
|
42
|
+
return { nodes, rootId };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds a simple branching tree for tests:
|
|
47
|
+
*
|
|
48
|
+
* root(u1)
|
|
49
|
+
* ├─ a1 (assistant, child 0) [activeChildIndex=0 → u2]
|
|
50
|
+
* │ └─ u2 [activeChildIndex=0 → a2]
|
|
51
|
+
* │ ├─ a2 (child 0)
|
|
52
|
+
* │ └─ a3 (child 1, regeneration of a2)
|
|
53
|
+
* └─ a1b (assistant, child 1, regeneration of a1)
|
|
54
|
+
* └─ u3
|
|
55
|
+
*
|
|
56
|
+
* Active path: u1 → a1 → u2 → a2 (activeChildIndex=0 everywhere)
|
|
57
|
+
*/
|
|
58
|
+
function makeBranchingTree(): ConversationTree {
|
|
59
|
+
return makeTree(
|
|
60
|
+
{
|
|
61
|
+
u1: makeTreeMessage({ id: 'u1', role: 'user', content: 'Hello' }),
|
|
62
|
+
a1: makeTreeMessage({
|
|
63
|
+
id: 'a1',
|
|
64
|
+
parentId: 'u1',
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
content: 'Hi',
|
|
67
|
+
}),
|
|
68
|
+
u2: makeTreeMessage({
|
|
69
|
+
id: 'u2',
|
|
70
|
+
parentId: 'a1',
|
|
71
|
+
role: 'user',
|
|
72
|
+
content: 'Question',
|
|
73
|
+
}),
|
|
74
|
+
a2: makeTreeMessage({
|
|
75
|
+
id: 'a2',
|
|
76
|
+
parentId: 'u2',
|
|
77
|
+
role: 'assistant',
|
|
78
|
+
content: 'Answer 1',
|
|
79
|
+
}),
|
|
80
|
+
a3: makeTreeMessage({
|
|
81
|
+
id: 'a3',
|
|
82
|
+
parentId: 'u2',
|
|
83
|
+
role: 'assistant',
|
|
84
|
+
content: 'Answer 2',
|
|
85
|
+
}),
|
|
86
|
+
a1b: makeTreeMessage({
|
|
87
|
+
id: 'a1b',
|
|
88
|
+
parentId: 'u1',
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
content: 'Alt hi',
|
|
91
|
+
}),
|
|
92
|
+
u3: makeTreeMessage({
|
|
93
|
+
id: 'u3',
|
|
94
|
+
parentId: 'a1b',
|
|
95
|
+
role: 'user',
|
|
96
|
+
content: 'Follow-up',
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
'u1',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('createEmptyTree', () => {
|
|
104
|
+
it('returns an empty tree', () => {
|
|
105
|
+
const tree = createEmptyTree();
|
|
106
|
+
expect(tree.nodes).toEqual({});
|
|
107
|
+
expect(tree.rootId).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getLinearPath', () => {
|
|
112
|
+
it('returns an empty array for an empty tree', () => {
|
|
113
|
+
expect(getLinearPath(createEmptyTree())).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns the active path from root following activeChildIndex', () => {
|
|
117
|
+
const tree = makeBranchingTree();
|
|
118
|
+
const path = getLinearPath(tree);
|
|
119
|
+
|
|
120
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns a different path after changing activeChildIndex', () => {
|
|
124
|
+
const tree = makeBranchingTree();
|
|
125
|
+
tree.nodes.u1.activeChildIndex = 1;
|
|
126
|
+
const path = getLinearPath(tree);
|
|
127
|
+
|
|
128
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1b', 'u3']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('enriches messages with siblingIds and siblingIndex when siblings exist', () => {
|
|
132
|
+
const tree = makeBranchingTree();
|
|
133
|
+
const path = getLinearPath(tree);
|
|
134
|
+
|
|
135
|
+
const a1Msg = path.find((m) => m.id === 'a1')!;
|
|
136
|
+
expect(a1Msg.siblingIds).toEqual(['a1', 'a1b']);
|
|
137
|
+
expect(a1Msg.siblingIndex).toBe(0);
|
|
138
|
+
|
|
139
|
+
const a2Msg = path.find((m) => m.id === 'a2')!;
|
|
140
|
+
expect(a2Msg.siblingIds).toEqual(['a2', 'a3']);
|
|
141
|
+
expect(a2Msg.siblingIndex).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not set siblingIds for nodes without siblings', () => {
|
|
145
|
+
const tree = makeBranchingTree();
|
|
146
|
+
const path = getLinearPath(tree);
|
|
147
|
+
|
|
148
|
+
const u2Msg = path.find((m) => m.id === 'u2')!;
|
|
149
|
+
expect(u2Msg.siblingIds).toBeUndefined();
|
|
150
|
+
expect(u2Msg.siblingIndex).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not set siblingIds on the root node', () => {
|
|
154
|
+
const tree = makeBranchingTree();
|
|
155
|
+
const path = getLinearPath(tree);
|
|
156
|
+
|
|
157
|
+
const rootMsg = path[0];
|
|
158
|
+
expect(rootMsg.id).toBe('u1');
|
|
159
|
+
expect(rootMsg.siblingIds).toBeUndefined();
|
|
160
|
+
expect(rootMsg.siblingIndex).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('preserves message fields', () => {
|
|
164
|
+
const tree = makeBranchingTree();
|
|
165
|
+
const path = getLinearPath(tree);
|
|
166
|
+
|
|
167
|
+
const u1Msg = path[0];
|
|
168
|
+
expect(u1Msg.role).toBe('user');
|
|
169
|
+
expect(u1Msg.content).toBe('Hello');
|
|
170
|
+
expect(u1Msg.status).toBe('completed');
|
|
171
|
+
expect(u1Msg.createdAt).toEqual(new Date('2025-01-01'));
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('addNode', () => {
|
|
176
|
+
it('adds the first node as root', () => {
|
|
177
|
+
const tree = createEmptyTree();
|
|
178
|
+
const node = makeTreeMessage({ id: 'u1', content: 'Hello' });
|
|
179
|
+
|
|
180
|
+
addNode(tree, node);
|
|
181
|
+
|
|
182
|
+
expect(tree.nodes.u1).toBe(node);
|
|
183
|
+
expect(tree.rootId).toBe('u1');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('appends a child and updates parent.childrenIds', () => {
|
|
187
|
+
const tree = createEmptyTree();
|
|
188
|
+
addNode(tree, makeTreeMessage({ id: 'u1' }));
|
|
189
|
+
addNode(
|
|
190
|
+
tree,
|
|
191
|
+
makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(tree.nodes.u1.childrenIds).toEqual(['a1']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('sets parent.activeChildIndex to point at the new child', () => {
|
|
198
|
+
const tree = createEmptyTree();
|
|
199
|
+
addNode(tree, makeTreeMessage({ id: 'u1' }));
|
|
200
|
+
addNode(
|
|
201
|
+
tree,
|
|
202
|
+
makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(0);
|
|
206
|
+
|
|
207
|
+
addNode(
|
|
208
|
+
tree,
|
|
209
|
+
makeTreeMessage({ id: 'a2', parentId: 'u1', role: 'assistant' }),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('makes the new node the last in the active path', () => {
|
|
216
|
+
const tree = createEmptyTree();
|
|
217
|
+
addNode(tree, makeTreeMessage({ id: 'u1' }));
|
|
218
|
+
addNode(
|
|
219
|
+
tree,
|
|
220
|
+
makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
|
|
221
|
+
);
|
|
222
|
+
addNode(tree, makeTreeMessage({ id: 'u2', parentId: 'a1' }));
|
|
223
|
+
|
|
224
|
+
const path = getLinearPath(tree);
|
|
225
|
+
expect(path[path.length - 1].id).toBe('u2');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('supports adding sibling nodes (regeneration)', () => {
|
|
229
|
+
const tree = createEmptyTree();
|
|
230
|
+
addNode(tree, makeTreeMessage({ id: 'u1' }));
|
|
231
|
+
addNode(
|
|
232
|
+
tree,
|
|
233
|
+
makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
|
|
234
|
+
);
|
|
235
|
+
addNode(
|
|
236
|
+
tree,
|
|
237
|
+
makeTreeMessage({ id: 'a2', parentId: 'u1', role: 'assistant' }),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(tree.nodes.u1.childrenIds).toEqual(['a1', 'a2']);
|
|
241
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(1);
|
|
242
|
+
|
|
243
|
+
const path = getLinearPath(tree);
|
|
244
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a2']);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('updateNode', () => {
|
|
249
|
+
it('updates content of an existing node', () => {
|
|
250
|
+
const tree = makeBranchingTree();
|
|
251
|
+
updateNode(tree, 'a1', { content: 'Updated hi' });
|
|
252
|
+
expect(tree.nodes.a1.content).toBe('Updated hi');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('updates status of an existing node', () => {
|
|
256
|
+
const tree = makeBranchingTree();
|
|
257
|
+
updateNode(tree, 'a1', { status: 'sending' });
|
|
258
|
+
expect(tree.nodes.a1.status).toBe('sending');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('is a no-op for nonexistent id', () => {
|
|
262
|
+
const tree = makeBranchingTree();
|
|
263
|
+
const nodeCount = Object.keys(tree.nodes).length;
|
|
264
|
+
updateNode(tree, 'nonexistent', { content: 'x' });
|
|
265
|
+
expect(Object.keys(tree.nodes).length).toBe(nodeCount);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('updateNodeId', () => {
|
|
270
|
+
it('re-keys a node from old to new id', () => {
|
|
271
|
+
const tree = makeBranchingTree();
|
|
272
|
+
updateNodeId(tree, 'a2', 'a2-server');
|
|
273
|
+
|
|
274
|
+
expect(tree.nodes['a2-server']).toBeDefined();
|
|
275
|
+
expect(tree.nodes['a2']).toBeUndefined();
|
|
276
|
+
expect(tree.nodes['a2-server'].id).toBe('a2-server');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('updates parent.childrenIds reference', () => {
|
|
280
|
+
const tree = makeBranchingTree();
|
|
281
|
+
updateNodeId(tree, 'a2', 'a2-server');
|
|
282
|
+
|
|
283
|
+
expect(tree.nodes.u2.childrenIds).toContain('a2-server');
|
|
284
|
+
expect(tree.nodes.u2.childrenIds).not.toContain('a2');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('updates children.parentId references', () => {
|
|
288
|
+
const tree = makeBranchingTree();
|
|
289
|
+
updateNodeId(tree, 'u2', 'u2-server');
|
|
290
|
+
|
|
291
|
+
expect(tree.nodes.a2.parentId).toBe('u2-server');
|
|
292
|
+
expect(tree.nodes.a3.parentId).toBe('u2-server');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('updates rootId when renaming the root', () => {
|
|
296
|
+
const tree = makeBranchingTree();
|
|
297
|
+
updateNodeId(tree, 'u1', 'u1-server');
|
|
298
|
+
expect(tree.rootId).toBe('u1-server');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('is a no-op for nonexistent oldId', () => {
|
|
302
|
+
const tree = makeBranchingTree();
|
|
303
|
+
updateNodeId(tree, 'nonexistent', 'whatever');
|
|
304
|
+
expect(tree.nodes['whatever']).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('navigate', () => {
|
|
309
|
+
it('updates parent.activeChildIndex to the target sibling', () => {
|
|
310
|
+
const tree = makeBranchingTree();
|
|
311
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(0);
|
|
312
|
+
|
|
313
|
+
navigate(tree, 'a1b');
|
|
314
|
+
|
|
315
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('changes the linear path accordingly', () => {
|
|
319
|
+
const tree = makeBranchingTree();
|
|
320
|
+
|
|
321
|
+
navigate(tree, 'a1b');
|
|
322
|
+
|
|
323
|
+
const path = getLinearPath(tree);
|
|
324
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1b', 'u3']);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('navigating back restores the original path', () => {
|
|
328
|
+
const tree = makeBranchingTree();
|
|
329
|
+
|
|
330
|
+
navigate(tree, 'a1b');
|
|
331
|
+
navigate(tree, 'a1');
|
|
332
|
+
|
|
333
|
+
const path = getLinearPath(tree);
|
|
334
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('navigates between leaf siblings', () => {
|
|
338
|
+
const tree = makeBranchingTree();
|
|
339
|
+
|
|
340
|
+
navigate(tree, 'a3');
|
|
341
|
+
expect(tree.nodes.u2.activeChildIndex).toBe(1);
|
|
342
|
+
|
|
343
|
+
const path = getLinearPath(tree);
|
|
344
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a3']);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('is a no-op for nonexistent targetId', () => {
|
|
348
|
+
const tree = makeBranchingTree();
|
|
349
|
+
const originalIndex = tree.nodes.u1.activeChildIndex;
|
|
350
|
+
|
|
351
|
+
navigate(tree, 'nonexistent');
|
|
352
|
+
|
|
353
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(originalIndex);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('is a no-op for root node (has no parent)', () => {
|
|
357
|
+
const tree = makeBranchingTree();
|
|
358
|
+
|
|
359
|
+
navigate(tree, 'u1');
|
|
360
|
+
|
|
361
|
+
expect(tree.nodes.u1.activeChildIndex).toBe(0);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('integration: full branching scenario', () => {
|
|
366
|
+
it('simulates send → regenerate → navigate workflow', () => {
|
|
367
|
+
const tree = createEmptyTree();
|
|
368
|
+
|
|
369
|
+
addNode(tree, makeTreeMessage({ id: 'u1', content: 'Hello' }));
|
|
370
|
+
addNode(
|
|
371
|
+
tree,
|
|
372
|
+
makeTreeMessage({
|
|
373
|
+
id: 'a1',
|
|
374
|
+
parentId: 'u1',
|
|
375
|
+
role: 'assistant',
|
|
376
|
+
content: 'Hi there',
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
addNode(
|
|
380
|
+
tree,
|
|
381
|
+
makeTreeMessage({ id: 'u2', parentId: 'a1', content: 'Tell me more' }),
|
|
382
|
+
);
|
|
383
|
+
addNode(
|
|
384
|
+
tree,
|
|
385
|
+
makeTreeMessage({
|
|
386
|
+
id: 'a2',
|
|
387
|
+
parentId: 'u2',
|
|
388
|
+
role: 'assistant',
|
|
389
|
+
content: 'Sure thing',
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
let path = getLinearPath(tree);
|
|
394
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
|
|
395
|
+
|
|
396
|
+
addNode(
|
|
397
|
+
tree,
|
|
398
|
+
makeTreeMessage({
|
|
399
|
+
id: 'a3',
|
|
400
|
+
parentId: 'u2',
|
|
401
|
+
role: 'assistant',
|
|
402
|
+
content: 'Certainly!',
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
path = getLinearPath(tree);
|
|
407
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a3']);
|
|
408
|
+
|
|
409
|
+
navigate(tree, 'a2');
|
|
410
|
+
path = getLinearPath(tree);
|
|
411
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
|
|
412
|
+
|
|
413
|
+
const a2Msg = path.find((m) => m.id === 'a2')!;
|
|
414
|
+
expect(a2Msg.siblingIds).toEqual(['a2', 'a3']);
|
|
415
|
+
expect(a2Msg.siblingIndex).toBe(0);
|
|
416
|
+
|
|
417
|
+
navigate(tree, 'a3');
|
|
418
|
+
path = getLinearPath(tree);
|
|
419
|
+
const a3Msg = path.find((m) => m.id === 'a3')!;
|
|
420
|
+
expect(a3Msg.siblingIds).toEqual(['a2', 'a3']);
|
|
421
|
+
expect(a3Msg.siblingIndex).toBe(1);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { BaseMessage, ConversationTree, TreeMessage } from '../types';
|
|
2
|
+
|
|
3
|
+
export function createEmptyTree(): ConversationTree {
|
|
4
|
+
return { nodes: {}, rootId: null };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Walks from rootId following activeChildIndex at each node.
|
|
9
|
+
* Enriches each node with sibling metadata for the UI.
|
|
10
|
+
*/
|
|
11
|
+
export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
12
|
+
const { nodes, rootId } = tree;
|
|
13
|
+
if (!rootId || !nodes[rootId]) return [];
|
|
14
|
+
|
|
15
|
+
const path: BaseMessage[] = [];
|
|
16
|
+
let current: string | null = rootId;
|
|
17
|
+
|
|
18
|
+
while (current !== null) {
|
|
19
|
+
const node: TreeMessage | undefined = nodes[current];
|
|
20
|
+
if (!node) break;
|
|
21
|
+
|
|
22
|
+
const base: BaseMessage = {
|
|
23
|
+
id: node.id,
|
|
24
|
+
role: node.role,
|
|
25
|
+
content: node.content,
|
|
26
|
+
createdAt: node.createdAt,
|
|
27
|
+
status: node.status,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (node.parentId !== null) {
|
|
31
|
+
const parent = nodes[node.parentId];
|
|
32
|
+
if (parent && parent.childrenIds.length > 1) {
|
|
33
|
+
base.siblingIds = [...parent.childrenIds];
|
|
34
|
+
base.siblingIndex = parent.childrenIds.indexOf(node.id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
path.push(base);
|
|
39
|
+
|
|
40
|
+
if (node.childrenIds.length === 0) break;
|
|
41
|
+
current = node.childrenIds[node.activeChildIndex];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function addNode(tree: ConversationTree, node: TreeMessage): void {
|
|
48
|
+
tree.nodes[node.id] = node;
|
|
49
|
+
|
|
50
|
+
if (node.parentId !== null) {
|
|
51
|
+
const parent = tree.nodes[node.parentId];
|
|
52
|
+
if (parent && !parent.childrenIds.includes(node.id)) {
|
|
53
|
+
parent.childrenIds.push(node.id);
|
|
54
|
+
parent.activeChildIndex = parent.childrenIds.length - 1;
|
|
55
|
+
}
|
|
56
|
+
} else if (tree.rootId === null) {
|
|
57
|
+
tree.rootId = node.id;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function updateNode(
|
|
62
|
+
tree: ConversationTree,
|
|
63
|
+
id: string,
|
|
64
|
+
updates: Partial<Pick<TreeMessage, 'content' | 'status' | 'role'>>,
|
|
65
|
+
): void {
|
|
66
|
+
const node = tree.nodes[id];
|
|
67
|
+
if (!node) return;
|
|
68
|
+
|
|
69
|
+
if (updates.content !== undefined) node.content = updates.content;
|
|
70
|
+
if (updates.status !== undefined) node.status = updates.status;
|
|
71
|
+
if (updates.role !== undefined) node.role = updates.role;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function updateNodeId(
|
|
75
|
+
tree: ConversationTree,
|
|
76
|
+
oldId: string,
|
|
77
|
+
newId: string,
|
|
78
|
+
): void {
|
|
79
|
+
const node = tree.nodes[oldId];
|
|
80
|
+
if (!node) return;
|
|
81
|
+
|
|
82
|
+
delete tree.nodes[oldId];
|
|
83
|
+
node.id = newId;
|
|
84
|
+
tree.nodes[newId] = node;
|
|
85
|
+
|
|
86
|
+
if (node.parentId !== null) {
|
|
87
|
+
const parent = tree.nodes[node.parentId];
|
|
88
|
+
if (parent) {
|
|
89
|
+
const idx = parent.childrenIds.indexOf(oldId);
|
|
90
|
+
if (idx !== -1) parent.childrenIds[idx] = newId;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const childId of node.childrenIds) {
|
|
95
|
+
const child = tree.nodes[childId];
|
|
96
|
+
if (child) child.parentId = newId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (tree.rootId === oldId) tree.rootId = newId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Navigate to a different branch by updating the parent's activeChildIndex
|
|
104
|
+
* to point at the target sibling.
|
|
105
|
+
*/
|
|
106
|
+
export function navigate(tree: ConversationTree, targetId: string): void {
|
|
107
|
+
const node = tree.nodes[targetId];
|
|
108
|
+
if (!node || node.parentId === null) return;
|
|
109
|
+
|
|
110
|
+
const parent = tree.nodes[node.parentId];
|
|
111
|
+
if (!parent) return;
|
|
112
|
+
|
|
113
|
+
const idx = parent.childrenIds.indexOf(targetId);
|
|
114
|
+
if (idx === -1) return;
|
|
115
|
+
|
|
116
|
+
parent.activeChildIndex = idx;
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise an unknown thrown value into an `Error` instance.
|
|
3
|
+
* Wraps non-Error values (strings, plain objects, etc.) so callers can
|
|
4
|
+
* always rely on `.message` and a stable type.
|
|
5
|
+
*/
|
|
6
|
+
export function toError(value: unknown): Error {
|
|
7
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
8
|
+
}
|