maidr 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/maidr.js +116 -104
- package/dist/maidr.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# maidr: Multimodal Access and Interactive Data Representation
|
|
10
10
|
|
|
11
|
-
maidr (pronounced as 'mader') is a system for non-visual access and control of statistical plots. It aims to provide an inclusive experience for users with visual impairments by offering multiple modes of interaction: braille, text, and sonification (BTS). This comprehensive approach enhances the accessibility of data visualization and encourages a multi-modal exploration on visualization. Check out the current build: [maidr Demo](https://xability.github.io/maidr/
|
|
11
|
+
maidr (pronounced as 'mader') is a system for non-visual access and control of statistical plots. It aims to provide an inclusive experience for users with visual impairments by offering multiple modes of interaction: braille, text, and sonification (BTS). This comprehensive approach enhances the accessibility of data visualization and encourages a multi-modal exploration on visualization. Check out the current build: [maidr Demo](https://xability.github.io/maidr/galleries/index.html). You may also clone or download the GitHub repo, navigate to the ./user_study_pilot folder, and open any of the html files in your browser.
|
|
12
12
|
|
|
13
13
|
## Table of Contents
|
|
14
14
|
|
package/dist/maidr.js
CHANGED
|
@@ -76,6 +76,26 @@ class Constants {
|
|
|
76
76
|
globalMinMax = true;
|
|
77
77
|
ariaMode = 'assertive'; // assertive (default) / polite
|
|
78
78
|
|
|
79
|
+
userSettingsKeys = [
|
|
80
|
+
'vol',
|
|
81
|
+
'autoPlayRate',
|
|
82
|
+
'brailleDisplayLength',
|
|
83
|
+
'colorSelected',
|
|
84
|
+
'MIN_FREQUENCY',
|
|
85
|
+
'MAX_FREQUENCY',
|
|
86
|
+
'keypressInterval',
|
|
87
|
+
'ariaMode',
|
|
88
|
+
'openAIAuthKey',
|
|
89
|
+
'geminiAuthKey',
|
|
90
|
+
'skillLevel',
|
|
91
|
+
'skillLevelOther',
|
|
92
|
+
'LLMModel',
|
|
93
|
+
'LLMPreferences',
|
|
94
|
+
'LLMOpenAiMulti',
|
|
95
|
+
'LLMGeminiMulti',
|
|
96
|
+
'autoInitLLM',
|
|
97
|
+
];
|
|
98
|
+
|
|
79
99
|
// LLM settings
|
|
80
100
|
openAIAuthKey = null; // OpenAI authentication key, set in menu
|
|
81
101
|
geminiAuthKey = null; // Gemini authentication key, set in menu
|
|
@@ -389,7 +409,9 @@ class Menu {
|
|
|
389
409
|
<tr>
|
|
390
410
|
<td>Open GenAI Chat</td>
|
|
391
411
|
<td>${
|
|
392
|
-
constants.
|
|
412
|
+
constants.isMac
|
|
413
|
+
? constants.alt
|
|
414
|
+
: constants.control
|
|
393
415
|
} + Shift + ?</td>
|
|
394
416
|
</tr>
|
|
395
417
|
<tr>
|
|
@@ -454,12 +476,12 @@ class Menu {
|
|
|
454
476
|
<option value="basic">Basic</option>
|
|
455
477
|
<option value="intermediate">Intermediate</option>
|
|
456
478
|
<option value="expert">Expert</option>
|
|
457
|
-
<option value="other">
|
|
479
|
+
<option value="other">Other: describe in your own words</option>
|
|
458
480
|
</select>
|
|
459
481
|
<label for="skill_level">Level of skill in statistical charts</label>
|
|
460
482
|
</p>
|
|
461
483
|
<p id="skill_level_other_container" class="hidden"><input type="text" placeholder="Very basic" id="skill_level_other"> <label for="skill_level_other">Describe your level of skill in statistical charts</label></p>
|
|
462
|
-
<p><label for="LLM_preferences">
|
|
484
|
+
<p><label for="LLM_preferences">Custom instructions for the chat response</label></p>
|
|
463
485
|
<p><textarea id="LLM_preferences" rows="4" cols="50" placeholder="I'm a stats undergrad and work with Python. I prefer a casual tone, and favor information accuracy over creative description; just the facts please!"></textarea></p>
|
|
464
486
|
</div>
|
|
465
487
|
</div>
|
|
@@ -605,13 +627,22 @@ class Menu {
|
|
|
605
627
|
|
|
606
628
|
// trigger notification that LLM will be reset
|
|
607
629
|
// this is done on change of LLM model, multi settings, or skill level
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
'
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
]
|
|
630
|
+
let LLMResetIds = [
|
|
631
|
+
'LLM_model',
|
|
632
|
+
'openai_multi',
|
|
633
|
+
'gemini_multi',
|
|
634
|
+
'skill_level',
|
|
635
|
+
'LLM_preferences',
|
|
636
|
+
];
|
|
637
|
+
for (let i = 0; i < LLMResetIds.length; i++) {
|
|
638
|
+
constants.events.push([
|
|
639
|
+
document.getElementById(LLMResetIds[i]),
|
|
640
|
+
'change',
|
|
641
|
+
function (e) {
|
|
642
|
+
menu.NotifyOfLLMReset();
|
|
643
|
+
},
|
|
644
|
+
]);
|
|
645
|
+
}
|
|
615
646
|
}
|
|
616
647
|
|
|
617
648
|
/**
|
|
@@ -855,6 +886,13 @@ class Menu {
|
|
|
855
886
|
) {
|
|
856
887
|
shouldReset = true;
|
|
857
888
|
}
|
|
889
|
+
if (
|
|
890
|
+
!shouldReset &&
|
|
891
|
+
constants.LLMPreferences !=
|
|
892
|
+
document.getElementById('LLM_preferences').value
|
|
893
|
+
) {
|
|
894
|
+
shouldReset = true;
|
|
895
|
+
}
|
|
858
896
|
if (
|
|
859
897
|
!shouldReset &&
|
|
860
898
|
constants.LLMModel != document.getElementById('LLM_model').value
|
|
@@ -882,24 +920,21 @@ class Menu {
|
|
|
882
920
|
*/
|
|
883
921
|
SaveDataToLocalStorage() {
|
|
884
922
|
let data = {};
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
data.MIN_FREQUENCY = constants.MIN_FREQUENCY;
|
|
890
|
-
data.MAX_FREQUENCY = constants.MAX_FREQUENCY;
|
|
891
|
-
data.keypressInterval = constants.keypressInterval;
|
|
892
|
-
data.ariaMode = constants.ariaMode;
|
|
893
|
-
data.openAIAuthKey = constants.openAIAuthKey;
|
|
894
|
-
data.geminiAuthKey = constants.geminiAuthKey;
|
|
895
|
-
data.skillLevel = constants.skillLevel;
|
|
896
|
-
data.skillLevelOther = constants.skillLevelOther;
|
|
897
|
-
data.LLMModel = constants.LLMModel;
|
|
898
|
-
data.LLMPreferences = constants.LLMPreferences;
|
|
899
|
-
data.LLMOpenAiMulti = constants.LLMOpenAiMulti;
|
|
900
|
-
data.LLMGeminiMulti = constants.LLMGeminiMulti;
|
|
901
|
-
data.autoInitLLM = constants.autoInitLLM;
|
|
923
|
+
for (let i = 0; i < constants.userSettingsKeys.length; i++) {
|
|
924
|
+
data[constants.userSettingsKeys[i]] =
|
|
925
|
+
constants[constants.userSettingsKeys[i]];
|
|
926
|
+
}
|
|
902
927
|
localStorage.setItem('settings_data', JSON.stringify(data));
|
|
928
|
+
|
|
929
|
+
// also save to tracking if we're doing that
|
|
930
|
+
if (constants.isTracking) {
|
|
931
|
+
// but not auth keys
|
|
932
|
+
data.openAIAuthKey = 'hidden';
|
|
933
|
+
data.geminiAuthKey = 'hidden';
|
|
934
|
+
// and need a timestamp
|
|
935
|
+
data.timestamp = new Date().toISOString();
|
|
936
|
+
tracker.SetData('settings', data);
|
|
937
|
+
}
|
|
903
938
|
}
|
|
904
939
|
/**
|
|
905
940
|
* Loads data from local storage and updates the constants object with the retrieved values, to be loaded into the menu
|
|
@@ -907,23 +942,10 @@ class Menu {
|
|
|
907
942
|
LoadDataFromLocalStorage() {
|
|
908
943
|
let data = JSON.parse(localStorage.getItem('settings_data'));
|
|
909
944
|
if (data) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
constants.MIN_FREQUENCY = data.MIN_FREQUENCY;
|
|
915
|
-
constants.MAX_FREQUENCY = data.MAX_FREQUENCY;
|
|
916
|
-
constants.keypressInterval = data.keypressInterval;
|
|
917
|
-
constants.ariaMode = data.ariaMode;
|
|
918
|
-
constants.openAIAuthKey = data.openAIAuthKey;
|
|
919
|
-
constants.geminiAuthKey = data.geminiAuthKey;
|
|
920
|
-
constants.skillLevel = data.skillLevel;
|
|
921
|
-
constants.skillLevelOther = data.skillLevelOther;
|
|
922
|
-
constants.LLMModel = data.LLMModel ? data.LLMModel : constants.LLMModel;
|
|
923
|
-
constants.LLMPreferences = data.LLMPreferences;
|
|
924
|
-
constants.LLMOpenAiMulti = data.LLMOpenAiMulti;
|
|
925
|
-
constants.LLMGeminiMulti = data.LLMGeminiMulti;
|
|
926
|
-
constants.autoInitLLM = data.autoInitLLM;
|
|
945
|
+
for (let i = 0; i < constants.userSettingsKeys.length; i++) {
|
|
946
|
+
constants[constants.userSettingsKeys[i]] =
|
|
947
|
+
data[constants.userSettingsKeys[i]];
|
|
948
|
+
}
|
|
927
949
|
}
|
|
928
950
|
this.PopulateData();
|
|
929
951
|
this.UpdateHtml();
|
|
@@ -944,7 +966,16 @@ class ChatLLM {
|
|
|
944
966
|
this.CreateComponent();
|
|
945
967
|
this.SetEvents();
|
|
946
968
|
if (constants.autoInitLLM) {
|
|
947
|
-
|
|
969
|
+
// only run if we have API keys set
|
|
970
|
+
if (
|
|
971
|
+
(constants.LLMModel == 'openai' && constants.openAIAuthKey) ||
|
|
972
|
+
(constants.LLMModel == 'gemini' && constants.geminiAuthKey) ||
|
|
973
|
+
(constants.LLMModel == 'multi' &&
|
|
974
|
+
constants.openAIAuthKey &&
|
|
975
|
+
constants.geminiAuthKey)
|
|
976
|
+
) {
|
|
977
|
+
this.InitChatMessage();
|
|
978
|
+
}
|
|
948
979
|
}
|
|
949
980
|
}
|
|
950
981
|
|
|
@@ -975,11 +1006,10 @@ class ChatLLM {
|
|
|
975
1006
|
<p><button type="button">What is the title?</button></p>
|
|
976
1007
|
<p><button type="button">What are the high and low values?</button></p>
|
|
977
1008
|
<p><button type="button">What is the general shape of the chart?</button></p>
|
|
978
|
-
<p><button type="button" id="more_suggestions">More</button></p>
|
|
979
1009
|
</div>
|
|
980
|
-
<div id="more_suggestions_container" class="
|
|
1010
|
+
<div id="more_suggestions_container" class="LLM_suggestions">
|
|
981
1011
|
<p><button type="button">Please provide the title of this visualization, then provide a description for someone who is blind or low vision. Include general overview of axes and the data at a high-level.</button></p>
|
|
982
|
-
<p><button type="button">For the visualization I shared, please provide the following (where applicable): mean, standard deviation,
|
|
1012
|
+
<p><button type="button">For the visualization I shared, please provide the following (where applicable): mean, standard deviation, extreme, correlations, relational comparisons like greater than OR lesser than.</button></p>
|
|
983
1013
|
<p><button type="button">Based on the visualization shared, address the following: Do you observe any unforeseen trends? If yes, what? Please convey any complex multi-faceted patterns present. Can you identify any noteworthy exceptions that aren't readily apparent through non-visual methods of analysis?</button></p>
|
|
984
1014
|
<p><button type="button">Provide context to help explain the data depicted in this visualization based on domain-specific insight.</button></p>
|
|
985
1015
|
</div>
|
|
@@ -1029,12 +1059,7 @@ class ChatLLM {
|
|
|
1029
1059
|
document,
|
|
1030
1060
|
'keyup',
|
|
1031
1061
|
function (e) {
|
|
1032
|
-
if (
|
|
1033
|
-
((e.ctrlKey || e.metaKey) &&
|
|
1034
|
-
e.shiftKey &&
|
|
1035
|
-
(e.key == '?' || e.key == '¿')) ||
|
|
1036
|
-
(e.metaKey && e.altKey && (e.key == '?' || e.key == '¿'))
|
|
1037
|
-
) {
|
|
1062
|
+
if ((e.key == '?' && (e.ctrlKey || e.metaKey)) || e.key == '¿') {
|
|
1038
1063
|
chatLLM.Toggle();
|
|
1039
1064
|
}
|
|
1040
1065
|
},
|
|
@@ -1063,21 +1088,6 @@ class ChatLLM {
|
|
|
1063
1088
|
]);
|
|
1064
1089
|
|
|
1065
1090
|
// ChatLLM suggestion events
|
|
1066
|
-
// the more button
|
|
1067
|
-
constants.events.push([
|
|
1068
|
-
document.getElementById('more_suggestions'),
|
|
1069
|
-
'click',
|
|
1070
|
-
function (e) {
|
|
1071
|
-
document
|
|
1072
|
-
.getElementById('more_suggestions_container')
|
|
1073
|
-
.classList.toggle('hidden');
|
|
1074
|
-
// focus on button right after the more button
|
|
1075
|
-
document
|
|
1076
|
-
.querySelector('#more_suggestions_container > p > button')
|
|
1077
|
-
.focus();
|
|
1078
|
-
document.getElementById('more_suggestions').remove();
|
|
1079
|
-
},
|
|
1080
|
-
]);
|
|
1081
1091
|
// actual suggestions:
|
|
1082
1092
|
let suggestions = document.querySelectorAll(
|
|
1083
1093
|
'#chatLLM .LLM_suggestions button:not(#more_suggestions)'
|
|
@@ -1188,7 +1198,7 @@ class ChatLLM {
|
|
|
1188
1198
|
markdown = markdown.replace(/\n{3,}/g, '\n\n');
|
|
1189
1199
|
|
|
1190
1200
|
try {
|
|
1191
|
-
navigator.clipboard.writeText(markdown);
|
|
1201
|
+
navigator.clipboard.writeText(markdown); // note: this fails if you're on the inspector. That's fine as it'll never happen to real users
|
|
1192
1202
|
} catch (err) {
|
|
1193
1203
|
console.error('Failed to copy: ', err);
|
|
1194
1204
|
}
|
|
@@ -1358,6 +1368,7 @@ class ChatLLM {
|
|
|
1358
1368
|
|
|
1359
1369
|
if (data.error) {
|
|
1360
1370
|
chatLLM.DisplayChatMessage(LLMName, 'Error processing request.', true);
|
|
1371
|
+
chatLLM.WaitingSound(false);
|
|
1361
1372
|
} else {
|
|
1362
1373
|
chatLLM.DisplayChatMessage(LLMName, text);
|
|
1363
1374
|
}
|
|
@@ -1368,10 +1379,12 @@ class ChatLLM {
|
|
|
1368
1379
|
} else {
|
|
1369
1380
|
if (!data.error) {
|
|
1370
1381
|
data.error = 'Error processing request.';
|
|
1382
|
+
chatLLM.WaitingSound(false);
|
|
1371
1383
|
}
|
|
1372
1384
|
}
|
|
1373
1385
|
if (data.error) {
|
|
1374
1386
|
chatLLM.DisplayChatMessage(LLMName, 'Error processing request.', true);
|
|
1387
|
+
chatLLM.WaitingSound(false);
|
|
1375
1388
|
} else {
|
|
1376
1389
|
// todo: display actual response
|
|
1377
1390
|
}
|
|
@@ -1472,7 +1485,7 @@ class ChatLLM {
|
|
|
1472
1485
|
.catch((error) => {
|
|
1473
1486
|
chatLLM.WaitingSound(false);
|
|
1474
1487
|
console.error('Error:', error);
|
|
1475
|
-
chatLLM.DisplayChatMessage(
|
|
1488
|
+
chatLLM.DisplayChatMessage('OpenAI', 'Error processing request.', true);
|
|
1476
1489
|
// also todo: handle errors somehow
|
|
1477
1490
|
});
|
|
1478
1491
|
}
|
|
@@ -1562,6 +1575,8 @@ class ChatLLM {
|
|
|
1562
1575
|
// Process the response
|
|
1563
1576
|
chatLLM.ProcessLLMResponse(result.response, 'gemini');
|
|
1564
1577
|
} catch (error) {
|
|
1578
|
+
chatLLM.WaitingSound(false);
|
|
1579
|
+
chatLLM.DisplayChatMessage('Gemini', 'Error processing request.', true);
|
|
1565
1580
|
console.error('Error in GeminiPrompt:', error);
|
|
1566
1581
|
throw error; // Rethrow the error for further handling if necessary
|
|
1567
1582
|
}
|
|
@@ -1623,11 +1638,6 @@ class ChatLLM {
|
|
|
1623
1638
|
ResetLLM() {
|
|
1624
1639
|
// clear the main chat history
|
|
1625
1640
|
document.getElementById('chatLLM_chat_history').innerHTML = '';
|
|
1626
|
-
// unhide the more button
|
|
1627
|
-
document
|
|
1628
|
-
.getElementById('more_suggestions_container')
|
|
1629
|
-
.classList.add('hidden');
|
|
1630
|
-
document.getElementById('more_suggestions').classList.remove('hidden');
|
|
1631
1641
|
|
|
1632
1642
|
// reset the data
|
|
1633
1643
|
this.requestJson = null;
|
|
@@ -2081,7 +2091,7 @@ class Tracker {
|
|
|
2081
2091
|
DataSetup() {
|
|
2082
2092
|
let prevData = this.GetTrackerData();
|
|
2083
2093
|
if (prevData) {
|
|
2084
|
-
// good to go already, do nothing
|
|
2094
|
+
// good to go already, do nothing, but make sure we have our containers
|
|
2085
2095
|
} else {
|
|
2086
2096
|
let data = {};
|
|
2087
2097
|
data.userAgent = Object.assign(navigator.userAgent);
|
|
@@ -2089,8 +2099,10 @@ class Tracker {
|
|
|
2089
2099
|
data.language = Object.assign(navigator.language);
|
|
2090
2100
|
data.platform = Object.assign(navigator.platform);
|
|
2091
2101
|
data.events = [];
|
|
2102
|
+
data.settings = [];
|
|
2092
2103
|
|
|
2093
2104
|
this.SaveTrackerData(data);
|
|
2105
|
+
this.SaveSettings();
|
|
2094
2106
|
}
|
|
2095
2107
|
}
|
|
2096
2108
|
|
|
@@ -2137,6 +2149,17 @@ class Tracker {
|
|
|
2137
2149
|
this.DataSetup();
|
|
2138
2150
|
}
|
|
2139
2151
|
|
|
2152
|
+
SaveSettings() {
|
|
2153
|
+
// fetch all settings, push to data.settings
|
|
2154
|
+
let settings = JSON.parse(localStorage.getItem('settings_data'));
|
|
2155
|
+
if (settings) {
|
|
2156
|
+
// don't store their auth keys
|
|
2157
|
+
settings.openAIAuthKey = 'hidden';
|
|
2158
|
+
settings.geminiAuthKey = 'hidden';
|
|
2159
|
+
this.SetData('settings', settings);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2140
2163
|
/**
|
|
2141
2164
|
* Logs an event with various properties to the tracker data.
|
|
2142
2165
|
* @param {Event} e - The event to log.
|
|
@@ -2280,6 +2303,7 @@ class Tracker {
|
|
|
2280
2303
|
constants.plotOrientation == 'vert' ? position.x : position.y;
|
|
2281
2304
|
let sectionPos =
|
|
2282
2305
|
constants.plotOrientation == 'vert' ? position.y : position.x;
|
|
2306
|
+
let sectionLabel = plot.sections[sectionPos];
|
|
2283
2307
|
|
|
2284
2308
|
if (!this.isUndefinedOrNull(plot.x_group_label)) {
|
|
2285
2309
|
x_label = plot.x_group_label;
|
|
@@ -2289,42 +2313,26 @@ class Tracker {
|
|
|
2289
2313
|
}
|
|
2290
2314
|
if (constants.plotOrientation == 'vert') {
|
|
2291
2315
|
if (plotPos > -1 && sectionPos > -1) {
|
|
2292
|
-
if (
|
|
2293
|
-
|
|
2294
|
-
) {
|
|
2295
|
-
y_tickmark = plot.plotData[plotPos][sectionPos].label;
|
|
2316
|
+
if (!this.isUndefinedOrNull(sectionLabel)) {
|
|
2317
|
+
y_tickmark = sectionLabel;
|
|
2296
2318
|
}
|
|
2297
2319
|
if (!this.isUndefinedOrNull(plot.x_labels[position.x])) {
|
|
2298
2320
|
x_tickmark = plot.x_labels[position.x];
|
|
2299
2321
|
}
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
) {
|
|
2303
|
-
value = plot.plotData[plotPos][sectionPos].values;
|
|
2304
|
-
} else if (
|
|
2305
|
-
!this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].y)
|
|
2306
|
-
) {
|
|
2307
|
-
value = plot.plotData[plotPos][sectionPos].y;
|
|
2322
|
+
if (!this.isUndefinedOrNull(plot.plotData[plotPos][sectionLabel])) {
|
|
2323
|
+
value = plot.plotData[plotPos][sectionLabel];
|
|
2308
2324
|
}
|
|
2309
2325
|
}
|
|
2310
2326
|
} else {
|
|
2311
2327
|
if (plotPos > -1 && sectionPos > -1) {
|
|
2312
|
-
if (
|
|
2313
|
-
|
|
2314
|
-
) {
|
|
2315
|
-
x_tickmark = plot.plotData[plotPos][sectionPos].label;
|
|
2328
|
+
if (!this.isUndefinedOrNull(sectionLabel)) {
|
|
2329
|
+
x_tickmark = sectionLabel;
|
|
2316
2330
|
}
|
|
2317
2331
|
if (!this.isUndefinedOrNull(plot.y_labels[position.y])) {
|
|
2318
2332
|
y_tickmark = plot.y_labels[position.y];
|
|
2319
2333
|
}
|
|
2320
|
-
if (
|
|
2321
|
-
|
|
2322
|
-
) {
|
|
2323
|
-
value = plot.plotData[plotPos][sectionPos].values;
|
|
2324
|
-
} else if (
|
|
2325
|
-
!this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].x)
|
|
2326
|
-
) {
|
|
2327
|
-
value = plot.plotData[plotPos][sectionPos].x;
|
|
2334
|
+
if (!this.isUndefinedOrNull(plot.plotData[plotPos][sectionLabel])) {
|
|
2335
|
+
value = plot.plotData[plotPos][sectionLabel];
|
|
2328
2336
|
}
|
|
2329
2337
|
}
|
|
2330
2338
|
}
|
|
@@ -2361,10 +2369,14 @@ class Tracker {
|
|
|
2361
2369
|
|
|
2362
2370
|
SetData(key, value) {
|
|
2363
2371
|
let data = this.GetTrackerData();
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
} else {
|
|
2372
|
+
let arrayKeys = ['events', 'ChatHistory', 'settings'];
|
|
2373
|
+
if (!arrayKeys.includes(key)) {
|
|
2367
2374
|
data[key] = value;
|
|
2375
|
+
} else {
|
|
2376
|
+
if (!data[key]) {
|
|
2377
|
+
data[key] = [];
|
|
2378
|
+
}
|
|
2379
|
+
data[key].push(value);
|
|
2368
2380
|
}
|
|
2369
2381
|
this.SaveTrackerData(data);
|
|
2370
2382
|
}
|