File size: 4,207 Bytes
7334a07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Tree manipulation utilities for the conversation graph.
 * Nodes are stored as a flat map: { [nodeId]: ConversationNode }
 */

/**
 * Get the path from root to a specific node (inclusive).
 * Returns an array of nodeIds ordered root → target.
 */
export function getAncestryPath(nodes, nodeId) {
  const path = [];
  let current = nodeId;
  while (current) {
    path.unshift(current);
    const node = nodes[current];
    if (!node) break;
    current = node.parentId;
  }
  return path;
}

/**
 * Get direct children of a node.
 */
export function getChildren(nodes, nodeId) {
  return Object.values(nodes).filter((n) => n.parentId === nodeId);
}

/**
 * Get all descendant nodeIds (BFS order).
 */
export function getDescendants(nodes, nodeId) {
  const descendants = [];
  const queue = [nodeId];
  while (queue.length > 0) {
    const current = queue.shift();
    const children = getChildren(nodes, current);
    for (const child of children) {
      descendants.push(child.id);
      queue.push(child.id);
    }
  }
  return descendants;
}

/**
 * Get the deepest leaf node in a subtree, following the first child at each level.
 * Used for finding the "default" branch path.
 */
export function getDeepestLeaf(nodes, nodeId) {
  let current = nodeId;
  while (true) {
    const children = getChildren(nodes, current);
    if (children.length === 0) return current;
    // Follow the first (oldest) child
    children.sort((a, b) => a.timestamp - b.timestamp);
    current = children[0].id;
  }
}

/**
 * Build the messages array for LLM context from root to a specific node.
 * Returns an array of { role, content } messages.
 */
export function buildConversationHistory(nodes, nodeId) {
  const path = getAncestryPath(nodes, nodeId);
  const messages = [
    {
      role: 'system',
      content:
        'You are a helpful, concise assistant. Keep responses focused and well-structured.',
    },
  ];

  for (const id of path) {
    const node = nodes[id];
    if (!node) continue;
    if (node.userMessage) {
      messages.push({ role: 'user', content: node.userMessage });
    }
    if (node.assistantMessage) {
      messages.push({ role: 'assistant', content: node.assistantMessage });
    }
  }
  return messages;
}

/**
 * Build messages for generating a response at a specific node.
 * Includes all parent context but only the user message of the target node.
 */
export function buildGenerationContext(nodes, nodeId) {
  const path = getAncestryPath(nodes, nodeId);
  const messages = [
    {
      role: 'system',
      content:
        'You are a helpful, concise assistant. Keep responses focused and well-structured.',
    },
  ];

  for (let i = 0; i < path.length; i++) {
    const node = nodes[path[i]];
    if (!node) continue;
    if (node.userMessage) {
      messages.push({ role: 'user', content: node.userMessage });
    }
    // Only include assistant message for nodes before the target
    if (i < path.length - 1 && node.assistantMessage) {
      messages.push({ role: 'assistant', content: node.assistantMessage });
    }
  }
  return messages;
}

/**
 * Convert the conversation tree into react-force-graph data format.
 */
export function toGraphData(nodes, mainBranchPath = [], activeBranchPath = []) {
  const mainSet = new Set(mainBranchPath);
  const activeSet = new Set(activeBranchPath);

  const graphNodes = Object.values(nodes).map((node) => ({
    id: node.id,
    name: node.userMessage
      ? node.userMessage.slice(0, 60) + (node.userMessage.length > 60 ? '…' : '')
      : 'Start',
    userMessage: node.userMessage || '',
    assistantMessage: node.assistantMessage || '',
    status: node.status,
    isMain: mainSet.has(node.id),
    isActive: activeSet.has(node.id),
    isRoot: !node.parentId,
    childCount: getChildren(nodes, node.id).length,
    depth: getAncestryPath(nodes, node.id).length - 1,
  }));

  const graphLinks = Object.values(nodes)
    .filter((n) => n.parentId)
    .map((n) => ({
      source: n.parentId,
      target: n.id,
      isMain: mainSet.has(n.parentId) && mainSet.has(n.id),
      isActive: activeSet.has(n.parentId) && activeSet.has(n.id),
    }));

  return { nodes: graphNodes, links: graphLinks };
}