maidr 2.3.1 → 2.5.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 CHANGED
@@ -13,11 +13,15 @@ maidr (pronounced as 'mader') is a system for non-visual access and control of s
13
13
  ## Table of Contents
14
14
 
15
15
  1. [Usage](#usage)
16
- 2. [Controls](#controls)
17
- 3. [Braille Generation](#braille-generation)
18
- 4. [License](#license)
19
- 5. [Contact](#contact)
20
- 6. [Acknowledgments](#acknowledgments)
16
+ 1. [Controls](#controls)
17
+ 1. [Braille Generation](#braille-generation)
18
+ 1. [API](#api)
19
+ 1. [Binders](#binders)
20
+ 1. [Papers](#papers)
21
+ 1. [License](#license)
22
+ 1. [Contact](#contact)
23
+ 1. [Acknowledgments](#acknowledgments)
24
+
21
25
 
22
26
  ## Usage
23
27
 
@@ -47,7 +51,7 @@ To use maidr, follow these steps:
47
51
  </html>
48
52
  ```
49
53
 
50
- 3. Add your data: Include your data as a json schema directly in the HTML file. There should be a single `maidr` object with the following properties, or an array of objects if multiple charts exist on the page. Your json schema may look like so: (values for demonstration purposes)
54
+ 3. Add your data: Include your data as a json schema directly in the HTML file. There should be a single `maidr` object with the following properties, or an array of objects if multiple plots exist on the page. Your json schema may look like so: (values for demonstration purposes)
51
55
 
52
56
  ```javascript
53
57
  // a single plot
@@ -74,7 +78,7 @@ To use maidr, follow these steps:
74
78
  data: ...
75
79
  }
76
80
 
77
- // or, multiple charts
81
+ // or, multiple plots
78
82
  let maidr = [
79
83
  {
80
84
  type: 'box',
@@ -220,7 +224,7 @@ For more information and examples, refer to the example HTML files provided in t
220
224
 
221
225
  ## Controls
222
226
 
223
- To interact with the charts using maidr, follow these steps:
227
+ To interact with the plots using maidr, follow these steps:
224
228
 
225
229
  1. Press the **Tab** key to focus on the SVG element.
226
230
  2. Use the **arrow keys** to move around the plot.
@@ -355,6 +359,53 @@ In the braille representation of segmented bar plots, braille depends on where y
355
359
 
356
360
  In the Braille representation of a lineplot, braille is nearly identical to the above barplot: data values are encoded as Braille characters based on their relative magnitude within the plot. Low values are denoted by Braille characters that have dots only along the bottom, while high values are indicated by characters that have dots higher up.
357
361
 
362
+ ## API
363
+
364
+ maidr is available via a restful API. Learn more about the usage at [maidr-api](https://github.com/xability/maidr-api) repo.
365
+
366
+ ## Binders
367
+
368
+ We currently provide the following binders, all of which can be found at each separate repo:
369
+
370
+ - [x] Python binder for matplotlib and seaborn: [py_maidr](https://github.com/xability/py_maidr).
371
+
372
+ - [ ] R binder for ggplot2: [r_maidr](https://github.com/xability/r_maidr).
373
+
374
+ ## Papers
375
+
376
+ To learn more about the theoretical background and user study results, we recommend you read and cite the following papers.
377
+
378
+ 1. [MAIDR: Making Statistical Visualizations Accessible with Multimodal Data Representation](https://arxiv.org/abs/2403.00717):
379
+
380
+ ```tex
381
+ @inproceedings{seoMAIDR2024,
382
+ title = {{{MAIDR}}: Making Statistical Visualizations Accessible with Multimodal Data Representation},
383
+ booktitle = {Proceedings of the {{SIGCHI Conference}} on {{Human Factors}} in {{Computing Systems}}},
384
+ author = {Seo, JooYoung and Xia, Yilin and Lee, Bongshin and McCurry, Sean and Yam, Yu Jun},
385
+ year = {2024},
386
+ doi = {10.1145/3613904.3642730}
387
+ }
388
+ ```
389
+
390
+ 1. [Born Accessible Data Science and Visualization Courses: Challenges of Developing Curriculum to be Taught by Blind Instructors to Blind Students](https://arxiv.org/abs/2403.02568v1):
391
+
392
+ ```tex
393
+ @misc{seoBornAccessibleData2024,
394
+ title = {Born {{Accessible Data Science}} and {{Visualization Courses}}: {{Challenges}} of {{Developing Curriculum}} to Be {{Taught}} by {{Blind Instructors}} to {{Blind Students}}},
395
+ shorttitle = {Born {{Accessible Data Science}} and {{Visualization Courses}}},
396
+ author = {Seo, JooYoung and O'Modhrain, Sile and Xia, Yilin and Kamath, Sanchita and Lee, Bongshin and Coughlan, James M.},
397
+ year = {2024},
398
+ month = mar,
399
+ number = {arXiv:2403.02568},
400
+ eprint = {2403.02568},
401
+ primaryclass = {cs},
402
+ publisher = {{arXiv}},
403
+ urldate = {2024-03-08},
404
+ archiveprefix = {arxiv},
405
+ keywords = {Computer Science - Human-Computer Interaction}
406
+ }
407
+ ```
408
+
358
409
  ## License
359
410
 
360
411
  This project is licensed under the GPL 3 License.
package/dist/maidr.js CHANGED
@@ -86,6 +86,7 @@ class Constants {
86
86
  'You are a helpful assistant describing the chart to a blind person. ';
87
87
  skillLevel = 'basic'; // basic / intermediate / expert
88
88
  skillLevelOther = ''; // custom skill level
89
+ autoInitLLM = true; // auto initialize LLM on page load
89
90
 
90
91
  // user controls (not exposed to menu, with shortcuts usually)
91
92
  showDisplay = 1; // true / false
@@ -273,6 +274,7 @@ class Resources {
273
274
  openai: 'OpenAI Vision',
274
275
  gemini: 'Gemini Pro Vision',
275
276
  multi: 'Multiple AI',
277
+ processing: 'Processing Chart...',
276
278
  },
277
279
  },
278
280
  };
@@ -428,6 +430,9 @@ class Menu {
428
430
  <span id="gemini_multi_container" class="hidden"><input type="checkbox" id="gemini_multi" name="gemini_multi" aria-label="Use Gemini in Multi modal mode"></span>
429
431
  <input type="password" id="gemini_auth_key"><button aria-label="Delete Gemini key" title="Delete Gemini key" id="delete_gemini_key" class="invis_button">&times;</button><label for="gemini_auth_key">Gemini Authentication Key</label>
430
432
  </p>
433
+ <p><input type="checkbox" ${
434
+ constants.autoInitLLM ? 'checked' : ''
435
+ } id="init_llm_on_load" name="init_llm_on_load"><label for="init_llm_on_load">Start first LLM chat chart load</label></p>
431
436
  <p>
432
437
  <select id="skill_level">
433
438
  <option value="basic">Basic</option>
@@ -443,7 +448,7 @@ class Menu {
443
448
  </div>
444
449
  </div>
445
450
  <div class="modal-footer">
446
- <button type="button" id="save_and_close_menu">Save and Close</button>
451
+ <button type="button" id="save_and_close_menu" aria-labelledby="save_and_close_text"><span id="save_and_close_text">Save and Close</span></button>
447
452
  <button type="button" id="close_menu">Close</button>
448
453
  </div>
449
454
  </div>
@@ -579,6 +584,16 @@ class Menu {
579
584
  }
580
585
  },
581
586
  ]);
587
+
588
+ // trigger notification that LLM will be reset
589
+ // this is done on change of LLM model, multi settings, or skill level
590
+ constants.events.push([
591
+ document.getElementById('LLM_model'),
592
+ 'change',
593
+ function (e) {
594
+ menu.NotifyOfLLMReset();
595
+ },
596
+ ]);
582
597
  }
583
598
 
584
599
  /**
@@ -720,13 +735,16 @@ class Menu {
720
735
  document.getElementById('LLM_preferences').value =
721
736
  constants.LLMPreferences;
722
737
  }
738
+ if (document.getElementById('LLM_reset_notification')) {
739
+ document.getElementById('LLM_reset_notification').remove();
740
+ }
723
741
  }
724
742
 
725
743
  /**
726
744
  * Saves the data from the HTML elements into the constants object.
727
745
  */
728
746
  SaveData() {
729
- this.HandleLLMChanges();
747
+ let shouldReset = this.ShouldLLMReset();
730
748
 
731
749
  constants.vol = document.getElementById('vol').value;
732
750
  constants.autoPlayRate = document.getElementById('autoplay_rate').value;
@@ -746,9 +764,9 @@ class Menu {
746
764
  document.getElementById('skill_level_other').value;
747
765
  constants.LLMModel = document.getElementById('LLM_model').value;
748
766
  constants.LLMPreferences = document.getElementById('LLM_preferences').value;
749
-
750
767
  constants.LLMOpenAiMulti = document.getElementById('openai_multi').checked;
751
768
  constants.LLMGeminiMulti = document.getElementById('gemini_multi').checked;
769
+ constants.autoInitLLM = document.getElementById('init_llm_on_load').checked;
752
770
 
753
771
  // aria
754
772
  if (document.getElementById('aria_mode_assertive').checked) {
@@ -759,6 +777,12 @@ class Menu {
759
777
 
760
778
  this.SaveDataToLocalStorage();
761
779
  this.UpdateHtml();
780
+
781
+ if (shouldReset) {
782
+ if (chatLLM) {
783
+ chatLLM.ResetLLM();
784
+ }
785
+ }
762
786
  }
763
787
 
764
788
  /**
@@ -775,13 +799,37 @@ class Menu {
775
799
  document
776
800
  .getElementById(constants.announcement_container_id)
777
801
  .setAttribute('aria-live', constants.ariaMode);
802
+
803
+ document.getElementById('init_llm_on_load').checked = constants.autoInitLLM;
778
804
  }
779
805
 
806
+ /**
807
+ * Notifies the user that the LLM will be reset.
808
+ */
809
+ NotifyOfLLMReset() {
810
+ let html =
811
+ '<p id="LLM_reset_notification">Note: Changes in LLM settings will reset any existing conversation.</p>';
812
+
813
+ if (document.getElementById('LLM_reset_notification')) {
814
+ document.getElementById('LLM_reset_notification').remove();
815
+ }
816
+ document
817
+ .getElementById('save_and_close_menu')
818
+ .insertAdjacentHTML('beforebegin', html);
819
+
820
+ // add to aria button text
821
+ document
822
+ .getElementById('save_and_close_menu')
823
+ .setAttribute(
824
+ 'aria-labelledby',
825
+ 'save_and_close_text LLM_reset_notification'
826
+ );
827
+ }
780
828
  /**
781
829
  * Handles changes to the LLM model and multi-modal settings.
782
830
  * We reset if we change the LLM model, multi settings, or skill level.
783
831
  */
784
- HandleLLMChanges() {
832
+ ShouldLLMReset() {
785
833
  let shouldReset = false;
786
834
  if (
787
835
  !shouldReset &&
@@ -805,11 +853,7 @@ class Menu {
805
853
  shouldReset = true;
806
854
  }
807
855
 
808
- if (shouldReset) {
809
- if (chatLLM) {
810
- chatLLM.ResetChatHistory();
811
- }
812
- }
856
+ return shouldReset;
813
857
  }
814
858
 
815
859
  /**
@@ -836,6 +880,7 @@ class Menu {
836
880
  data.LLMPreferences = constants.LLMPreferences;
837
881
  data.LLMOpenAiMulti = constants.LLMOpenAiMulti;
838
882
  data.LLMGeminiMulti = constants.LLMGeminiMulti;
883
+ data.autoInitLLM = constants.autoInitLLM;
839
884
  localStorage.setItem('settings_data', JSON.stringify(data));
840
885
  }
841
886
  /**
@@ -860,6 +905,7 @@ class Menu {
860
905
  constants.LLMPreferences = data.LLMPreferences;
861
906
  constants.LLMOpenAiMulti = data.LLMOpenAiMulti;
862
907
  constants.LLMGeminiMulti = data.LLMGeminiMulti;
908
+ constants.autoInitLLM = data.autoInitLLM;
863
909
  }
864
910
  this.PopulateData();
865
911
  this.UpdateHtml();
@@ -875,8 +921,12 @@ class ChatLLM {
875
921
  constructor() {
876
922
  this.firstTime = true;
877
923
  this.firstMulti = true;
924
+ this.shown = false;
878
925
  this.CreateComponent();
879
926
  this.SetEvents();
927
+ if (constants.autoInitLLM) {
928
+ this.InitChatMessage();
929
+ }
880
930
  }
881
931
 
882
932
  /**
@@ -895,8 +945,11 @@ class ChatLLM {
895
945
  </button>
896
946
  </div>
897
947
  <div class="modal-body">
948
+ <div id="chatLLM_chat_history_wrapper">
898
949
  <div id="chatLLM_chat_history" aria-live="${constants.ariaMode}" aria-relevant="additions">
899
950
  </div>
951
+ <p id="chatLLM_copy_all_wrapper"><button id="chatLLM_copy_all">Copy all to clipboard</button></p>
952
+ </div>
900
953
  <div id="chatLLM_content">
901
954
  <p><input type="text" id="chatLLM_input" class="form-control" name="chatLLM_input" aria-labelledby="chatLLM_title" size="50"></p>
902
955
  <div class="LLM_suggestions">
@@ -1038,8 +1091,47 @@ class ChatLLM {
1038
1091
  document.getElementById('reset_chatLLM'),
1039
1092
  'click',
1040
1093
  function (e) {
1041
- chatLLM.Toggle(false);
1042
- chatLLM.ResetChatHistory();
1094
+ chatLLM.ResetLLM();
1095
+ },
1096
+ ]);
1097
+
1098
+ // bookmark:
1099
+ //
1100
+ // have LLM run on init (#425)
1101
+ // quiet first, but if they open window, it does the beep and aria live alert
1102
+ // make toggle in settings to yes / no auto initiate LLM (or wait for window to open)
1103
+ // as part of this, fix reset so it loads the LLM again without refreshing the window,
1104
+ // left undone from the other request
1105
+
1106
+ // copy to clipboard
1107
+ constants.events.push([
1108
+ document.getElementById('chatLLM_copy_all'),
1109
+ 'click',
1110
+ function (e) {
1111
+ let text = document.getElementById('chatLLM_chat_history').innerText;
1112
+ // need newlines instead of paragraphs headings etc
1113
+ text = text.replace(/<p>/g, '\n').replace(/<\/p>/g, '\n');
1114
+ text = text.replace(/<h\d>/g, '\n').replace(/<\/h\d>/g, '\n');
1115
+ text = text.replace(/<.*?>/g, '');
1116
+
1117
+ navigator.clipboard.writeText(text);
1118
+ },
1119
+ ]);
1120
+ constants.events.push([
1121
+ document.getElementById('chatLLM_chat_history'),
1122
+ 'click',
1123
+ function (e) {
1124
+ // we're delegating here, so set the event on child .chatLLM_message_copy_button
1125
+ if (e.target.matches('.chatLLM_message_copy_button')) {
1126
+ // get the innerText of the element before the button
1127
+ let text = e.target.closest('p').previousElementSibling.innerText;
1128
+ // need newlines instead of paragraphs headings etc
1129
+ text = text.replace(/<p>/g, '\n').replace(/<\/p>/g, '\n');
1130
+ text = text.replace(/<h\d>/g, '\n').replace(/<\/h\d>/g, '\n');
1131
+ text = text.replace(/<.*?>/g, '');
1132
+
1133
+ navigator.clipboard.writeText(text);
1134
+ }
1043
1135
  },
1044
1136
  ]);
1045
1137
  }
@@ -1057,7 +1149,7 @@ class ChatLLM {
1057
1149
  async Submit(text, firsttime = false) {
1058
1150
  // start waiting sound
1059
1151
  if (constants.playLLMWaitingSound) {
1060
- chatLLM.WaitingSound(true);
1152
+ this.WaitingSound(true);
1061
1153
  }
1062
1154
 
1063
1155
  let img = null;
@@ -1103,7 +1195,7 @@ class ChatLLM {
1103
1195
  let delay = 1000;
1104
1196
  let freq = 440; // a440 babee
1105
1197
  constants.waitingInterval = setInterval(function () {
1106
- if (audio) {
1198
+ if (audio && chatLLM.shown) {
1107
1199
  audio.playOscillator(freq, 0.2, 0);
1108
1200
  }
1109
1201
  }, delay);
@@ -1115,6 +1207,15 @@ class ChatLLM {
1115
1207
  }
1116
1208
  }
1117
1209
 
1210
+ InitChatMessage() {
1211
+ // get name from resource
1212
+ let LLMName = resources.GetString(constants.LLMModel);
1213
+ this.firstTime = false;
1214
+ this.DisplayChatMessage(LLMName, resources.GetString('processing'), true);
1215
+ let defaultPrompt = this.GetDefaultPrompt();
1216
+ this.Submit(defaultPrompt, true);
1217
+ }
1218
+
1118
1219
  /**
1119
1220
  * Processes the response from the LLM and displays it to the user.
1120
1221
  * @function
@@ -1122,7 +1223,7 @@ class ChatLLM {
1122
1223
  */
1123
1224
  ProcessLLMResponse(data, model) {
1124
1225
  chatLLM.WaitingSound(false);
1125
- console.log('LLM response: ', data);
1226
+ //console.log('LLM response: ', data);
1126
1227
  let text = '';
1127
1228
  let LLMName = resources.GetString(model);
1128
1229
 
@@ -1368,6 +1469,12 @@ class ChatLLM {
1368
1469
  <p class="chatLLM_message_text">${text}</p>
1369
1470
  </div>
1370
1471
  `;
1472
+ // add a copy button to actual messages
1473
+ if (user != 'User' && text != resources.GetString('processing')) {
1474
+ html += `
1475
+ <p class="chatLLM_message_copy"><button class="chatLLM_message_copy_button">Copy</button></p>
1476
+ `;
1477
+ }
1371
1478
 
1372
1479
  this.RenderChatMessage(html);
1373
1480
  }
@@ -1385,7 +1492,7 @@ class ChatLLM {
1385
1492
  /**
1386
1493
  * Resets the chat history window
1387
1494
  */
1388
- ResetChatHistory() {
1495
+ ResetLLM() {
1389
1496
  // clear the main chat history
1390
1497
  document.getElementById('chatLLM_chat_history').innerHTML = '';
1391
1498
  // unhide the more button
@@ -1397,6 +1504,11 @@ class ChatLLM {
1397
1504
  // reset the data
1398
1505
  this.requestJson = null;
1399
1506
  this.firstTime = true;
1507
+
1508
+ // and start over, if enabled
1509
+ if (constants.autoInitLLM) {
1510
+ chatLLM.InitChatMessage();
1511
+ }
1400
1512
  }
1401
1513
 
1402
1514
  /**
@@ -1430,6 +1542,7 @@ class ChatLLM {
1430
1542
  onoff = false;
1431
1543
  }
1432
1544
  }
1545
+ chatLLM.shown = onoff;
1433
1546
  if (onoff) {
1434
1547
  // open
1435
1548
  this.whereWasMyFocus = document.activeElement;
@@ -1439,16 +1552,6 @@ class ChatLLM {
1439
1552
  .getElementById('chatLLM_modal_backdrop')
1440
1553
  .classList.remove('hidden');
1441
1554
  document.querySelector('#chatLLM .close').focus();
1442
-
1443
- // first time, send default query
1444
- if (this.firstTime) {
1445
- // get name from resource
1446
- let LLMName = resources.GetString(constants.LLMModel);
1447
- this.firstTime = false;
1448
- this.DisplayChatMessage(LLMName, 'Processing Chart...', true);
1449
- let defaultPrompt = this.GetDefaultPrompt();
1450
- this.Submit(defaultPrompt, true);
1451
- }
1452
1555
  } else {
1453
1556
  // close
1454
1557
  document.getElementById('chatLLM').classList.add('hidden');