PageSourceSearch

https://aim-review-app.web.app/oop/trainingClass.js?v=20260602-duplicates

js aim-review-app.web.app collected 2026-10-03 12:16:22 UTC 93,883 bytes, 2,224 lines download raw bytes

1const TRAINING_CLASS_WORKER_URL = 'oop/trainingClass_worker.js?v=20260602-duplicates';
2
3class TRAINING_DATA {
4  constructor(status_id, vect_names_id, vect_progress_id, vect_bar_progress_id, training_progress_id, training_bar_progress_id, training_message_id, trainingChart1, trainingChart2, trainingChart3, trainingChart4, display_1_id, display_2_id, display_3_id, display_4_id, display_5_id,  ml_config, predictions_config, worker_transformers_flag){
5    this.status_id = status_id;
6    this.display_1_id = display_1_id;
7    this.display_2_id = display_2_id;
8    this.display_3_id = display_3_id;
9    this.display_4_id = display_4_id;
10    this.display_5_id = display_5_id;
11
12    this.ml_config = {window: ml_config};
13    this.predictions_config = {button: predictions_config};
14
15    this.trainingChart1 = trainingChart1;
16    this.trainingChart2 = trainingChart2;
17    this.trainingChart3 = trainingChart3;
18    this.trainingChart4 = trainingChart4;
19
20    this.data_array = [];
21    // Accessible copy of titles and abstracts.
22    this.data_titles = [];
23    this.data_abstracts = [];
24    this.data_titles_and_abstracts = [];
25    this.data_labels = [];
26    this.data_ids = [];
27
28    // Missing or unlabelled data.
29    this.data_array_missing = [];
30
31    // Vector variables.
32    this.tfidf_vectObj = [];
33
34    this.lsa_vectObj = [];
35
36    this.doc2vec_tokens = [];
37    this.doc2vec_vocab = [];
38    this.doc2vec_vocab_freq = [];
39    this.doc2vec_model = [];
40
41    // Generic vector.
42    this.vectors = [];
43    this.vector_names = [];
44    this.vector_config = [];
45
46    this.stopwords = ["0o", "0s", "3a", "3b", "3d", "6b", "6o", "a", "a1", "a2", "a3", "a4", "ab", "able", "about", "above", "abst", "ac", "accordance", "according", "accordingly", "across", "act", "actually", "ad", "added", "adj", "ae", "af", "affected", "affecting", "affects", "after", "afterwards", "ag", "again", "against", "ah", "ain", "ain't", "aj", "al", "all", "allow", "allows", "almost", "alone", "along", "already", "also", "although", "always", "am", "among", "amongst", "amoungst", "amount", "an", "and", "announce", "another", "any", "anybody", "anyhow", "anymore", "anyone", "anything", "anyway", "anyways", "anywhere", "ao", "ap", "apart", "apparently", "appear", "appreciate", "appropriate", "approximately", "ar", "are", "aren", "arent", "aren't", "arise", "around", "as", "a's", "aside", "ask", "asking", "associated", "at", "au", "auth", "av", "available", "aw", "away", "awfully", "ax", "ay", "az", "b", "b1", "b2", "b3", "ba", "back", "bc", "bd", "be", "became", "because", "become", "becomes", "becoming", "been", "before", "beforehand", "begin", "beginning", "beginnings", "begins", "behind", "being", "believe", "below", "beside", "besides", "best", "better", "between", "beyond", "bi", "bill", "biol", "bj", "bk", "bl", "bn", "both", "bottom", "bp", "br", "brief", "briefly", "bs", "bt", "bu", "but", "bx", "by", "c", "c1", "c2", "c3", "ca", "call", "came", "can", "cannot", "cant", "can't", "cause", "causes", "cc", "cd", "ce", "certain", "certainly", "cf", "cg", "ch", "changes", "ci", "cit", "cj", "cl", "clearly", "cm", "c'mon", "cn", "co", "com", "come", "comes", "con", "concerning", "consequently", "consider", "considering", "contain", "containing", "contains", "corresponding", "could", "couldn", "couldnt", "couldn't", "course", "cp", "cq", "cr", "cry", "cs", "c's", "ct", "cu", "currently", "cv", "cx", "cy", "cz", "d", "d2", "da", "date", "dc", "dd", "de", "definitely", "describe", "described", "despite", "detail", "df", "di", "did", "didn", "didn't", "different", "dj", "dk", "dl", "do", "does", "doesn", "doesn't", "doing", "don", "done", "don't", "down", "downwards", "dp", "dr", "ds", "dt", "du", "due", "during", "dx", "dy", "e", "e2", "e3", "ea", "each", "ec", "ed", "edu", "ee", "ef", "effect", "eg", "ei", "eight", "eighty", "either", "ej", "el", "eleven", "else", "elsewhere", "em", "empty", "en", "end", "ending", "enough", "entirely", "eo", "ep", "eq", "er", "es", "especially", "est", "et", "et-al", "etc", "eu", "ev", "even", "ever", "every", "everybody", "everyone", "everything", "everywhere", "ex", "exactly", "example", "except", "ey", "f", "f2", "fa", "far", "fc", "few", "ff", "fi", "fifteen", "fifth", "fify", "fill", "find", "fire", "first", "five", "fix", "fj", "fl", "fn", "fo", "followed", "following", "follows", "for", "former", "formerly", "forth", "forty", "found", "four", "fr", "from", "front", "fs", "ft", "fu", "full", "further", "furthermore", "fy", "g", "ga", "gave", "ge", "get", "gets", "getting", "gi", "give", "given", "gives", "giving", "gj", "gl", "go", "goes", "going", "gone", "got", "gotten", "gr", "greetings", "gs", "gy", "h", "h2", "h3", "had", "hadn", "hadn't", "happens", "hardly", "has", "hasn", "hasnt", "hasn't", "have", "haven", "haven't", "having", "he", "hed", "he'd", "he'll", "hello", "help", "hence", "her", "here", "hereafter", "hereby", "herein", "heres", "here's", "hereupon", "hers", "herself", "hes", "he's", "hh", "hi", "hid", "him", "himself", "his", "hither", "hj", "ho", "home", "hopefully", "how", "howbeit", "however", "how's", "hr", "hs", "http", "hu", "hundred", "hy", "i", "i2", "i3", "i4", "i6", "i7", "i8", "ia", "ib", "ibid", "ic", "id", "i'd", "ie", "if", "ig", "ignored", "ih", "ii", "ij", "il", "i'll", "im", "i'm", "immediate", "immediately", "importance", "important", "in", "inasmuch", "inc", "indeed", "index", "indicate", "indicated", "indicates", "information", "inner", "insofar", "instead", "interest", "into", "invention", "inward", "io", "ip", "iq", "ir", "is", "isn", "isn't", "it", "itd", "it'd", "it'll", "its", "it's", "itself", "iv", "i've", "ix", "iy", "iz", "j", "jj", "jr", "js", "jt", "ju", "just", "k", "ke", "keep", "keeps", "kept", "kg", "kj", "km", "know", "known", "knows", "ko", "l", "l2", "la", "largely", "last", "lately", "later", "latter", "latterly", "lb", "lc", "le", "least", "les", "less", "lest", "let", "lets", "let's", "lf", "like", "liked", "likely", "line", "little", "lj", "ll", "ll", "ln", "lo", "look", "looking", "looks", "los", "lr", "ls", "lt", "ltd", "m", "m2", "ma", "made", "mainly", "make", "makes", "many", "may", "maybe", "me", "mean", "means", "meantime", "meanwhile", "merely", "mg", "might", "mightn", "mightn't", "mill", "million", "mine", "miss", "ml", "mn", "mo", "more", "moreover", "most", "mostly", "move", "mr", "mrs", "ms", "mt", "mu", "much", "mug", "must", "mustn", "mustn't", "my", "myself", "n", "n2", "na", "name", "namely", "nay", "nc", "nd", "ne", "near", "nearly", "necessarily", "necessary", "need", "needn", "needn't", "needs", "neither", "never", "nevertheless", "new", "next", "ng", "ni", "nine", "ninety", "nj", "nl", "nn", "no", "nobody", "non", "none", "nonetheless", "noone", "nor", "normally", "nos", "not", "noted", "nothing", "novel", "now", "nowhere", "nr", "ns", "nt", "ny", "o", "oa", "ob", "obtain", "obtained", "obviously", "oc", "od", "of", "off", "often", "og", "oh", "oi", "oj", "ok", "okay", "ol", "old", "om", "omitted", "on", "once", "one", "ones", "only", "onto", "oo", "op", "oq", "or", "ord", "os", "ot", "other", "others", "otherwise", "ou", "ought", "our", "ours", "ourselves", "out", "outside", "over", "overall", "ow", "owing", "own", "ox", "oz", "p", "p1", "p2", "p3", "page", "pagecount", "pages", "par", "part", "particular", "particularly", "pas", "past", "pc", "pd", "pe", "per", "perhaps", "pf", "ph", "pi", "pj", "pk", "pl", "placed", "please", "plus", "pm", "pn", "po", "poorly", "possible", "possibly", "potentially", "pp", "pq", "pr", "predominantly", "present", "presumably", "previously", "primarily", "probably", "promptly", "proud", "provides", "ps", "pt", "pu", "put", "py", "q", "qj", "qu", "que", "quickly", "quite", "qv", "r", "r2", "ra", "ran", "rather", "rc", "rd", "re", "readily", "really", "reasonably", "recent", "recently", "ref", "refs", "regarding", "regardless", "regards", "related", "relatively", "research", "research-articl", "respectively", "resulted", "resulting", "results", "rf", "rh", "ri", "right", "rj", "rl", "rm", "rn", "ro", "rq", "rr", "rs", "rt", "ru", "run", "rv", "ry", "s", "s2", "sa", "said", "same", "saw", "say", "saying", "says", "sc", "sd", "se", "sec", "second", "secondly", "section", "see", "seeing", "seem", "seemed", "seeming", "seems", "seen", "self", "selves", "sensible", "sent", "serious", "seriously", "seven", "several", "sf", "shall", "shan", "shan't", "she", "shed", "she'd", "she'll", "shes", "she's", "should", "shouldn", "shouldn't", "should've", "show", "showed", "shown", "showns", "shows", "si", "side", "significant", "significantly", "similar", "similarly", "since", "sincere", "six", "sixty", "sj", "sl", "slightly", "sm", "sn", "so", "some", "somebody", "somehow", "someone", "somethan", "something", "sometime", "sometimes", "somewhat", "somewhere", "soon", "sorry", "sp", "specifically", "specified", "specify", "specifying", "sq", "sr", "ss", "st", "still", "stop", "strongly", "sub", "substantially", "successfully", "such", "sufficiently", "suggest", "sup", "sure", "sy", "system", "sz", "t", "t1", "t2", "t3", "take", "taken", "taking", "tb", "tc", "td", "te", "tell", "ten", "tends", "tf", "th", "than", "thank", "thanks", "thanx", "that", "that'll", "thats", "that's", "that've", "the", "their", "theirs", "them", "themselves", "then", "thence", "there", "thereafter", "thereby", "thered", "therefore", "therein", "there'll", "thereof", "therere", "theres", "there's", "thereto", "thereupon", "there've", "these", "they", "theyd", "they'd", "they'll", "theyre", "they're", "they've", "thickv", "thin", "think", "third", "this", "thorough", "thoroughly", "those", "thou", "though", "thoughh", "thousand", "three", "throug", "through", "throughout", "thru", "thus", "ti", "til", "tip", "tj", "tl", "tm", "tn", "to", "together", "too", "took", "top", "toward", "towards", "tp", "tq", "tr", "tried", "tries", "truly", "try", "trying", "ts", "t's", "tt", "tv", "twelve", "twenty", "twice", "two", "tx", "u", "u201d", "ue", "ui", "uj", "uk", "um", "un", "under", "unfortunately", "unless", "unlike", "unlikely", "until", "unto", "uo", "up", "upon", "ups", "ur", "us", "use", "used", "useful", "usefully", "usefulness", "uses", "using", "usually", "ut", "v", "va", "value", "various", "vd", "ve", "ve", "very", "via", "viz", "vj", "vo", "vol", "vols", "volumtype", "vq", "vs", "vt", "vu", "w", "wa", "want", "wants", "was", "wasn", "wasnt", "wasn't", "way", "we", "wed", "we'd", "welcome", "well", "we'll", "well-b", "went", "were", "we're", "weren", "werent", "weren't", "we've", "what", "whatever", "what'll", "whats", "what's", "when", "whence", "whenever", "when's", "where", "whereafter", "whereas", "whereby", "wherein", "wheres", "where's", "whereupon", "wherever", "whether", "which", "while", "whim", "whither", "who", "whod", "whoever", "whole", "who'll", "whom", "whomever", "whos", "who's", "whose", "why", "why's", "wi", "widely", "will", "willing", "wish", "with", "within", "without", "wo", "won", "wonder", "wont", "won't", "words", "world", "would", "wouldn", "wouldnt", "wouldn't", "www", "x", "x1", "x2", "x3", "xf", "xi", "xj", "xk", "xl", "xn", "xo", "xs", "xt", "xv", "xx", "y", "y2", "yes", "yet", "yj", "yl", "you", "youd", "you'd", "you'll", "your", "youre", "you're", "yours", "yourself", "yourselves", "you've", "yr", "ys", "yt", "z", "zero", "zi", "zz"];
47
48    this.stopwords_multilingual = [];
49    fetch('/stopwords/stopwords-iso.json').then(res => res.json()).then(data => {
50      this.stopwords_multilingual = data;
51      //Create all stopwords
52      this.stopwords_multilingual.all = Object.values(this.stopwords_multilingual).flat().filter((v, i, a) => a.indexOf(v) === i);
53    });
54
55
56    //Vectorization progress variables.
57    this.vect_names_id = vect_names_id;
58    this.vect_progress_id = vect_progress_id;
59    this.vect_bar_progress_id = vect_bar_progress_id;
60
61    this.training_progress_id = training_progress_id;
62    this.training_bar_progress_id = training_bar_progress_id;
63    this.training_message_id = training_message_id;
64    this.training_data = [];
65    this.prediction_data = [];
66
67    //Create webworker.
68    this.worker_transformers_flag = worker_transformers_flag === true;
69    this.worker = new Worker(TRAINING_CLASS_WORKER_URL);
70
71    if (this.worker_transformers_flag){
72      this.worker_sentence_transformers = new Worker('oop/transformers_worker.js', {type: 'module'});
73    } else {
74      this.worker_sentence_transformers = [];
75    }
76
77
78    // Miscellaneous.
79    this.list_of_colours = [
80      "#42A5F5", // Blue
81      "#FF5252", // Red
82      "#4CAF50", // Green
83      "#FFD740", // Yellow
84      "#FF4081", // Pink
85      "#8BC34A", // Light Green
86      "#FF9800", // Orange
87      "#9C27B0", // Purple
88      "#795548", // Brown
89      "#00BCD4", // Cyan
90      "#FF5722", // Deep Orange
91      "#3F51B5", // Indigo
92      "#9E9E9E", // Grey
93      "#607D8B", // Blue Grey
94      "#F44336", // Dark Red
95      "#2196F3", // Dark Blue
96      "#00ACC1", // Dark Cyan
97      "#9C27B0"  // Dark Purple
98    ];
99
100
101    //Result variables and plotting.
102    this.proba_threshold = 0.5;
103    this.prediction_label_mode = "threshold";
104    this.prediction_relevant_threshold = 0.95;
105    this.prediction_irrelevant_threshold = 0.05;
106    this.roc_curves = [];
107    this.roc_curve_mean = [];
108    this.metrics = [];
109    this.metrics_stats = [];
110
111
112    //Active learning variables.
113    this.sorting_probabilities = [];
114
115
116    //Similarity tool.
117    this.similarity_array = [];
118    this.similarity_summary = null;
119    this.currentSimIndex = 0;
120
121    this.similarity_pairs_action_discarded = [];
122    this.similarity_array_discarded = [];
123    this.discarded_similarity_indexes = new Set();
124    this.currentDiscardedIndex = 0;
125
126  }
127
128  _get_element(id) {
129    return typeof _ === "function" && id ? _(id) : null;
130  }
131
132  _format_number(value, digits = 2, fallback = "n/a") {
133    if (value === null || value === undefined || value === "") {
134      return fallback;
135    }
136
137    const number = Number(value);
138    return Number.isFinite(number) ? number.toFixed(digits) : fallback;
139  }
140
141  _format_percent(value, digits = 2, fallback = "n/a") {
142    if (value === null || value === undefined || value === "") {
143      return fallback;
144    }
145
146    const number = Number(value);
147    return Number.isFinite(number) ? (100 * number).toFixed(digits) + "%" : fallback;
148  }
149
150  _set_progress_bar(barId, progress) {
151    const barEl = this._get_element(barId);
152    if (!barEl) {
153      return;
154    }
155
156    const safeProgress = Number.isFinite(Number(progress))
157      ? Math.min(1, Math.max(0, Number(progress)))
158      : 0;
159    const percentage = (safeProgress * 100).toFixed(1) + "%";
160    barEl.style.width = percentage;
161    barEl.innerHTML = percentage;
162  }
163
164  _restart_workers() {
165    if (this.worker instanceof Worker) {
166      this.worker.terminate();
167    }
168
169    if (this.worker_sentence_transformers instanceof Worker) {
170      this.worker_sentence_transformers.terminate();
171    }
172
173    this.worker = new Worker(TRAINING_CLASS_WORKER_URL);
174    this.worker_sentence_transformers = this.worker_transformers_flag
175      ? new Worker('oop/transformers_worker.js', {type: 'module'})
176      : [];
177  }
178
179  _has_training_results() {
180    return Boolean(
181      this.training_data &&
182      typeof this.training_data === "object" &&
183      Array.isArray(this.training_data.ensemble_cv1_ypred_best_stacked) &&
184      Array.isArray(this.training_data.ensemble_cv1_y)
185    );
186  }
187
188  _has_prediction_results() {
189    return Boolean(
190      this.prediction_data &&
191      typeof this.prediction_data === "object" &&
192      Array.isArray(this.prediction_data.y_proba_stacked) &&
193      Array.isArray(this.prediction_data.y_label_stacked)
194    );
195  }
196
197  get data_labels() {
198    return this._build_entry_field_arrays({ labels: true }).labels;
199  }
200
201  set data_labels(value) {
202    this._data_labels_compat = Array.isArray(value) ? value : [];
203  }
204
205  get data_ids() {
206    return this._build_entry_field_arrays({ ids: true }).ids;
207  }
208
209  set data_ids(value) {
210    this._data_ids_compat = Array.isArray(value) ? value : [];
211  }
212
213  _rebuild_cached_entry_fields() {
214    const fields = this._build_entry_field_arrays();
215    this.data_titles = fields.titles;
216    this.data_abstracts = fields.abstracts;
217    this.data_titles_and_abstracts = fields.titlesAndAbstracts;
218    this.data_labels = fields.labels;
219    this.data_ids = fields.ids;
220    return fields;
221  }
222
223  _clear_cached_entry_fields() {
224    this.data_titles = [];
225    this.data_abstracts = [];
226    this.data_titles_and_abstracts = [];
227    this.data_labels = [];
228    this.data_ids = [];
229  }
230
231  _build_entry_field_arrays(entries = this.data_array, options = null) {
232    if (options == null && entries && typeof entries === "object" && !Array.isArray(entries)) {
233      options = entries;
234      entries = this.data_array;
235    }
236
237    const safeEntries = Array.isArray(entries) ? entries : [];
238    const useDefaults = !options || Object.keys(options).length === 0;
239    const includeTitles = useDefaults || options.titles === true;
240    const includeAbstracts = useDefaults || options.abstracts === true;
241    const includeText = useDefaults || options.text === true || options.titlesAndAbstracts === true;
242    const includeLabels = useDefaults || options.labels === true;
243    const includeIds = useDefaults || options.ids === true;
244    const titles = includeTitles ? new Array(safeEntries.length) : [];
245    const abstracts = includeAbstracts ? new Array(safeEntries.length) : [];
246    const titlesAndAbstracts = includeText ? new Array(safeEntries.length) : [];
247    const labels = includeLabels ? new Array(safeEntries.length) : [];
248    const ids = includeIds ? new Array(safeEntries.length) : [];
249
250    for (let index = 0; index < safeEntries.length; index++) {
251      const entry = safeEntries[index] || {};
252      const needsTitle = includeTitles || includeText;
253      const needsAbstract = includeAbstracts || includeText;
254      const title = needsTitle ? (entry.title ?? "") : "";
255      const abstract = needsAbstract ? (entry.abstract ?? "") : "";
256
257      if (includeTitles) titles[index] = title;
258      if (includeAbstracts) abstracts[index] = abstract;
259      if (includeText) titlesAndAbstracts[index] = `${title} ${abstract}`.trim();
260      if (includeLabels) labels[index] = entry.label ?? null;
261      if (includeIds) ids[index] = entry.paper_id ?? null;
262    }
263
264    return { titles, abstracts, titlesAndAbstracts, labels, ids };
265  }
266
267  _encode_numeric_matrix_for_snapshot(matrix) {
268    if (!Array.isArray(matrix) || matrix.length === 0) return matrix;
269
270    const rowLengths = matrix.map(row => Array.isArray(row) || ArrayBuffer.isView(row) ? row.length : -1);
271    if (rowLengths.some(length => length < 0)) return matrix;
272
273    const totalLength = rowLengths.reduce((sum, length) => sum + length, 0);
274    if (totalLength === 0) {
275      return {
276        __snapshot_type: "NumericMatrixFloat32",
277        row_lengths: rowLengths,
278        value: snapshot_array_buffer_to_base64(new Float32Array(0).buffer)
279      };
280    }
281
282    const flattened = new Float32Array(totalLength);
283    let offset = 0;
284    for (const row of matrix) {
285      for (let i = 0; i < row.length; i++) {
286        const value = Number(row[i]);
287        if (!Number.isFinite(value)) return matrix;
288        flattened[offset + i] = value;
289      }
290      offset += row.length;
291    }
292
293    return {
294      __snapshot_type: "NumericMatrixFloat32",
295      row_lengths: rowLengths,
296      value: snapshot_array_buffer_to_base64(flattened.buffer)
297    };
298  }
299
300  _decode_numeric_matrix_from_snapshot(encoded) {
301    if (!encoded || encoded.__snapshot_type !== "NumericMatrixFloat32" || !Array.isArray(encoded.row_lengths)) {
302      return encoded;
303    }
304
305    const buffer = snapshot_base64_to_array_buffer(encoded.value);
306    const values = new Float32Array(buffer);
307    const matrix = [];
308    let offset = 0;
309
310    encoded.row_lengths.forEach(length => {
311      const safeLength = Math.max(0, Math.floor(Number(length) || 0));
312      matrix.push(Array.from(values.subarray(offset, offset + safeLength)));
313      offset += safeLength;
314    });
315
316    return matrix;
317  }
318
319  _export_vectors_for_snapshot(compactVectors = false) {
320    if (!compactVectors) return this.vectors;
321
322    return (Array.isArray(this.vectors) ? this.vectors : []).map(vectorLayer =>
323      this._encode_numeric_matrix_for_snapshot(vectorLayer)
324    );
325  }
326
327  _import_vectors_from_snapshot(vectors) {
328    return (Array.isArray(vectors) ? vectors : []).map(vectorLayer =>
329      this._decode_numeric_matrix_from_snapshot(vectorLayer)
330    );
331  }
332
333  _export_training_results_for_snapshot(options = {}) {
334    const includeVectorInputs = options.includeVectorInputs === true;
335    if (!this.training_data || typeof this.training_data !== "object" || Array.isArray(this.training_data)) {
336      return this.training_data || [];
337    }
338
339    const result = { ...this.training_data };
340    if (!includeVectorInputs) {
341      [
342        "X_all",
343        "X",
344        "X_unlabelled",
345        "x_all",
346        "x",
347        "x_unlabelled"
348      ].forEach(key => {
349        if (Object.prototype.hasOwnProperty.call(result, key)) {
350          delete result[key];
351        }
352      });
353      result.vector_inputs_omitted = true;
354    }
355
356    return result;
357  }
358
359  export_snapshot_state(options = {}) {
360    const includeModelCaches = options.includeModelCaches === true;
361    const compactVectors = options.compactVectors === true;
362    const includeVectors = options.includeVectors !== false;
363    const includeEntries = options.includeEntries !== false;
364    const includeTrainingVectorInputs = options.includeTrainingVectorInputs !== false;
365
366    return {
367      data_array: includeEntries ? this.data_array : [],
368      data_titles: [],
369      data_abstracts: [],
370      data_titles_and_abstracts: [],
371      data_labels: [],
372      data_ids: [],
373      data_array_missing: includeEntries ? this.data_array_missing : [],
374      tfidf_vectObj: includeModelCaches ? this.tfidf_vectObj : [],
375      lsa_vectObj: includeModelCaches ? this.lsa_vectObj : [],
376      doc2vec_tokens: includeModelCaches ? this.doc2vec_tokens : [],
377      doc2vec_vocab: includeModelCaches ? this.doc2vec_vocab : [],
378      doc2vec_vocab_freq: includeModelCaches ? this.doc2vec_vocab_freq : [],
379      doc2vec_model: includeModelCaches ? this.doc2vec_model : [],
380      vectors: includeVectors ? this._export_vectors_for_snapshot(compactVectors) : [],
381      vector_names: this.vector_names,
382      vector_config: this.vector_config,
383      vectors_compact: compactVectors,
384      training_data: this._export_training_results_for_snapshot({
385        includeVectorInputs: includeTrainingVectorInputs
386      }),
387      prediction_data: this.prediction_data,
388      proba_threshold: this.proba_threshold,
389      prediction_label_mode: this.prediction_label_mode,
390      prediction_relevant_threshold: this.prediction_relevant_threshold,
391      prediction_irrelevant_threshold: this.prediction_irrelevant_threshold,
392      roc_curves: this.roc_curves,
393      roc_curve_mean: this.roc_curve_mean,
394      metrics: this.metrics,
395      metrics_stats: this.metrics_stats,
396      auc_roc: this.auc_roc,
397      auc_roc_stats: this.auc_roc_stats,
398      sorting_probabilities: this.sorting_probabilities,
399      similarity_array: this.similarity_array,
400      similarity_summary: this.similarity_summary,
401      currentSimIndex: this.currentSimIndex,
402      similarity_pairs_action_discarded: this.similarity_pairs_action_discarded,
403      similarity_array_discarded: this.similarity_array_discarded,
404      discarded_similarity_indexes: Array.from(this.discarded_similarity_indexes || []),
405      currentDiscardedIndex: this.currentDiscardedIndex
406    };
407  }
408
409  import_snapshot_state(state) {
410    if (!state || typeof state !== "object") {
411      throw new Error("Invalid training snapshot data.");
412    }
413
414    this._restart_workers();
415
416    this.data_array = Array.isArray(state.data_array) ? state.data_array : [];
417    this.data_array_missing = Array.isArray(state.data_array_missing) ? state.data_array_missing : [];
418    this._clear_cached_entry_fields();
419
420    this.tfidf_vectObj = Array.isArray(state.tfidf_vectObj) ? state.tfidf_vectObj : [];
421    this.lsa_vectObj = Array.isArray(state.lsa_vectObj) ? state.lsa_vectObj : [];
422    this.doc2vec_tokens = Array.isArray(state.doc2vec_tokens) ? state.doc2vec_tokens : [];
423    this.doc2vec_vocab = Array.isArray(state.doc2vec_vocab) ? state.doc2vec_vocab : [];
424    this.doc2vec_vocab_freq = Array.isArray(state.doc2vec_vocab_freq) ? state.doc2vec_vocab_freq : [];
425    this.doc2vec_model = state.doc2vec_model || [];
426
427    this.vectors = this._import_vectors_from_snapshot(state.vectors);
428    this.vector_names = Array.isArray(state.vector_names) ? state.vector_names : [];
429    this.vector_config = Array.isArray(state.vector_config) ? state.vector_config : [];
430    this.training_data = state.training_data || [];
431    this.prediction_data = state.prediction_data || [];
432    this.proba_threshold = Number.isFinite(Number(state.proba_threshold)) ? Number(state.proba_threshold) : 0.5;
433    this.prediction_label_mode = state.prediction_label_mode === "confidence" ? "confidence" : "threshold";
434    this.prediction_relevant_threshold = Number.isFinite(Number(state.prediction_relevant_threshold))
435      ? Math.min(1, Math.max(0, Number(state.prediction_relevant_threshold)))
436      : 0.95;
437    this.prediction_irrelevant_threshold = Number.isFinite(Number(state.prediction_irrelevant_threshold))
438      ? Math.min(1, Math.max(0, Number(state.prediction_irrelevant_threshold)))
439      : 0.05;
440    if (this.prediction_irrelevant_threshold >= this.prediction_relevant_threshold) {
441      this.prediction_irrelevant_threshold = 0.05;
442      this.prediction_relevant_threshold = 0.95;
443    }
444    this.roc_curves = Array.isArray(state.roc_curves) ? state.roc_curves : [];
445    this.roc_curve_mean = Array.isArray(state.roc_curve_mean) ? state.roc_curve_mean : [];
446    this.metrics = Array.isArray(state.metrics) ? state.metrics : [];
447    this.metrics_stats = Array.isArray(state.metrics_stats) ? state.metrics_stats : [];
448    this.auc_roc = Array.isArray(state.auc_roc) ? state.auc_roc : [];
449    this.auc_roc_stats = Array.isArray(state.auc_roc_stats) ? state.auc_roc_stats : [];
450    this.sorting_probabilities = Array.isArray(state.sorting_probabilities) ? state.sorting_probabilities : [];
451
452    this.similarity_array = Array.isArray(state.similarity_array) ? state.similarity_array : [];
453    this.similarity_summary = state.similarity_summary || null;
454    this.currentSimIndex = Math.min(
455      Math.max(Number.isInteger(Number(state.currentSimIndex)) ? Number(state.currentSimIndex) : 0, 0),
456      Math.max(this.similarity_array.length - 1, 0)
457    );
458    this.similarity_pairs_action_discarded = Array.isArray(state.similarity_pairs_action_discarded) ? state.similarity_pairs_action_discarded 
458: [];
459    this.similarity_array_discarded = Array.isArray(state.similarity_array_discarded) ? state.similarity_array_discarded : [];
460    this.discarded_similarity_indexes = new Set(Array.isArray(state.discarded_similarity_indexes) ? state.discarded_similarity_indexes : []);
461    this.currentDiscardedIndex = Math.min(
462      Math.max(Number.isInteger(Number(state.currentDiscardedIndex)) ? Number(state.currentDiscardedIndex) : 0, 0),
463      Math.max(this.similarity_pairs_action_discarded.length - 1, 0)
464    );
465  }
466
467  restore_snapshot_progress(display_config = { model: 1, metric: "ACC" }) {
468    const vectorNamesEl = this.vect_names_id ? _(this.vect_names_id) : null;
469    const vectorRunningEl = this.vect_progress_id ? _(this.vect_progress_id) : null;
470    const vectorBarEl = this.vect_bar_progress_id ? _(this.vect_bar_progress_id) : null;
471    const trainingRunningEl = this.training_progress_id ? _(this.training_progress_id) : null;
472    const trainingBarEl = this.training_bar_progress_id ? _(this.training_bar_progress_id) : null;
473    const trainingMessageEl = this.training_message_id ? _(this.training_message_id) : null;
474    const hasVectors = Array.isArray(this.vectors) && this.vectors.length > 0;
475    const hasTraining = this._has_training_results();
476    const hasPredictions = this._has_prediction_results();
477
478    if (vectorNamesEl) {
479      vectorNamesEl.innerHTML = hasVectors ? this.vector_names.join("<br>") : "";
480    }
481    if (vectorRunningEl) {
482      vectorRunningEl.innerHTML = hasVectors ? "Vectorization restored from snapshot." : "No vectorization running";
483    }
484    if (vectorBarEl) {
485      const percent = hasVectors ? 100 : 0;
486      vectorBarEl.style.width = percent + "%";
487      vectorBarEl.innerHTML = percent + "%";
488      vectorBarEl.setAttribute("aria-valuenow", String(percent));
489    }
490
491    if (trainingRunningEl) {
492      trainingRunningEl.innerHTML = hasTraining ? "Finished training" : "No training running";
493    }
494    if (trainingBarEl) {
495      const percent = hasTraining ? 100 : 0;
496      trainingBarEl.style.width = percent + "%";
497      trainingBarEl.innerHTML = percent + "%";
498      trainingBarEl.setAttribute("aria-valuenow", String(percent));
499    }
500    if (trainingMessageEl && hasTraining) {
501      trainingMessageEl.innerHTML = "Training results restored from snapshot.";
502    }
503
504    if (hasTraining) {
505      try {
506        if ((!Array.isArray(this.roc_curve_mean) || this.roc_curve_mean.length === 0) && this.training_data.ensemble_cv1_ypred_best_stacked) {
507          this.calculate_roc_curve();
508        }
509        if (this.trainingChart1) {
510          this.update_training_progress_2(display_config);
511        }
512        if (this.trainingChart2 && this.display_3_id) {
513          this.report_training_results();
514        }
515      } catch (error) {
516        console.warn("Training results were restored, but the charts could not be redrawn.", error);
517      }
518    }
519
520    if (hasPredictions && this.trainingChart3 && this.trainingChart4) {
521      try {
522        this.report_predicted_results();
523      } catch (error) {
524        console.warn("Prediction results were restored, but the charts could not be redrawn.", error);
525      }
526    }
527  }
528
529  add_entries(data) {
530    for (var i = 0; i < data.length; i++) {
531      let entry = data[i];
532
533      // Check for missing required fields
534      if (entry.title == null || entry.abstract == null || entry.paper_id == null) {
535        this.data_array_missing.push(entry);
536      } else {
537        // Ensure label is either 0 or 1; otherwise, set to null
538        if (entry.label !== 0 && entry.label !== 1) {
539          entry.label = null;
540        }
541
542        this.data_array.push(entry);
543      }
544    }
545    this._clear_cached_entry_fields();
546  };
547
548  //Add all entries without taking into account if they are in the selected_for_training or not.
549  add_entries_active(data){
550    for (var i = 0; i < data.length; i++) {
551      if (data[i].title == null || data[i].abstract == null || data[i].paper_id == null) {
552        this.data_array_missing.push(data[i]);
553      } else {
554        this.data_array.push(data[i]);
555      }
556    }
557    this._clear_cached_entry_fields();
558  };
559
560
561  reset_entries_active(){
562    this.data_array_missing = [];
563    this.data_array = [];
564    this.data_titles = [];
565    this.data_abstracts = [];
566    this.data_titles_and_abstracts = [];
567    this.data_labels = [];
568    this.data_ids = [];
569
570  };
571
572  sync_entries_active(data) {
573    this.reset_entries_active();
574    this.add_entries_active(Array.isArray(data) ? data : []);
575  }
576
577  _normalize_publication_indexes(indexes, expectedLength) {
578    return Array.from(new Set((Array.isArray(indexes) ? indexes : [indexes])
579      .map(index => Math.floor(Number(index)))
580      .filter(index => Number.isFinite(index) && index >= 0 && index < expectedLength)))
581      .sort((a, b) => a - b);
582  }
583
584  _clear_model_cache_after_publication_change() {
585    this.tfidf_vectObj = [];
586    this.lsa_vectObj = [];
587    this.doc2vec_tokens = [];
588    this.doc2vec_vocab = [];
589    this.doc2vec_vocab_freq = [];
590    this.doc2vec_model = [];
591    this.training_data = [];
592    this.prediction_data = [];
593    this.metrics = [];
594    this.metrics_stats = [];
595    this.auc_roc = [];
596    this.auc_roc_stats = [];
597    this.roc_curves = [];
598    this.roc_curve_mean = [];
599  }
600
601  remove_entries_by_indexes(indexes, expectedLength = this.data_array.length) {
602    const oldLength = Math.max(0, Math.floor(Number(expectedLength) || 0));
603    const removedIndexes = this._normalize_publication_indexes(indexes, oldLength);
604    if (removedIndexes.length === 0) {
605      return { oldLength, removedIndexes, vectorRows: [], probabilitiesWereAligned: false };
606    }
607
608    const removed = new Set(removedIndexes);
609    const vectorRows = [];
610    const keptVectors = [];
611    const keptVectorNames = [];
612    const keptVectorConfig = [];
613
614    this.vectors.forEach((layer, layerIndex) => {
615      if (Array.isArray(layer) && layer.length === oldLength) {
616        vectorRows.push({
617          layerIndex: keptVectors.length,
618          name: this.vector_names[layerIndex],
619          config: this.vector_config[layerIndex],
620          rows: new Map(removedIndexes.map(index => [index, layer[index]]))
621        });
622        keptVectors.push(layer.filter((_, index) => !removed.has(index)));
623        keptVectorNames.push(this.vector_names[layerIndex]);
624        keptVectorConfig.push(this.vector_config[layerIndex]);
625      }
626    });
627
628    this.vectors = keptVectors;
629    this.vector_names = keptVectorNames;
630    this.vector_config = keptVectorConfig;
631
632    const probabilitiesWereAligned = this.sorting_probabilities.length === oldLength;
633    const receipt = {
634      oldLength,
635      removedIndexes,
636      vectorRows,
637      probabilitiesWereAligned,
638      sortingProbabilities: probabilitiesWereAligned
639        ? new Map(removedIndexes.map(index => [index, this.sorting_probabilities[index]]))
640        : new Map()
641    };
642
643    if (this.data_array.length === oldLength) {
644      this.data_array = this.data_array.filter((_, index) => !removed.has(index));
645      this._clear_cached_entry_fields();
646    }
647
648    this.sorting_probabilities = probabilitiesWereAligned
649      ? this.sorting_probabilities.filter((_, index) => !removed.has(index))
650      : [];
651    this._clear_model_cache_after_publication_change();
652    return receipt;
653  }
654
655  restore_entry_at_index(insertIndex, originalIndex, receipt) {
656    if (!receipt || !Array.isArray(receipt.vectorRows)) return;
657
658    receipt.vectorRows.forEach(snapshot => {
659      const row = snapshot.rows.get(originalIndex);
660      if (Array.isArray(this.vectors[snapshot.layerIndex]) && row !== undefined) {
661        this.vectors[snapshot.layerIndex].splice(insertIndex, 0, row);
662      }
663    });
664
665    if (
666      receipt.probabilitiesWereAligned &&
667      receipt.sortingProbabilities.has(originalIndex) &&
668      this.sorting_probabilities.length === this.data_array.length
669    ) {
670      this.sorting_probabilities.splice(insertIndex, 0, receipt.sortingProbabilities.get(originalIndex));
671    } else if (this.sorting_probabilities.length > 0) {
672      this.sorting_probabilities = [];
673    }
674
675    this._clear_model_cache_after_publication_change();
676  }
677
678  vectorize_data(methods, language, univBERT_config, tfidf_config, lsa_config, doc2vec_config, specter_config, sentence_transformers_config, sentence_transformers_api_config){
679
680
681    // Chain promises for sequential execution
682    let promiseChainVect = Promise.resolve();
683
684    //Determine the language
685    if (language == "english"){
686      this.stopwords = this.stopwords_multilingual.en;
687    } else if (language == "multilingual"){
688      this.stopwords = this.stopwords_multilingual.all;
689    };
690
691
692    //univBERT
693    if (methods[0]) {
694      promiseChainVect = promiseChainVect.then(() => this.universalBERT_vectorize_webworker(univBERT_config));
695    }
696    // TF-IDF
697    if (methods[1]) {
698      promiseChainVect = promiseChainVect.then(() => this.tfidf_vectorize_webworker(tfidf_config));
699    }
700    // LSA
701    if (methods[2]) {
702      promiseChainVect = promiseChainVect.then(() => this.lsa_vectorize_webworker(tfidf_config, lsa_config));
703    };
704    // Doc2Vec
705    if (methods[3]) {
706      promiseChainVect = promiseChainVect.then(() => this.doc2vec_vectorize_webworker(doc2vec_config));
707    };
708
709    // SPECTER BERT
710    if (methods[4]) {
711      promiseChainVect = promiseChainVect.then(() => this.specter_vectorize_webworker(specter_config));
712    };
713
714    // Sentence transformers
715    if (methods[5]) {
716      promiseChainVect = promiseChainVect.then(() => this.sentence_transformers_vectorize_webworker(sentence_transformers_config));
717    };
718
719    // Sentence transformers
720    if (methods[6]) {
721      promiseChainVect = promiseChainVect.then(() => this.sentence_transformers_api_vectorize_webworker(sentence_transformers_api_config));
722    };
723
724    // Wait for all vectorizations to finish.
725    promiseChainVect.then(() => {
726
727      //Finish vectorization.
728      this.finish_bar_vectorization();
729
730    });
731
732    // Return the promise chain in case any action happens after.
733    return promiseChainVect;
734  };
735
736
737
738    tfidf_vectorize_webworker(tfidf_config, vect_graph){
739      return new Promise((resolve, reject) => {
740        const vect_name = generateUniqueName('TF-IDF', this.vector_names);
741        const entryFields = this._build_entry_field_arrays({ text: true });
742        const totalEntries = Math.max(1, entryFields.titlesAndAbstracts.length);
743        this.new_vectorization_message(vect_name);
744        let norm, analyzer;
745        if (tfidf_config[0]) {
746          norm = 'l2';
747        } else {
748          norm = 'l1';
749        };
750        if (tfidf_config[1]) {
751          analyzer = 'word';
752        } else {
753          analyzer = 'char';
754        };
755
756        this.worker.postMessage({
757          method: 'tf-idf',
758          data: entryFields.titlesAndAbstracts,
759          tfidfConfig: tfidf_config,
760          norm: norm,
761          analyzer: analyzer,
762          stopwords: this.stopwords
763        });
764
765        this.worker.onmessage = (event) => {
766          const { tfidfVectors, tfidfVectObj } = event.data;
767          this.tfidf_vectObj = tfidfVectObj;
768          this.vectors.push(tfidfVectors);
769          this.vector_names.push(vect_name);
770          this.vector_config.push(tfidf_config);
771          // Update graph.
772          this.update_bar_vectorization(tfidfVectors.length / totalEntries, vect_name);
773          resolve(); // Resolve the promise when done
774        };
775      });
776    };
777
778
779    lsa_vectorize_webworker(tfidf_config, lsa_config){
780      return new Promise((resolve, reject) => {
781        let norm, analyzer;
782        const vect_name = generateUniqueName('LSA', this.vector_names);
783        const entryFields = this._build_entry_field_arrays({ text: true });
784        const totalEntries = Math.max(1, entryFields.titlesAndAbstracts.length);
785        this.new_vectorization_message(vect_name);
786        if (tfidf_config[0]) {
787          norm = 'l2';
788        } else {
789          norm = 'l1';
790        };
791        if (tfidf_config[1]) {
792          analyzer = 'word';
793        } else {
794          analyzer = 'char';
795        };
796
797        this.worker.postMessage({
798          method: 'lsa',
799          data: entryFields.titlesAndAbstracts,
800          tfidfConfig: tfidf_config,
801          lsaConfig: lsa_config,
802          norm: norm,
803          analyzer: analyzer,
804          stopwords: this.stopwords
805        });
806
807        this.worker.onmessage = (event) => {
808          const {lsaVectors, tfidfVectors, lsaObj, tfidfVectObj} = event.data;
809          this.lsa_vectObj = lsaObj;
810          this.vectors.push(lsaVectors);
811          this.vector_names.push(vect_name);
812          this.vector_config.push([tfidf_config, lsa_config]);
813
814          // Update graph.
815          this.update_bar_vectorization(lsaVectors.length / totalEntries, vect_name);
816          resolve(); // Resolve the promise when done
817        };
818      });
819    };
820
821    universalBERT_vectorize_webworker(univBERT_config) {
822      return new Promise((resolve, reject) => {
823        // Clear the vectors.
824
825        let universalBERT_vectors = [];
826        const vect_name = generateUniqueName('univBERT', this.vector_names);
827        const entryFields = this._build_entry_field_arrays({ titles: true, abstracts: true, text: true });
828        const totalEntries = Math.max(1, entryFields.titlesAndAbstracts.length);
829        this.new_vectorization_message(vect_name);
830        //Post task to webworker.
831        this.worker.postMessage({
832          method: 'univBERT',
833          data_titles_and_abstracts: entryFields.titlesAndAbstracts,
834          data_titles: entryFields.titles,
835          data_abstracts: entryFields.abstracts,
836          univBERT_config: univBERT_config
837        });
838
839        this.worker.onmessage = (event) => {
840          const {array} = event.data;
841
842          universalBERT_vectors.push(...array);
843
844          // Update graph.
845          this.update_bar_vectorization(universalBERT_vectors.length / totalEntries, vect_name);
846          // resolve promise if vectorisation is finished.
847          if (universalBERT_vectors.length == entryFields.titlesAndAbstracts.length){
848            this.vectors.push(universalBERT_vectors);;
849            this.vector_names.push(vect_name);
850            this.vector_config.push(univBERT_config);
851
852            resolve();
853          };
854        };
855      });
856    };
857
858
859
860    specter_vectorize_webworker(specter_config) {
861      return new Promise((resolve, reject) => {
862        //Clear the vector array.
863        this.specter_vectors = [];
864        let specter_vectors = [];
865        const vect_name = generateUniqueName('SPECTER', this.vector_names);
866        const totalEntries = Math.max(1, this.data_array.length);
867        this.new_vectorization_message(vect_name);
868
869        //Post task to webworker.
870        this.worker.postMessage({
871          method: 'SPECTER',
872          data_array: this.data_array,
873          specter_config: specter_config
874        });
875
876        this.worker.onmessage = (event) => {
877          const {array} = event.data;
878          specter_vectors.push(...array);
879
880          // Update graph.
881          this.update_bar_vectorization(specter_vectors.length / totalEntries, vect_name);
882          // resolve promise if vectorisation is finished.
883          if (specter_vectors.length == this.data_array.length){
884            this.vectors.push(specter_vectors);
885            this.vector_names.push(vect_name);
886            this.vector_config.push(specter_config);
887            resolve();
888          };
889        };
890      });
891    };
892
893
894
895    doc2vec_vectorize_webworker(doc2vec_config){
896      return new Promise((resolve, reject) => {
897        //Reset vectors.
898        this.doc2vec_tokens = [];
899        this.doc2vec_vocab = [];
900        this.doc2vec_model = [];
901        const vect_name = generateUniqueName('Doc2Vec', this.vector_names);
902        const entryFields = this._build_entry_field_arrays({ text: true });
903        this.new_vectorization_message(vect_name);
904        //Post task to webworker.
905        this.worker.postMessage({
906          method: 'Doc2Vec',
907          data_titles_and_abstracts: entryFields.titlesAndAbstracts,
908          doc2vec_config: doc2vec_config,
909          doc2vec_stopwords:this.stopwords
910        });
911
912        this.worker.onmessage = (event) => {
913          if (event.data.type == 'doc2vec_progress'){
914            // Update graph.
915            this.update_bar_vectorization((event.data.epoch+1)/doc2vec_config.epochs, vect_name);
916          } else if (event.data.type == 'doc2vec_final') {
917            this.doc2vec_tokens = event.data.doc2vec_tokens;
918            this.doc2vec_vocab = event.data.doc2vec_vocab;
919            this.doc2vec_vocab_freq = event.data.doc2vec_vocab_freq;
920            this.doc2vec_model = event.data.doc2vec_model;
921            this.vectors.push(event.data.doc2vec_vectors);
922            this.vector_names.push(vect_name);
923            this.vector_config.push(doc2vec_config);
924            resolve();
925          };
926        };
927      });
928    };
929
930
931    sentence_transformers_vectorize_webworker(sentence_transformers_config) {
932      return new Promise((resolve, reject) => {
933        //Clear the vector array.
934        let sentence_transformers_vectors = [];
935        const vect_name = generateUniqueName('Transformers (js)', this.vector_names);
936        const entryFields = this._build_entry_field_arrays({ titles: true, abstracts: true, text: true });
937        const totalEntries = Math.max(1, entryFields.titlesAndAbstracts.length);
938        this.new_vectorization_message(vect_name);
939
940        //Post task to webworker.
941        this.worker_sentence_transformers.postMessage({
942          method: 'transformers.js',
943          data_titles_and_abstracts: entryFields.titlesAndAbstracts,
944          data_titles: entryFields.titles,
945          data_abstracts: entryFields.abstracts,
946          sentence_transformers_config:sentence_transformers_config
947        });
948
949        this.worker_sentence_transformers.onmessage = (event) => {
950          const {array} = event.data;
951          sentence_transformers_vectors.push(...array);
952
953          // Update graph.
954          this.update_bar_vectorization(sentence_transformers_vectors.length / totalEntries, vect_name);
955          // resolve promise if vectorisation is finished.
956          if (sentence_transformers_vectors.length == entryFields.titlesAndAbstracts.length){
957            this.vectors.push(convertTypedArrays(sentence_transformers_vectors));
958            this.vector_names.push(vect_name);
959            this.vector_config.push(sentence_transformers_config);
960            resolve();
961          };
962        };
963      });
964    };
965
966    sentence_transformers_api_vectorize_webworker(sentence_transformers_api_config) {
967      return new Promise((resolve, reject) => {
968        //Clear the vector array.
969        let sentence_transformers_api_vectors = [];
970        const vect_name = generateUniqueName('Transformers (API)', this.vector_names);
971        const entryFields = this._build_entry_field_arrays({ titles: true, abstracts: true, text: true });
972        const totalEntries = Math.max(1, entryFields.titlesAndAbstracts.length);
973        this.new_vectorization_message(vect_name);
974
975        //Post task to webworker.
976        this.worker_sentence_transformers.postMessage({
977          method: 'transformers.api',
978          data_titles_and_abstracts: entryFields.titlesAndAbstracts,
979          data_titles: entryFields.titles,
980          data_abstracts: entryFields.abstracts,
981          stopwords: this.stopwords,
982          sentence_transformers_api_config:sentence_transformers_api_config
983        });
984
985        this.worker_sentence_transformers.onmessage = (event) => {
986          const {array} = event.data;
987          sentence_transformers_api_vectors.push(...array);
988
989          // Update graph.
990          this.update_bar_vectorization(sentence_transformers_api_vectors.length / totalEntries, vect_name);
991          // resolve promise if vectorisation is finished.
992          if (sentence_transformers_api_vectors.length == entryFields.titlesAndAbstracts.length){
993            this.vectors.push(convertTypedArrays(sentence_transformers_api_vectors));
994            this.vector_names.push(vect_name);
995            this.vector_config.push(sentence_transformers_api_config);
996            resolve();
997          };
998        };
999      });
1000    };
1001
1002
1003
1004
1005    update_bar_vectorization(progress, vect_type) {
1006        // Add name to list
1007        const namesEl = this._get_element(this.vect_names_id);
1008        if (namesEl && !namesEl.innerHTML.includes(vect_type)) {
1009            namesEl.innerHTML += " <b>" + vect_type + "</b>";
1010        }
1011
1012        // Add method being processed
1013        const progressEl = this._get_element(this.vect_progress_id);
1014        if (progressEl) {
1015            progressEl.innerHTML = "Performing <b>" + vect_type + "</b> vectorization.";
1016        }
1017
1018        // Update the bar
1019        this._set_progress_bar(this.vect_bar_progress_id, progress);
1020    };
1021
1022    new_vectorization_message(vect_type) {
1023        const el = _(this.vect_progress_id);
1024        if (el) {
1025            el.innerHTML = "Initializing <b>" + vect_type + "</b> vectorization.";
1026        }
1027    };
1028
1029    finish_bar_vectorization() {
1030        const el = _(this.vect_progress_id);
1031        if (el) {
1032            el.innerHTML = "Finished vectorization";
1033        }
1034    };
1035
1036    reset_vectorization() {
1037        const namesEl = _(this.vect_names_id);
1038        if (namesEl) namesEl.innerHTML = '';
1039
1040        const progressEl = _(this.vect_progress_id);
1041        if (progressEl) progressEl.innerHTML = 'No vectorization running';
1042
1043        const barEl = _(this.vect_bar_progress_id);
1044        if (barEl) {
1045            barEl.style.width = "0%";
1046            barEl.innerHTML = "0%";
1047        }
1048
1049        // Clear vector arrays
1050        this.vectors = [];
1051        this.vector_names = [];
1052        this.vector_config = [];
1053    }
1054
1055
1056
1057
1058    training(classif, classif_config, classifier_config_stacked, classif_opt, cv_config, ensemble_config, random_state, display_config){
1059      //Check that at least 1 vectorization is on.
1060      if (this.vectors.length > 0) {
1061        this.train_models_webworker(classif, classif_config, classifier_config_stacked, classif_opt, cv_config, ensemble_config, random_state, display_config);
1062      };
1063    };
1064
1065    train_models_webworker(classif, classif_config, classif_config_stacked, classif_opt, cv_config, ensemble_config, random_state, display_config){
1066      const entryFields = this._build_entry_field_arrays({ labels: true, ids: true });
1067
1068      // Disable config panel.
1069      _(this.ml_config.window).disabled = true;
1070
1071      // Disable config panel.
1072      _(this.predictions_config.button).disabled = true;
1073
1074      //Post task to webworker.
1075      this.worker.postMessage({
1076        method: 'Training',
1077        classif: classif,
1078        classif_config: classif_config,
1079        classif_config_stacked: classif_config_stacked,
1080        classif_opt: classif_opt,
1081        cv_config: cv_config,
1082        ensemble_config: ensemble_config,
1083        random_state: random_state,
1084        vectors: this.vectors,
1085        vector_names: this.vector_names,
1086        data_labels: entryFields.labels,
1087        data_ids: entryFields.ids
1088      });
1089
1090      this.worker.onmessage = (event) => {
1091        if (event.data.type == 'training_config'){
1092          this.update_display_config(event.data.training_descriptor);
1093        } else if (event.data.type == 'training_progress_1') {
1094          this.update_training_progress_1(cv_config, classif_opt, event.data.opt_idx, event.data.perm1, event.data.perm2, event.data.fold1, event.data.fold2, event.data.message);
1095        } else if (event.data.type == 'training_progress_2') {
1096          this.training_data = event.data.training_data;
1097          this.update_training_progress_2(display_config);
1098        } else if (event.data.type == 'training_progress_3') {
1099          this.update_training_progress_3();
1100        } else if(event.data.type == 'training_progress_4') {
1101          this.update_training_progress_4(cv_config, event.data.perm1, event.data.fold1, event.data.message);
1102        } else if (event.data.type == 'training_final') {
1103          this.finish_training_progress();
1104          //Get data from worker.
1105          this.training_data = event.data.training_data;
1106
1107          // Calculate roc curve.
1108          this.calculate_roc_curve();
1109
1110          //Store the training threshold.
1111          this.proba_threshold = this.training_data.classif_config.threshold;
1112          //Report the training results.
1113          this.report_training_results();
1114
1115          // Enable config panel.
1116          _(this.ml_config.window).disabled = false;
1117
1118          //Enable predictions.
1119          _(this.predictions_config.button).disabled = false;
1120
1121          //Predict unseen labels if available.
1122          if (this.training_data.y_unlabelled.length > 0){
1123            this.predict_unseen_labels();
1124          };
1125        };
1126      };
1127    };
1128
1129    //Update display with training information.
1130    update_display_config(training_descriptor){
1131      _(this.training_message_id).innerHTML = training_descriptor;
1132    };
1133
1134
1135    //Function to display the training performance.
1136    update_training_progress_1(cv_config, classif_opt, opt_idx, perm1, perm2, fold1, fold2, message){
1137      //Print section of training.
1138      _(this.training_progress_id).innerHTML = "CV<sub>Outer</sub>
1138 [" + (perm1+1) + ", " + (fold1+1) + "], CV<sub>Inner</sub> [" + (perm2+1) + ", " + (fold2+1) + "], param: " + (opt_idx+1) + "/" +classif_opt.length + message;
1139      let total_steps = classif_opt.length * cv_config[0] * cv_config[1] * cv_config[2] * cv_config[3];
1140      let done_steps = (opt_idx) * cv_config[0] * cv_config[1] * cv_config[2] * cv_config[3] + (perm1) * cv_config[1] * cv_config[2] * cv_config[3] + (fold1) * cv_config[2] * cv_config[3] + (perm2) * cv_config[3] + fold2 + 1;
1141
1142      //Update the bar.
1143      this._set_progress_bar(this.training_bar_progress_id, total_steps > 0 ? done_steps / total_steps : 0);
1144    };
1145
1146
1147    update_training_progress_2(display_config){
1148
1149      let cv1_plus = this.training_data.mean_cv1[display_config.model - 1].map((x, i) => 100*(x + this.training_data.std_cv1[0][i]));
1150      let cv1 = this.training_data.mean_cv1[display_config.model- 1].map(x => 100*x);
1151      let cv1_minus = this.training_data.mean_cv1[display_config.model- 1].map((x, i) => 100*(x - this.training_data.std_cv1[0][i]));
1152
1153      let cv2_plus = this.training_data.mean_cv2[display_config.model- 1].map((x, i) => 100*(x + this.training_data.std_cv2[0][i]));
1154      let cv2 = this.training_data.mean_cv2[display_config.model- 1].map(x => 100*x);
1155      let cv2_minus = this.training_data.mean_cv2[display_config.model- 1].map((x, i) => 100*(x - this.training_data.std_cv2[0][i]));
1156
1157
1158      // Clear existing data
1159      this.trainingChart1.data.datasets[0].data = [];
1160      this.trainingChart1.data.datasets[1].data = [];
1161
1162      //Change the data.
1163      this.trainingChart1.data.datasets[0].data = cv1_plus;
1164      this.trainingChart1.data.datasets[1].data = cv1;
1165      this.trainingChart1.data.datasets[2].data = cv1_minus;
1166
1167      this.trainingChart1.data.datasets[3].data = cv2_plus;
1168      this.trainingChart1.data.datasets[4].data = cv2;
1169      this.trainingChart1.data.datasets[5].data = cv2_minus;
1170
1171
1172      this.trainingChart1.data.labels = this.training_data.classif_opt;
1173      _(this.display_1_id).innerHTML = "CV performance vs. parameter of model with "+ this.training_data.X_names[display_config.model - 1];
1174
1175      this.trainingChart1.options.scales.xAxes[0].ticks.callback = function(value, index, values){
1176        return index;
1177      };
1178
1179      this.trainingChart1.options.scales.yAxes[0].ticks.max = index_of_max(flatten([cv1_plus, cv2_plus]))[0] + 1;
1180      this.trainingChart1.options.scales.yAxes[0].ticks.min = index_of_min(flatten([cv1_minus, cv2_minus]))[0] - 1;
1181      this.trainingChart1.options.scales.yAxes[0].scaleLabel.labelString = display_config.metric + " (%)";
1182      // Update the chart
1183      this.trainingChart1.update();
1184    };
1185
1186    update_training_progress_3(){
1187      _(this.training_progress_id).innerHTML = "Finished model discovery, retraining with optimal parameters";
1188      //Update the bar.
1189      _(this.training_bar_progress_id).style.width = "0%";
1190      _(this.training_bar_progress_id).innerHTML = "0%";
1191    }
1192
1193    update_training_progress_4(cv_config, perm1, fold1, message){
1194      //Print section of training.
1195      _(this.training_progress_id).innerHTML = "CV<sub>Outer</sub> [" + (perm1+1) + ", " + (fold1+1) + "]" + message;
1196      let total_steps = cv_config[0] * cv_config[1];
1197      let done_steps = (perm1) * cv_config[1] + (fold1) + 1;
1198
1199      //Update the bar.
1200      this._set_progress_bar(this.training_bar_progress_id, total_steps > 0 ? done_steps / total_steps : 0);
1201    };
1202
1203
1204    reset_training(){
1205      _(this.training_progress_id).innerHTML = "No training running";
1206      _(this.training_bar_progress_id).style.width = "0%";
1207      _(this.training_bar_progress_id).innerHTML = "0%";
1208      _(this.training_message_id).innerHTML = "";
1209    };
1210
1211    report_training_results(){
1212      //Calculate the metrics
1213      this.calculate_performance();
1214
1215      const metric = index => Array.isArray(this.metrics_stats?.[index])
1216        ? this.metrics_stats[index][0]
1217        : null;
1218      const setMetric = (suffix, html) => {
1219        const el = this._get_element(this.display_3_id + suffix);
1220        if (el) {
1221          el.innerHTML = html;
1222        }
1223      };
1224
1225      //Report the metrics.
1226      setMetric("ACC", "<b>ACC</b>: " +  this._format_percent(metric(1)));
1227      setMetric("BAC", "<b>BAC</b>: " +  this._format_percent(metric(0)));
1228      setMetric("AUC", "<b>AUC</b>: " +  this._format_number(this.auc_roc_stats?.[0]));
1229      setMetric("TPR/FPR", "<b>TPR/FPR</b>: " +  this._format_percent(metric(2)) + "/" + this._format_percent(metric(3)));
1230      setMetric("TNR/FNR", "<b>TNR/FNR</b>: " +  this._format_percent(metric(4)) + "/" + this._format_percent(metric(5)));
1231      setMetric("PPV/NPV", "<b>PPV/NPV</b>: " +  this._format_percent(metric(6)) + "/" + this._format_percent(metric(7)));
1232      setMetric("F1", "<b>F1</b>: " +  this._format_number(metric(8)));
1233      setMetric("MCC", "<b>MCC</b>: " +  this._format_number(metric(9)));
1234      //Display the roc curve.
1235      this.display_roc_curve();
1236    };
1237
1238    calculate_performance(){
1239      let y_pred = this.training_data.ensemble_cv1_ypred_best_stacked.map(x=>flatten(x).map(z=>Number(z>this.proba_threshold)));
1240      this.metrics = transpose(y_pred.map((iypred, index) => calculateMetrics(flatten(this.training_data.ensemble_cv1_y[index]), flatten(iypred))));
1241      this.metrics_stats = this.metrics.map(x=>[average(x), std(x)]);
1242      this.auc_roc = this.roc_curves.map(x=>auc_roc(x));
1243      this.auc_roc_stats = [average(this.auc_roc), std(this.auc_roc)];
1244    };
1245
1246    calculate_roc_curve(){
1247      //Calculate the ROC curve.
1248      this.roc_curves = this.training_data.ensemble_cv1_ypred_best_stacked.map((x, index) => calculateROC(flatten(this.training_data.ensemble_cv1_y[index]), flatten(x)));
1249      this.roc_curve_mean = averageROC(this.roc_curves);
1250    };
1251
1252    display_roc_curve(){
1253      if (!this.trainingChart2?.data?.datasets || this.trainingChart2.data.datasets.length < 5) {
1254        return;
1255      }
1256
1257      const metric = index => {
1258        const rawValue = this.metrics_stats?.[index]?.[0];
1259        if (rawValue === null || rawValue === undefined || rawValue === "") {
1260          return null;
1261        }
1262
1263        const value = Number(rawValue);
1264        return Number.isFinite(value) ? value : null;
1265      };
1266      const tpr = metric(2);
1267      const fpr = metric(3);
1268
1269      // Add decision threshold line for TPR.
1270      this.trainingChart2.data.datasets[1].data = [];
1271      this.trainingChart2.data.datasets[1].data = tpr == null ? [] : [{x:0, y:tpr}, {x:1, y:tpr}];
1272
1273      // Add decision threshold line for FPR.
1274      this.trainingChart2.data.datasets[2].data = [];
1275      this.trainingChart2.data.datasets[2].data = fpr == null ? [] : [{x:fpr, y:0}, {x:fpr, y:1}];
1276      // Add threshold point.
1277      this.trainingChart2.data.datasets[3].data = [];
1278      this.trainingChart2.data.datasets[3].data = tpr == null || fpr == null ? [] : [{x:fpr, y:tpr}];
1279
1280      //ROC curve.
1281      this.trainingChart2.data.datasets[4].data = [];
1282      this.trainingChart2.data.datasets[4].data = Array.isArray(this.roc_curve_mean) ? this.roc_curve_mean : [];
1283
1284      const displayEl = this._get_element(this.display_2_id);
1285      if (displayEl) {
1286        displayEl.innerHTML = "ROC curve";
1287      }
1288
1289      // Update the chart
1290      this.trainingChart2.update();
1291    };
1292
1293    roc_curve_click(event, array, probability_input_id){
1294      //Check if click is the ROC curve.
1295      if (array.length != 0){
1296        if (array[0]._datasetIndex == 4){
1297
1298          //Get probability.
1299          this.proba_threshold = this.roc_curve_mean[array[0]._index].probability;
1300
1301          //Change value in input.
1302          _(probability_input_id).value = this.proba_threshold;
1303
1304          //Report the results.
1305          this.report_training_results();
1306
1307          //Predict unseen labels if available.
1308          if (this.training_data.y_unlabelled.length>0){
1309            this.predict_unseen_labels();
1310          };
1311        };
1312      }
1313    };
1314
1315    doc_index_proba_click(event, array, text_ids){
1316      let doc_data;
1317      //Check if click is in one of the documents.
1318      if (array.length != 0){
1319        if (array[0]._datasetIndex == 0){
1320          $('#'+text_ids.window).modal('show');
1321          //Extract doc data.
1322          doc_data = this.data_array.filter((_,i) => this.training_data.prediction_mask[i])[array[0]._index];
1323          _(text_ids.doi).innerHTML = 'DOI: <a href="https://doi.org/'+doc_data.paper_id+'" target="_blank">'+doc_data.paper_id+'</a>';
1324          _(text_ids.title).innerHTML = "Title: "+doc_data.title;
1325          _(text_ids.abstract).innerHTML = "Abstract: "+doc_data.abstract;
1326        }
1327      }
1328    };
1329
1330    probability_threshold_changed(probability){
1331      const parsedProbability = Number(probability);
1332      this.proba_threshold = Number.isFinite(parsedProbability)
1333        ? Math.min(1, Math.max(0, parsedProbability))
1334        : 0.5;
1335
1336      //Report the results.
1337      this.report_training_results();
1338
1339      //Predict unseen labels if available.
1340      if (this.training_data.y_unlabelled.length>0){
1341        if (this._has_prediction_results()) {
1342          this.refresh_prediction_labels();
1343        } else {
1344          this.predict_unseen_labels();
1345        }
1346      };
1347    }
1348
1349    set_prediction_label_policy(mode, irrelevantThreshold, relevantThreshold){
1350      const nextMode = mode === "confidence" ? "confidence" : "threshold";
1351      const nextIrrelevant = Math.min(1, Math.max(0, Number(irrelevantThreshold)));
1352      const nextRelevant = Math.min(1, Math.max(0, Number(relevantThreshold)));
1353
1354      if (nextMode === "confidence" &&
1355          (!Number.isFinite(nextIrrelevant) || !Number.isFinite(nextRelevant) ||
1356           nextIrrelevant >= nextRelevant)) {
1357        throw new Error("The irrelevant confidence must be lower than the relevant confidence.");
1358      }
1359
1360      this.prediction_label_mode = nextMode;
1361      if (nextMode === "confidence") {
1362        this.prediction_irrelevant_threshold = nextIrrelevant;
1363        this.prediction_relevant_threshold = nextRelevant;
1364      }
1365      if (this._has_prediction_results()) this.refresh_prediction_labels();
1366    }
1367
1368    build_prediction_labels(probabilities){
1369      const safeProbabilities = Array.isArray(probabilities) ? probabilities : [];
1370      if (this.prediction_label_mode === "confidence") {
1371        return safeProbabilities.map(value => {
1372          const probability = Number(value);
1373          if (!Number.isFinite(probability)) return null;
1374          if (probability >= this.prediction_relevant_threshold) return 1;
1375          if (probability <= this.prediction_irrelevant_threshold) return 0;
1376          return null;
1377        });
1378      }
1379      return safeProbabilities.map(value => Number(value) > this.proba_threshold ? 1 : 0);
1380    }
1381
1382    refresh_prediction_labels(){
1383      if (!this.prediction_data || !Array.isArray(this.prediction_data.y_proba_stacked)) return;
1384      this.prediction_data.y_label_stacked = this.build_prediction_labels(
1385        this.prediction_data.y_proba_stacked
1386      );
1387      this.report_predicted_results();
1388    }
1389
1390    optimise_probability_theshold(parameter, proba_display){
1391      //Record performances.
1392      let metric = null;
1393      let bestPerformance = -Infinity;
1394      let bestThreshold = null;
1395
1396      //Metrics
1397      if (parameter == "ACC"){
1398        metric = 1;
1399      } else if (parameter == "BAC"){
1400        metric = 0;
1401      } else if (parameter == "TPR"){
1402        metric = 2;
1403      } else if (parameter == "TNR"){
1404        metric = 4;
1405      } else if (parameter == "PPV"){
1406        metric = 6;
1407      } else if (parameter == "NPV"){
1408        metric = 7;
1409      } else if (parameter == "F1"){
1410        metric = 8;
1411      } else if (parameter == "MCC"){
1412        metric = 9;
1413      } else if (parameter == "P4"){
1414        metric = 10;
1415      };
1416
1417      this.roc_curve_mean.forEach(point => {
1418        //record the performance.
1419        this.proba_threshold = point.probability;
1420        this.calculate_performance();
1421
1422        let currentPerformance = this.metrics_stats[metric][0];
1423
1424        // Check if this is the best performance so far
1425        if (currentPerformance > bestPerformance) {
1426          bestPerformance = currentPerformance;
1427          bestThreshold = point.probability;
1428        }
1429      });
1430
1431      //Show optimised probability
1432      this.probability_threshold_changed(bestThreshold);
1433      _(proba_display).value = bestThreshold;
1434    };
1435
1436
1437    finish_training_progress(){
1438      _(this.training_progress_id).innerHTML = "Finished training";
1439    };
1440
1441    stop_training(){
1442      //Terminate the webworker process and iniciate again.
1443      this.worker.terminate();
1444      this.worker_sentence_transformers.terminate();
1445
1446      this.worker = new Worker(TRAINING_CLASS_WORKER_URL);
1447      this.worker_sentence_transformers = new Worker('oop/transformers_worker.js', {type: 'module'});
1448
1449      // Enable the config panel again
1450      _(this.ml_config.window).disabled = false;
1451
1452      //Clear training data.
1453      this.training_data = [];
1454
1455      // Disable prediction button.
1456      _(this.predictions_config.button).disabled = true;
1457
1458      this.reset_training();
1459    };
1460
1461    stop_vectorization(){
1462      //Terminate the webworker process and iniciate again.
1463      this.worker.terminate();
1464      this.worker_sentence_transformers.terminate();
1465
1466      this.worker = new Worker(TRAINING_CLASS_WORKER_URL);
1467      this.worker_sentence_transformers = new Worker('oop/transformers_worker.js', {type: 'module'});
1468
1469      // Reset vectorization bar.
1470      this.reset_vectorization();
1471
1472      // Enable config panel.
1473      _(this.ml_config.window).disabled = false;
1474
1475      // Remove all vectors.
1476      this.vectors = [];
1477      this.vector_names = [];
1478      this.vector_config = [];
1479    };
1480
1481    predict_unseen_labels(){
1482      //Check that vectorization and model training has been performed.
1483      if (this.vectors.length != 0) {
1484        if (this.training_data.length != 0){
1485          this.predict_unseen_labels_webworker();
1486        }
1487      };
1488    };
1489
1490
1491    predict_unseen_labels_webworker(){
1492      this.worker.postMessage({
1493        method: 'Prediction',
1494        tr_res: this.training_data,
1495        proba_threshold: this.proba_threshold
1496      });
1497
1498      this.worker.onmessage = (event) => {
1499        if (event.data.type == 'prediction_final'){
1500
1501          //Store predicted labels.
1502          this.prediction_data = event.data.prediction_data;
1503          this.prediction_data.y_label_stacked = this.build_prediction_labels(
1504            this.prediction_data.y_proba_stacked
1505          );
1506
1507          //Report the results in the chart.
1508          this.report_predicted_results();
1509        };
1510      };
1511    };
1512
1513
1514    report_predicted_results(){
1515      //Chart3 update.
1516      let rel_percentage = countOccurrences(this.prediction_data.y_label_stacked, 1);
1517      let irrel_percentage = countOccurrences(this.prediction_data.y_label_stacked, 0);
1518      let unlabelled_percentage = this.prediction_data.y_label_stacked.length - rel_percentage - irrel_percentage;
1519      let prediction_mask = this.training_data.prediction_mask;
1520      this.trainingChart3.data.labels = ["Relevant", "Irrelevant", "Left unlabelled"];
1521      this.trainingChart3.data.datasets[0].backgroundColor = ["#4CAF50", "#FF5252", "#9E9E9E"];
1522      this.trainingChart3.data.datasets[0].data = [rel_percentage, irrel_percentage, unlabelled_percentage];
1523
1524      // Update the chart
1525      this.trainingChart3.update();
1526      _(this.display_4_id).innerHTML = "Classification of unlabelled entries";
1527
1528      //Chart4 update.
1529      // Add threshold line.
1530      const entryFields = this._build_entry_field_arrays({ ids: true });
1531      this.trainingChart4.options.lineAt = this.prediction_label_mode === "confidence"
1532        ? [this.prediction_irrelevant_threshold, this.prediction_relevant_threshold]
1533        : [this.proba_threshold];
1534      this.trainingChart4.options.scales.xAxes[0].ticks.callback = function(value, index, values){
1535        return index;
1536      };
1537      this.trainingChart4.data.datasets[0].data = this.prediction_data.y_proba_stacked;
1538      this.trainingChart4.data.labels = entryFields.ids.filter((_,i) => prediction_mask[i]);
1539      this.trainingChart4.data.datasets[0].backgroundColor = this.prediction_data.y_label_stacked.map(x => {
1540        if (x == 0) {
1541          return "#FF5252";
1542        } else if (x == 1) {
1543          return "#4CAF50";
1544        }
1545        return "#9E9E9E";
1546      });
1547
1548      this.trainingChart4.update();
1549
1550      _(this.display_5_id).innerHTML = "Relevance probability of entries";
1551    };
1552
1553
1554    save_predicted_results(filename) {
1555      const wb = XLSX.utils.book_new();
1556
1557      try {
1558        // --- Prepare Data ---
1559        const prediction_mask = this.training_data.prediction_mask;
1560        const training_mask = this.training_data.training_mask;
1561
1562        const data_prediction = this.data_array.filter((_, i) => prediction_mask[i]);
1563        const data_training = this.data_array.filter((_, i) => training_mask[i]);
1564
1565        const label = copy_array(this.prediction_data.y_label_stacked);
1566        const probability = copy_array(this.prediction_data.y_proba_stacked);
1567
1568        // --- Sheet 1: All ---
1569        let counter = 0;
1570        const data_all = this.data_array.map((entry, index) => {
1571          const newEntry = { ...entry };
1572          if (prediction_mask[index]) {
1573            newEntry.label = label[counter];
1574            counter++;
1575          }
1576          return newEntry;
1577        });
1578        XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data_all), "All");
1579
1580        // --- Sheet 2: Training ---
1581        XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data_training), "Training");
1582
1583        // --- Sheet 3: Predictions ---
1584        const dataWithLabels = data_prediction.map((entry, index) => ({
1585          ...entry,
1586          label: label[index],
1587          probability: probability[index]
1588        }));
1589        XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(dataWithLabels), "Predictions");
1590
1591        // --- Try Excel Export ---
1592        XLSX.writeFile(wb, filename);
1593
1594      } catch (error) {
1595        alert('⚠️ Length of text is longer than maximum possible in XLSX, falling back to CSV.');
1596        const baseName = filename.replace(/\.[^/.]+$/, ""); // remove extension
1597        // Fallback: save as UTF-8 CSV files
1598
1599          // Rebuild data since we may be outside try block
1600          const prediction_mask = this.training_data.prediction_mask;
1601          const training_mask = this.training_data.training_mask;
1602
1603          const data_prediction = this.data_array.filter((_, i) => prediction_mask[i]);
1604          const data_training = this.data_array.filter((_, i) => training_mask[i]);
1605
1606          const label = copy_array(this.prediction_data.y_label_stacked);
1607          const probability = copy_array(this.prediction_data.y_proba_stacked);
1608
1609          // All entries
1610          let counter = 0;
1611          const data_all = this.data_array.map((entry, index) => {
1612            const newEntry = { ...entry };
1613            if (prediction_mask[index]) {
1614              newEntry.label = label[counter];
1615              counter++;
1616            }
1617            return newEntry;
1618          });
1619
1620          // Predictions
1621          const dataWithLabels = data_prediction.map((entry, index) => ({
1622            ...entry,
1623            label: label[index],
1624            probability: probability[index]
1625          }));
1626
1627          // Save CSVs
1628          saveCSV(data_all, baseName, "All");
1629          saveCSV(data_training, baseName, "Training");
1630          saveCSV(dataWithLabels, baseName, "Predictions");
1631      }
1632    };
1633
1634    export_ris(filename, option = "relevant") {
1635
1636      // ---------------------------------------
1637      // Rebuild "All" data exactly as in export
1638      // ---------------------------------------
1639      const prediction_mask = this.training_data.prediction_mask;
1640      const training_mask = this.training_data.training_mask; // kept for parity
1641
1642      const label = copy_array(this.prediction_data.y_label_stacked);
1643      const probability = copy_array(this.prediction_data.y_proba_stacked);
1644
1645      let counter = 0;
1646      const data_all = this.data_array.map((entry, index) => {
1647        const newEntry = { ...entry };
1648        if (prediction_mask[index]) {
1649          newEntry.label = label[counter];
1650          newEntry.probability = probability[counter];
1651          counter++;
1652        }
1653        return newEntry;
1654      });
1655
1656      // ---------------------------------------
1657      // Filter entries
1658      // ---------------------------------------
1659      let entries;
1660      if (option === "relevant") {
1661        entries = data_all.filter(entry => entry.label === 1);
1662      } else if (option === "irrelevant") {
1663        entries = data_all.filter(entry => entry.label === 0);
1664      } else if (option === "all") {
1665        entries = data_all;
1666      } else {
1667        return;
1668      }
1669
1670      if (!entries.length) return;
1671
1672      const escapeRIS = (text) =>
1673        text ? text.replace(/\r?\n/g, ' ').replace(/\t/g, ' ').trim() : '';
1674
1675      // ---------------------------------------
1676      // Build RIS content (standard order)
1677      // ---------------------------------------
1678      const risContent = entries.map(entry => {
1679        let risEntry = '';
1680
1681        // 1️⃣ Type
1682        risEntry += `TY  - JOUR\n`;
1683
1684        // 2️⃣ Title
1685        if (entry.title) {
1686          risEntry += `TI  - ${escapeRIS(entry.title)}\n`;
1687        }
1688
1689        // 3️⃣ Authors
1690        if (entry.authors) {
1691          entry.authors
1692            .split(';')
1693            .map(a => a.trim())
1694            .filter(Boolean)
1695            .forEach(author => {
1696              risEntry += `AU  - ${escapeRIS(author)}\n`;
1697            });
1698        }
1699
1700        // 4️⃣ Year
1701        if (entry.year) {
1702          risEntry += `PY  - ${escapeRIS(entry.year)}\n`;
1703        }
1704
1705        // 5️⃣ Journal
1706        if (entry.journal) {
1707          risEntry += `T2  - ${escapeRIS(entry.journal)}\n`;
1708        }
1709
1710        // 6️⃣ Abstract
1711        if (entry.abstract) {
1712          risEntry += `AB  - ${escapeRIS(entry.abstract)}\n`;
1713        }
1714
1715        // 7️⃣ Keywords
1716        if (entry.keywords) {
1717          entry.keywords
1718            .split(';')
1719            .map(k => k.trim())
1720            .filter(Boolean)
1721            .forEach(keyword => {
1722              risEntry += `KW  - ${escapeRIS(keyword)}\n`;
1723            });
1724        }
1725
1726        // 8️⃣ DOI or ID
1727        if (entry.paper_id != null) {
1728          const paperIdStr = String(entry.paper_id);
1729          if (paperIdStr.includes('/')) {
1730            risEntry += `DO  - ${escapeRIS(paperIdStr)}\n`;
1731          } else {
1732            risEntry += `ID  - ${escapeRIS(paperIdStr)}\n`;
1733          }
1734        }
1735
1736        // 9️⃣ Notes + label (+ probability if present)
1737        let noteText = entry.notes ? escapeRIS(entry.notes) + ' ' : '';
1738        noteText += `Label: ${entry.label === 1 ? 'included' : 'excluded'}`;
1739
1740        if (entry.probability != null) {
1741          noteText += ` | Probability: ${entry.probability}`;
1742        }
1743
1744        risEntry += `N1  - ${noteText}\n`;
1745
1746        // 🔟 End record
1747        risEntry += `ER  - \n`;
1748
1749        return risEntry;
1750      }).join('\n');
1751
1752      // ---------------------------------------
1753      // Download RIS file
1754      // ---------------------------------------
1755      const blob = new Blob([risContent], {
1756        type: "application/x-research-info-systems"
1757      });
1758
1759      const link = document.createElement("a");
1760      const downloadUrl = URL.createObjectURL(blob);
1761      link.href = downloadUrl;
1762      link.download = filename.endsWith('.ris') ? filename : filename + '.ris';
1763      document.body.appendChild(link);
1764      link.click();
1765      document.body.removeChild(link);
1766      setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
1767    }
1768
1769
1770
1771
1772    save_performance_metrics(filename){
1773      let metrics = deep_copy_array(this.metrics);
1774      let metrics_stats = deep_copy_array(this.metrics_stats);
1775
1776      //Add auc roc.
1777      metrics.push(deep_copy_array(this.auc_roc));
1778      metrics_stats.push(deep_copy_array(this.auc_roc_stats));
1779
1780      //Add threshold to metrics.
1781      metrics
1782
1783      //transpose
1784      metrics = transpose(metrics);
1785      metrics_stats = transpose(metrics_stats);
1786
1787      let index_array = metrics.map((x,i)=>'Perm '+ i).concat('Avg.', 'Std');
1788      index_array.unshift('');
1789      let names_array = ['BAC', 'ACC', 'TPR', 'FPR', 'TNR', 'FNR', 'PPV', 'NPV', 'F1', 'MCC','P4', 'TP', 'TN', 'FP', 'FN', 'AUC'];
1790
1791
1792
1793      metrics.unshift(names_array);
1794      //Push avg. and std
1795      metrics.push(metrics_stats[0]);
1796      metrics.push(metrics_stats[1]);
1797
1798      //Add index.
1799      let table = metrics.map((x,i)=>[index_array[i], ...x]);
1800      let aoa = XLSX.utils.aoa_to_sheet(table);
1801      const wb = XLSX.utils.book_new();
1802      XLSX.utils.book_append_sheet(wb, aoa, 'Metrics');
1803
1804      // Write to file
1805      XLSX.writeFile(wb, filename);
1806
1807    };
1808
1809    save_roc_curve(filename){
1810      // Prepare the data for export
1811      let rocData = [];
1812
1813      // Add the ROC curve data to the array
1814      this.roc_curve_mean.forEach(point => {
1815          rocData.push([point.x, point.y, point.probability]);
1816      });
1817
1818      //Add prob. Threshold
1819      rocData[0].push(this.proba_threshold);
1820
1821      rocData.unshift(['False Positive Rate', 'True Positive Rate', 'Probability/Score', 'Threshold']);
1822      let aoa = XLSX.utils.aoa_to_sheet(rocData);
1823      const wb = XLSX.utils.book_new();
1824      XLSX.utils.book_append_sheet(wb, aoa, 'Mean ROC');
1825
1826      // Write to file
1827      XLSX.writeFile(wb, filename);
1828    };
1829
1830
1831    //Save the vectors to excel.
1832    save_vectors(filename){
1833      const wb = XLSX.utils.book_new();
1834      let aoa, name_array;
1835
1836      for (i = 0; i<this.vectors.length;++i){
1837        aoa = deep_copy_2d_array(this.vectors[i]);
1838        aoa = XLSX.utils.aoa_to_sheet(aoa);
1839        XLSX.utils.book_append_sheet(wb, aoa, this.vector_names[i]);
1840      };
1841
1842      //Write the excel.
1843      XLSX.writeFile(wb, filename);
1844    };
1845
1846    save_vectors_csv() {
1847      for (let i = 0; i < this.vectors.length; ++i) {
1848        const vectorData = this.vectors[i];
1849        const vectorName = this.vector_names[i];
1850
1851        // Convert the 2D array to CSV format
1852        const csvContent = vectorData.map(row => row.join(',')).join('\n');
1853
1854        // Create a Blob from the CSV content
1855        const blob = new Blob([csvContent], { type:'text/csv'});
1856
1857        // Create a link element for downloading the file
1858        const link = document.createElement('a');
1859        const downloadUrl = URL.createObjectURL(blob);
1860        link.href = downloadUrl;
1861        link.download = `${vectorName}.csv`;
1862
1863        // Append the link, trigger download, and remove the link
1864        document.body.appendChild(link);
1865        link.click();
1866        document.body.removeChild(link);
1867        setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
1868      }
1869    };
1870
1871
1872    load_vectors(data, names, progress_id, warning_id){
1873      let status = '';
1874      let warning_status = '';
1875      for (var i = 0; i<names.length;++i){
1876        if (data[i].length == this.data_array.length){
1877          this.vectors.push(data[i]);
1878          this.vector_names.push(names[i]);
1879          status = status + ' ' + names[i];
1880        } else {
1881          warning_status = warning_status + ' ' + names[i];
1882        };
1883      };
1884      //Print message in data loading.
1885      _(progress_id).innerHTML = status + " vectors loaded";
1886      if (warning_status != ''){_(warning_id).innerHTML = '<b>Warning</b>: '+ warning_status + ' not loaded due to size mismatch with previously loaded entries.'};
1887    };
1888
1889
1890    //Active training function.
1891    active_training(classif, classif_config, classif_config_stacked, ensemble_config, random_state){
1892
1893      let promiseChainTrain = Promise.resolve();
1894
1895      if (this.vectors.length > 0) {
1896        promiseChainTrain = promiseChainTrain.then(() => this.active_training_model_webworker(classif, classif_config, classif_config_stacked, ensemble_config, random_state))
1897
1898      };
1899
1900      return promiseChainTrain;
1901    };
1902
1903    active_training_model_webworker(classif, classif_config, classif_config_stacked, ensemble_config, random_state){
1904      return new Promise((resolve, reject) => {
1905      const entryFields = this._build_entry_field_arrays({ labels: true, ids: true });
1906      //Post task to webworker.
1907      this.worker.postMessage({
1908        method: 'ActiveTraining',
1909        classif: classif,
1910        classif_config: classif_config,
1911        classif_config_stacked: classif_config_stacked,
1912        ensemble_config: ensemble_config,
1913        random_state: random_state,
1914        vectors: this.vectors,
1915        vector_names: this.vector_names,
1916        data_labels: entryFields.labels,
1917        data_ids: entryFields.ids
1918      });
1919
1920      this.worker.onmessage = (event) => {
1921        if (event.data.type == 'active_prediction'){
1922          this.sorting_probabilities =  event.data.prediction_data;
1923          resolve();
1924        }
1925      };
1926    });
1927    };
1928
1929
1930    calculate_similarity_scores(option, language, display, scanMode = "fuzzy", comparisonText = "all", samplePercentage = 100) {
1931      const entryFields = this._build_entry_field_arrays({ titles: true, abstracts: true, ids: true });
1932
1933      //Determine the language
1934      if (language == "english"){
1935        this.stopwords = this.stopwords_multilingual.en;
1936      } else if (language == "multilingual"){
1937        this.stopwords = this.stopwords_multilingual.all;
1938      };
1939
1940
1941      return new Promise((resolve, reject) => {
1942      //Post task to webworker.
1943      this.worker.postMessage({
1944        method: 'SimilarityMeasures',
1945        data_titles: entryFields.titles,
1946        data_abstracts: entryFields.abstracts,
1947        data_ids: entryFields.ids,
1948        stopwords: this.stopwords,
1949        option: option,
1950        scan_mode: scanMode,
1951        comparison_text: comparisonText,
1952        sample_percentage: samplePercentage
1953      });
1954
1955      this.worker.onmessage = (event) => {
1956        if (event.data.type == 'progress'){
1957          const workerPercentage = Number(event.data.progress);
1958          const currentEntry = Number(event.data.i);
1959          const fallbackPercentage = Number.isFinite(currentEntry)
1960            ? 100 * (currentEntry + 1) / Math.max(1, this.data_array.length)
1961            : 0;
1962          const percentage = Math.min(
1963            100,
1964            Math.max(0, Number.isFinite(workerPercentage) ? workerPercentage : fallbackPercentage)
1965          );
1966          _(display).textContent = event.data.message || `Progress: ${percentage.toFixed(2)}%`;
1967
1968        } else if (event.data.type == 'final'){
1969          this.similarity_array =  event.data.similarity_array;
1970          this.similarity_summary = event.data.summary || null;
1971          this.similarity_pairs_action_discarded = [];
1972          this.similarity_array_discarded = [];
1973          this.discarded_similarity_indexes = new Set();
1974          this.currentSimIndex = 0;
1975          this.currentDiscardedIndex = 0;
1976          resolve();
1977        };
1978      };
1979      this.worker.onerror = event => {
1980        reject(new Error(event.message || "Similarity calculation failed."));
1981      };
1982    });
1983  };
1984
1985  sort_similarity_scores(sort_method) {
1986    if (!this.similarity_array || this.similarity_array.length === 0) return;
1987
1988    this.similarity_array.sort((a, b) => {
1989      let aSim, bSim;
1990
1991      switch (sort_method) {
1992        case 'title_similarity':
1993          aSim = (typeof a.titleSimilarity === 'number' && !isNaN(a.titleSimilarity)) ? a.titleSimilarity : -Infinity;
1994          bSim = (typeof b.titleSimilarity === 'number' && !isNaN(b.titleSimilarity)) ? b.titleSimilarity : -Infinity;
1995          break;
1996
1997        case 'abstract_similarity':
1998          aSim = (typeof a.abstractSimilarity === 'number' && !isNaN(a.abstractSimilarity)) ? a.abstractSimilarity : -Infinity;
1999          bSim = (typeof b.abstractSimilarity === 'number' && !isNaN(b.abstractSimilarity)) ? b.abstractSimilarity : -Infinity;
2000          break;
2001
2002        case 'mean_similarity':
2003        default:
2004          aSim = (typeof a.meanSimilarity === 'number' && !isNaN(a.meanSimilarity)) ? a.meanSimilarity : -Infinity;
2005          bSim = (typeof b.meanSimilarity === 'number' && !isNaN(b.meanSimilarity)) ? b.meanSimilarity : -Infinity;
2006          break;
2007      }
2008
2009      return bSim - aSim; // descending order
2010    });
2011  }
2012
2013  discard_similarity_indexes(indexes) {
2014    if (!this.similarity_array || this.similarity_array.length === 0) return;
2015
2016    const discardedIndexes = new Set((Array.isArray(indexes) ? indexes : [indexes])
2017      .map(index => Math.floor(Number(index)))
2018      .filter(index => Number.isFinite(index) && index >= 0 && !this.discarded_similarity_indexes.has(index)));
2019    if (discardedIndexes.size === 0) return;
2020
2021    discardedIndexes.forEach(index => {
2022      const representativePair = this.similarity_array.find(pair => pair.indexA === index || pair.indexB === index);
2023      if (!representativePair) return;
2024
2025      this.similarity_pairs_action_discarded.push({
2026        discardedIndex: index,
2027        discardedType: representativePair.indexA === index ? "A" : "B",
2028        pair: { ...representativePair }
2029      });
2030      this.discarded_similarity_indexes.add(index);
2031    });
2032
2033    const discardedPairs = this.similarity_array.filter(pair =>
2034      discardedIndexes.has(pair.indexA) || discardedIndexes.has(pair.indexB)
2035    );
2036    const discardedPairKeys = new Set(this.similarity_array_discarded.map(pair => `${pair.indexA}:${pair.indexB}`));
2037    discardedPairs.forEach(pair => {
2038      const key = `${pair.indexA}:${pair.indexB}`;
2039      if (!discardedPairKeys.has(key)) {
2040        this.similarity_array_discarded.push(pair);
2041        discardedPairKeys.add(key);
2042      }
2043    });
2044    this.similarity_array = this.similarity_array.filter(pair =>
2045      !discardedIndexes.has(pair.indexA) && !discardedIndexes.has(pair.indexB)
2046    );
2047    this.currentSimIndex = Math.min(this.currentSimIndex, Math.max(this.similarity_array.length - 1, 0));
2048  }
2049
2050  restore_similarity_action() {
2051    if (!this.similarity_pairs_action_discarded || this.similarity_pairs_action_discarded.length === 0) return null;
2052
2053    const discardedEntry = this.similarity_pairs_action_discarded[this.currentDiscardedIndex];
2054    if (!discardedEntry) return null;
2055
2056    const discardedIndex = discardedEntry.discardedIndex;
2057    this.discarded_similarity_indexes.delete(discardedIndex);
2058    const pairsToRestore = this.similarity_array_discarded.filter(pair =>
2059      (pair.indexA === discardedIndex || pair.indexB === discardedIndex) &&
2060      !this.discarded_similarity_indexes.has(pair.indexA) &&
2061      !this.discarded_similarity_indexes.has(pair.indexB)
2062    );
2063    const activePairKeys = new Set(this.similarity_array.map(pair => `${pair.indexA}:${pair.indexB}`));
2064    pairsToRestore.forEach(pair => {
2065      const key = `${pair.indexA}:${pair.indexB}`;
2066      if (!activePairKeys.has(key)) {
2067        this.similarity_array.push(pair);
2068        activePairKeys.add(key);
2069      }
2070    });
2071
2072    const restoredPairKeys = new Set(pairsToRestore.map(pair => `${pair.indexA}:${pair.indexB}`));
2073    this.similarity_array_discarded = this.similarity_array_discarded.filter(pair =>
2074      !restoredPairKeys.has(`${pair.indexA}:${pair.indexB}`)
2075    );
2076    this.similarity_pairs_action_discarded.splice(this.currentDiscardedIndex, 1);
2077    this.currentDiscardedIndex = Math.min(
2078      this.currentDiscardedIndex,
2079      Math.max(this.similarity_pairs_action_discarded.length - 1, 0)
2080    );
2081    return discardedEntry;
2082  }
2083
2084
2085  remove_similarity_pairs_with_index(type) {
2086    if (!this.similarity_array || this.similarity_array.length === 0) return;
2087
2088    // --- Identify the index being discarded ---
2089    const currentPair = this.similarity_array[similarity_data_active.currentSimIndex];
2090    let index;
2091    if (type === "A") {
2092      index = currentPair.indexA;
2093    } else if (type === "B") {
2094      index = currentPair.indexB;
2095    } else {
2096      return; // Invalid type
2097    }
2098
2099    // --- 1️⃣ Store the specific pair and which option was discarded ---
2100    this.similarity_pairs_action_discarded.push({
2101      discardedIndex: index,
2102      discardedType: type,
2103      pair: { ...currentPair}
2104    });
2105
2106    // --- 2️⃣ Identify and store all pairs that will be discarded as a result ---
2107    const discardedPairs = this.similarity_array.filter(pair =>
2108      pair.indexA === index || pair.indexB === index
2109    );
2110
2111    this.similarity_array_discarded.push(...discardedPairs);
2112
2113    // --- Filter out the removed pairs from active list ---
2114    this.similarity_array = this.similarity_array.filter(pair =>
2115      pair.indexA !== index && pair.indexB !== index
2116    );
2117
2118    // --- Adjust currentSimIndex safely ---
2119    if (this.similarity_array.length > 0) {
2120      if (this.currentSimIndex >= this.similarity_array.length) {
2121        this.currentSimIndex = this.similarity_array.length - 1;
2122      }
2123    } else {
2124      this.currentSimIndex = 0; // reset safely if all are removed
2125    }
2126  }
2127
2128
2129  add_similarity_pairs_with_index() {
2130    if (!this.similarity_pairs_action_discarded || this.similarity_pairs_action_discarded.length === 0) return;
2131
2132    const discardedEntry = similarity_data_active.similarity_pairs_action_discarded[similarity_data_active.currentDiscardedIndex];
2133    if (!discardedEntry) return;
2134
2135    const discardedIndex = discardedEntry.discardedIndex;
2136
2137    // 1 Find all discarded pairs related to this index
2138    const pairsToRestore = this.similarity_array_discarded.filter(pair =>
2139      pair.indexA === discardedIndex || pair.indexB === discardedIndex
2140    );
2141
2142    if (pairsToRestore.length === 0) return;
2143
2144    // 2 Add them back to the active similarity array
2145    this.similarity_array.push(...pairsToRestore);
2146
2147    // 3 Remove those restored pairs from discarded list
2148    this.similarity_array_discarded = this.similarity_array_discarded.filter(pair =>
2149      pair.indexA !== discardedIndex && pair.indexB !== discardedIndex
2150    );
2151
2152    // 4 Remove the entry from action_discarded
2153    this.similarity_pairs_action_discarded.splice(similarity_data_active.currentDiscardedIndex, 1);
2154
2155    // 5 Adjust index safely
2156    if (this.similarity_pairs_action_discarded.length > 0) {
2157      if (similarity_data_active.currentDiscardedIndex >= this.similarity_pairs_action_discarded.length) {
2158        similarity_data_active.currentDiscardedIndex = this.similarity_pairs_action_discarded.length - 1;
2159      }
2160    } else {
2161      similarity_data_active.currentDiscardedIndex = 0;
2162    }
2163  }
2164
2165
2166
2167
2168
2169
2170
2171    cleanup() {
2172      // Terminate main worker safely
2173      if (this.worker instanceof Worker) {
2174        this.worker.terminate();
2175        this.worker = null;
2176      }
2177
2178      // Terminate transformers worker safely
2179      if (this.worker_sentence_transformers instanceof Worker) {
2180        this.worker_sentence_transformers.terminate();
2181        this.worker_sentence_transformers = null;
2182      }
2183
2184        // Clear training and prediction data
2185        this.training_data = [];
2186        this.prediction_data = [];
2187
2188        //Clear similarity array.
2189        this.similarity_array = [];
2190        this.similarity_summary = null;
2191        this.similarity_pairs_action_discarded = [];
2192        this.similarity_array_discarded = [];
2193        this.discarded_similarity_indexes = new Set();
2194
2195
2196        // Clear vector data and names
2197        this.vectors = [];
2198        this.vector_names = [];
2199        this.vector_config = [];
2200
2201        // Reset probability threshold
2202        this.proba_threshold = 0;
2203
2204        // Clear metrics and ROC data
2205        this.metrics = [];
2206        this.metrics_stats = [];
2207        this.auc_roc = [];
2208        this.auc_roc_stats = [];
2209        this.roc_curves = [];
2210        this.roc_curve_mean = [];
2211
2212        // Clear document index data
2213        this.data_array = [];
2214        this._clear_cached_entry_fields();
2215        this.training_data = {
2216            prediction_mask: [],
2217            y_unlabelled: [],
2218            y: [],
2219            ensemble_cv1_ypred_best_stacked: []
2220        };
2221    }
2222
2223
2224  };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.