ping98k commited on
Commit
f2e1fb8
·
1 Parent(s): 88218fc

Implement event handler functions for heatmap, K-Means, cluster plot, and naming events; refactor main.js to use these handlers.

Browse files
Files changed (5) hide show
  1. clusterplot_event.js +70 -0
  2. heatmap_event.js +164 -0
  3. kmeans_event.js +41 -0
  4. main.js +9 -308
  5. naming_event.js +37 -0
clusterplot_event.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Handles cluster plot (UMAP scatter) event
2
+ import { getLineEmbeddings } from './embedding.js';
3
+ import { plotScatter } from './plotting.js';
4
+
5
+ const task = "Given a textual input sentence, retrieve relevant categories that best describe it.";
6
+
7
+ export async function handleClusterPlotEvent() {
8
+ const progressBar = document.getElementById("progress-bar");
9
+ const progressBarInner = document.getElementById("progress-bar-inner");
10
+ progressBar.style.display = "block";
11
+ progressBarInner.style.width = "0%";
12
+
13
+ // Recalculate embeddings from current textarea
14
+ const text = document.getElementById("input").value;
15
+ // Remove ## lines for embedding
16
+ const lines = text.split(/\n/).map(x => x.trim()).filter(x => x && !x.startsWith("##"));
17
+ const embeddings = await getLineEmbeddings(lines, task);
18
+ const n = embeddings.length;
19
+ if (n < 2) return;
20
+
21
+ // Parse clusters from textarea (split by triple newlines)
22
+ const groups = text.split(/\n{3,}/);
23
+ const k = groups.length;
24
+ // Build labels array: for each line, assign the cluster index it belongs to
25
+ let labels = [];
26
+ let lineIdx = 0;
27
+ for (let c = 0; c < k; ++c) {
28
+ const groupLines = groups[c].split('\n').map(x => x.trim()).filter(x => x && !x.startsWith('##'));
29
+ for (let i = 0; i < groupLines.length; ++i) {
30
+ labels[lineIdx++] = c;
31
+ }
32
+ }
33
+ if (labels.length !== n) return;
34
+
35
+ // UMAP projection
36
+ const { UMAP } = await import('https://cdn.jsdelivr.net/npm/umap-js@1.4.0/+esm');
37
+ const nNeighbors = Math.max(1, Math.min(lines.length - 1, 15));
38
+ const umap = new UMAP({ nComponents: 2, nNeighbors, minDist: 0.2, metric: "cosine" });
39
+ const proj = umap.fit(embeddings);
40
+ // Group lines by cluster
41
+ const clustered = Array.from({ length: k }, () => []);
42
+ for (let i = 0; i < lines.length; ++i)
43
+ clustered[labels[i]].push(lines[i]);
44
+ // Prepare scatter plot traces
45
+ const colors = ["red", "blue", "green", "orange", "purple", "cyan", "magenta", "yellow", "brown", "black", "lime", "navy", "teal", "olive", "maroon", "pink", "gray", "gold", "aqua", "indigo"];
46
+ // Try to extract cluster names from textarea headers
47
+ const clusterNames = groups.map(g => {
48
+ const m = g.match(/^##\s*(.*)/m);
49
+ return m ? m[1].trim() : null;
50
+ });
51
+ const placeholderNames = clusterNames.map((name, i) => name || `Cluster ${i + 1}`);
52
+ const traces = Array.from({ length: k }, (_, c) => ({
53
+ x: [], y: [], text: [],
54
+ mode: "markers", type: "scatter",
55
+ name: placeholderNames[c],
56
+ marker: { color: colors[c % colors.length], size: 12, line: { width: 1, color: "#333" } }
57
+ }));
58
+ for (let i = 0; i < lines.length; ++i) {
59
+ traces[labels[i]].x.push(proj[i][0]);
60
+ traces[labels[i]].y.push(proj[i][1]);
61
+ traces[labels[i]].text.push(lines[i]);
62
+ }
63
+ plotScatter(traces, k);
64
+ window.traces = traces;
65
+ // Optionally update textarea with cluster names as markdown headers
66
+ document.getElementById("input").value = clustered.map((g, i) =>
67
+ `## ${placeholderNames[i]}\n${g.join("\n")}`
68
+ ).join("\n\n\n");
69
+ progressBarInner.style.width = "100%";
70
+ }
heatmap_event.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Handles the heatmap event and group similarity logic
2
+ import { getGroupEmbeddings, getLineEmbeddings } from './embedding.js';
3
+ import { plotHeatmap } from './plotting.js';
4
+
5
+ const task = "Given a textual input sentence, retrieve relevant categories that best describe it.";
6
+
7
+ export async function handleHeatmapEvent() {
8
+ const progressBar = document.getElementById("progress-bar");
9
+ const progressBarInner = document.getElementById("progress-bar-inner");
10
+ progressBar.style.display = "block";
11
+ progressBarInner.style.width = "0%";
12
+
13
+ const text = document.getElementById("input").value;
14
+ // Get search sort mode from dropdown
15
+ const searchSortMode = document.getElementById("search-sort-mode")?.value || "group";
16
+ const search_by_max_search_line = searchSortMode === "line";
17
+ const search_by_max_search_group = searchSortMode === "group";
18
+
19
+ // Find the index of the search cluster (case-insensitive)
20
+ const clusterNames = text.split(/\n/)
21
+ .map(x => x.trim())
22
+ .filter(x => x && x.startsWith('##'))
23
+ .map(x => x.replace(/^##\s*/, ''));
24
+ let searchIdx = clusterNames.findIndex(name => name.toLowerCase().includes('search'));
25
+
26
+ const groups = text.split(/\n{3,}/);
27
+ // Get group embeddings (removes ## lines internally)
28
+ const groupEmbeddings = await getGroupEmbeddings(groups, task);
29
+ const n = groupEmbeddings.length;
30
+ progressBarInner.style.width = "30%";
31
+ // Cosine similarity matrix
32
+ const sim = [];
33
+ for (let i = 0; i < n; i++) {
34
+ const row = [];
35
+ for (let j = 0; j < n; j++) {
36
+ let dot = 0, na = 0, nb = 0;
37
+ for (let k = 0; k < groupEmbeddings[i].length; k++) {
38
+ dot += groupEmbeddings[i][k] * groupEmbeddings[j][k];
39
+ na += groupEmbeddings[i][k] ** 2;
40
+ nb += groupEmbeddings[j][k] ** 2;
41
+ }
42
+ row.push(dot / Math.sqrt(na * nb));
43
+ }
44
+ sim.push(row);
45
+ }
46
+ progressBarInner.style.width = "60%";
47
+ // Reorder clusters if search cluster is found
48
+ let order = Array.from({ length: n }, (_, i) => i);
49
+ if (searchIdx !== -1) {
50
+ // Compute similarity to search cluster
51
+ const simToSearch = sim[searchIdx].map((v, i) => ({ idx: i, sim: v }));
52
+ // Exclude searchIdx from sorting
53
+ const others = simToSearch.filter(x => x.idx !== searchIdx);
54
+ others.sort((a, b) => b.sim - a.sim); // descending similarity
55
+ order = [searchIdx, ...others.map(x => x.idx)];
56
+ }
57
+ // Reorder sim matrix and clusterNames
58
+ const simOrdered = order.map(i => order.map(j => sim[i][j]));
59
+ const xLabels = (clusterNames && clusterNames.length === n)
60
+ ? order.map(i => clusterNames[i])
61
+ : order.map((_, i) => `Group ${i + 1}`);
62
+ plotHeatmap(simOrdered, xLabels, xLabels);
63
+
64
+ // Reorder text if search cluster is found
65
+ // First group is search then follow by hight sim group
66
+ // in each group order by high sim line
67
+ if (searchIdx !== -1 && search_by_max_search_line) {
68
+ const searchLines = groups[searchIdx]
69
+ .split("\n")
70
+ .filter(l => l && !l.startsWith("##"));
71
+ const searchEmbeds = await getLineEmbeddings(searchLines, task);
72
+
73
+ const cleanGroups = groups.map(g =>
74
+ g.split("\n").filter(l => l && !l.startsWith("##"))
75
+ );
76
+ const allLines = cleanGroups.flat();
77
+ const allEmbeds = await getLineEmbeddings(allLines, task);
78
+
79
+ const cosine = (a, b) => {
80
+ let dot = 0, na = 0, nb = 0;
81
+ for (let i = 0; i < a.length; i++) {
82
+ dot += a[i] * b[i];
83
+ na += a[i] * a[i];
84
+ nb += b[i] * b[i];
85
+ }
86
+ return na && nb ? dot / Math.sqrt(na * nb) : 0;
87
+ };
88
+
89
+ const score = e =>
90
+ Math.max(...searchEmbeds.map(se => cosine(se, e)));
91
+
92
+ const idxByGroup = [];
93
+ let p = 0;
94
+ for (const g of cleanGroups) {
95
+ idxByGroup.push(Array.from({ length: g.length }, (_, i) => p + i));
96
+ p += g.length;
97
+ }
98
+
99
+ const sorted = order.map(g =>
100
+ idxByGroup[g]
101
+ .map(i => ({ t: allLines[i], s: score(allEmbeds[i]) }))
102
+ .sort((a, b) => b.s - a.s)
103
+ .map(o => o.t)
104
+ );
105
+
106
+ const finalText = order
107
+ .map((gIdx, i) => {
108
+ const header =
109
+ clusterNames?.length === n ? clusterNames[gIdx] : `Group ${i + 1}`;
110
+ return `## ${header}\n${sorted[i].join("\n")}`;
111
+ })
112
+ .join("\n\n\n");
113
+
114
+ document.getElementById("input").value = finalText;
115
+ }
116
+
117
+ if (searchIdx !== -1 && search_by_max_search_group) {
118
+ const refEmbed = groupEmbeddings[searchIdx];
119
+
120
+ const cleanGroups = groups.map(g =>
121
+ g.split("\n").filter(l => l && !l.startsWith("##"))
122
+ );
123
+
124
+ const allLines = cleanGroups.flat();
125
+ const allEmbeds = await getLineEmbeddings(allLines, task);
126
+
127
+ const idxByGroup = [];
128
+ let p = 0;
129
+ for (const g of cleanGroups) {
130
+ idxByGroup.push(Array.from({ length: g.length }, (_, i) => p + i));
131
+ p += g.length;
132
+ }
133
+
134
+ const cosine = (a, b) => {
135
+ let dot = 0,
136
+ na = 0,
137
+ nb = 0;
138
+ for (let i = 0; i < a.length; i++) {
139
+ dot += a[i] * b[i];
140
+ na += a[i] * a[i];
141
+ nb += b[i] * b[i];
142
+ }
143
+ return na && nb ? dot / Math.sqrt(na * nb) : 0;
144
+ };
145
+
146
+ const sortedLines = order.map(gIdx =>
147
+ idxByGroup[gIdx]
148
+ .map(i => ({ t: allLines[i], s: cosine(refEmbed, allEmbeds[i]) }))
149
+ .sort((a, b) => b.s - a.s)
150
+ .map(o => o.t)
151
+ );
152
+
153
+ const finalText = order
154
+ .map((gIdx, i) => {
155
+ const header =
156
+ clusterNames?.length === n ? clusterNames[gIdx] : `Group ${i + 1}`;
157
+ return `## ${header}\n${sortedLines[i].join("\n")}`;
158
+ })
159
+ .join("\n\n\n");
160
+
161
+ document.getElementById("input").value = finalText;
162
+ }
163
+ progressBarInner.style.width = "100%";
164
+ }
kmeans_event.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Handles K-Means and Balanced K-Means clustering event
2
+ import { getLineEmbeddings } from './embedding.js';
3
+ import { kmeans, balancedKMeans } from './clustering.js';
4
+
5
+ const task = "Given a textual input sentence, retrieve relevant categories that best describe it.";
6
+
7
+ export async function handleKMeansEvent() {
8
+ const progressBar = document.getElementById("progress-bar");
9
+ const progressBarInner = document.getElementById("progress-bar-inner");
10
+ progressBar.style.display = "block";
11
+ progressBarInner.style.width = "0%";
12
+
13
+ const text = document.getElementById("input").value;
14
+ // Remove ## lines for embedding
15
+ const lines = text.split(/\n/).map(x => x.trim()).filter(x => x && !x.startsWith("##"));
16
+ const embeddings = await getLineEmbeddings(lines, task);
17
+ const n = embeddings.length;
18
+ if (n < 2) return;
19
+ const requestedK = parseInt(document.getElementById("kmeans-k").value) || 3;
20
+ const k = Math.max(2, Math.min(requestedK, n));
21
+ // Read clustering type and beta
22
+ const clusteringType = document.getElementById("kmeans-type").value;
23
+ const beta = parseFloat(document.getElementById("kmeans-beta").value) || 0.01;
24
+ let labels;
25
+ if (clusteringType === "balancedKMeans") {
26
+ labels = balancedKMeans(embeddings, k, beta).labels;
27
+ } else {
28
+ labels = kmeans(embeddings, k).labels;
29
+ }
30
+
31
+ // Build clustered text for textarea
32
+ const clustered = Array.from({ length: k }, () => []);
33
+ for (let i = 0; i < n; ++i)
34
+ clustered[labels[i]].push(lines[i]);
35
+ const clusterNames = Array.from({ length: k }, (_, c) => `Cluster ${c + 1}`);
36
+ document.getElementById("input").value = clustered.map((g, i) =>
37
+ `## ${clusterNames[i]}\n${g.join("\n")}`
38
+ ).join("\n\n\n");
39
+
40
+ progressBarInner.style.width = "100%";
41
+ }
main.js CHANGED
@@ -1,310 +1,11 @@
1
- import { getGroupEmbeddings, getLineEmbeddings } from './embedding.js';
2
- import { kmeans, balancedKMeans } from './clustering.js';
3
- import { plotHeatmap, plotScatter, updateScatter } from './plotting.js';
4
- import { nameCluster } from './cluster_naming.js';
5
- import { prompt_cluster } from './prompt_cluster.js';
6
 
7
- const task = "Given a textual input sentence, retrieve relevant categories that best describe it.";
8
 
9
- // Heatmap event
10
- // Handles group similarity heatmap
11
- // Uses group-level embeddings
12
-
13
- document.getElementById("heatmap-btn").onclick = async () => {
14
- const progressBar = document.getElementById("progress-bar");
15
- const progressBarInner = document.getElementById("progress-bar-inner");
16
- progressBar.style.display = "block";
17
- progressBarInner.style.width = "0%";
18
-
19
- const text = document.getElementById("input").value;
20
- // Get search sort mode from dropdown
21
- const searchSortMode = document.getElementById("search-sort-mode")?.value || "group";
22
- const search_by_max_search_line = searchSortMode === "line";
23
- const search_by_max_search_group = searchSortMode === "group";
24
-
25
- // Find the index of the search cluster (case-insensitive)
26
- const clusterNames = text.split(/\n/)
27
- .map(x => x.trim())
28
- .filter(x => x && x.startsWith('##'))
29
- .map(x => x.replace(/^##\s*/, ''));
30
- let searchIdx = clusterNames.findIndex(name => name.toLowerCase().includes('search'));
31
-
32
- const groups = text.split(/\n{3,}/);
33
- // Get group embeddings (removes ## lines internally)
34
- const groupEmbeddings = await getGroupEmbeddings(groups, task);
35
- const n = groupEmbeddings.length;
36
- progressBarInner.style.width = "30%";
37
- // Cosine similarity matrix
38
- const sim = [];
39
- for (let i = 0; i < n; i++) {
40
- const row = [];
41
- for (let j = 0; j < n; j++) {
42
- let dot = 0, na = 0, nb = 0;
43
- for (let k = 0; k < groupEmbeddings[i].length; k++) {
44
- dot += groupEmbeddings[i][k] * groupEmbeddings[j][k];
45
- na += groupEmbeddings[i][k] ** 2;
46
- nb += groupEmbeddings[j][k] ** 2;
47
- }
48
- row.push(dot / Math.sqrt(na * nb));
49
- }
50
- sim.push(row);
51
- }
52
- progressBarInner.style.width = "60%";
53
- // Reorder clusters if search cluster is found
54
- let order = Array.from({ length: n }, (_, i) => i);
55
- if (searchIdx !== -1) {
56
- // Compute similarity to search cluster
57
- const simToSearch = sim[searchIdx].map((v, i) => ({ idx: i, sim: v }));
58
- // Exclude searchIdx from sorting
59
- const others = simToSearch.filter(x => x.idx !== searchIdx);
60
- others.sort((a, b) => b.sim - a.sim); // descending similarity
61
- order = [searchIdx, ...others.map(x => x.idx)];
62
- }
63
- // Reorder sim matrix and clusterNames
64
- const simOrdered = order.map(i => order.map(j => sim[i][j]));
65
- const xLabels = (clusterNames && clusterNames.length === n)
66
- ? order.map(i => clusterNames[i])
67
- : order.map((_, i) => `Group ${i + 1}`);
68
- plotHeatmap(simOrdered, xLabels, xLabels);
69
-
70
- // Reorder text if search cluster is found
71
- // First group is search then follow by hight sim group
72
- // in each group order by high sim line
73
- if (searchIdx !== -1 && search_by_max_search_line) {
74
- const searchLines = groups[searchIdx]
75
- .split("\n")
76
- .filter(l => l && !l.startsWith("##"));
77
- const searchEmbeds = await getLineEmbeddings(searchLines, task);
78
-
79
- const cleanGroups = groups.map(g =>
80
- g.split("\n").filter(l => l && !l.startsWith("##"))
81
- );
82
- const allLines = cleanGroups.flat();
83
- const allEmbeds = await getLineEmbeddings(allLines, task);
84
-
85
- const cosine = (a, b) => {
86
- let dot = 0, na = 0, nb = 0;
87
- for (let i = 0; i < a.length; i++) {
88
- dot += a[i] * b[i];
89
- na += a[i] * a[i];
90
- nb += b[i] * b[i];
91
- }
92
- return na && nb ? dot / Math.sqrt(na * nb) : 0;
93
- };
94
-
95
- const score = e =>
96
- Math.max(...searchEmbeds.map(se => cosine(se, e)));
97
-
98
- const idxByGroup = [];
99
- let p = 0;
100
- for (const g of cleanGroups) {
101
- idxByGroup.push(Array.from({ length: g.length }, (_, i) => p + i));
102
- p += g.length;
103
- }
104
-
105
- const sorted = order.map(g =>
106
- idxByGroup[g]
107
- .map(i => ({ t: allLines[i], s: score(allEmbeds[i]) }))
108
- .sort((a, b) => b.s - a.s)
109
- .map(o => o.t)
110
- );
111
-
112
- const finalText = order
113
- .map((gIdx, i) => {
114
- const header =
115
- clusterNames?.length === n ? clusterNames[gIdx] : `Group ${i + 1}`;
116
- return `## ${header}\n${sorted[i].join("\n")}`;
117
- })
118
- .join("\n\n\n");
119
-
120
- document.getElementById("input").value = finalText;
121
- }
122
-
123
-
124
- if (searchIdx !== -1 && search_by_max_search_group) {
125
- const refEmbed = groupEmbeddings[searchIdx];
126
-
127
- const cleanGroups = groups.map(g =>
128
- g.split("\n").filter(l => l && !l.startsWith("##"))
129
- );
130
-
131
- const allLines = cleanGroups.flat();
132
- const allEmbeds = await getLineEmbeddings(allLines, task);
133
-
134
- const idxByGroup = [];
135
- let p = 0;
136
- for (const g of cleanGroups) {
137
- idxByGroup.push(Array.from({ length: g.length }, (_, i) => p + i));
138
- p += g.length;
139
- }
140
-
141
- const cosine = (a, b) => {
142
- let dot = 0,
143
- na = 0,
144
- nb = 0;
145
- for (let i = 0; i < a.length; i++) {
146
- dot += a[i] * b[i];
147
- na += a[i] * a[i];
148
- nb += b[i] * b[i];
149
- }
150
- return na && nb ? dot / Math.sqrt(na * nb) : 0;
151
- };
152
-
153
- const sortedLines = order.map(gIdx =>
154
- idxByGroup[gIdx]
155
- .map(i => ({ t: allLines[i], s: cosine(refEmbed, allEmbeds[i]) }))
156
- .sort((a, b) => b.s - a.s)
157
- .map(o => o.t)
158
- );
159
-
160
- const finalText = order
161
- .map((gIdx, i) => {
162
- const header =
163
- clusterNames?.length === n ? clusterNames[gIdx] : `Group ${i + 1}`;
164
- return `## ${header}\n${sortedLines[i].join("\n")}`;
165
- })
166
- .join("\n\n\n");
167
-
168
- document.getElementById("input").value = finalText;
169
- }
170
- progressBarInner.style.width = "100%";
171
- };
172
-
173
- // K-Means event: only clustering, no plotting
174
-
175
- document.getElementById("kmeans-btn").onclick = async () => {
176
- const progressBar = document.getElementById("progress-bar");
177
- const progressBarInner = document.getElementById("progress-bar-inner");
178
- progressBar.style.display = "block";
179
- progressBarInner.style.width = "0%";
180
-
181
- const text = document.getElementById("input").value;
182
- // Remove ## lines for embedding
183
- const lines = text.split(/\n/).map(x => x.trim()).filter(x => x && !x.startsWith("##"));
184
- const embeddings = await getLineEmbeddings(lines, task);
185
- const n = embeddings.length;
186
- if (n < 2) return;
187
- const requestedK = parseInt(document.getElementById("kmeans-k").value) || 3;
188
- const k = Math.max(2, Math.min(requestedK, n));
189
- // Read clustering type and beta
190
- const clusteringType = document.getElementById("kmeans-type").value;
191
- const beta = parseFloat(document.getElementById("kmeans-beta").value) || 0.01;
192
- let labels;
193
- if (clusteringType === "balancedKMeans") {
194
- labels = balancedKMeans(embeddings, k, beta).labels;
195
- } else {
196
- labels = kmeans(embeddings, k).labels;
197
- }
198
-
199
- // Build clustered text for textarea
200
- const clustered = Array.from({ length: k }, () => []);
201
- for (let i = 0; i < n; ++i)
202
- clustered[labels[i]].push(lines[i]);
203
- const clusterNames = Array.from({ length: k }, (_, c) => `Cluster ${c + 1}`);
204
- document.getElementById("input").value = clustered.map((g, i) =>
205
- `## ${clusterNames[i]}\n${g.join("\n")}`
206
- ).join("\n\n\n");
207
-
208
- progressBarInner.style.width = "100%";
209
- };
210
-
211
- // Cluster Plot event: only plotting, no clustering
212
-
213
- document.getElementById("clusterplot-btn").onclick = async () => {
214
- const progressBar = document.getElementById("progress-bar");
215
- const progressBarInner = document.getElementById("progress-bar-inner");
216
- progressBar.style.display = "block";
217
- progressBarInner.style.width = "0%";
218
-
219
- // Recalculate embeddings from current textarea
220
- const text = document.getElementById("input").value;
221
- // Remove ## lines for embedding
222
- const lines = text.split(/\n/).map(x => x.trim()).filter(x => x && !x.startsWith("##"));
223
- const embeddings = await getLineEmbeddings(lines, task);
224
- const n = embeddings.length;
225
- if (n < 2) return;
226
-
227
- // Parse clusters from textarea (split by triple newlines)
228
- const groups = text.split(/\n{3,}/);
229
- const k = groups.length;
230
- // Build labels array: for each line, assign the cluster index it belongs to
231
- let labels = [];
232
- let lineIdx = 0;
233
- for (let c = 0; c < k; ++c) {
234
- const groupLines = groups[c].split('\n').map(x => x.trim()).filter(x => x && !x.startsWith('##'));
235
- for (let i = 0; i < groupLines.length; ++i) {
236
- labels[lineIdx++] = c;
237
- }
238
- }
239
- if (labels.length !== n) return;
240
-
241
- // UMAP projection
242
- const { UMAP } = await import('https://cdn.jsdelivr.net/npm/umap-js@1.4.0/+esm');
243
- const nNeighbors = Math.max(1, Math.min(lines.length - 1, 15));
244
- const umap = new UMAP({ nComponents: 2, nNeighbors, minDist: 0.2, metric: "cosine" });
245
- const proj = umap.fit(embeddings);
246
- // Group lines by cluster
247
- const clustered = Array.from({ length: k }, () => []);
248
- for (let i = 0; i < lines.length; ++i)
249
- clustered[labels[i]].push(lines[i]);
250
- // Prepare scatter plot traces
251
- const colors = ["red", "blue", "green", "orange", "purple", "cyan", "magenta", "yellow", "brown", "black", "lime", "navy", "teal", "olive", "maroon", "pink", "gray", "gold", "aqua", "indigo"];
252
- // Try to extract cluster names from textarea headers
253
- const clusterNames = groups.map(g => {
254
- const m = g.match(/^##\s*(.*)/m);
255
- return m ? m[1].trim() : null;
256
- });
257
- const placeholderNames = clusterNames.map((name, i) => name || `Cluster ${i + 1}`);
258
- const traces = Array.from({ length: k }, (_, c) => ({
259
- x: [], y: [], text: [],
260
- mode: "markers", type: "scatter",
261
- name: placeholderNames[c],
262
- marker: { color: colors[c % colors.length], size: 12, line: { width: 1, color: "#333" } }
263
- }));
264
- for (let i = 0; i < lines.length; ++i) {
265
- traces[labels[i]].x.push(proj[i][0]);
266
- traces[labels[i]].y.push(proj[i][1]);
267
- traces[labels[i]].text.push(lines[i]);
268
- }
269
- plotScatter(traces, k);
270
- window.traces = traces;
271
- // Optionally update textarea with cluster names as markdown headers
272
- document.getElementById("input").value = clustered.map((g, i) =>
273
- `## ${placeholderNames[i]}\n${g.join("\n")}`
274
- ).join("\n\n\n");
275
- progressBarInner.style.width = "100%";
276
- };
277
-
278
- document.getElementById("naming-btn").onclick = async () => {
279
- const progressBar = document.getElementById("progress-bar");
280
- const progressBarInner = document.getElementById("progress-bar-inner");
281
- progressBar.style.display = "block";
282
- progressBarInner.style.width = "0%";
283
-
284
- const text = document.getElementById("input").value;
285
- // Reconstruct clusters from textarea (split by triple newlines)
286
- const clustered = text.split(/\n{3,}/).map(group =>
287
- group.split('\n').filter(line => line && !line.startsWith('##'))
288
- );
289
- const k = clustered.length;
290
-
291
- const clusterNames = [];
292
- for (let c = 0; c < k; ++c) {
293
- progressBarInner.style.width = `${Math.round(((c + 1) / k) * 100)}%`;
294
- const name = await nameCluster(clustered[c]);
295
- clusterNames.push(name || `Cluster ${c + 1}`);
296
- // Update UMAP scatter plot legend with new cluster names
297
- if (window.traces && window.traces[c]) {
298
- window.traces[c].name = clusterNames[c];
299
- }
300
- }
301
- // Update textarea with cluster names as markdown headers
302
- document.getElementById("input").value = clustered.map((g, i) =>
303
- `## ${clusterNames[i]}\n${g.join("\n")}`
304
- ).join("\n\n\n");
305
- // Update UMAP scatter plot if traces are available
306
- if (window.traces) {
307
- updateScatter(window.traces, k);
308
- }
309
- progressBarInner.style.width = "100%";
310
- };
 
1
+ import { handleHeatmapEvent } from './heatmap_event.js';
2
+ import { handleKMeansEvent } from './kmeans_event.js';
3
+ import { handleClusterPlotEvent } from './clusterplot_event.js';
4
+ import { handleNamingEvent } from './naming_event.js';
 
5
 
6
+ // Attach event handlers to buttons
7
 
8
+ document.getElementById("heatmap-btn").onclick = handleHeatmapEvent;
9
+ document.getElementById("kmeans-btn").onclick = handleKMeansEvent;
10
+ document.getElementById("clusterplot-btn").onclick = handleClusterPlotEvent;
11
+ document.getElementById("naming-btn").onclick = handleNamingEvent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
naming_event.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Handles cluster naming event
2
+ import { nameCluster } from './cluster_naming.js';
3
+ import { updateScatter } from './plotting.js';
4
+
5
+ export async function handleNamingEvent() {
6
+ const progressBar = document.getElementById("progress-bar");
7
+ const progressBarInner = document.getElementById("progress-bar-inner");
8
+ progressBar.style.display = "block";
9
+ progressBarInner.style.width = "0%";
10
+
11
+ const text = document.getElementById("input").value;
12
+ // Reconstruct clusters from textarea (split by triple newlines)
13
+ const clustered = text.split(/\n{3,}/).map(group =>
14
+ group.split('\n').filter(line => line && !line.startsWith('##'))
15
+ );
16
+ const k = clustered.length;
17
+
18
+ const clusterNames = [];
19
+ for (let c = 0; c < k; ++c) {
20
+ progressBarInner.style.width = `${Math.round(((c + 1) / k) * 100)}%`;
21
+ const name = await nameCluster(clustered[c]);
22
+ clusterNames.push(name || `Cluster ${c + 1}`);
23
+ // Update UMAP scatter plot legend with new cluster names
24
+ if (window.traces && window.traces[c]) {
25
+ window.traces[c].name = clusterNames[c];
26
+ }
27
+ }
28
+ // Update textarea with cluster names as markdown headers
29
+ document.getElementById("input").value = clustered.map((g, i) =>
30
+ `## ${clusterNames[i]}\n${g.join("\n")}`
31
+ ).join("\n\n\n");
32
+ // Update UMAP scatter plot if traces are available
33
+ if (window.traces) {
34
+ updateScatter(window.traces, k);
35
+ }
36
+ progressBarInner.style.width = "100%";
37
+ }