From 7594f649890d27d558be35c9d40a0aa8211aec67 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Thu, 6 Jun 2024 22:20:24 -0400
Subject: Flashcard changes - menu added

---
 src/client/views/nodes/FieldView.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 818d26956..138f00492 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,6 +50,7 @@ export interface FieldViewSharedProps {
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
     isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
+    dontSelect: () => boolean | undefined;
     childFilters: () => string[];
     childFiltersByRanges: () => string[];
     styleProvider: Opt<StyleProviderFuncType>;
-- 
cgit v1.2.3-70-g09d2


From 6131fd8fc6cb1245ccfbc5d03d33ffd1498e83f4 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Mon, 8 Jul 2024 15:12:16 -0400
Subject: Speech to phonemes progress

---
 report.20231129.000028.55430.0.001.json            | 1343 ++++++++++++++++++++
 src/client/apis/gpt/GPT.ts                         |    9 +-
 src/client/util/CurrentUserUtils.ts                |    1 +
 src/client/util/DictationManager.ts                |    2 +-
 src/client/views/.MetadataEntryMenu.tsx.icloud     |  Bin 0 -> 171 bytes
 src/client/views/.Touchable.tsx.icloud             |  Bin 0 -> 161 bytes
 .../views/collections/CollectionCarouselView.tsx   |    5 +-
 src/client/views/nodes/.LinkAnchorBox.scss.icloud  |  Bin 0 -> 168 bytes
 src/client/views/nodes/AudioBox.tsx                |   19 +-
 src/client/views/nodes/ComparisonBox.scss          |   10 +-
 src/client/views/nodes/ComparisonBox.tsx           |  258 ++--
 src/client/views/nodes/FieldView.tsx               |    2 +-
 src/client/views/nodes/ImageBox.tsx                |   78 +-
 src/client/views/nodes/LinkAnchorBox.tsx           |  115 ++
 .../nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav     |  Bin 0 -> 765006 bytes
 .../views/nodes/formattedText/FormattedTextBox.tsx |    7 +-
 src/fields/RichTextField.ts                        |    9 +-
 17 files changed, 1743 insertions(+), 115 deletions(-)
 create mode 100644 report.20231129.000028.55430.0.001.json
 create mode 100644 src/client/views/.MetadataEntryMenu.tsx.icloud
 create mode 100644 src/client/views/.Touchable.tsx.icloud
 create mode 100644 src/client/views/nodes/.LinkAnchorBox.scss.icloud
 create mode 100644 src/client/views/nodes/LinkAnchorBox.tsx
 create mode 100644 src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/report.20231129.000028.55430.0.001.json b/report.20231129.000028.55430.0.001.json
new file mode 100644
index 000000000..a29d550ae
--- /dev/null
+++ b/report.20231129.000028.55430.0.001.json
@@ -0,0 +1,1343 @@
+
+{
+  "header": {
+    "reportVersion": 1,
+    "event": "Allocation failed - JavaScript heap out of memory",
+    "trigger": "FatalError",
+    "filename": "report.20231129.000028.55430.0.001.json",
+    "dumpEventTime": "2023-11-29T00:00:28Z",
+    "dumpEventTimeStamp": "1701234028056",
+    "processId": 55430,
+    "cwd": "/Users/sarah/Desktop/dash/Dash-Web",
+    "commandLine": [
+      "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+      "--max-old-space-size=2048",
+      "/Users/sarah/Desktop/dash/Dash-Web/node_modules/ts-node-dev/lib/wrap.js",
+      "/Users/sarah/Desktop/dash/Dash-Web/node_modules/fork-ts-checker-webpack-plugin/lib/service.js"
+    ],
+    "nodejsVersion": "v12.16.0",
+    "wordSize": 64,
+    "arch": "x64",
+    "platform": "darwin",
+    "componentVersions": {
+      "node": "12.16.0",
+      "v8": "7.8.279.23-node.31",
+      "uv": "1.34.0",
+      "zlib": "1.2.11",
+      "brotli": "1.0.7",
+      "ares": "1.15.0",
+      "modules": "72",
+      "nghttp2": "1.40.0",
+      "napi": "5",
+      "llhttp": "2.0.4",
+      "http_parser": "2.9.3",
+      "openssl": "1.1.1d",
+      "cldr": "35.1",
+      "icu": "64.2",
+      "tz": "2019c",
+      "unicode": "12.1"
+    },
+    "release": {
+      "name": "node",
+      "lts": "Erbium",
+      "headersUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0-headers.tar.gz",
+      "sourceUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0.tar.gz"
+    },
+    "osName": "Darwin",
+    "osRelease": "22.6.0",
+    "osVersion": "Darwin Kernel Version 22.6.0: Wed Jul  5 22:21:56 PDT 2023; root:xnu-8796.141.3~6/RELEASE_X86_64",
+    "osMachine": "x86_64",
+    "cpus": [
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 19491620,
+        "nice": 0,
+        "sys": 10530730,
+        "idle": 128029940,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1041080,
+        "nice": 0,
+        "sys": 832510,
+        "idle": 156711500,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 17230140,
+        "nice": 0,
+        "sys": 7308720,
+        "idle": 133550270,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1101080,
+        "nice": 0,
+        "sys": 813690,
+        "idle": 156678120,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 14246350,
+        "nice": 0,
+        "sys": 5654800,
+        "idle": 138314740,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1142930,
+        "nice": 0,
+        "sys": 790770,
+        "idle": 156668310,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 12228700,
+        "nice": 0,
+        "sys": 4533960,
+        "idle": 141551990,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1165420,
+        "nice": 0,
+        "sys": 773220,
+        "idle": 156669740,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 9805920,
+        "nice": 0,
+        "sys": 3386750,
+        "idle": 145216650,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1177080,
+        "nice": 0,
+        "sys": 759980,
+        "idle": 156674610,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 8806950,
+        "nice": 0,
+        "sys": 2889820,
+        "idle": 146774530,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1166250,
+        "nice": 0,
+        "sys": 737790,
+        "idle": 156711830,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 7865920,
+        "nice": 0,
+        "sys": 2438950,
+        "idle": 148205800,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1155370,
+        "nice": 0,
+        "sys": 719580,
+        "idle": 156743860,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 7478040,
+        "nice": 0,
+        "sys": 2248690,
+        "idle": 148810130,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1131070,
+        "nice": 0,
+        "sys": 695000,
+        "idle": 156797450,
+        "irq": 0
+      }
+    ],
+    "networkInterfaces": [
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "127.0.0.1",
+        "netmask": "255.0.0.0",
+        "family": "IPv4"
+      },
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "::1",
+        "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::1",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 1
+      },
+      {
+        "name": "en5",
+        "internal": false,
+        "mac": "ac:de:48:00:11:22",
+        "address": "fe80::aede:48ff:fe00:1122",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 4
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "fe80::1467:9dad:c3d1:73a7",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 6
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "10.38.21.14",
+        "netmask": "255.255.192.0",
+        "family": "IPv4"
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "fde6:e673:d766:4b72:1481:e813:77a2:da34",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "2620:6e:6000:3100:148e:201a:1a33:145d",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "2620:6e:6000:3100:5c7f:8d3f:76ba:694b",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "awdl0",
+        "internal": false,
+        "mac": "12:df:77:23:4c:4e",
+        "address": "fe80::10df:77ff:fe23:4c4e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 9
+      },
+      {
+        "name": "llw0",
+        "internal": false,
+        "mac": "12:df:77:23:4c:4e",
+        "address": "fe80::10df:77ff:fe23:4c4e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 12
+      },
+      {
+        "name": "utun0",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::6616:29f1:d83d:2f8d",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 14
+      },
+      {
+        "name": "utun1",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::b4dc:ec9a:5d3d:18ab",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 15
+      },
+      {
+        "name": "utun2",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::ce81:b1c:bd2c:69e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 16
+      },
+      {
+        "name": "utun3",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::bd9f:d9d8:700d:2011",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 17
+      },
+      {
+        "name": "utun4",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::9a49:1de7:e220:2bb",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 18
+      },
+      {
+        "name": "utun5",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::93fe:38cf:1393:3191",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 19
+      },
+      {
+        "name": "utun6",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::781d:651c:69a7:3d42",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 20
+      },
+      {
+        "name": "utun7",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::b39:d094:2a6d:e0c5",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 21
+      }
+    ],
+    "host": "sarahs-mbp-2.devices.brown.edu"
+  },
+  "javascriptStack": {
+    "message": "No stack.",
+    "stack": [
+      "Unavailable."
+    ]
+  },
+  "nativeStack": [
+    {
+      "pc": "0x000000010015c8ca",
+      "symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, v8::Local<v8::String>) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100080f3e",
+      "symbol": "node::OnFatalError(char const*, char const*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100185467",
+      "symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100185403",
+      "symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x000000010030b5f5",
+      "symbol": "v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x000000010030ccc4",
+      "symbol": "v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100309b37",
+      "symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100307afd",
+      "symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x00000001003132ba",
+      "symbol": "v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100313341",
+      "symbol": "v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x00000001002e065b",
+      "symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100618a18",
+      "symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100950c19",
+      "symbol": "Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    }
+  ],
+  "javascriptHeap": {
+    "totalMemory": 2154991616,
+    "totalCommittedMemory": 2153815368,
+    "usedMemory": 2136222480,
+    "availableMemory": 51343040,
+    "memoryLimit": 2197815296,
+    "heapSpaces": {
+      "read_only_space": {
+        "memorySize": 262144,
+        "committedMemory": 33088,
+        "capacity": 32808,
+        "used": 32808,
+        "available": 0
+      },
+      "new_space": {
+        "memorySize": 8388608,
+        "committedMemory": 8388528,
+        "capacity": 4189824,
+        "used": 69544,
+        "available": 4120280
+      },
+      "old_space": {
+        "memorySize": 1946566656,
+        "committedMemory": 1946346224,
+        "capacity": 1939183704,
+        "used": 1938974448,
+        "available": 209256
+      },
+      "code_space": {
+        "memorySize": 14323712,
+        "committedMemory": 13779552,
+        "capacity": 12481792,
+        "used": 12481792,
+        "available": 0
+      },
+      "map_space": {
+        "memorySize": 1576960,
+        "committedMemory": 1394440,
+        "capacity": 1280720,
+        "used": 1280720,
+        "available": 0
+      },
+      "large_object_space": {
+        "memorySize": 183824384,
+        "committedMemory": 183824384,
+        "capacity": 183380384,
+        "used": 183380384,
+        "available": 0
+      },
+      "code_large_object_space": {
+        "memorySize": 49152,
+        "committedMemory": 49152,
+        "capacity": 2784,
+        "used": 2784,
+        "available": 0
+      },
+      "new_large_object_space": {
+        "memorySize": 0,
+        "committedMemory": 0,
+        "capacity": 4189824,
+        "used": 0,
+        "available": 4189824
+      }
+    }
+  },
+  "resourceUsage": {
+    "userCpuSeconds": 306.691,
+    "kernelCpuSeconds": 15.3869,
+    "cpuConsumptionPercent": 11.2772,
+    "maxRss": 2231285841920,
+    "pageFaults": {
+      "IORequired": 110,
+      "IONotRequired": 2487718
+    },
+    "fsActivity": {
+      "reads": 0,
+      "writes": 0
+    }
+  },
+  "libuv": [
+  ],
+  "environmentVariables": {
+    "npm_config_save_dev": "",
+    "npm_config_legacy_bundling": "",
+    "npm_config_dry_run": "",
+    "npm_package_dependencies_request": "^2.88.2",
+    "npm_package_dependencies_express_flash": "0.0.2",
+    "npm_package_dependencies__fortawesome_fontawesome_svg_core": "^6.3.0",
+    "NVM_INC": "/Users/sarah/.nvm/versions/node/v12.16.0/include/node",
+    "npm_config_viewer": "man",
+    "npm_config_only": "",
+    "npm_config_commit_hooks": "true",
+    "npm_config_browser": "",
+    "npm_package_gitHead": "5754c1c056ec62e17b5d7badddc4a8fc3637e09f",
+    "npm_package_dependencies_webpack_dev_middleware": "^5.3.1",
+    "npm_package_dependencies_webpack_cli": "^4.10.0",
+    "npm_package_devDependencies_prettier": "^2.7.1",
+    "npm_package_devDependencies_awesome_typescript_loader": "^5.2.1",
+    "npm_package_devDependencies__types_archiver": "^3.1.1",
+    "npm_config_also": "",
+    "npm_package_dependencies_react_jsx_parser": "^1.29.0",
+    "npm_package_dependencies_mongoose": "^5.13.14",
+    "npm_package_dependencies_connect_flash": "^0.1.1",
+    "npm_package_browser_child_process": "false",
+    "npm_config_sign_git_commit": "",
+    "npm_config_rollback": "true",
+    "npm_package_dependencies_material_ui": "^0.20.2",
+    "npm_package_devDependencies__types_sharp": "^0.23.1",
+    "npm_package_devDependencies__types_passport_local": "^1.0.34",
+    "npm_package_devDependencies__types_dotenv": "^6.1.1",
+    "npm_package_devDependencies__types_cookie_parser": "^1.4.2",
+    "TERM_PROGRAM": "Apple_Terminal",
+    "NODE": "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "npm_config_usage": "",
+    "npm_config_audit": "true",
+    "npm_package_dependencies_reveal_js": "^4.3.0",
+    "npm_package_dependencies_process": "^0.11.10",
+    "npm_package_dependencies_pdfjs": "^2.4.7",
+    "npm_package_dependencies_html_to_image": "^0.1.3",
+    "npm_package_devDependencies_file_loader": "^3.0.1",
+    "npm_package_devDependencies__types_express_flash": "0.0.0",
+    "npm_package_scripts_monitor": "cross-env MONITORED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node src/server/index.ts",
+    "INIT_CWD": "/Users/sarah/Desktop/dash/Dash-Web",
+    "npm_package_dependencies_rehype_raw": "^6.1.1",
+    "npm_package_dependencies_react_audio_waveform": "0.0.5",
+    "npm_package_dependencies_path_browserify": "^1.0.1",
+    "npm_package_dependencies_nodemailer": "^5.1.1",
+    "npm_package_dependencies_axios": "^0.19.2",
+    "npm_package_devDependencies_typescript": "^4.7.4",
+    "NVM_CD_FLAGS": "-q",
+    "npm_config_globalignorefile": "/Users/sarah/.nvm/versions/node/v12.16.0/etc/npmignore",
+    "npm_package_dependencies_react_grid_layout": "^1.3.4",
+    "npm_package_dependencies_prosemirror_find_replace": "^0.9.0",
+    "npm_package_dependencies_normalize_css": "^8.0.1",
+    "npm_package_devDependencies_mocha": "^5.2.0",
+    "npm_package_devDependencies__types_express_session": "^1.17.5",
+    "SHELL": "/bin/zsh",
+    "TERM": "xterm-256color",
+    "npm_config_shell": "/bin/zsh",
+    "npm_config_maxsockets": "50",
+    "npm_config_init_author_url": "",
+    "npm_package_dependencies_prosemirror_dev_tools": "^3.1.0",
+    "npm_package_dependencies_p_limit": "^2.2.0",
+    "npm_package_dependencies_bson": "^4.6.1",
+    "npm_package_dependencies__types_dom_speech_recognition": "0.0.1",
+    "npm_package_dependencies__emotion_styled": "^11.11.0",
+    "npm_package_devDependencies_style_loader": "^0.23.1",
+    "npm_package_devDependencies__types_react_datepicker": "^3.1.8",
+    "npm_config_shrinkwrap": "true",
+    "npm_config_parseable": "",
+    "npm_config_metrics_registry": "https://registry.npmjs.org/",
+    "npm_package_dependencies_xregexp": "^4.4.1",
+    "npm_package_dependencies_shelljs": "^0.8.5",
+    "npm_package_dependencies_bezier_curve": "^1.0.0",
+    "npm_package_dependencies__mui_icons_material": "^5.11.16",
+    "npm_package_devDependencies_tslint": "^5.20.1",
+    "npm_package_devDependencies__types_react_transition_group": "^4.4.5",
+    "npm_package_scripts_tsc": "tsc",
+    "TMPDIR": "/var/folders/yk/p_39q8jn673c5p8_66mcxm7r0000gn/T/",
+    "npm_config_timing": "",
+    "npm_config_init_license": "ISC",
+    "npm_package_dependencies_socket_io": "^2.5.0",
+    "npm_package_dependencies_probe_image_size": "^4.0.0",
+    "npm_package_dependencies_canvas": "^2.9.3",
+    "npm_package_dependencies__hig_theme_data": "^2.23.1",
+    "npm_package_devDependencies__types_react_select": "^3.1.2",
+    "npm_package_devDependencies__types_prosemirror_model": "^1.16.1",
+    "CONDA_SHLVL": "1",
+    "npm_config_if_present": "",
+    "npm_package_dependencies_typescript_collections": "^1.3.3",
+    "npm_package_dependencies_rimraf": "^3.0.0",
+    "npm_package_dependencies_react_autosuggest": "^9.4.3",
+    "npm_package_dependencies_flexlayout_react": "^0.3.11",
+    "npm_package_dependencies_find_in_files": "^0.5.0",
+    "npm_package_devDependencies__types_chai": "^4.3.0",
+    "CONDA_PROMPT_MODIFIER": "(base) ",
+    "TERM_PROGRAM_VERSION": "447",
+    "npm_package_dependencies_prosemirror_inputrules": "^1.1.3",
+    "npm_package_dependencies_bcrypt_nodejs": "0.0.3",
+    "npm_package_dependencies_async": "^2.6.2",
+    "npm_config_sign_git_tag": "",
+    "npm_config_init_author_email": "",
+    "npm_config_cache_max": "Infinity",
+    "npm_package_dependencies_uuid": "^3.4.0",
+    "npm_package_dependencies_supercluster": "^7.1.4",
+    "npm_package_dependencies_remark_gfm": "^3.0.1",
+    "npm_package_dependencies_connect_mongo": "^2.0.3",
+    "npm_package_dependencies_browser_assert": "^1.2.1",
+    "npm_package_devDependencies_sass_loader": "^7.3.1",
+    "npm_package_scripts_start_release_debug": "cross-env RELEASE=true USE_AZURE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --inspect -- src/server/index.ts",
+    "npm_config_preid": "",
+    "npm_config_long": "",
+    "npm_config_local_address": "",
+    "npm_config_git_tag_version": "true",
+    "npm_config_cert": "",
+    "npm_package_dependencies_js_datepicker": "^4.6.6",
+    "npm_package_devDependencies__types_webpack_hot_middleware": "^2.25.6",
+    "npm_package_devDependencies__types_mongodb": "^3.6.20",
+    "npm_package_devDependencies__types_mocha": "^5.2.6",
+    "TERM_SESSION_ID": "4AACD142-8780-44F4-A4FA-BB53E1BF6081",
+    "npm_config_registry": "https://registry.npmjs.org/",
+    "npm_config_noproxy": "",
+    "npm_config_fetch_retries": "2",
+    "npm_package_dependencies_react_compound_slider": "^2.5.0",
+    "npm_package_dependencies_prosemirror_history": "^1.2.0",
+    "npm_package_devDependencies__types_react_color": "^2.17.6",
+    "npm_package_devDependencies__types_google_maps_react": "^2.0.5",
+    "npm_package_devDependencies__types_color": "^3.0.3",
+    "npm_package_dependencies_react_dom": "^18.2.0",
+    "npm_package_dependencies_passport_local": "^1.0.0",
+    "npm_package_dependencies__octokit_core": "^4.0.4",
+    "npm_package_devDependencies__types_async": "^2.4.1",
+    "npm_package_scripts_debug": "cross-env  NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts",
+    "npm_package_scripts_oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts",
+    "npm_config_versions": "",
+    "npm_config_message": "%s",
+    "npm_config_key": "",
+    "npm_package_readmeFilename": "README.md",
+    "npm_package_dependencies_react_refresh_typescript": "^2.0.7",
+    "npm_package_dependencies_react_dropzone": "^14.2.3",
+    "npm_package_dependencies_image_size": "^0.7.5",
+    "npm_package_dependencies_html_to_text": "^5.1.1",
+    "npm_package_dependencies_express_validator": "^5.3.1",
+    "npm_package_devDependencies_eslint_plugin_jsx_a11y": "^6.6.0",
+    "npm_package_node_child_process": "empty",
+    "npm_package_dependencies_react_resizable_rotatable_draggable": "^0.2.0",
+    "npm_package_dependencies_got": "^12.0.1",
+    "npm_package_dependencies__types_d3_color": "^2.0.3",
+    "npm_package_devDependencies_webpack": "^5.69.1",
+    "npm_package_devDependencies__types_nodemailer": "^4.6.6",
+    "npm_package_description": "Install Node.js, then, from the project directory, run",
+    "NVM_DIR": "/Users/sarah/.nvm",
+    "USER": "sarah",
+    "npm_package_dependencies__types_d3_scale": "^3.3.2",
+    "npm_package_devDependencies_dotenv": "^8.6.0",
+    "npm_package_devDependencies__types_react": "^18.0.15",
+    "npm_package_devDependencies__types_prosemirror_transform": "^1.1.5",
+    "npm_package_devDependencies__types_prosemirror_history": "^1.0.3",
+    "npm_package_dependencies_readline": "^1.3.0",
+    "npm_package_dependencies__types_supercluster": "^7.1.0",
+    "npm_package_dependencies__azure_storage_blob": "^12.14.0",
+    "npm_config_globalconfig": "/Users/sarah/.nvm/versions/node/v12.16.0/etc/npmrc",
+    "npm_package_dependencies_depcheck": "^0.9.2",
+    "npm_package_dependencies__types_web": "0.0.53",
+    "CONDA_EXE": "/Users/sarah/miniconda3/bin/conda",
+    "npm_config_prefer_online": "",
+    "npm_config_logs_max": "10",
+    "npm_config_always_auth": "",
+    "npm_package_dependencies_react_icons": "^4.3.1",
+    "npm_package_dependencies_passport_google_oauth20": "^2.0.0",
+    "npm_package_devDependencies_webpack_dev_server": "^3.11.3",
+    "npm_package_devDependencies__types_brotli": "^1.3.1",
+    "npm_package_dependencies_url_loader": "^1.1.2",
+    "npm_package_dependencies_stream_browserify": "^3.0.0",
+    "npm_package_dependencies_prosemirror_transform": "^1.3.4",
+    "npm_package_dependencies_lodash": "^4.17.21",
+    "npm_package_dependencies_i": "^0.3.7",
+    "npm_package_devDependencies_tslint_loader": "^3.6.0",
+    "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.dBaRxB6a53/Listeners",
+    "npm_package_dependencies_words_to_numbers": "^1.5.1",
+    "npm_package_dependencies_valid_url": "^1.0.9",
+    "npm_package_dependencies_styled_components": "^4.4.1",
+    "npm_package_dependencies_csv_parser": "^3.0.0",
+    "npm_package_dependencies_class_transformer": "^0.2.0",
+    "npm_package_devDependencies_eslint": "^8.36.0",
+    "npm_package_devDependencies__types_prosemirror_inputrules": "^1.0.4",
+    "npm_package_devDependencies__types_express": "^4.17.13",
+    "__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0x0",
+    "npm_execpath": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/bin/npm-cli.js",
+    "npm_config_global_style": "",
+    "npm_config_cache_lock_retries": "10",
+    "npm_package_dependencies_wikijs": "^6.3.3",
+    "npm_package_dependencies_bluebird": "^3.7.2",
+    "npm_package_devDependencies__types_react_typist": "^2.0.3",
+    "npm_config_update_notifier": "true",
+    "npm_config_cafile": "",
+    "npm_package_dependencies_util": "^0.12.4",
+    "npm_package_dependencies_raw_loader": "^1.0.0",
+    "npm_package_dependencies_https_browserify": "^1.0.0",
+    "npm_package_dependencies_brotli": "^1.3.3",
+    "npm_package_dependencies__mui_material": "^5.13.1",
+    "npm_package_dependencies__fortawesome_react_fontawesome": "^0.2.0",
+    "npm_package_devDependencies__types_passport_google_oauth20": "^2.0.11",
+    "npm_package_dependencies_cors": "^2.8.5",
+    "npm_package_dependencies_bezier_js": "^4.1.1",
+    "npm_package_dependencies__fortawesome_free_brands_svg_icons": "^6.3.0",
+    "npm_config_heading": "npm",
+    "npm_config_audit_level": "low",
+    "npm_package_dependencies_chrome": "^0.1.0",
+    "npm_package_dependencies__react_three_fiber": "^6.2.3",
+    "npm_package_devDependencies_eslint_plugin_prettier": "^4.2.1",
+    "npm_package_devDependencies_copy_webpack_plugin": "^4.6.0",
+    "npm_package_devDependencies__types_react_measure": "^2.0.8",
+    "npm_package_devDependencies__types_react_dom": "^18.0.6",
+    "npm_package_devDependencies__types_mobile_detect": "^1.3.4",
+    "_CE_CONDA": "",
+    "npm_config_searchlimit": "20",
+    "npm_config_read_only": "",
+    "npm_config_offline": "",
+    "npm_config_fetch_retry_mintimeout": "10000",
+    "npm_package_dependencies_react_typist": "^2.0.5",
+    "npm_package_dependencies_mobx_react_devtools": "^6.1.1",
+    "npm_package_dependencies_md5_file": "^5.0.0",
+    "npm_package_dependencies_forever_agent": "^0.6.1",
+    "npm_package_devDependencies__types_xregexp": "^4.4.0",
+    "npm_package_devDependencies__types_typescript": "^2.0.0",
+    "npm_package_devDependencies__types_request": "^2.48.8",
+    "npm_package_devDependencies__types_prosemirror_commands": "^1.0.4",
+    "npm_config_json": "",
+    "npm_config_access": "",
+    "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}",
+    "npm_package_dependencies__fortawesome_free_solid_svg_icons": "^6.3.0",
+    "npm_package_devDependencies__types_socket_io": "^2.1.13",
+    "PATH": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/sarah/Desktop/dash/Dash-Web/node_modules/.bin:/Users/sarah/.nvm/versions/node/v12.16.0/bin:/Users/sarah/miniconda3/bin:/Users/sarah/miniconda3/condabin:/Users/sarah/.elan/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin",
+    "npm_config_allow_same_version": "",
+    "npm_package_dependencies_webrtc_adapter": "^7.7.1",
+    "npm_package_dependencies_react_reveal": "^1.2.2",
+    "npm_package_dependencies_prosemirror_schema_list": "^1.1.6",
+    "npm_package_dependencies__material_ui_core": "^4.12.3",
+    "npm_package_devDependencies__types_rimraf": "^2.0.5",
+    "npm_package_devDependencies__types_connect_flash": "0.0.34",
+    "npm_config_https_proxy": "",
+    "npm_config_engine_strict": "",
+    "npm_config_description": "true",
+    "npm_package_dependencies_pug": "^2.0.4",
+    "npm_package_dependencies_prosemirror_keymap": "^1.1.5",
+    "npm_package_dependencies_pdfjs_dist": "^2.14.305",
+    "npm_package_dependencies_mobile_detect": "^1.4.5",
+    "npm_package_dependencies_image_size_stream": "^1.1.0",
+    "npm_package_dependencies_golden_layout": "^1.5.9",
+    "npm_package_dependencies_child_process": "^1.0.2",
+    "npm_package_dependencies__types_d3_axis": "^2.1.3",
+    "_": "/Users/sarah/Desktop/dash/Dash-Web/node_modules/.bin/cross-env",
+    "LaunchInstanceID": "16109DB5-CDB2-454A-9613-8F6F534702AC",
+    "npm_config_userconfig": "/Users/sarah/.npmrc",
+    "npm_config_init_module": "/Users/sarah/.npm-init.js",
+    "npm_package_dependencies__react_google_maps_api": "^2.7.0",
+    "CONDA_PREFIX": "/Users/sarah/miniconda3",
+    "__CFBundleIdentifier": "com.apple.Terminal",
+    "npm_config_cidr": "",
+    "npm_package_dependencies_puppeteer": "^3.3.0",
+    "npm_package_dependencies_prosemirror_view": "^1.26.5",
+    "npm_package_dependencies_mongodb": "^3.7.3",
+    "npm_package_dependencies_google_auth_library": "^4.2.4",
+    "npm_package_dependencies_bootstrap": "^4.6.1",
+    "npm_package_devDependencies_eslint_config_airbnb": "^19.0.4",
+    "PWD": "/Users/sarah/desktop/dash/dash-web",
+    "npm_config_user": "501",
+    "npm_config_node_version": "12.16.0",
+    "npm_package_dependencies_node_sass": "^4.14.1",
+    "npm_package_dependencies_howler": "^2.2.3",
+    "npm_package_dependencies_expressjs": "^1.0.1",
+    "npm_package_dependencies_core_js": "^3.28.0",
+    "npm_package_dependencies_browndash_components": "^0.1.36",
+    "npm_package_devDependencies_eslint_plugin_react_hooks": "^4.6.0",
+    "npm_package_devDependencies__types_lodash": "^4.14.179",
+    "JAVA_HOME": "/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home",
+    "npm_lifecycle_event": "start",
+    "npm_package_dependencies_react_table": "^6.11.5",
+    "npm_package_dependencies_react_loading": "^2.0.3",
+    "npm_package_dependencies_mobx": "^5.15.7",
+    "npm_package_dependencies_babel": "^6.23.0",
+    "npm_package_devDependencies_jsdom": "^15.2.1",
+    "npm_package_devDependencies_chai": "^4.3.6",
+    "npm_config_save": "true",
+    "npm_config_ignore_prepublish": "",
+    "npm_config_editor": "vi",
+    "npm_config_auth_type": "legacy",
+    "npm_package_dependencies_npm": "^6.14.18",
+    "npm_package_dependencies_node_stream_zip": "^1.15.0",
+    "npm_package_dependencies_image_data_uri": "^2.0.1",
+    "npm_package_scripts_start_release": "cross-env RELEASE=true USE_AZURE=false NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+    "npm_package_name": "dash",
+    "LANG": "en_US.UTF-8",
+    "npm_config_tag": "latest",
+    "npm_config_script_shell": "",
+    "npm_package_dependencies_query_string": "^6.14.1",
+    "npm_package_dependencies_mobx_utils": "^5.6.2",
+    "npm_package_dependencies_file_saver": "^2.0.5",
+    "npm_package_dependencies_body_parser": "^1.19.2",
+    "npm_package_dependencies__types_reveal": "^3.3.33",
+    "npm_package_devDependencies_eslint_plugin_import": "^2.26.0",
+    "npm_package_devDependencies__types_prosemirror_view": "^1.23.1",
+    "npm_config_progress": "true",
+    "npm_config_global": "",
+    "npm_config_before": "",
+    "npm_package_dependencies_xoauth2": "^1.2.0",
+    "npm_package_dependencies_standard_http_error": "^2.0.1",
+    "npm_package_dependencies_react_loader_spinner": "^5.3.4",
+    "npm_package_dependencies_http_browserify": "^1.7.0",
+    "npm_package_dependencies__types_d3_selection": "^2.0.1",
+    "npm_package_dependencies__hig_flyout": "^1.3.1",
+    "npm_package_devDependencies_fork_ts_checker_webpack_plugin": "^1.6.0",
+    "npm_package_scripts_build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production",
+    "npm_package_scripts_start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+    "npm_config_searchstaleness": "900",
+    "npm_config_optional": "true",
+    "npm_config_ham_it_up": "",
+    "npm_package_dependencies_sharp": "^0.23.4",
+    "npm_package_dependencies_rc_switch": "^1.9.2",
+    "npm_package_dependencies_googlephotos": "^0.2.5",
+    "npm_package_dependencies_exifr": "^7.1.3",
+    "npm_package_dependencies__types_google_maps": "^3.2.3",
+    "npm_package_dependencies__types_bezier_js": "^4.1.0",
+    "npm_package_dependencies__ffmpeg_core": "0.10.0",
+    "npm_package_devDependencies_ts_loader": "^5.3.3",
+    "npm_package_devDependencies__types_bcrypt_nodejs": "0.0.30",
+    "XPC_FLAGS": "0x0",
+    "npm_config_save_prod": "",
+    "npm_config_force": "",
+    "npm_config_bin_links": "true",
+    "npm_package_devDependencies__types_youtube": "0.0.39",
+    "npm_config_searchopts": "",
+    "npm_package_dependencies_react_beautiful_dnd": "^13.1.0",
+    "npm_package_dependencies_jszip": "^3.7.1",
+    "npm_package_dependencies_csv_stringify": "^6.3.0",
+    "npm_package_devDependencies__types_react_icons": "^3.0.0",
+    "npm_config_node_gyp": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
+    "npm_config_depth": "Infinity",
+    "npm_package_dependencies_google_maps_react": "^2.0.6",
+    "npm_package_dependencies_express_session": "^1.17.2",
+    "npm_package_devDependencies_eslint_plugin_node": "^11.1.0",
+    "npm_package_devDependencies_eslint_config_prettier": "^8.5.0",
+    "npm_package_main": "index.js",
+    "npm_config_sso_poll_frequency": "500",
+    "npm_config_rebuild_bundle": "true",
+    "npm_package_dependencies_chart_js": "^3.8.0",
+    "npm_package_dependencies__emotion_react": "^11.11.0",
+    "npm_package_devDependencies__types_prosemirror_menu": "^1.0.6",
+    "npm_package_devDependencies__types_prosemirror_keymap": "^1.0.4",
+    "npm_package_devDependencies__types_pdfjs_dist": "^2.10.378",
+    "npm_package_devDependencies__types_exif": "^0.6.3",
+    "npm_package_version": "1.0.0",
+    "_CE_M": "",
+    "XPC_SERVICE_NAME": "0",
+    "npm_config_unicode": "true",
+    "npm_package_dependencies_typescript_language_server": "^0.4.0",
+    "npm_package_dependencies_prosemirror_model": "^1.18.1",
+    "npm_package_dependencies__ffmpeg_ffmpeg": "0.10.0",
+    "SHLVL": "2",
+    "HOME": "/Users/sarah",
+    "npm_config_fetch_retry_maxtimeout": "60000",
+    "npm_package_dependencies_request_promise": "^4.2.6",
+    "npm_package_dependencies_react_markdown": "^8.0.3",
+    "npm_package_dependencies__hig_theme_context": "^2.1.3",
+    "npm_package_devDependencies__types_react_autosuggest": "^9.3.14",
+    "npm_package_devDependencies__types_mongoose": "^5.11.97",
+    "npm_package_devDependencies__types_d3": "^7.4.0",
+    "npm_package_devDependencies__types_animejs": "^2.0.2",
+    "npm_package_scripts_test": "mocha -r ts-node/register test/**/*.ts",
+    "npm_config_tag_version_prefix": "v",
+    "npm_config_strict_ssl": "true",
+    "npm_config_sso_type": "oauth",
+    "npm_config_scripts_prepend_node_path": "warn-only",
+    "npm_config_save_prefix": "^",
+    "npm_config_loglevel": "notice",
+    "npm_config_ca": "",
+    "npm_package_dependencies_three": "^0.127.0",
+    "npm_package_dependencies_openai": "^3.2.1",
+    "npm_package_dependencies_mobx_react": "^5.4.4",
+    "npm_package_dependencies_google_translate_api_browser": "^3.0.1",
+    "npm_package_dependencies_cookie_parser": "^1.4.6",
+    "npm_package_dependencies_adm_zip": "^0.4.16",
+    "npm_package_devDependencies_eslint_config_node": "^4.1.0",
+    "npm_config_save_exact": "",
+    "npm_config_group": "20",
+    "npm_config_fetch_retry_factor": "10",
+    "npm_config_dev": "",
+    "npm_package_devDependencies_webpack_hot_middleware": "^2.25.1",
+    "npm_package_devDependencies_cross_env": "^5.2.1",
+    "npm_config_version": "",
+    "npm_config_prefer_offline": "",
+    "npm_config_cache_lock_stale": "60000",
+    "npm_package_devDependencies__types_prosemirror_state": "^1.2.8",
+    "npm_package_devDependencies__types_body_parser": "^1.19.2",
+    "npm_config_otp": "",
+    "npm_config_cache_min": "10",
+    "npm_package_dependencies_react_color": "^2.19.3",
+    "npm_package_dependencies_d3": "^7.6.1",
+    "npm_package_devDependencies_ts_node": "^10.9.1",
+    "npm_package_devDependencies__types_react_grid_layout": "^1.3.2",
+    "npm_config_searchexclude": "",
+    "npm_config_cache": "/Users/sarah/.npm",
+    "npm_package_dependencies_tough_cookie": "^4.0.0",
+    "npm_package_dependencies_googleapis": "^40.0.0",
+    "npm_package_devDependencies__types_valid_url": "^1.0.3",
+    "npm_package_devDependencies__types_passport": "^1.0.9",
+    "npm_package_devDependencies__types_adm_zip": "^0.4.34",
+    "CONDA_PYTHON_EXE": "/Users/sarah/miniconda3/bin/python",
+    "LOGNAME": "sarah",
+    "npm_lifecycle_script": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+    "npm_config_color": "true",
+    "npm_package_dependencies_solr_node": "^1.2.1",
+    "npm_package_dependencies_react_transition_group": "^4.4.2",
+    "npm_package_dependencies_iink_js": "^1.5.4",
+    "npm_package_dependencies_html_webpack_plugin": "^5.5.0",
+    "npm_config_proxy": "",
+    "npm_config_package_lock": "true",
+    "npm_package_dependencies_prosemirror_state": "^1.4.1",
+    "npm_package_dependencies_nodemon": "^1.19.4",
+    "npm_package_dependencies_function_plot": "^1.22.8",
+    "npm_package_dependencies_equation_editor_react": "github:bobzel/equation-editor-react#useLocally",
+    "npm_package_devDependencies__types_socket_io_parser": "^3.0.0",
+    "CLASSPATH": "/Users/sarah/Downloads/cs15/*:.",
+    "npm_config_package_lock_only": "",
+    "npm_config_fund": "true",
+    "npm_package_dependencies_react": "^18.2.0",
+    "npm_package_dependencies_bingmaps_react": "^1.2.10",
+    "npm_package_devDependencies_scss_loader": "0.0.1",
+    "npm_package_devDependencies__types_cookie_session": "^2.0.44",
+    "npm_config_save_optional": "",
+    "npm_package_dependencies_textarea_caret": "^3.1.0",
+    "npm_package_dependencies_react_measure": "^2.5.2",
+    "npm_package_dependencies_exif": "^0.6.0",
+    "NVM_BIN": "/Users/sarah/.nvm/versions/node/v12.16.0/bin",
+    "CONDA_DEFAULT_ENV": "base",
+    "npm_config_ignore_scripts": "",
+    "npm_config_user_agent": "npm/6.14.7 node/v12.16.0 darwin x64",
+    "npm_package_dependencies_react_resizable": "^1.11.1",
+    "npm_package_dependencies_prosemirror_commands": "^1.2.1",
+    "npm_package_dependencies_memorystream": "^0.3.1",
+    "npm_package_dependencies_formidable": "1.2.1",
+    "npm_package_devDependencies__types_uuid": "^3.4.10",
+    "npm_config_cache_lock_wait": "10000",
+    "npm_package_dependencies_socket_io_client": "^2.5.0",
+    "npm_package_dependencies_recharts": "^2.1.12",
+    "npm_package_dependencies_react_chartjs_2": "^4.3.0",
+    "npm_package_dependencies_fluent_ffmpeg": "^2.1.2",
+    "npm_package_dependencies__types_cors": "^2.8.12",
+    "npm_package_devDependencies__types_node": "^10.17.60",
+    "npm_package_devDependencies__types_file_saver": "^2.0.5",
+    "npm_config_production": "",
+    "npm_package_dependencies_jsonschema": "^1.4.0",
+    "npm_package_dependencies_ffmpeg": "0.0.4",
+    "npm_package_dependencies_cookie_session": "^2.0.0",
+    "npm_package_dependencies_color": "^3.2.1",
+    "npm_package_devDependencies__types_webpack": "^4.41.32",
+    "npm_package_devDependencies__types_request_promise": "^4.1.48",
+    "npm_package_devDependencies__types_prosemirror_schema_list": "^1.0.3",
+    "npm_config_send_metrics": "",
+    "npm_config_save_bundle": "",
+    "npm_package_dependencies_web_request": "^1.0.7",
+    "npm_package_dependencies_react_datepicker": "^3.8.0",
+    "npm_package_dependencies_express": "^4.17.3",
+    "npm_package_dependencies_D": "^1.0.0",
+    "npm_package_dependencies__types_formidable": "1.0.31",
+    "npm_package_devDependencies__types_rc_switch": "^1.9.2",
+    "npm_package_devDependencies__types_prosemirror_dev_tools": "^2.1.0",
+    "npm_package_devDependencies__types_jquery": "^3.5.14",
+    "npm_config_umask": "0022",
+    "npm_config_node_options": "",
+    "npm_config_init_version": "1.0.0",
+    "npm_package_dependencies_https": "^1.0.0",
+    "npm_package_dependencies_array_batcher": "^1.2.3",
+    "npm_package_dependencies__fortawesome_free_regular_svg_icons": "^6.3.0",
+    "npm_package_devDependencies__types_shelljs": "^0.8.11",
+    "npm_package_devDependencies__types_libxmljs": "^0.18.7",
+    "npm_package_devDependencies__types_express_validator": "^3.0.0",
+    "npm_package_devDependencies__types_bluebird": "^3.5.36",
+    "npm_config_init_author_name": "",
+    "npm_config_git": "git",
+    "npm_config_scope": "",
+    "npm_package_dependencies_react_select": "^3.2.0",
+    "npm_package_dependencies_pdf_parse": "^1.1.1",
+    "npm_package_dependencies_colors": "^1.4.0",
+    "npm_package_dependencies_archiver": "^3.1.1",
+    "npm_package_devDependencies_css_loader": "^2.1.1",
+    "npm_package_devDependencies__types_socket_io_client": "^1.4.36",
+    "SECURITYSESSIONID": "186a6",
+    "npm_config_unsafe_perm": "true",
+    "npm_config_tmp": "/var/folders/yk/p_39q8jn673c5p8_66mcxm7r0000gn/T",
+    "npm_config_onload_script": "",
+    "npm_package_dependencies_serializr": "^1.5.4",
+    "npm_package_dependencies_fit_curve": "^0.1.7",
+    "npm_package_dependencies__webscopeio_react_textarea_autocomplete": "^4.9.1",
+    "npm_package_dependencies__types_three": "^0.126.2",
+    "npm_package_devDependencies_ts_node_dev": "^2.0.0",
+    "npm_node_execpath": "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "npm_config_prefix": "/Users/sarah/.nvm/versions/node/v12.16.0",
+    "npm_config_link": "",
+    "npm_config_format_package_lock": "true",
+    "npm_package_dependencies_passport": "^0.4.0",
+    "npm_package_devDependencies_eslint_plugin_react": "^7.30.1",
+    "npm_package_devDependencies__types_react_table": "^6.8.9",
+    "npm_package_devDependencies__types_react_reconciler": "^0.26.4",
+    "NODE_OPTIONS": "--max_old_space_size=4096",
+    "TS_NODE_DEV": "true",
+    "_CLIENT_OPENAI_KEY": "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",
+    "VIPSHOME": "/usr/local/Cellar/vips/8.8.1",
+    "TYPESCRIPT_PATH": "/Users/sarah/Desktop/dash/Dash-Web/node_modules/typescript/lib/typescript.js",
+    "TSCONFIG": "/Users/sarah/Desktop/dash/Dash-Web/tsconfig.json",
+    "COMPILER_OPTIONS": "{}",
+    "TSLINT": "true",
+    "CONTEXT": "/Users/sarah/Desktop/dash/Dash-Web",
+    "TSLINTAUTOFIX": "false",
+    "ESLINT": "false",
+    "ESLINT_OPTIONS": "{}",
+    "WATCH": "",
+    "WORK_DIVISION": "1",
+    "MEMORY_LIMIT": "2048",
+    "CHECK_SYNTACTIC_ERRORS": "false",
+    "USE_INCREMENTAL_API": "true",
+    "VUE": "false"
+  },
+  "userLimits": {
+    "core_file_size_blocks": {
+      "soft": 0,
+      "hard": "unlimited"
+    },
+    "data_seg_size_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "file_size_blocks": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_locked_memory_bytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_memory_size_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "open_files": {
+      "soft": 1048575,
+      "hard": "unlimited"
+    },
+    "stack_size_bytes": {
+      "soft": 8388608,
+      "hard": 67104768
+    },
+    "cpu_time_seconds": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_user_processes": {
+      "soft": 2784,
+      "hard": 4176
+    },
+    "virtual_memory_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    }
+  },
+  "sharedObjects": [
+    "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
+    "/usr/lib/libobjc.A.dylib",
+    "/System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal",
+    "/usr/lib/liboah.dylib",
+    "/usr/lib/libfakelink.dylib",
+    "/usr/lib/libicucore.A.dylib",
+    "/usr/lib/libSystem.B.dylib",
+    "/System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking",
+    "/usr/lib/libc++abi.dylib",
+    "/usr/lib/libc++.1.dylib",
+    "/usr/lib/system/libcache.dylib",
+    "/usr/lib/system/libcommonCrypto.dylib",
+    "/usr/lib/system/libcompiler_rt.dylib",
+    "/usr/lib/system/libcopyfile.dylib",
+    "/usr/lib/system/libcorecrypto.dylib",
+    "/usr/lib/system/libdispatch.dylib",
+    "/usr/lib/system/libdyld.dylib",
+    "/usr/lib/system/libkeymgr.dylib",
+    "/usr/lib/system/libmacho.dylib",
+    "/usr/lib/system/libquarantine.dylib",
+    "/usr/lib/system/libremovefile.dylib",
+    "/usr/lib/system/libsystem_asl.dylib",
+    "/usr/lib/system/libsystem_blocks.dylib",
+    "/usr/lib/system/libsystem_c.dylib",
+    "/usr/lib/system/libsystem_collections.dylib",
+    "/usr/lib/system/libsystem_configuration.dylib",
+    "/usr/lib/system/libsystem_containermanager.dylib",
+    "/usr/lib/system/libsystem_coreservices.dylib",
+    "/usr/lib/system/libsystem_darwin.dylib",
+    "/usr/lib/system/libsystem_dnssd.dylib",
+    "/usr/lib/system/libsystem_featureflags.dylib",
+    "/usr/lib/system/libsystem_info.dylib",
+    "/usr/lib/system/libsystem_m.dylib",
+    "/usr/lib/system/libsystem_malloc.dylib",
+    "/usr/lib/system/libsystem_networkextension.dylib",
+    "/usr/lib/system/libsystem_notify.dylib",
+    "/usr/lib/system/libsystem_sandbox.dylib",
+    "/usr/lib/system/libsystem_secinit.dylib",
+    "/usr/lib/system/libsystem_kernel.dylib",
+    "/usr/lib/system/libsystem_platform.dylib",
+    "/usr/lib/system/libsystem_pthread.dylib",
+    "/usr/lib/system/libsystem_symptoms.dylib",
+    "/usr/lib/system/libsystem_trace.dylib",
+    "/usr/lib/system/libunwind.dylib",
+    "/usr/lib/system/libxpc.dylib",
+    "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
+    "/usr/lib/libDiagnosticMessagesClient.dylib",
+    "/usr/lib/libenergytrace.dylib",
+    "/usr/lib/libbsm.0.dylib",
+    "/usr/lib/libz.1.dylib",
+    "/usr/lib/system/libkxld.dylib",
+    "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
+    "/System/Library/Frameworks/Security.framework/Versions/A/Security",
+    "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
+    "/usr/lib/libapple_nghttp2.dylib",
+    "/usr/lib/libcompression.dylib",
+    "/usr/lib/libnetwork.dylib",
+    "/usr/lib/libsqlite3.dylib",
+    "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
+    "/System/Library/Frameworks/Network.framework/Versions/A/Network",
+    "/usr/lib/libCoreEntitlements.dylib",
+    "/System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity",
+    "/System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer",
+    "/usr/lib/libMobileGestalt.dylib",
+    "/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression",
+    "/usr/lib/libcoretls.dylib",
+    "/usr/lib/libcoretls_cfhelpers.dylib",
+    "/usr/lib/libpam.2.dylib",
+    "/usr/lib/libxar.1.dylib",
+    "/System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout",
+    "/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
+    "/usr/lib/libarchive.2.dylib",
+    "/usr/lib/libxml2.2.dylib",
+    "/usr/lib/liblangid.dylib",
+    "/System/Library/Frameworks/Combine.framework/Versions/A/Combine",
+    "/usr/lib/swift/libswiftCore.dylib",
+    "/usr/lib/swift/libswiftCoreFoundation.dylib",
+    "/usr/lib/swift/libswiftDarwin.dylib",
+    "/usr/lib/swift/libswiftDispatch.dylib",
+    "/usr/lib/swift/libswiftIOKit.dylib",
+    "/usr/lib/swift/libswiftObjectiveC.dylib",
+    "/usr/lib/swift/libswiftXPC.dylib",
+    "/usr/lib/swift/libswift_Concurrency.dylib",
+    "/usr/lib/swift/libswift_StringProcessing.dylib",
+    "/usr/lib/swift/libswiftos.dylib",
+    "/System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo",
+    "/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer",
+    "/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
+    "/usr/lib/libpcap.A.dylib",
+    "/usr/lib/libdns_services.dylib",
+    "/usr/lib/liblzma.5.dylib",
+    "/usr/lib/libbz2.1.0.dylib",
+    "/usr/lib/libiconv.2.dylib",
+    "/usr/lib/libcharset.1.dylib",
+    "/usr/lib/swift/libswift_RegexParser.dylib",
+    "/usr/lib/libheimdal-asn1.dylib",
+    "/usr/lib/libCheckFix.dylib",
+    "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
+    "/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP",
+    "/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
+    "/usr/lib/libmecab.dylib",
+    "/usr/lib/libCRFSuite.dylib",
+    "/usr/lib/libgermantok.dylib",
+    "/usr/lib/libThaiTokenizer.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib",
+    "/System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL",
+    "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
+    "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
+    "/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS",
+    "/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
+    "/usr/lib/libutil.dylib",
+    "/System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary",
+    "/System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore",
+    "/usr/lib/libapp_launch_measurement.dylib",
+    "/System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity",
+    "/usr/lib/libmis.dylib",
+    "/System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices",
+    "/System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper",
+    "/System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics",
+    "/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce",
+    "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
+    "/usr/lib/libxslt.1.dylib",
+    "/usr/lib/libcmph.dylib",
+    "/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
+    "/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData",
+    "/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon",
+    "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement",
+    "/usr/lib/libTLE.dylib",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
+    "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
+    "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
+    "/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
+    "/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib",
+    "/System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices",
+    "/System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator",
+    "/System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient",
+    "/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
+    "/System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia",
+    "/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
+    "/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
+    "/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
+    "/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
+    "/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
+    "/System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox",
+    "/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard",
+    "/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
+    "/usr/lib/libexpat.1.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
+    "/usr/lib/libate.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
+    "/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler",
+    "/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
+    "/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay",
+    "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libllvm-flatbuffers.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
+    "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libGPUCompilerUtils.dylib",
+    "/System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore",
+    "/usr/lib/libspindump.dylib",
+    "/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
+    "/System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation",
+    "/System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime",
+    "/System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport",
+    "/System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata",
+    "/System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore",
+    "/System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk",
+    "/usr/lib/libAudioStatistics.dylib",
+    "/System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy",
+    "/usr/lib/libSMC.dylib",
+    "/System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI",
+    "/usr/lib/libAudioToolboxUtility.dylib",
+    "/System/Library/PrivateFrameworks/OSAServicesClient.framework/Versions/A/OSAServicesClient",
+    "/usr/lib/libperfcheck.dylib",
+    "/System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit",
+    "/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
+    "/usr/lib/libRosetta.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions",
+    "/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools",
+    "/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary",
+    "/usr/lib/libIOReport.dylib",
+    "/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
+    "/System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing",
+    "/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
+    "/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer",
+    "/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices",
+    "/System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers",
+    "/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libhvf.dylib",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib",
+    "/usr/lib/swift/libswiftMetal.dylib",
+    "/usr/lib/swift/libswiftsimd.dylib",
+    "/System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport",
+    "/System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI",
+    "/usr/lib/libcups.2.dylib",
+    "/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
+    "/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
+    "/usr/lib/libresolv.9.dylib",
+    "/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
+    "/System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib",
+    "/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
+    "/System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio",
+    "/System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities",
+    "/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
+    "/System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession",
+    "/System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth",
+    "/System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience",
+    "/System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib",
+    "/System/Library/PrivateFrameworks/AudioResourceArbitration.framework/Versions/A/AudioResourceArbitration",
+    "/System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog",
+    "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
+    "/System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth",
+    "/System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit",
+    "/System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils",
+    "/System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras",
+    "/System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211",
+    "/System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag",
+    "/Users/sarah/Desktop/dash/Dash-Web/node_modules/fsevents/build/Release/fse.node"
+  ]
+}
\ No newline at end of file
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index caddc41a2..8e08265d6 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -13,6 +13,7 @@ enum GPTCallType {
     MERMAID = 'mermaid',
     DATA = 'data',
     STACK = 'stack',
+    PRONUNCIATION = 'pronunciation',
 }
 
 type GPTCallOpts = {
@@ -58,7 +59,13 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
-        prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate.',
+        prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate. If it is comparing two words, look for spelling and not capitalization and not punctuation.',
+    },
+    pronunciation: {
+        model: 'gpt-4-turbo',
+        maxTokens: 1024,
+        temp: 0,
+        prompt: 'List unique differences between the pronunciation of the ',
     },
 };
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index e095bc659..c712dba21 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -262,6 +262,7 @@ export class CurrentUserUtils {
             }
             return plotly;
         }
+        
         const plotlyView = (opts:DocumentOptions) => {
             const rtfield = new RichTextField(JSON.stringify(
                 {doc: {type:"doc",content:[
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index bc9fe813f..b9a465515 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -350,7 +350,7 @@ export namespace DictationManager {
                         const head = 3;
                         const anchor = head + prompt.length;
                         const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
-                        proto.data = new RichTextField(proseMirrorState);
+                        proto.data = new RichTextField(proseMirrorState, prompt);
                         proto.backgroundColor = '#eeffff';
                         target.props.addDocTab(newBox, OpenWhere.addRight);
                     },
diff --git a/src/client/views/.MetadataEntryMenu.tsx.icloud b/src/client/views/.MetadataEntryMenu.tsx.icloud
new file mode 100644
index 000000000..ada9927c9
Binary files /dev/null and b/src/client/views/.MetadataEntryMenu.tsx.icloud differ
diff --git a/src/client/views/.Touchable.tsx.icloud b/src/client/views/.Touchable.tsx.icloud
new file mode 100644
index 000000000..e458bbe47
Binary files /dev/null and b/src/client/views/.Touchable.tsx.icloud differ
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 6976deea5..c45b1494b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,7 +2,7 @@
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { Docs } from '../../documents/Documents';
 import * as React from 'react';
@@ -56,6 +56,8 @@ export class CollectionCarouselView extends CollectionSubView() {
         Doc.setDocFilter(this.Document, 'star', undefined, 'match');
         this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
+        this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
+        console.log(this.carouselItems.length);
     }
 
     componentWillUnmount() {
@@ -202,6 +204,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     };
 
     @computed get content() {
+        trace();
         if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
             this.move(1);
         }
diff --git a/src/client/views/nodes/.LinkAnchorBox.scss.icloud b/src/client/views/nodes/.LinkAnchorBox.scss.icloud
new file mode 100644
index 000000000..193619551
Binary files /dev/null and b/src/client/views/nodes/.LinkAnchorBox.scss.icloud differ
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 9deed4de4..5d0ed7eab 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../fields/DateField';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DateCast, NumCast } from '../../../fields/Types';
+import { Cast, DateCast, DocCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
 import { formatTime } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
@@ -29,6 +29,7 @@ import './AudioBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { OpenWhere } from './OpenWhere';
+import axios from 'axios';
 
 /**
  * AudioBox
@@ -257,6 +258,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
             if (!(result instanceof Error)) {
                 this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+                this.Document.url = result.accessPaths.agnostic.client;
+                await this.pushInfo();
             }
         };
         this._recordStart = new Date().getTime();
@@ -284,9 +287,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    pushInfo = async () => {
+        const audio = {
+            file: this.path,
+        };
+        const response = await axios.post('http://localhost:105/recognize/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        this.Document[DocData].phoneticTranscription = response.data['transcription'];
+        console.log('RESPONSE: ' + response.data['transcription']);
+    };
+
     // context menu
     specificContextMenu = (): void => {
         const funcs: ContextMenuProps[] = [];
+        // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' });
         funcs.push({
             description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
             event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 41ff56514..56a1e4fcc 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -26,11 +26,12 @@
     .submit-button {
         position: absolute;
         padding-bottom: 10px;
+        padding-top: 5px;
         padding-left: 5px;
         padding-right: 5px;
         // width: 80%;
         border-radius: 2px;
-        height: 15%;
+        height: 17%;
         display: inline-block;
         bottom: 0;
         // right: 0;
@@ -48,8 +49,13 @@
             }
         }
 
+        &.pronunciation {
+            width: 40%;
+            align-items: center;
+            justify-content: center;
+        }
         &.submit {
-            width: 80%;
+            width: 40%;
             // float: right;
 
             // position: absolute;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3d2a1f0a9..ce29a63b4 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,4 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { MathJax, MathJaxContext } from 'better-react-mathjax';
 import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -33,6 +34,8 @@ import { FollowLinkScript } from '../../documents/DocUtils';
 import { schema } from '../nodes/formattedText/schema_rts';
 import { Id } from '../../../fields/FieldSymbols';
 import axios from 'axios';
+import ReactMarkdown from 'react-markdown';
+import { WebField, nullAudio } from '../../../fields/URLField';
 
 const API_URL = 'https://api.unsplash.com/search/photos';
 @observer
@@ -51,9 +54,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _outputValue = '';
     @observable private _loading = false;
     @observable private _isEmpty = false;
+    @observable private _audio: Doc = Docs.Create.TextDocument('');
     @observable childActive = false;
     @observable _yRelativeToTop: boolean = true;
     @observable _animating = '';
+    @observable mathJaxConfig = {
+        loader: { load: ['input/asciimath'] },
+    };
     private _ref = React.createRef<HTMLDivElement>();
 
     get revealOp() {
@@ -237,11 +244,71 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this.childActive = true;
     };
 
-    @action handleRenderGPTClick = () => {
+    @action handleRenderGPTClick = async () => {
         // Call the GPT model and get the output
+        // await this.pushInfo();
+        // console.log('PHONETIC TRANSCRIPTION: ' + DocCast(this._audio)[DocData]);
+        // this.Document.audio = this._audio;
+        console.log('Phonetic transcription: ' + DocCast(this.Document.audio).phoneticTranscription);
+        const phonTrans = DocCast(this.Document.audio).phoneticTranscription;
+        if (phonTrans) {
+            this._inputValue = StrCast(phonTrans);
+            console.log('INPUT:' + this._inputValue);
+            this.askGPTPhonemes(this._inputValue);
+        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
         this._outputValue = '';
-        if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+    };
+
+    askGPTPhonemes = async (phonemes: string) => {
+        const question0 = 'These phonemes should match "what is your name": ' + phonemes + 'Use the structure of this response as guidance: "Your pronunciation of the vowel in "what" is not front enough. It should be pronounced like /uh/."';
+        const question =
+            'Match the following phonemes with each word in "what is your name": ' +
+            phonemes +
+            '. Note if a letter is added or missing that changes the meaning. If the mismatches are not allophones of the same phoneme and they are far away from each other on the vowel chart, list the difference. For the mismatches, use the structure of this response as guidance: "Your pronunciation of the vowel in "what" is not front enough. It should be pronounced like /uh/."';
+        const question1 =
+            'Consider all phonetic transcriptions of "what is your name" with different vowel pronunications. Compares these phonemes with that phonetic transcription: ' +
+            phonemes +
+            '. If the differences are not allophones of the same phoneme and they are far away from each other on the vowel chart, list the difference. If it is missing or added a letter, say that.';
+        //Only describe sound changes that will change the meaning drastically. Provide two sentences describing this. Do not list differences that do not change the meaning.';
+        const question2 = 'Is this a valid phonetic transcription of the phrase "what is your name": ' + phonemes + '.';
+        // If the difference found will definitely make the word be not understood and change the meaning, then list it. If the difference is minimal or the sound matches, do not list it.';
+        //These phonemes are supposed to match the pronunciation of ' +
+        //'hello: ' +
+        //phonemes +
+        //'. If there is a difference in sound that would change the meaning of the word or sentence, such as "pen" vs. "pin", describe that. Otherwise say "good job."';
+        // Identify any differences in pronunciation that would change the meaning of the intended word or sentence and only list differences that would change the meaning. If there are no major differences, say "Good job." If there are differences, describe it in terms of sounds in sentences.';
+        // const question =
+        //     'These phonemes are supposed to match the pronunciation of ' +
+        //     StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text) +
+        //     '. Identify any differences in pronunciation that would change the meaning of the intended word or sentence.';
+        console.log(question);
+        const res = await gptAPICall(question, GPTCallType.PRONUNCIATION);
+        console.log('GPT: ' + res);
+        if (!res) {
+            console.error('GPT call failed');
+            return;
+        }
+        // const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+        // const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
+        // const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
+        // this._loading = true;
+    };
+
+    pushInfo = async () => {
+        const formData = new FormData();
+
+        console.log(DocCast(this._audio).dataDoc);
+        const audio = {
+            file: this._audio.url,
+        };
+        const response = await axios.post('http://localhost:105/recognize/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        this.Document.phoneticTranscription = response.data['transcription'];
+        console.log('RESPONSE: ' + response.data['transcription']);
     };
 
     @action handleHover = () => {
@@ -391,22 +458,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         var collectionArr: Doc[] = [];
         for (let i = 1; i < senArr?.length!; i++) {
             const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
-            // newDoc.text = senArr![i];
-            // const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-            // newDoc[this.fieldKey + '_0'] = Docs.Create.TextDocument(dataSplit[1]);
-            // newDoc[this.fieldKey + '_1'] = Docs.Create.TextDocument(dataSplit[0]);
-            // newDoc['backgroundColor'] = 'lightgray';
-            // newDoc.image =
+
             if (StrCast(senArr![i]).includes('Keyword: ')) {
-                console.log('Here');
-                const img = await this.fetchImages(StrCast(senArr![i]).split('Keyword: ')[1]);
-                console.log(img);
-                DocCast(newDoc).image = img;
-                // DocCast(DocCast(newDoc).dataDoc)['image'] = img;
-                Doc.AddToMyOverlay(img);
+                const question = StrCast(senArr![i]).split('Keyword: ');
+                const img = await this.fetchImages(question[1]);
+                // newDoc['image'] = img;
+                // const newDoc = Docs.Create.TextDocument(dataSplit[1]);
+                const textSide1 = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0];
+                const textDoc1 = Docs.Create.TextDocument(question[0]);
+                const rtfiel = new RichTextField(
+                    JSON.stringify({
+                        doc: {
+                            type: 'doc',
+                            content: [
+                                {
+                                    type: 'paragraph',
+                                    attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                                    content: [
+                                        { type: 'text', text: question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0] },
+                                        { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img![Id] } },
+                                    ],
+                                },
+                            ],
+                        },
+                        selection: { type: 'text', anchor: 2, head: 2 },
+                    }),
+                    textSide1
+                );
+
+                textDoc1[DocData].text = rtfiel;
+                DocCast(newDoc)[DocData][this.fieldKey + '_1'] = textDoc1;
+
+                DocCast(newDoc)[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1]);
+                // Doc.AddToMyOverlay(img!);
             }
 
-            console.log('ARR' + i + senArr![i]);
             collectionArr.push(newDoc);
         }
         this.createFlashcardPile(collectionArr, true);
@@ -473,7 +559,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (callType == GPTCallType.CHATCARD) {
                 DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
                 // this.flipFlashcard();
-            } else if (callType == GPTCallType.QUIZ) this._outputValue = res;
+            } else if (callType == GPTCallType.QUIZ) {
+                console.log(this._inputValue);
+                this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+            }
             // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
             // this._outputValue = res;
             else if (callType === GPTCallType.FLASHCARD) {
@@ -511,7 +600,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 if (i.className !== 'ProseMirror-separator') await this.getImageDesc(i.src);
             }
             this._loading = false;
-            this.flipFlashcard();
+            // this.flipFlashcard();
         }
         // console.log('HI' + this.ProseRef?.getElementsByTagName('img'));
     };
@@ -558,22 +647,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 _height: 150,
                 title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
             });
-            // return imageSnapshot;
             imageSnapshot['x'] = this.layoutDoc['x'];
             imageSnapshot['y'] = this.layoutDoc['y'];
-
-            // const newDoc = Docs.Create.TextDocument(selection);
-            // newDoc.text = selection;
-            // newDoc['backgroundColor'] = 'lightgray';
-
-            // Doc.AddToMyOverlay(imageSnapshot);
             return imageSnapshot;
-            return data.results[0].urls.small;
-            // Doc.AddEmbedding(newDoc, imageSnapshot);
-            // Doc.MakeEmbedding(imageSnapshot);
-            // return imageSnapshot;
-            // imageSnapshot['zIndex'] = 20000;
-            // this._props.DocumentView?.()._props.addDocument?.(newDoc);
         } catch (error) {
             console.log(error);
         }
@@ -660,6 +736,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         ContextMenu.Instance.displayMenu(x, y);
     };
 
+    evaluatePronunciation = () => {
+        const newAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100 });
+        this.Document.audio = newAudio[DocData];
+        // DocCast(this.Document.embedContainer)()._props.addDocument?.(newAudio);
+        this._props.DocumentView?.()._props.addDocument?.(newAudio);
+        // Doc.AddToMyOverlay(newAudio);
+    };
+
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -680,6 +764,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor'];
 
             return targetDoc || layoutString ? (
+                // <MathJaxContext config={this.mathJaxConfig}>
+                //     <MathJax>
                 <>
                     <DocumentView
                         // eslint-disable-next-line react/jsx-props-no-spreading
@@ -701,8 +787,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this.childActive ? undefined : returnNone}
                     />
+                    {/* </MathJax> */}
                     {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
-                </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
+                    {/* </MathJaxContext> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` */}
+                </>
             ) : (
                 <div className="placeholder">
                     <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
@@ -732,79 +820,36 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[1]);
-                if (this.Document.image) DocCast(newDoc).image = DocCast(this.Document.image);
-                // console.log('D' + this.Document.image);
-                //if (DocCast(DocCast(newDoc).dataDoc)) DocCast(DocCast(newDoc).dataDoc)['image'] = this.dataDoc['image'];
-
-                // console.log('HI' + this.Document.image);
-                // const imageSnapshot = Docs.Create.ImageDocument(StrCast(this.Document.image), {
-                //     _nativeWidth: Doc.NativeWidth(this.layoutDoc),
-                //     _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-                //     x: NumCast(this.layoutDoc.x),
-                //     y: NumCast(this.layoutDoc.y),
-                //     onClick: FollowLinkScript(),
-                //     _width: 150,
-                //     _height: 150,
-                //     title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
-                // });
-                // // return imageSnapshot;
-                // imageSnapshot['x'] = this.layoutDoc['x'];
-                // imageSnapshot['y'] = this.layoutDoc['y'];
-
-                // const newDoc = Docs.Create.TextDocument(selection);
-                // newDoc.text = selection;
-                // newDoc['backgroundColor'] = 'lightgray';
-                // newDoc.data = imageSnapshot;
-                // this.createDropTarget(this., this.fieldKey + '_0', 0)
-                // Doc.AddEmbedding(imageSnapshot, newDoc);
-                // Doc.SetContainer(imageSnapshot, newDoc);
-                // Doc.AddToMyOverlay(imageSnapshot);
-
-                // if (StrCast(this.dataDoc.data).includes('Keyword: ')) {
-                //     console.log('HERE' + this.dataDoc.data);
-                //     this.fetchImages(StrCast(this.dataDoc.data).split('Keyword: ')[1]);
-                // }
-                //     // const node = schema.nodes.dashDoc.create({
-                //     //     width: NumCast(newDoc._width),
-                //     //     height: NumCast(newDoc._height),
-                //     //     title: 'dashDoc',
-                //     //     docId: newDoc[Id],
-                //     //     float: 'unset',
-                //     // });
-                //     // Doc.AddEmbedding(images!, newDoc);
-                //     // Doc.SetContainer(images, this.newDoc);
-                // } else {
-                // newDoc.text = dataSplit[1];
-                // newDoc['backgroundColor'] = 'lightgray';
                 this.addDoc(newDoc, this.fieldKey + '_0');
-                // this.addDoc()
-                // }
-                // newDoc?.addDocument?.(images);
-
-                // if there is text from the pdf ai cards, put the question on the front side.
-                // eslint-disable-next-line prefer-destructuring
-                // newDoc.text = dataSplit[1];
-                // newDoc['backgroundColor'] = 'lightgray';
-                // this.addDoc(newDoc, this.fieldKey + '_0');
-                // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1];
-                // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1];
-                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text);
-                //console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
             }
 
             if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
                 this.addDoc(newDoc, this.fieldKey + '_1');
-                // if there is text from the pdf ai cards, put the answer on the alternate side.
-                // eslint-disable-next-line prefer-destructuring
-
-                // newDoc[DocData].text = dataSplit[0];
-                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
-                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_1']).text);
-                // DocCast(this.dataDoc[this.props.fieldKey + '_1'])[DocData].text = dataSplit[0];
-                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
-                // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = dataSplit[0];
+                // if (this.Document.image) {
+                //     console.log('ID: ' + DocCast(this.Document.image)[Id]);
+                //     const rtfiel = new RichTextField(
+                //         JSON.stringify({
+                //             doc: {
+                //                 type: 'doc',
+                //                 content: [
+                //                     {
+                //                         type: 'paragraph',
+                //                         attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                //                         content: [
+                //                             { type: 'text', text: dataSplit[0] },
+                //                             { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: DocCast(this.Document.image)[Id] } },
+                //                         ],
+                //                     },
+                //                 ],
+                //             },
+                //             selection: { type: 'text', anchor: 2, head: 2 },
+                //         })
+                //     );
+
+                //     newDoc[DocData].text = rtfiel;
+                // }
             }
 
             // render the QuizCards
@@ -819,7 +864,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <textarea
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
-                                onScroll={e => e.stopPropagation()}
+                                onScroll={e => {
+                                    e.stopPropagation();
+                                    e.preventDefault();
+                                }}
                                 placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
                                 readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea>
 
@@ -834,19 +882,25 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 <div
                                     className="submit-buttonschema-header-button"
                                     onPointerDown={e => this.openContextMenu(e.clientX, e.clientY)}
-                                    style={{ position: 'absolute', top: '1px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}>
+                                    style={{ position: 'absolute', top: '5px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}>
                                     <FontAwesomeIcon color={'white'} icon="caret-down" />
                                 </div>
                                 <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '', borderRadius: '2px' }}>
                                     {<FontAwesomeIcon icon="microphone" size="lg" />}
                                 </button>
+                                <button
+                                    className="submit-buttonpronunciation"
+                                    onClick={this.evaluatePronunciation}
+                                    style={{ display: 'inline-flex', alignItems: 'center', background: this._listening ? 'lightgray' : '', borderRadius: '2px', width: '100%' }}>
+                                    Evaluate Pronunciation
+                                </button>
 
                                 {this.layoutDoc[`_${this._props.fieldKey}_usePath`] !== 'alternate' ? (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '300%' }}>
+                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
                                         Submit
                                     </button>
                                 ) : (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}>
+                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
                                         Redo the Question
                                     </button>
                                 )}
@@ -871,7 +925,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     }}
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
-                    {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null}
+                    {/* {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null} */}
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
                     {this._loading ? (
                         <div className="loading-spinner" style={{ position: 'absolute' }}>
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 138f00492..b9c3528d3 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,7 +50,7 @@ export interface FieldViewSharedProps {
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
     isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
-    dontSelect: () => boolean | undefined;
+    dontSelect?: () => boolean | undefined;
     childFilters: () => string[];
     childFiltersByRanges: () => string[];
     styleProvider: Opt<StyleProviderFuncType>;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 93c07f3a8..ab7605829 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -384,17 +384,93 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._loading = false;
     };
 
+    getImageLabels = async () => {
+        this._loading = true;
+        try {
+            const hrefBase64 = await this.createCanvas();
+            // const hw = await gptImageLabel(hrefBase64, 'Find the image dimensions. Return as height and width.');
+            const response = await gptImageLabel(
+                hrefBase64,
+                //'What is the height and width of the image'
+                'For each group of words in the image, find the x-coordinate and ycoordinate of the top left corner. Find the width and height of the group. Return this information in this format with the correct information replacing the underscores: "observed word: _, x: _, y: _, width: _, height: _," No additional text, asterisks and put it all in one line. Divide the x and width by the width of the image. Divide the y and the height by the height of the image.'
+            );
+            // console.log(hw);
+            console.log('RESPONSE: ' + response);
+            this.createTextboxes(response);
+
+            //AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
+        } catch (error) {
+            console.log('Error');
+        }
+        this._loading = false;
+    };
+
+    createTextboxes = (response: string) => {
+        const groups = response.replace('*', '').toLowerCase().split('observed word: ');
+        groups.shift();
+        for (var i = 0; i < groups.length; i++) {
+            console.log('Group ' + i + ': ' + groups[i]);
+        }
+        // console.log('GROUPS: ' + groups);
+        groups.forEach(
+            group => {
+                const groupArr = group.split(', ');
+                const word = groupArr[0];
+                const x = parseFloat(groupArr[1].substring(3));
+                const y = parseFloat(groupArr[2].substring(3));
+                const width = parseFloat(groupArr[3].substring(7));
+                const height = parseFloat(groupArr[4].substring(8));
+                const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
+                const w = AnchorMenu.Instance.marqueeWidth * scaling;
+                const h = AnchorMenu.Instance.marqueeHeight * scaling;
+
+                const newCol = Docs.Create.TextDocument(word, {
+                    _width: w * width,
+                    //width * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']),
+                    _height: h * height + 20,
+                    //height * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']),
+                    _layout_fitWidth: true,
+                    _layout_autoHeight: true,
+                });
+                newCol.x = x * w;
+                newCol.y = y * h;
+                // newCol.x = x * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+                // newCol.y = y * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+                newCol.zIndex = 1000;
+                newCol.forceActive = true;
+                newCol.quiz = true;
+                this.addDocument(newCol);
+            }
+            // console.log(group);
+        );
+    };
+
     @action
     setRef = (iref: HTMLImageElement | null) => {
         this._imageRef = iref;
     };
 
+    pushInfo = async () => {
+        const formData = new FormData();
+
+        const newArticle = {
+            file: '/files/audio/6b412a6222d631a7fff8a8320.mp3',
+        };
+        const response = await axios.post('http://localhost:105/recognize/', newArticle, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        console.log('RESPONSE: ' + response.data['transcription']);
+    };
+
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
             // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' });
-            funcs.push({ description: 'Get Images', event: () => this.handleSelection('Cats'), icon: 'redo-alt' });
+            // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' });
+            funcs.push({ description: 'Get Labels', event: this.getImageLabels, icon: 'redo-alt' });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
new file mode 100644
index 000000000..0a4325d8c
--- /dev/null
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -0,0 +1,115 @@
+import { action, computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
+import { DragManager, dropActionType } from '../../util/DragManager';
+import { LinkFollower } from '../../util/LinkFollower';
+import { SelectionManager } from '../../util/SelectionManager';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LinkAnchorBox.scss';
+import { LinkInfo } from './LinkDocPreview';
+const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+@observer
+export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(LinkAnchorBox, fieldKey);
+    }
+    _doubleTap = false;
+    _lastTap: number = 0;
+    _ref = React.createRef<HTMLDivElement>();
+    _isOpen = false;
+    _timeout: NodeJS.Timeout | undefined;
+
+    constructor(props: FieldViewProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        this._props.setContentViewBox?.(this);
+    }
+
+    @computed get linkSource() {
+        return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource);
+    }
+
+    onPointerDown = (e: React.PointerEvent) => {
+        const linkSource = this.linkSource;
+        linkSource &&
+            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
+                if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false);
+                else this._props.select(false);
+            });
+    };
+    onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
+        const cdiv = this._ref?.current?.parentElement;
+        if (!this._isOpen && cdiv) {
+            const bounds = cdiv.getBoundingClientRect();
+            const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+            const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+            if (separation > 100) {
+                const dragData = new DragManager.DocumentDragData([this.Document]);
+                dragData.dropAction = dropActionType.embed;
+                dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
+                DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
+                return true;
+            } else {
+                this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
+                this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+                this.layoutDoc.link_autoMoveAnchors = false;
+            }
+        }
+        return false;
+    });
+
+    specificContextMenu = (e: React.MouseEvent): void => {};
+
+    render() {
+        TraceMobx();
+        const small = this._props.PanelWidth() <= 1; // this happens when rendered in a treeView
+        const x = NumCast(this.layoutDoc[this.fieldKey + '_x'], 100);
+        const y = NumCast(this.layoutDoc[this.fieldKey + '_y'], 100);
+        const background = this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.BackgroundColor + ':anchor');
+        const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
+        const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
+        const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
+        const selView = SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_1')
+            ? 'link_anchor_1'
+            : SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_2')
+              ? 'link_anchor_2'
+              : '';
+        return (
+            <div
+                ref={this._ref}
+                title={targetTitle}
+                className={`linkAnchorBox-cont${small ? '-small' : ''}`}
+                onPointerEnter={e =>
+                    LinkInfo.SetLinkInfo({
+                        DocumentView: this.DocumentView,
+                        styleProvider: this._props.styleProvider,
+                        linkSrc: this.linkSource,
+                        linkDoc: this.Document,
+                        showHeader: true,
+                        location: [e.clientX, e.clientY + 20],
+                        noPreview: false,
+                    })
+                }
+                onPointerDown={this.onPointerDown}
+                onContextMenu={this.specificContextMenu}
+                style={{
+                    border: selView && this.dataDoc[selView] === this.dataDoc[this.fieldKey] ? `solid ${MEDIUM_GRAY} 2px` : undefined,
+                    background,
+                    left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
+                    top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
+                    transform: `scale(${anchorScale})`,
+                    cursor: 'grab',
+                }}
+            />
+        );
+    }
+}
diff --git a/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav
new file mode 100644
index 000000000..dc71e7886
Binary files /dev/null and b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav differ
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3c12db965..274330d31 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -3,7 +3,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { baseKeymap, selectAll } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
@@ -361,10 +361,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData;
                 if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
-                    textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                     if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                         if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
+                            textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
                             dataDoc[this.fieldKey] =
                                 numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
@@ -373,6 +373,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             unchanged = false;
                         }
                     } else if (rtField) {
+                        textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
                         this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
@@ -2206,7 +2207,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         />
                     </div>
                     {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
-                    {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
+                    {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle}
                     {this.audioHandle}
                     {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
                 </div>
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index 3f13f7e6d..613bb0fd1 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -13,10 +13,15 @@ export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Text: string;
 
-    constructor(data: string, text: string = '') {
+    /**
+     * NOTE: if 'text' doesn't match the plain text of 'data', this can cause infinite loop problems or other artifacts when rendered.
+     * @param data this is the formatted text representation of the RTF
+     * @param text this is the plain text of whatever text is in the 'data'
+     */
+    constructor(data: string, text: string) {
         super();
         this.Data = data;
-        this.Text = text;
+        this.Text = text; // ideally, we'd compute 'text' from 'data' by doing what Prosemirror does at run-time ... just need to figure out how to write that function accurately
     }
 
     Empty() {
-- 
cgit v1.2.3-70-g09d2


From 11244a698bce594982ee5dca55b9695bb774451c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 7 Oct 2024 18:13:38 -0400
Subject: moved all quiz code out of LabelBox and ImageBox and into
 StyleProviderQuiz.  changed quizBoxes and quizMode to be stored as Doc
 metadata. Extended styles to cover contextMenuItems.   remove
 this.setListening() from comparisonBox until contextMenu selectedVal is
 fixed.

---
 src/client/views/StyleProp.ts            |   2 +
 src/client/views/StyleProvider.tsx       |  10 +-
 src/client/views/StyleProviderQuiz.scss  |  40 ++++
 src/client/views/StyleProviderQuiz.tsx   | 391 +++++++++++++++++++++++++++++++
 src/client/views/nodes/ComparisonBox.tsx |   1 -
 src/client/views/nodes/DocumentView.tsx  |   3 +
 src/client/views/nodes/FieldView.tsx     |   2 +
 src/client/views/nodes/ImageBox.scss     |  41 ----
 src/client/views/nodes/ImageBox.tsx      | 365 ++---------------------------
 src/client/views/nodes/LabelBox.tsx      |  52 +---
 10 files changed, 466 insertions(+), 441 deletions(-)
 create mode 100644 src/client/views/StyleProviderQuiz.scss
 create mode 100644 src/client/views/StyleProviderQuiz.tsx

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index dd5b98cfe..44d3bf757 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -21,4 +21,6 @@ export enum StyleProp {
     FontFamily = 'fontFamily', // font family of text
     FontWeight = 'fontWeight', // font weight of text
     Highlighting = 'highlighting', // border highlighting
+    ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
+    AnchorMenuItems = 'anchorMenuItems',
 }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 16f6aa40b..44bea57eb 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -21,6 +21,7 @@ import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
 import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
+import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
 import { TagsView } from './TagsView';
@@ -89,6 +90,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         containerViewPath,
         childFilters,
         hideCaptions,
+        hideFilterStatus,
         showTitle,
         childFiltersByRanges,
         renderDepth,
@@ -110,6 +112,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
     const color = () => styleProvider?.(doc, props, StyleProp.Color) as string;
     const opacity = () => styleProvider?.(doc, props, StyleProp.Opacity);
     const layoutShowTitle = () => styleProvider?.(doc, props, StyleProp.ShowTitle) as string;
+
+    // bcz: For now, this is how to add custom-stylings (like a Quiz styling) for app-specific purposes.  The quiz styling will short-circuit
+    //      the regular stylings for items that it controls (eg., things with a quiz field, or images)
+    const quizProp = styleProviderQuiz.quizStyleProvider(doc, props, property);
+    if (quizProp !== undefined) return quizProp;
+
     // prettier-ignore
     switch (property.split(':')[0]) {
         case StyleProp.TreeViewIcon: {
@@ -318,7 +326,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length
                         ? 'orange' // 'inheritsFilter'
                         : undefined;
-                return !showFilterIcon || props?.hideFilterStatus ? null : (
+                return !showFilterIcon || hideFilterStatus ? null : (
                     <div className="styleProvider-filter">
                         <Dropdown
                             type={Type.TERT}
diff --git a/src/client/views/StyleProviderQuiz.scss b/src/client/views/StyleProviderQuiz.scss
new file mode 100644
index 000000000..2f52c8dec
--- /dev/null
+++ b/src/client/views/StyleProviderQuiz.scss
@@ -0,0 +1,40 @@
+.loading-spinner {
+    position: absolute;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    // left: 50%;
+    // top: 50%;
+    z-index: 200;
+    font-size: 20px;
+    font-weight: bold;
+    color: #17175e;
+}
+
+.check-icon {
+    position: absolute;
+    right: 40;
+    bottom: 10;
+    color: green;
+    display: inline-block;
+    font-size: 20px;
+    overflow: hidden;
+}
+
+.redo-icon {
+    position: absolute;
+    right: 10;
+    bottom: 10;
+    color: black;
+    display: inline-block;
+    font-size: 20px;
+    overflow: hidden;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
new file mode 100644
index 000000000..8973ada95
--- /dev/null
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -0,0 +1,391 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import axios from 'axios';
+import * as React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
+import { List } from '../../fields/List';
+import { NumCast, StrCast } from '../../fields/Types';
+import { GPTCallType, gptAPICall, gptImageLabel } from '../apis/gpt/GPT';
+import { Docs } from '../documents/Documents';
+import { ContextMenu } from './ContextMenu';
+import { ContextMenuProps } from './ContextMenuItem';
+import { StyleProp } from './StyleProp';
+import { AnchorMenu } from './pdf/AnchorMenu';
+import { DocumentViewProps } from './nodes/DocumentView';
+import { FieldViewProps } from './nodes/FieldView';
+import { ImageBox } from './nodes/ImageBox';
+import { ImageUtility } from './nodes/generativeFill/generativeFillUtils/ImageHandler';
+import './StyleProviderQuiz.scss';
+
+export namespace styleProviderQuiz {
+    enum quizMode {
+        SMART = 'smart',
+        NORMAL = 'normal',
+        NONE = 'none',
+    }
+
+    async function selectUrlToBase64(blob: Blob): Promise<string> {
+        try {
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader();
+                reader.readAsDataURL(blob);
+                reader.onloadend = () => resolve(reader.result as string);
+                reader.onerror = error => reject(error);
+            });
+        } catch (error) {
+            console.error('Error:', error);
+            throw error;
+        }
+    }
+    /**
+     * Creates label boxes over text on the image to be filled in.
+     * @param boxes
+     * @param texts
+     */
+    async function createBoxes(img: ImageBox, boxes: [[[number, number]]], texts: [string]) {
+        img.Document._quizBoxes = new List<Doc>([]);
+        for (let i = 0; i < boxes.length; i++) {
+            const coords = boxes[i] ? boxes[i] : [];
+            const width = coords[1][0] - coords[0][0];
+            const height = coords[2][1] - coords[0][1];
+            const text = texts[i];
+
+            const newCol = Docs.Create.LabelDocument({
+                _width: width,
+                _height: height,
+                _layout_fitWidth: true,
+                title: '',
+            });
+            const scaling = 1 / (img._props.NativeDimScaling?.() || 1);
+            newCol.x = coords[0][0] + NumCast(img.marqueeref.current?.left) * scaling;
+            newCol.y = coords[0][1] + NumCast(img.marqueeref.current?.top) * scaling;
+
+            newCol.zIndex = 1000;
+            newCol.forceActive = true;
+            newCol.quiz = text;
+            newCol[DocData].textTransform = 'none';
+            Doc.AddDocToList(img.Document, '_quizBoxes', newCol);
+            img.addDocument(newCol);
+            //   img._loading = false;
+        }
+    }
+
+    /**
+     * Calls backend to find any text on an image. Gets the text and the
+     * coordinates of the text and creates label boxes at those locations.
+     * @param quiz
+     * @param i
+     */
+    async function pushInfo(imgBox: ImageBox, quiz: quizMode, i?: string) {
+        imgBox.Document._quizMode = quiz;
+        const quizBoxes = DocListCast(imgBox.Document.quizBoxes);
+        if (!quizBoxes.length) {
+            //    this._loading = true;
+
+            const img = {
+                file: i ? i : imgBox.paths[0],
+                drag: i ? 'drag' : 'full',
+                smart: quiz,
+            };
+            const response = await axios.post('http://localhost:105/labels/', img, {
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+            });
+            if (response.data['boxes'].length != 0) {
+                createBoxes(imgBox, response.data['boxes'], response.data['text']);
+            } else {
+                // this._loading = false;
+            }
+        } else quizBoxes.forEach(box => (box.hidden = false));
+    }
+
+    async function createCanvas(img: ImageBox) {
+        const canvas = document.createElement('canvas');
+        const scaling = 1 / (img._props.NativeDimScaling?.() || 1);
+        const w = AnchorMenu.Instance.marqueeWidth * scaling;
+        const h = AnchorMenu.Instance.marqueeHeight * scaling;
+        canvas.width = w;
+        canvas.height = h;
+        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
+        if (ctx) {
+            img.imageRef && ctx.drawImage(img.imageRef, NumCast(img.marqueeref.current?.left) * scaling, NumCast(img.marqueeref.current?.top) * scaling, w, h, 0, 0, w, h);
+        }
+        const blob = await ImageUtility.canvasToBlob(canvas);
+        return selectUrlToBase64(blob);
+    }
+    /**
+     * Create flashcards from an image.
+     */
+    async function getImageDesc(img: ImageBox) {
+        // this._loading = true;
+        try {
+            const hrefBase64 = await createCanvas(img);
+            const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
+            AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc['x']), NumCast(img.layoutDoc['y']));
+        } catch (error) {
+            console.log('Error', error);
+        }
+        // this._loading = false;
+    }
+
+    /**
+     * Calls the createCanvas and pushInfo methods to convert the
+     * image to a form that can be passed to GPT and find the locations
+     * of the text.
+     */
+    async function makeLabels(img: ImageBox) {
+        try {
+            const hrefBase64 = await createCanvas(img);
+            pushInfo(img, quizMode.NORMAL, hrefBase64);
+        } catch (error) {
+            console.log('Error', error);
+        }
+    }
+
+    /**
+     * Determines whether two words should be considered
+     * the same, allowing minor typos.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function levenshteinDistance(str1: string, str2: string) {
+        const len1 = str1.length;
+        const len2 = str2.length;
+        const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0));
+
+        if (len1 === 0) return len2;
+        if (len2 === 0) return len1;
+
+        for (let i = 0; i <= len1; i++) dp[i][0] = i;
+        for (let j = 0; j <= len2; j++) dp[0][j] = j;
+
+        for (let i = 1; i <= len1; i++) {
+            for (let j = 1; j <= len2; j++) {
+                const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
+                dp[i][j] = Math.min(
+                    dp[i - 1][j] + 1, // deletion
+                    dp[i][j - 1] + 1, // insertion
+                    dp[i - 1][j - 1] + cost // substitution
+                );
+            }
+        }
+
+        return dp[len1][len2];
+    }
+
+    /**
+     * Different algorithm for determining string similarity.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function jaccardSimilarity(str1: string, str2: string) {
+        const set1 = new Set(str1.split(' '));
+        const set2 = new Set(str2.split(' '));
+
+        const intersection = new Set([...set1].filter(x => set2.has(x)));
+        const union = new Set([...set1, ...set2]);
+
+        return intersection.size / union.size;
+    }
+
+    /**
+     * Averages the jaccardSimilarity and levenshteinDistance scores
+     * to determine string similarity for the labelboxes answers and
+     * the users response.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function stringSimilarity(str1: string, str2: string) {
+        const levenshteinDist = levenshteinDistance(str1, str2);
+        const levenshteinScore = 1 - levenshteinDist / Math.max(str1.length, str2.length);
+
+        const jaccardScore = jaccardSimilarity(str1, str2);
+
+        // Combine the scores with a higher weight on Jaccard similarity
+        return 0.5 * levenshteinScore + 0.5 * jaccardScore;
+    }
+    /**
+     * Returns whether two strings are similar
+     * @param input
+     * @param target
+     * @returns
+     */
+    function compareWords(input: string, target: string) {
+        const distance = stringSimilarity(input.toLowerCase(), target.toLowerCase());
+        return distance >= 0.7;
+    }
+
+    /**
+     * GPT returns a hex color for what color the label box should be based on
+     * the correctness of the users answer.
+     * @param inputString
+     * @returns
+     */
+    function extractHexAndSentences(inputString: string) {
+        // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences
+        const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s;
+        const match = inputString.match(regex);
+
+        if (match) {
+            const hexNumber = match[1];
+            const sentences = match[2].trim();
+            return { hexNumber, sentences };
+        } else {
+            return { error: 'The input string does not match the expected format.' };
+        }
+    }
+    /**
+     * Check whether the contents of the label boxes on an image are correct.
+     */
+    function check(img: ImageBox) {
+        //this._loading = true;
+        img.quizBoxes.forEach(async doc => {
+            const input = StrCast(doc[DocData].title);
+            if (img.quizMode == quizMode.SMART && input) {
+                const questionText = 'Question: What was labeled in this image?';
+                const rubricText = ' Rubric: ' + StrCast(doc.quiz);
+                const queryText =
+                    questionText +
+                    ' UserAnswer: ' +
+                    input +
+                    '. ' +
+                    rubricText +
+                    '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
+                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
+                const hexSent = extractHexAndSentences(response);
+                doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+                doc.backgroundColor = '#' + hexSent.hexNumber;
+            } else {
+                const match = compareWords(input, StrCast(doc.quiz).trim());
+                doc.backgroundColor = match ? '#11c249' : '#eb2d2d';
+            }
+        });
+        //this._loading = false;
+    }
+
+    function redo(img: ImageBox) {
+        img.quizBoxes.forEach(doc => {
+            doc[DocData].title = '';
+            doc.backgroundColor = '#e4e4e4';
+        });
+    }
+
+    /**
+     * Get rid of all the label boxes on the images.
+     */
+    function exitQuizMode(img: ImageBox) {
+        img.Document._quizMode = quizMode.NONE;
+        DocListCast(img.Document._quizBoxes).forEach(box => {
+            box.hidden = true;
+        });
+    }
+
+    export function quizStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & DocumentViewProps>, property: string) {
+        const editLabelAnswer = (qdoc: Doc) => {
+            // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing.
+            if (!qdoc._editLabel) {
+                qdoc.title = StrCast(qdoc.quiz);
+            } else {
+                qdoc.quiz = StrCast(qdoc.title);
+                qdoc.title = '';
+            }
+            qdoc._editLabel = !qdoc._editLabel;
+        };
+        const editAnswer = (qdoc: Opt<Doc>) => {
+            return (
+                <Tooltip
+                    title={
+                        <div className="answer-tooltip" style={{ minWidth: '150px' }}>
+                            {qdoc?._editLabel ? 'save' : 'edit correct answer'}
+                        </div>
+                    }>
+                    <div className="answer-tool-tip" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => qdoc && editLabelAnswer(qdoc))}>
+                        <FontAwesomeIcon className="edit-icon" color={qdoc?._editLabel ? 'white' : 'black'} icon="pencil" size="sm" />
+                    </div>
+                </Tooltip>
+            );
+        };
+        const answerIcon = (qdoc: Opt<Doc>) => {
+            return (
+                <Tooltip
+                    title={
+                        <div className="answer-tooltip" style={{ minWidth: '150px' }}>
+                            {StrCast(qdoc?.quiz ?? '')}
+                        </div>
+                    }>
+                    <div className="answer-tool-tip">
+                        <FontAwesomeIcon className="q-icon" icon="circle" color="white" />
+                        <FontAwesomeIcon className="answer-icon" icon="question" />
+                    </div>
+                </Tooltip>
+            );
+        };
+        const checkIcon = (img: ImageBox) => (
+            <Tooltip title={<div className="dash-tooltip">Check</div>}>
+                <div className="check-icon" onPointerDown={() => check(img)}>
+                    <FontAwesomeIcon icon="circle-check" size="lg" />
+                </div>
+            </Tooltip>
+        );
+        const redoIcon = (img: ImageBox) => (
+            <Tooltip title={<div className="dash-tooltip">Redo</div>}>
+                <div className="redo-icon" onPointerDown={() => redo(img)}>
+                    <FontAwesomeIcon icon="redo-alt" size="lg" />
+                </div>
+            </Tooltip>
+        );
+
+        const imgBox = props?.DocumentView?.().ComponentView as ImageBox;
+        switch (property) {
+            case StyleProp.Decorations:
+                {
+                    if (doc?.quiz) {
+                        // this should only be set on Labels that are part of an image quiz
+                        return (
+                            <>
+                                {editAnswer(doc?.[DocData])}
+                                {answerIcon(doc)}
+                            </>
+                        );
+                    } else if (imgBox?.Document._quizMode && imgBox.Document._quizMode !== quizMode.NONE) {
+                        return (
+                            <>
+                                {checkIcon(imgBox)}
+                                {redoIcon(imgBox)}
+                            </>
+                        );
+                    }
+                }
+                break;
+            case StyleProp.ContextMenuItems:
+                if (imgBox) {
+                    const quizes: ContextMenuProps[] = [];
+                    quizes.push({
+                        description: 'Smart Check',
+                        event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.SMART) : () => exitQuizMode(imgBox),
+                        icon: 'pen-to-square',
+                    });
+                    quizes.push({
+                        description: 'Normal',
+                        event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.NORMAL) : () => exitQuizMode(imgBox),
+                        icon: 'pencil',
+                    });
+                    ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' });
+                }
+                break;
+            case StyleProp.AnchorMenuItems:
+                if (imgBox) {
+                    AnchorMenu.Instance.gptFlashcards = () => getImageDesc(imgBox);
+                    AnchorMenu.Instance.makeLabels = () => makeLabels(props?.DocumentView?.().ComponentView as ImageBox);
+                }
+        }
+        return undefined;
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 9fb8bc4d6..c32bbc803 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -44,7 +44,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-        this.setListening();
     }
 
     @observable private _inputValue = '';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5054432a9..04a31fd83 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -545,6 +545,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             return;
         }
 
+        const items = this._props.styleProvider?.(this.Document, this._props, StyleProp.ContextMenuItems) as ContextMenuProps[];
+        items?.forEach(item => ContextMenu.Instance.addItem(item));
+
         const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []);
         StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
             cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' })
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ca783d034..170966471 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -14,6 +14,7 @@ import { DocumentView } from './DocumentView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { OpenWhere } from './OpenWhere';
 import { WebField } from '../../../fields/URLField';
+import { ContextMenuProps } from '../ContextMenuItem';
 
 export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
 export type StyleProviderFuncType = (
@@ -23,6 +24,7 @@ export type StyleProviderFuncType = (
     property: string
 ) =>
     | Opt<FieldType>
+    | ContextMenuProps[]
     | { clipPath: string; jsx: JSX.Element }
     | JSX.Element
     | JSX.IntrinsicElements
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 4d199b360..3ffda5a35 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,44 +139,3 @@
 .imageBox-fadeBlocker-hover {
     opacity: 0;
 }
-
-.loading-spinner {
-    position: absolute;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-    // left: 50%;
-    // top: 50%;
-    z-index: 200;
-    font-size: 20px;
-    font-weight: bold;
-    color: #17175e;
-}
-
-.check-icon {
-    position: absolute;
-    right: 40;
-    bottom: 10;
-    color: green;
-    display: inline-block;
-    font-size: 20px;
-    overflow: hidden;
-}
-
-.redo-icon {
-    position: absolute;
-    right: 10;
-    bottom: 10;
-    color: black;
-    display: inline-block;
-    font-size: 20px;
-    overflow: hidden;
-}
-
-@keyframes spin {
-    to {
-        transform: rotate(360deg);
-    }
-}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 31f6df2ea..0b474076b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -17,13 +17,11 @@ import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Type
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
-import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -38,16 +36,8 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
-// import stringSimilarity from 'string-similarity';
-
-enum quizMode {
-    SMART = 'smart',
-    NORMAL = 'normal',
-    NONE = 'none',
-}
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -79,25 +69,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
+    _ffref = React.createRef<CollectionFreeFormView>();
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
     private _overlayIconRef = React.createRef<HTMLDivElement>();
-    private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    private _imageRef: HTMLImageElement | null = null; // <video> ref
-    @observable private _quizBoxes: Doc[] = [];
+    imageRef: HTMLImageElement | null = null; // <video> ref
+    marqueeref = React.createRef<MarqueeAnnotator>();
+
     @observable private _searchInput = '';
-    @observable private _quizMode = quizMode.NONE;
     @observable private _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>();
     @observable private _curSuffix = '';
     @observable private _error = '';
     @observable private _loading = false;
     @observable private _isHovering = false; // flag to switch between primary and alternate images on hover
-    _ffref = React.createRef<CollectionFreeFormView>();
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -110,6 +99,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
     };
 
+    @computed get quizBoxes() {
+        return DocListCast(this.Document.quizBoxes);
+    }
+    @computed get quizMode() {
+        return StrCast(this.Document._quizMode);
+    }
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
         const anchor =
@@ -315,331 +311,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return cropping;
     };
 
-    createCanvas = async () => {
-        const canvas = document.createElement('canvas');
-        const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
-        const w = AnchorMenu.Instance.marqueeWidth * scaling;
-        const h = AnchorMenu.Instance.marqueeHeight * scaling;
-        canvas.width = w;
-        canvas.height = h;
-        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
-        if (ctx) {
-            this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left) * scaling, NumCast(this._marqueeref.current?.top) * scaling, w, h, 0, 0, w, h);
-        }
-        const blob = await ImageUtility.canvasToBlob(canvas);
-        return ImageBox.selectUrlToBase64(blob);
-    };
-
-    createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
-        const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath;
-        const width = NumCast(this.layoutDoc._width) || 1;
-        const height = NumCast(this.layoutDoc._height);
-        const imageSnapshot = Docs.Create.ImageDocument(url, {
-            _nativeWidth: Doc.NativeWidth(this.layoutDoc),
-            _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-            x: NumCast(this.layoutDoc.x) + width,
-            y: NumCast(this.layoutDoc.y),
-            onClick: FollowLinkScript(),
-            _width: 150,
-            _height: (height / width) * 150,
-            title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
-        });
-        Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
-        Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
-        this._props.addDocument?.(imageSnapshot);
-        DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
-        setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
-    };
-
-    static selectUrlToBase64 = async (blob: Blob): Promise<string> => {
-        try {
-            return new Promise((resolve, reject) => {
-                const reader = new FileReader();
-                reader.readAsDataURL(blob);
-                reader.onloadend = () => resolve(reader.result as string);
-                reader.onerror = error => reject(error);
-            });
-        } catch (error) {
-            console.error('Error:', error);
-            throw error;
-        }
-    };
-
-    /**
-     * Calls backend to find any text on an image. Gets the text and the
-     * coordinates of the text and creates label boxes at those locations.
-     * @param quiz
-     * @param i
-     */
-    pushInfo = async (quiz: quizMode, i?: string) => {
-        this._quizMode = quiz;
-        this._loading = true;
-
-        const img = {
-            file: i ? i : this.paths[0],
-            drag: i ? 'drag' : 'full',
-            smart: quiz,
-        };
-        const response = await axios.post('http://localhost:105/labels/', img, {
-            headers: {
-                'Content-Type': 'application/json',
-            },
-        });
-        if (response.data['boxes'].length != 0) {
-            this.createBoxes(response.data['boxes'], response.data['text']);
-        } else {
-            this._loading = false;
-        }
-    };
-
-    /**
-     * Creates label boxes over text on the image to be filled in.
-     * @param boxes
-     * @param texts
-     */
-    createBoxes = (boxes: [[[number, number]]], texts: [string]) => {
-        for (let i = 0; i < boxes.length; i++) {
-            const coords = boxes[i] ? boxes[i] : [];
-            const width = coords[1][0] - coords[0][0];
-            const height = coords[2][1] - coords[0][1];
-            const text = texts[i];
-
-            const newCol = Docs.Create.LabelDocument({
-                _width: width,
-                _height: height,
-                _layout_fitWidth: true,
-                title: '',
-            });
-            const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
-            newCol.x = coords[0][0] + NumCast(this._marqueeref.current?.left) * scaling;
-            newCol.y = coords[0][1] + NumCast(this._marqueeref.current?.top) * scaling;
-
-            newCol.zIndex = 1000;
-            newCol.forceActive = true;
-            newCol.quiz = text;
-            newCol.showQuiz = false;
-            newCol[DocData].textTransform = 'none';
-            this._quizBoxes.push(newCol);
-            this.addDocument(newCol);
-            this._loading = false;
-        }
-    };
-
-    /**
-     * Create flashcards from an image.
-     */
-    getImageDesc = async () => {
-        this._loading = true;
-        try {
-            const hrefBase64 = await this.createCanvas();
-            const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
-            AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
-        } catch (error) {
-            console.log('Error', error);
-        }
-        this._loading = false;
-    };
-
-    /**
-     * Calls the createCanvas and pushInfo methods to convert the
-     * image to a form that can be passed to GPT and find the locations
-     * of the text.
-     */
-    makeLabels = async () => {
-        try {
-            const hrefBase64 = await this.createCanvas();
-            this.pushInfo(quizMode.NORMAL, hrefBase64);
-        } catch (error) {
-            console.log('Error', error);
-        }
-    };
-
-    /**
-     * Determines whether two words should be considered
-     * the same, allowing minor typos.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    levenshteinDistance = (str1: string, str2: string) => {
-        const len1 = str1.length;
-        const len2 = str2.length;
-        const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0));
-
-        if (len1 === 0) return len2;
-        if (len2 === 0) return len1;
-
-        for (let i = 0; i <= len1; i++) dp[i][0] = i;
-        for (let j = 0; j <= len2; j++) dp[0][j] = j;
-
-        for (let i = 1; i <= len1; i++) {
-            for (let j = 1; j <= len2; j++) {
-                const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
-                dp[i][j] = Math.min(
-                    dp[i - 1][j] + 1, // deletion
-                    dp[i][j - 1] + 1, // insertion
-                    dp[i - 1][j - 1] + cost // substitution
-                );
-            }
-        }
-
-        return dp[len1][len2];
-    };
-
-    /**
-     * Different algorithm for determining string similarity.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    jaccardSimilarity = (str1: string, str2: string) => {
-        const set1 = new Set(str1.split(' '));
-        const set2 = new Set(str2.split(' '));
-
-        const intersection = new Set([...set1].filter(x => set2.has(x)));
-        const union = new Set([...set1, ...set2]);
-
-        return intersection.size / union.size;
-    };
-
-    /**
-     * Averages the jaccardSimilarity and levenshteinDistance scores
-     * to determine string similarity for the labelboxes answers and
-     * the users response.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    stringSimilarity(str1: string, str2: string) {
-        const levenshteinDist = this.levenshteinDistance(str1, str2);
-        const levenshteinScore = 1 - levenshteinDist / Math.max(str1.length, str2.length);
-
-        const jaccardScore = this.jaccardSimilarity(str1, str2);
-
-        // Combine the scores with a higher weight on Jaccard similarity
-        return 0.5 * levenshteinScore + 0.5 * jaccardScore;
-    }
-
-    @computed get checkIcon() {
-        return (
-            <Tooltip title={<div className="dash-tooltip">Check</div>}>
-                <div className="check-icon" onPointerDown={this.check}>
-                    <FontAwesomeIcon icon="circle-check" size="lg" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    @computed get redoIcon() {
-        return (
-            <Tooltip title={<div className="dash-tooltip">Redo</div>}>
-                <div className="redo-icon" onPointerDown={this.redo}>
-                    <FontAwesomeIcon icon="redo-alt" size="lg" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    /**
-     * Returns whether two strings are similar
-     * @param input
-     * @param target
-     * @returns
-     */
-    compareWords = (input: string, target: string) => {
-        const distance = this.stringSimilarity(input.toLowerCase(), target.toLowerCase());
-        return distance >= 0.7;
-    };
-
-    /**
-     * GPT returns a hex color for what color the label box should be based on
-     * the correctness of the users answer.
-     * @param inputString
-     * @returns
-     */
-    extractHexAndSentences = (inputString: string) => {
-        // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences
-        const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s;
-        const match = inputString.match(regex);
-
-        if (match) {
-            const hexNumber = match[1];
-            const sentences = match[2].trim();
-            return { hexNumber, sentences };
-        } else {
-            return { error: 'The input string does not match the expected format.' };
-        }
-    };
-
-    /**
-     * Check whether the contents of the label boxes on an image are correct.
-     */
-    check = () => {
-        this._loading = true;
-        this._quizBoxes.forEach(async doc => {
-            const input = StrCast(doc[DocData].title);
-            if (this._quizMode == quizMode.SMART && input) {
-                const questionText = 'Question: What was labeled in this image?';
-                const rubricText = ' Rubric: ' + StrCast(doc.quiz);
-                const queryText =
-                    questionText +
-                    ' UserAnswer: ' +
-                    input +
-                    '. ' +
-                    rubricText +
-                    '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
-                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
-                const hexSent = this.extractHexAndSentences(response);
-                doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
-                doc.backgroundColor = '#' + hexSent.hexNumber;
-            } else {
-                const match = this.compareWords(input, StrCast(doc.quiz));
-                doc.backgroundColor = match ? '#11c249' : '#eb2d2d';
-            }
-            doc.showQuiz = true;
-        });
-        this._loading = false;
-    };
-
-    redo = () => {
-        this._quizBoxes.forEach(doc => {
-            doc[DocData].title = '';
-            doc.backgroundColor = '#e4e4e4';
-            doc.showQuiz = false;
-        });
-    };
-
-    /**
-     * Get rid of all the label boxes on the images.
-     */
-    exitQuizMode = () => {
-        this._quizMode = quizMode.NONE;
-        this._quizBoxes.forEach(doc => {
-            this.removeDocument?.(doc);
-        });
-        this._quizBoxes = [];
-    };
-
-    @action
-    setRef = (iref: HTMLImageElement | null) => {
-        this._imageRef = iref;
-    };
-
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
-            const quizes: ContextMenuProps[] = [];
-            quizes.push({
-                description: 'Smart Check',
-                event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.SMART) : this.exitQuizMode,
-                icon: 'pen-to-square',
-            });
-            quizes.push({
-                description: 'Normal',
-                event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.NORMAL) : this.exitQuizMode,
-                icon: 'pencil',
-            });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
@@ -654,7 +329,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }),
                 icon: 'pencil-alt',
             });
-            ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' });
             ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
         }
     };
@@ -770,7 +444,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div className="imageBox-fader" style={{ opacity: backAlpha }}>
                     <img
                         alt=""
-                        ref={this.setRef}
+                        ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
                         key="paths"
                         src={srcpath}
                         style={{ transform, transformOrigin }}
@@ -811,7 +485,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 e,
                 action(moveEv => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                    this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
+                    this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
                     return true;
                 }),
                 returnFalse,
@@ -823,12 +497,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     finishMarquee = () => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
-        AnchorMenu.Instance.gptFlashcards = this.getImageDesc;
+        this._props.styleProvider?.(this.Document, this._props, StyleProp.AnchorMenuItems);
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
-        AnchorMenu.Instance.makeLabels = this.makeLabels;
-        AnchorMenu.Instance.marqueeWidth = this._marqueeref.current?.Width ?? 0;
-        AnchorMenu.Instance.marqueeHeight = this._marqueeref.current?.Height ?? 0;
-        this._marqueeref.current?.onTerminateSelection();
+        AnchorMenu.Instance.marqueeWidth = this.marqueeref.current?.Width ?? 0;
+        AnchorMenu.Instance.marqueeHeight = this.marqueeref.current?.Height ?? 0;
+        this.marqueeref.current?.onTerminateSelection();
         this._props.select(false);
     };
     focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
@@ -898,7 +571,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
                     <MarqueeAnnotator
                         Document={this.Document}
-                        ref={this._marqueeref}
+                        ref={this.marqueeref}
                         scrollTop={0}
                         annotationLayerScrollTop={0}
                         scaling={returnOne}
@@ -915,8 +588,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         // anchorMenuFlashcard={() => this.getImageDesc()}
                     />
                 )}
-                {this._quizMode != quizMode.NONE ? this.checkIcon : null}
-                {this._quizMode != quizMode.NONE ? this.redoIcon : null}
             </div>
         );
     }
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 058932457..07c0a114a 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,12 +1,8 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
 import { Property } from 'csstype';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as textfit from 'textfit';
-import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
 import { Field, FieldType } from '../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
@@ -49,48 +45,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
     }
 
-    @computed get answerIcon() {
-        return (
-            <Tooltip
-                title={
-                    <div className="answer-tooltip" style={{ minWidth: '150px' }}>
-                        {StrCast(this.Document.quiz)}
-                    </div>
-                }>
-                <div className="answer-tool-tip">
-                    <FontAwesomeIcon className="q-icon" icon="circle" color="white" />
-                    <FontAwesomeIcon className="answer-icon" icon="question" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    @computed get editAnswer() {
-        return (
-            <Tooltip
-                title={
-                    <div className="answer-tooltip" style={{ minWidth: '150px' }}>
-                        {this._editLabel ? 'save' : 'edit correct answer'}
-                    </div>
-                }>
-                <div className="answer-tool-tip" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.editLabelAnswer())}>
-                    <FontAwesomeIcon className="edit-icon" color={this._editLabel ? 'white' : 'black'} icon="pencil" size="sm" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    editLabelAnswer = () => {
-        // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing.
-        if (!this._editLabel) {
-            this.dataDoc.title = StrCast(this.Document.quiz);
-        } else {
-            this.Document.quiz = this.Title;
-            this.dataDoc.title = '';
-        }
-        this._editLabel = !this._editLabel;
-    };
-
     componentDidMount() {
         this._props.setContentViewBox?.(this);
     }
@@ -98,8 +52,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._timeout && clearTimeout(this._timeout);
     }
 
-    specificContextMenu = (): void => {};
-
     drop = (/* e: Event, de: DragManager.DropEvent */) => {
         return false;
     };
@@ -152,7 +104,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
         const label = this.Title.startsWith('#') ? null : this.Title;
         return (
-            <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+            <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
                 <div
                     className="labelBox-mainButton"
                     style={{
@@ -203,8 +155,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         {label}
                     </div>
                 </div>
-                {this.Document.showQuiz ? this.answerIcon : null}
-                {this.Document.showQuiz ? this.editAnswer : null}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 972839216c14baa5c9eaf80e1fb2fb2694bbb72c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 8 Oct 2024 22:51:46 -0400
Subject: modified how buttons are laid out on carousel and comparison views so
 that text boxes can reflow around them.  extracted flashcard pratice into its
 own component and applied it to carousel3D and carousel

---
 src/client/views/PropertiesView.tsx                |   7 +-
 .../collections/CollectionCarousel3DView.scss      |   1 +
 .../views/collections/CollectionCarousel3DView.tsx |  70 ++++-
 .../views/collections/CollectionCarouselView.scss  |  86 +-----
 .../views/collections/CollectionCarouselView.tsx   | 316 ++++++---------------
 .../views/collections/FlashcardPracticeUI.scss     |  64 +++++
 .../views/collections/FlashcardPracticeUI.tsx      | 172 +++++++++++
 src/client/views/nodes/ComparisonBox.scss          |  15 +
 src/client/views/nodes/ComparisonBox.tsx           |  92 ++++--
 src/client/views/nodes/FieldView.tsx               |   1 +
 .../nodes/formattedText/FormattedTextBox.scss      |   6 -
 .../views/nodes/formattedText/FormattedTextBox.tsx |  16 +-
 12 files changed, 475 insertions(+), 371 deletions(-)
 create mode 100644 src/client/views/collections/FlashcardPracticeUI.scss
 create mode 100644 src/client/views/collections/FlashcardPracticeUI.tsx

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 371d34173..442dab671 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -139,6 +139,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
+    @computed get isText() {
+        return this.selectedDoc?.type === DocumentType.RTF;
+    }
     @computed get isInk() {
         return this.selectedDoc?.type === DocumentType.INK;
     }
@@ -1199,8 +1202,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             // prettier-ignore
             <div className="transform-editor">
                 {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) }
-                {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
-                {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
+                {!this.isStack && !this.isText? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
+                {!this.isStack && !this.isText? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
                 {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.yPadding = val; })) }
                 {this.isInk ? this.controlPointsButton : null}
                 {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)}
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index a556d0fa7..42e112906 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -4,6 +4,7 @@
     position: relative;
     background-color: white;
     overflow: hidden;
+    display: flex;
 }
 
 .carousel-wrapper {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c5da8e037..1583f0e0c 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnZero } from '../../../ClientUtils';
@@ -9,12 +9,13 @@ import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
+import { Transform } from '../../util/Transform';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { Transform } from '../../util/Transform';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -24,6 +25,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     @computed get scrollSpeed() {
         return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
     }
+    _sideBtnWidth = 35;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
@@ -43,7 +47,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     };
 
     @computed get carouselItems() {
-        return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
+        return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK).filter(pair => !this._filterFunc?.(pair.layout));
     }
 
     centerScale = Number(CAROUSEL3D_CENTER_SCALE);
@@ -53,22 +57,17 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
     isChildContentActive = () => !!this.isContentActive();
+    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     childScreenLeftToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
             .scale(1 / this.sideScale);
     childScreenRightToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
             .scale(1 / this.sideScale);
     childCenterScreenToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(
                 -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
                 -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
@@ -119,7 +118,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     changeSlide = (direction: number) => {
         DocumentView.DeselectAll();
-        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length;
+        this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
     };
 
     onArrowClick = (direction: number) => {
@@ -192,6 +191,35 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return this.panelWidth() * (1 - index);
     }
 
+    /**
+     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
+    docViewProps = () => ({
+        ...this._props, //
+        isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+        isContentActive: this.isChildContentActive,
+        ScreenToLocalTransform: this.contentScreenToLocalXf,
+    });
+    carouselItemsFunc = () => this.carouselItems.map(pair => pair.layout);
+    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+    curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
+
     render() {
         return (
             <div
@@ -206,6 +234,22 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 </div>
                 {this.buttons}
                 <div className="dot-bar">{this.dots}</div>
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    carouselItems={this.carouselItemsFunc}
+                    childDocs={this.childDocs}
+                    advance={this.answered}
+                    curDoc={this.curDoc}
+                    practiceBtnOffset={this._sideBtnWidth * 4}
+                    layoutDoc={this.layoutDoc}
+                    maxWidgetScale={this.maxWidgetScale}
+                    uiBtnScaleTransform={this.uiBtnScaleTransform}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={this.docViewProps}
+                />
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 97952822e..757072453 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,23 +12,10 @@
         display: inline-block;
         width: 100%;
         user-select: none;
+        position: absolute;
+        top: 0;
+        left: 0;
     }
-    .message {
-        justify-content: center;
-        align-items: center;
-        display: flex;
-        height: 60%;
-        z-index: -1;
-        // margin: 15px;
-    }
-}
-
-.collectionCarouselView-addFlashcards {
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    z-index: -1;
-    pointer-events: none;
 }
 .collectionCarouselView-recentlyMissed {
     color: red;
@@ -39,10 +26,7 @@
     pointer-events: none;
 }
 .carouselView-back,
-.carouselView-fwd,
-.carouselView-remove,
-.carouselView-check,
-.carouselView-add {
+.carouselView-fwd {
     position: absolute;
     display: flex;
     width: 30;
@@ -66,68 +50,6 @@
     left: 0;
     transform-origin: top left;
 }
-.carouselView-add {
-    position: absolute;
-    bottom: 0;
-    left: 0;
-}
-.carouselView-remove {
-    left: 52%;
-}
-.carouselView-check {
-    right: 52%;
-}
-.carouselView-menu {
-    position: absolute;
-    flex-direction: column;
-    align-items: center;
-    display: flex;
-    top: 0px;
-    left: 0px;
-    width: 30;
-    transform-origin: top left;
-    border-radius: 5px;
-    color: rgba(255, 255, 255, 0.5);
-    background: rgba(0, 0, 0, 0.1);
-    .carouselView-practiceModes {
-        width: 100%;
-        display: flex;
-        flex-direction: column;
-        top: 0;
-        position: relative;
-        .carouselView-quiz {
-            position: relative;
-            display: flex;
-            height: 20px;
-            align-items: center;
-            margin: auto;
-            &:hover {
-                color: white;
-            }
-            & > svg {
-                height: 100%;
-                width: 100%;
-            }
-        }
-
-        .carouselView-practice {
-            position: relative;
-            display: flex;
-            flex-direction: column;
-            height: 20px;
-            align-items: center;
-            margin: auto;
-            &:hover {
-                color: white;
-            }
-            & > svg {
-                height: 100%;
-                width: 100%;
-            }
-        }
-    }
-}
-
 .carouselView-back:hover,
 .carouselView-fwd:hover {
     background: lightgray;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 559dcfe2a..64ddaac79 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,52 +1,35 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { StyleProp } from '../StyleProp';
-import { TagItem } from '../TagsView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
-enum cardMode {
-    STAR = 'star',
-    ALL = 'all',
-}
-enum practiceMode {
-    PRACTICE = 'practice',
-    QUIZ = 'quiz',
-}
-enum practiceVal {
-    MISSED = 'missed',
-    CORRECT = 'correct',
-}
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
-    get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
-    get sideField()     { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
-    get starField()     { return "#star"; } // prettier-ignore
-
-    _sideBtnWidth = 35;
 
     _fadeTimer: NodeJS.Timeout | undefined;
+    _sideBtnWidth = 35;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    @observable _last_index = this.carouselIndex;
+    @observable _last_opacity = 1;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
     }
 
-    @observable _last_index = this.carouselIndex;
-    @observable _last_opacity = 1;
-
     componentWillUnmount() {
         this._dropDisposer?.();
     }
@@ -58,38 +41,31 @@ export class CollectionCarouselView extends CollectionSubView() {
         }
     };
 
-    @computed get practiceMode() {
-        return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : '';
-    }
-    @computed get practiceMessage() {
-        const cardCount = this.carouselItems.length;
-        if (this.practiceMode) {
-            if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
-                return 'Finished! Click here to view all flashcards.';
-            }
-        }
-        return '';
-    }
-
-    @computed get filterMessage() {
-        const cardCount = this.carouselItems.length;
-        if (!this.practiceMessage) {
-            if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
-                return 'No tagged items. Click here to view all flash cards.';
-            }
-            if (this.practiceMode) {
-                if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
-            }
-        }
-        return '';
-    }
-    @computed get marginX()       { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
+    @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
     @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
     @computed get carouselItems() { return this.childDocs
         .filter(doc => doc.type !== DocumentType.LINK)
-        .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
+        .filter(doc => !this._filterFunc?.(doc))
     } // prettier-ignore
 
+    /**
+     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
     /**
      * Move forward or backward the specified number of Docs
      * @param dir signed number indicating Docs to move forward or backward
@@ -102,8 +78,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     /**
      * Goes to the next Doc in the stack subject to the currently selected filter option.
      */
-    advance = (e: React.MouseEvent) => {
-        e.stopPropagation();
+    advance = (e?: React.MouseEvent) => {
+        e?.stopPropagation();
         this.move(1);
     };
 
@@ -115,55 +91,23 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.move(-1);
     };
 
-    /*
-     * Toggles whether the 'star' metadata field is set on the current Doc
-     */
-    toggleStar = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        const curDoc = this.carouselItems[this.carouselIndex];
-        if (curDoc) {
-            if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField);
-            else TagItem.addTagToDoc(curDoc, this.starField);
-        }
-    };
-
-    /*
-     * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
-     */
-    setPracticeVal = (e: React.MouseEvent, val: string) => {
-        e.stopPropagation();
-        const curDoc = this.carouselItems[this.carouselIndex];
-        curDoc && (curDoc[this.practiceField] = val);
-        this.advance(e);
-    };
-
-    /**
-     * Sets the practice mode answer style for flashcards
-     * @param mode practiceMode or undefined for no practice
-     */
-    setPracticeMode = (mode: practiceMode | undefined) => {
-        this.layoutDoc.practiceMode = mode;
-        this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
-        if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
-    };
+    curDoc = () => this.carouselItems[this.carouselIndex];
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
         return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
     };
+    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
     contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
-    captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+    captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
     contentScreenToLocalXf = () =>
         this._props
             .ScreenToLocalTransform()
             .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
             .scale(this._props.NativeDimScaling?.() || 1);
-
-    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
-
     isChildContentActive = () =>
         this._props.isContentActive?.() === false
             ? false
@@ -172,10 +116,7 @@ export class CollectionCarouselView extends CollectionSubView() {
               : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                 ? false
                 : undefined;
-
     renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
-        const screenScale = this.ScreenToLocalBoxXf().Scale;
-        const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1);
         return (
             <DocumentView
                 {...this._props}
@@ -203,7 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 ScreenToLocalTransform={this.contentScreenToLocalXf}
                 PanelWidth={this.contentPanelWidth}
                 PanelHeight={this.contentPanelHeight}
-                xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller
+                screenXPadding={this.screenXPadding}
             />
         );
     };
@@ -214,7 +155,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         const fadeTime = 500;
         const lastDoc = this.carouselItems?.[this._last_index];
         return !lastDoc || this.carouselIndex === this._last_index ? null : (
-            <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}>
+            <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}>
                 {this.renderDoc(
                     lastDoc,
                     false, // hide captions if the carousel is configured to show the captions
@@ -235,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() {
             </div>
         );
     }
+    @computed get renderedDoc() {
+        const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
+        return this.renderDoc(this.curDoc(), !!carouselShowsCaptions);
+    }
+
     @computed get content() {
-        const index = this.carouselIndex;
-        const curDoc = this.carouselItems?.[index];
         const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
         const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
-        return !curDoc ? null : (
+        return !this.curDoc() ? null : (
             <>
                 <div className="collectionCarouselView-image" key="image">
-                    {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+                    {this.renderedDoc}
                     {this.overlay}
                 </div>
                 {!carouselShowsCaptions ? null : (
@@ -253,158 +197,70 @@ export class CollectionCarouselView extends CollectionSubView() {
                         onWheel={StopEvent}
                         style={{
                             borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string,
-                            marginRight: this.marginX,
-                            marginLeft: this.marginX,
-                            width: `calc(100% - ${this.marginX * 2}px)`,
+                            marginRight: this.captionMarginX,
+                            marginLeft: this.captionMarginX,
+                            width: `calc(100% - ${this.captionMarginX * 2}px)`,
                         }}>
-                        <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} />
+                        <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} />
                     </div>
                 )}
             </>
         );
     }
 
-    togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
-    toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
-    setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
-
-    @computed get filterDoc() {
-        return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
-    }
-    filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale);
-    filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
-
-    /**
-     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
-     */
-    @computed get contentScaling() {
-        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
-    }
-
-    /**
-     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
-     */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
-    /**
-     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
-     */
-    @computed get uiBtnScaleTransform() {
-        return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`;
-    }
-    @computed get menu() {
-        const curDoc = this.carouselItems?.[this.carouselIndex];
-        return (
-            <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}>
-                {!this.filterDoc ? null : (
-                    <DocumentView
-                        {...this._props}
-                        Document={this.filterDoc}
-                        TemplateDataDocument={undefined}
-                        LayoutTemplate={this._props.childLayoutTemplate}
-                        LayoutTemplateString={this._props.childLayoutString}
-                        renderDepth={this._props.renderDepth + 1}
-                        NativeWidth={returnZero}
-                        NativeHeight={returnZero}
-                        fitWidth={undefined}
-                        showTags={false}
-                        hideFilterStatus={true}
-                        containerViewPath={this.childContainerViewPath}
-                        setContentViewBox={undefined}
-                        onDoubleClickScript={this.onContentDoubleClick}
-                        onClickScript={this.onContentClick}
-                        isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
-                        isContentActive={this.isChildContentActive}
-                        hideCaptions={true}
-                        childFilters={this.childDocFilters}
-                        hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
-                        addDocument={this._props.addDocument}
-                        ScreenToLocalTransform={this.contentScreenToLocalXf}
-                        PanelWidth={this.filterWidth}
-                        PanelHeight={this.filterHeight}
-                    />
-                )}
-                <div
-                    className="carouselView-practiceModes"
-                    style={{
-                        transformOrigin: `0px ${-this.filterHeight()}px`,
-                        transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`,
-                        display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none',
-                    }}>
-                    <Tooltip title="Practice flashcards using GPT">
-                        <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
-                            <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
-                        </div>
-                    </Tooltip>
-                    <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
-                        <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
-                            <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
-                        </div>
-                    </Tooltip>
-                </div>
-            </div>
-        );
-    }
-    @computed get buttons() {
-        return (
+    @computed get navButtons() {
+        return this.Document._chromeHidden || !this.curDoc() ? null : (
             <>
-                <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}>
+                <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}>
                     <FontAwesomeIcon icon="chevron-left" size="2x" />
                 </div>
-                <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}>
+                <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}>
                     <FontAwesomeIcon icon="chevron-right" size="2x" />
                 </div>
-                {this.practiceMode == practiceMode.PRACTICE ? (
-                    <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
-                        <Tooltip title="Incorrect. View again later.">
-                            <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
-                                <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-                            </div>
-                        </Tooltip>
-                        <Tooltip title="Correct">
-                            <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
-                                <FontAwesomeIcon icon="check" color="green" size="1x" />
-                            </div>
-                        </Tooltip>
-                    </div>
-                ) : null}
             </>
         );
     }
 
+    docViewProps = () => ({
+        ...this._props, //
+        isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+        isContentActive: this.isChildContentActive,
+        ScreenToLocalTransform: this.contentScreenToLocalXf,
+    });
+    carouselItemsFunc = () => this.carouselItems;
+    answered = () => this.advance();
+    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+
     render() {
         return (
-            <div>
-                <div
-                    className="collectionCarouselView-outer"
-                    ref={this.createDashEventsTarget}
-                    style={{
-                        background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
-                        color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
-                        width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
-                        height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
-                        left: NumCast(this.layoutDoc._xMargin),
-                        top: NumCast(this.layoutDoc._yMargin),
-                    }}>
-                    {!this.practiceMessage && !this.filterMessage ? (
-                        this.content
-                    ) : (
-                        <p
-                            className="message"
-                            onClick={() => {
-                                if (this.filterMessage || this.practiceMessage) {
-                                    this.setPracticeMode(undefined);
-                                    Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove');
-                                }
-                            }}>
-                            {this.filterMessage || this.practiceMessage}
-                        </p>
-                    )}
-                </div>
-                {!this.Document._chromeHidden ? this.menu : null}
-                {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null}
+            <div
+                className="collectionCarouselView-outer"
+                ref={this.createDashEventsTarget}
+                style={{
+                    background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
+                    color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+                    width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
+                    height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
+                    left: NumCast(this.layoutDoc._xMargin),
+                    top: NumCast(this.layoutDoc._yMargin),
+                }}>
+                {this.content}
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    carouselItems={this.carouselItemsFunc}
+                    childDocs={this.childDocs}
+                    advance={this.answered}
+                    curDoc={this.curDoc}
+                    layoutDoc={this.layoutDoc}
+                    maxWidgetScale={this.maxWidgetScale}
+                    uiBtnScaleTransform={this.uiBtnScaleTransform}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={this.docViewProps}
+                />
+                {this.navButtons}
             </div>
         );
     }
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
new file mode 100644
index 000000000..53c26ad34
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -0,0 +1,64 @@
+.FlashcardPracticeUI-remove,
+.FlashcardPracticeUI-check {
+    position: absolute;
+    display: flex;
+    width: 30;
+    height: 30;
+    align-items: center;
+    border-radius: 5px;
+    justify-content: center;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+    &:hover {
+        color: white;
+    }
+}
+.FlashcardPracticeUI-remove {
+    left: 52%;
+}
+.FlashcardPracticeUI-check {
+    right: 52%;
+}
+.FlashcardPracticeUI-menu {
+    position: absolute;
+    flex-direction: column;
+    align-items: center;
+    display: flex;
+    top: 0px;
+    left: 0px;
+    width: 30;
+    transform-origin: top left;
+    border-radius: 5px;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+    .FlashcardPracticeUI-practiceModes {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        top: 0;
+        position: relative;
+        .FlashcardPracticeUI-quiz,
+        .FlashcardPracticeUI-practice {
+            position: relative;
+            display: flex;
+            height: 20px;
+            align-items: center;
+            margin: auto;
+            padding: 3px;
+            &:hover {
+                color: white;
+            }
+            & > svg {
+                height: 100%;
+                width: 100%;
+            }
+        }
+    }
+}
+.FlashcardPracticeUI-message {
+    z-index: 100;
+    position: relative;
+    margin: auto;
+    align-content: center;
+    width: max-content;
+}
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
new file mode 100644
index 000000000..032a405bf
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -0,0 +1,172 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Transform } from '../../util/Transform';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import './FlashcardPracticeUI.scss';
+
+enum practiceMode {
+    PRACTICE = 'practice',
+    QUIZ = 'quiz',
+}
+enum practiceVal {
+    MISSED = 'missed',
+    CORRECT = 'correct',
+}
+
+interface PracticeUIProps {
+    fieldKey: string;
+    layoutDoc: Doc;
+    carouselItems: () => Doc[];
+    childDocs: Doc[];
+    curDoc: () => Doc;
+    advance: (correct: boolean) => void;
+    renderDepth: number;
+    sideBtnWidth: number;
+    uiBtnScaleTransform: number;
+    ScreenToLocalBoxXf: () => Transform;
+    maxWidgetScale: number;
+    docViewProps: () => DocumentViewProps;
+    setFilterFunc: (func?: (doc: Doc) => boolean) => void;
+    practiceBtnOffset?: number;
+}
+@observer
+export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> {
+    constructor(props: PracticeUIProps) {
+        super(props);
+        makeObservable(this);
+        this._props.setFilterFunc(this.tryFilterOut);
+    }
+
+    componentWillUnmount(): void {
+        this._props.setFilterFunc(undefined);
+    }
+
+    get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
+
+    @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
+    @computed get practiceMode() {  return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+
+    btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
+    btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
+
+    /**
+     * Sets the practice mode answer style for flashcards
+     * @param mode practiceMode or undefined for no practice
+     */
+    setPracticeMode = (mode: practiceMode | undefined) => {
+        this._props.layoutDoc.practiceMode = mode;
+        this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined));
+    };
+
+    @computed get emptyMessage() {
+        const cardCount = this._props.carouselItems().length;
+        const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : '';
+        const filterMessage = practiceMessage
+            ? ''
+            : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount
+              ? 'No tagged items. Click here to view all flash cards.'
+              : this.practiceMode && !cardCount
+                ? 'No flashcards to show! Click here to leave practice mode'
+                : '';
+        return !practiceMessage && !filterMessage ? null : (
+            <p
+                className="FlashcardPracticeUI-message"
+                onClick={() => {
+                    if (filterMessage || practiceMessage) {
+                        this.setPracticeMode(undefined);
+                        Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove');
+                    }
+                }}>
+                {filterMessage || practiceMessage}
+            </p>
+        );
+    }
+
+    @computed get practiceButtons() {
+        /*
+         * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
+         */
+        const setPracticeVal = (e: React.MouseEvent, val: string) => {
+            e.stopPropagation();
+            const curDoc = this._props.curDoc();
+            curDoc && (curDoc[this.practiceField] = val);
+            this._props.advance?.(val === practiceVal.CORRECT);
+        };
+
+        return this.practiceMode == practiceMode.PRACTICE ? (
+            <div style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
+                <Tooltip title="Incorrect. View again later.">
+                    <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}>
+                        <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title="Correct">
+                    <div key="check" className="FlashcardPracticeUI-check" onClick={e => setPracticeVal(e, practiceVal.CORRECT)}>
+                        <FontAwesomeIcon icon="check" color="green" size="1x" />
+                    </div>
+                </Tooltip>
+            </div>
+        ) : null;
+    }
+    @computed get practiceModesMenu() {
+        const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray');
+        const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
+
+        return !this._props.curDoc()?._layout_isFlashcard ? null : (
+            <div
+                className="FlashcardPracticeUI-practiceModes"
+                style={{
+                    transformOrigin: `0px ${-this.btnHeight()}px`,
+                    transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`,
+                }}>
+                <Tooltip title="Practice flashcards using GPT">
+                    <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}>
+                        <FontAwesomeIcon icon="file-pen" color={setColor(practiceMode.QUIZ)} size="sm" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <div key="back" className="FlashcardPracticeUI-practice" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.PRACTICE)}>
+                        <FontAwesomeIcon icon="check" color={setColor(practiceMode.PRACTICE)} size="sm" />
+                    </div>
+                </Tooltip>
+            </div>
+        );
+    }
+    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    render() {
+        return (
+            <>
+                {this.emptyMessage}
+                {this.practiceButtons}
+                <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}>
+                    {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : (
+                        <DocumentView
+                            {...this._props.docViewProps()}
+                            Document={this.filterDoc}
+                            TemplateDataDocument={undefined}
+                            PanelWidth={this.btnWidth}
+                            PanelHeight={this.btnHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)}
+                            hideCaptions={true}
+                            hideFilterStatus={true}
+                            renderDepth={this._props.renderDepth + 1}
+                            fitWidth={undefined}
+                            showTags={false}
+                            setContentViewBox={undefined}
+                        />
+                    )}
+                    {this.practiceModesMenu}
+                </div>
+            </>
+        );
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index b7307f3a3..8156c50f6 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -296,3 +296,18 @@
         }
     }
 }
+.comparisonBox-bottomMenu {
+    transform-origin: bottom right;
+    width: max-content;
+    justify-content: space-between;
+    height: max-content;
+    position: absolute;
+    bottom: 0;
+    right: 2;
+    flex-direction: row-reverse;
+    display: flex;
+    cursor: pointer;
+    .comparisonBox-button {
+        padding-right: 8px;
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ef66c2b11..81e223028 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -68,7 +68,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return (
             <Tooltip title={<div className="dash-tooltip">flip</div>}>
                 <div
-                    className="formattedTextBox-alternateButton"
+                    className="comparisonBox-alternateButton ccomparisonBox-button"
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
                             if (!this.revealOp || this.revealOp === 'flip') {
@@ -81,42 +81,74 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
                         display: 'inline-block',
                     }}>
-                    <div key="alternate" className="formattedTextBox-flip">
-                        <FontAwesomeIcon icon="turn-up" size="1x" />
-                    </div>
+                    <FontAwesomeIcon icon="turn-up" size="xl" />
                 </div>
             </Tooltip>
         );
     }
 
+    _sideBtnWidth = 30;
+    /**
+     * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() {
+        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
+    }
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height)));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {
+        return this.maxWidgetScale * Math.min(1, this.contentScaling);
+    }
+
     @computed get flashcardMenu() {
         return (
-            <div>
-                <Tooltip
-                    title={this._frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={() => (!this._frontSide ? this.findImageTags() : null)}>
-                        <FontAwesomeIcon icon="lightbulb" size="xl" />
-                    </div>
-                </Tooltip>
-                {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
-                    <div>
-                        <Tooltip title={<div>Create a flashcard pile</div>}>
-                            <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
-                                <FontAwesomeIcon icon="folder-plus" size="xl" />
-                            </div>
-                        </Tooltip>
-                        <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
-                            <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={() => this.gptFlashcardPile()}>
-                                <FontAwesomeIcon icon="layer-group" size="xl" />
+            <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}>
+                {this.overlayAlternateIcon}
+                {!this._props.isContentActive() ? null : (
+                    <>
+                        {' '}
+                        {!this._frontSide ? null : (
+                            <Tooltip
+                                title={
+                                    <div className="dash-tooltip">{
+                                        !this._frontSide ? "Flip to front side to use GPT":
+                                                "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"}
+                                   </div> // prettier-ignore
+                                }>
+                                <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.findImageTags() : null)}>
+                                    <FontAwesomeIcon icon="lightbulb" size="xl" />
+                                </div>
+                            </Tooltip>
+                        )}
+                        {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
+                            <>
+                                <Tooltip title={<div>Create a flashcard pile</div>}>
+                                    <div className="comparisonBox-button" onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
+                                        <FontAwesomeIcon icon="folder-plus" size="xl" />
+                                    </div>
+                                </Tooltip>
+                                <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+                                    <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
+                                        <FontAwesomeIcon icon="layer-group" size="xl" />
+                                    </div>
+                                </Tooltip>
+                            </>
+                        )}
+                        <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
+                            <div className="comparisonBox-button" onClick={this.handleHover}>
+                                <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
                             </div>
                         </Tooltip>
-                    </div>
+                    </>
                 )}
-                <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={() => this.handleHover()}>
-                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
-                    </div>
-                </Tooltip>
             </div>
         );
     }
@@ -478,7 +510,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 this._loading = false;
                 return;
             }
-            this.flipFlashcard();
         }
         try {
             console.log(queryText);
@@ -655,6 +686,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     childActiveFunc = () => this._childActive;
 
+    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -687,6 +719,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
+                        ScreenToLocalTransform={this.contentScreenToLocalXf}
                         isContentActive={this.childActiveFunc}
                         isDocumentActive={returnFalse}
                         dontSelect={returnTrue}
@@ -808,8 +841,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
-                    {this._props.isContentActive() ? this.flashcardMenu : null}
-                    {this.overlayAlternateIcon}
+                    {this.flashcardMenu}
                 </div>
             );
         }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 170966471..c81631baa 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -51,6 +51,7 @@ export interface FieldViewSharedProps {
     LayoutTemplate?: () => Opt<Doc>;
     renderDepth: number;
     scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
+    screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView)
     xPadding?: number;
     yPadding?: number;
     dontRegisterView?: boolean;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index d6f13d9ee..f1ae1151f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -101,12 +101,6 @@ audiotag:hover {
     height: 22;
     cursor: default;
 }
-.formattedTextBox-flip {
-    align-items: center;
-    position: absolute;
-    right: 2px;
-    bottom: 4px;
-}
 
 .formattedTextBox-outer {
     position: relative;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c89737e1e..93153b453 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -763,10 +763,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
     };
     sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
-        const localDelta = this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
-            .transformDirection(delta[0], delta[1]);
+        const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta;
         const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
         const width = NumCast(this.layoutDoc._width) + localDelta[0];
         this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
@@ -1264,7 +1261,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._cachedLinks = Doc.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
         this._disposers.layout_autoHeight = reaction(
-            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
+            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }),
             autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
         );
         this._disposers.highlights = reaction(
@@ -2088,8 +2085,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const scale = this._props.NativeDimScaling?.() || 1;
         const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
         setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
-        const paddingX = Math.max(this._props.xPadding ?? 0, NumCast(this.layoutDoc._xMargin));
-        const paddingY = Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin));
+
+        const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) => 
+                [view._props.PanelWidth() / view.screenToLocalScale(), view._props.PanelHeight() / view.screenToLocalScale()]; // prettier-ignore
+        const scrMargin = [Math.max(0, (scrSize(2)[0] - scrSize(1)[0]) / 2), Math.max(0, (scrSize(2)[1] - scrSize(1)[1]) / 2)];
+        const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale);
+        const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale);
         const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
         return styleFromLayout?.height === '0px' ? null : (
             <div
@@ -2176,7 +2177,6 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, {
         _layout_nativeDimEditable: true,
         _layout_reflowVertical: true,
         _layout_reflowHorizontal: true,
-        _layout_noSidebar: true,
         defaultDoubleClick: 'ignore',
         systemIcon: 'BsFileEarmarkTextFill',
     },
-- 
cgit v1.2.3-70-g09d2


From 859bb1b733901e4697f1d53781af725e4830c607 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 14 Jan 2025 17:19:16 +0100
Subject: added drawing fill style reference

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 35 ++----------
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/ImageBox.scss               |  9 +---
 src/client/views/nodes/ImageBox.tsx                | 62 +++++++++++++---------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  2 +-
 src/server/ApiManagers/FireflyManager.ts           | 35 ++++++------
 7 files changed, 65 insertions(+), 80 deletions(-)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e1144f21a..66e0ba5d9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2194,38 +2194,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @observable private _regenInput = '';
     @observable private _drawingFillInput = '';
-    @observable private _canInteract = true;
     @observable private _regenLoading = false;
     @observable private _drawingFillLoading = false;
     @observable private _fireflyRefStrength = 50;
-    // _oldDrag: HTMLElement | null = null;
-
-    // onPassiveDrag = (e: PointerEvent) => e.stopPropagation();
-    // protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
-    //     // this._dropDisposer?.();
-    //     this._oldDrag?.removeEventListener('pointerdown', this.onPassiveDrag);
-    //     this._oldDrag = ele;
-    //     // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-    //     ele?.addEventListener('pointerdown', this.onPassiveDrag, { passive: false });
-    // };
-
-    componentAIViewHistory = () => {
-        return (
-            <div className="imageBox-aiView-history">
-                {/* {this._prevImgs.map(img => (
-                    <img
-                        className="imageBox-aiView-img"
-                        src={img.href}
-                        onClick={() => {
-                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                            this.dataDoc.ai_firefly_prompt = img.prompt;
-                            this.dataDoc.ai_firefly_seed = img.seed;
-                        }}
-                    />
-                ))} */}
-            </div>
-        );
-    };
 
     componentAIView = () => {
         const showRegenerate = this.Document[DocData].ai;
@@ -2241,7 +2212,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 aria-label="Edit instructions input"
                                 type="text"
                                 value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                onChange={action(e => (this._regenInput = e.target.value))}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
@@ -2264,7 +2235,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 <div className="collectionfreeformview-aiView-options-container">
                     <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
                     <div className="collectionfreeformview-aiView-options">
-                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => (this._drawingFillInput = e.target.value))} />
                         <div className="collectionfreeformview-aiView-strength">
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
@@ -2278,7 +2249,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 step={1}
                                 size="small"
                                 value={this._fireflyRefStrength}
-                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                onChange={action((e, val) => (this._fireflyRefStrength = val as number))}
                                 valueLabelDisplay="auto"
                             />
                             Reference Strength
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 640d27aa1..44b214644 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -718,6 +718,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _aiWinHeight = 100;
     rpw = () => this._props.PanelWidth() - this._aiWinHeight;
     rph = () => this.panelHeight() - this._aiWinHeight;
+
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 02d4d9adb..2e40f39ed 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -68,6 +68,7 @@ export interface FieldViewSharedProps {
     isGroupActive?: () => string | undefined; // is this document part of a group that is active
     // eslint-disable-next-line no-use-before-define
     setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
+
     PanelWidth: () => number;
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 759f2584b..94a497cf0 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -163,6 +163,7 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
@@ -179,13 +180,7 @@
     .imageBox-aiView-regenerate,
     .imageBox-aiView-options {
         display: flex;
-        flex-direction: row;
-        justify-content: center;
-        flex-direction: row;
+        margin: auto;
         gap: 5px;
     }
-
-    .imageBox-aiView-input {
-        width: 50%;
-    }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8a7fb99b1..3a9086777 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -96,8 +96,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // variables for AI Image Editor
     @observable private _regenInput = '';
-    @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
+    @observable private _regenLoading = false;
     @observable private _prevImgs: FireflyImageData[] = [];
 
     constructor(props: FieldViewProps) {
@@ -535,20 +534,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
+    protected _btnWidth = 50;
+    protected _inputWidth = 50;
+    protected _sideBtnMaxPanelPct = 0.12;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
+    @computed get maxWidgetSize() { return Math.min(this._btnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width) ? 1 : this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+    @computed get uiBtnScaling() { return this.maxWidgetSize / this._btnWidth; } // prettier-ignore
+    @computed get uiInputScaling() { return this.maxWidgetSize / this._inputWidth; } // prettier-ignore
+
     componentAIViewHistory = () => {
         const imgs: FireflyImageData[] = this.dataDoc.ai_firefly_history ? JSON.parse(StrCast(this.dataDoc.ai_firefly_history)) : [];
         return (
-            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
-                <Button
-                    text="Clear History"
-                    type={Type.SEC}
-                    style={{ marginTop: '5px' }}
-                    size={Size.XSMALL}
-                    onClick={action(() => {
-                        this._prevImgs = [];
-                        this.dataDoc.ai_firefly_history = undefined;
-                    })}
-                />
+            <div className="imageBox-aiView-history">
+                {imgs.length >= 2 && (
+                    <Button
+                        text="Clear History"
+                        type={Type.SEC}
+                        style={{ marginTop: '5px' }}
+                        size={Size.XSMALL}
+                        onClick={action(() => {
+                            this._prevImgs = [];
+                            this.dataDoc.ai_firefly_history = undefined;
+                        })}
+                    />
+                )}
                 {imgs.length >= 2 &&
                     imgs.map(img => (
                         <div>
@@ -574,26 +584,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
-                Edit Image with AI
+                <text>Edit Image with AI </text>
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
                         <div className="imageBox-aiView-regenerate">
                             <input
+                                style={{ transform: `scale(${this.uiInputScaling})` }}
                                 className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
                                 type="text"
                                 value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                onChange={action(e => (this._regenInput = e.target.value))}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
+                                style={{ transform: `scale(${this.uiBtnScaling})` }}
                                 text="Regenerate Image"
                                 type={Type.SEC}
-                                // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
                                 onClick={action(async () => {
-                                    this._regenerateLoading = true;
+                                    this._regenLoading = true;
                                     SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => {
                                         if (newImgs[0]) {
                                             const url = newImgs[0].pathname;
@@ -603,20 +612,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             this.dataDoc[this.fieldKey] = imgField;
                                             this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
                                             this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
-                                            this._regenerateLoading = false;
+                                            this._regenLoading = false;
                                             this._regenInput = '';
                                         }
                                     });
                                 })}
                             />
-                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
+                            <Button style={{ transform: `scale(${this.uiBtnScaling})` }} text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
                     </div>
                 )}
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
                     <div className="imageBox-aiView-options">
+                        {showRegenerate && (
+                            <text className="imageBox-aiView-subtitle" style={{ transform: `scale(${this.uiBtnScaling})` }}>
+                                More Options:
+                            </text>
+                        )}
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             type={Type.TERT}
                             text="Get Text"
                             icon={<FontAwesomeIcon icon="font" />}
@@ -632,11 +646,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             }}
                         />
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             type={Type.TERT}
                             text="Generative Fill"
                             icon={<FontAwesomeIcon icon="fill" />}
                             color={SettingsManager.userBackgroundColor}
-                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             onClick={action(() => {
                                 ImageEditorData.Open = true;
@@ -646,11 +660,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             })}
                         />
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             type={Type.TERT}
                             text="Expand"
                             icon={<FontAwesomeIcon icon="expand" />}
                             color={SettingsManager.userBackgroundColor}
-                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             onClick={() => {
                                 Networking.PostToServer('/expandImage', {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 57327631a..57e75e86a 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -48,7 +48,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then(prompt => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength: strength, presets: styles, styleRef: styleUrl})
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength: strength, presets: styles, styleUrl: styleUrl})
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 185752404..d92db1522 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,11 +20,11 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string | undefined) =>
         this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) =>
+            response?.json().then((data: { access_token: string }) => {
                 //prettier-ignore
-                fetch('https://firefly-api.adobe.io/v3/images/generate', {
+                return fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
                         ['Content-Type', 'application/json'],
@@ -43,12 +43,14 @@ export default class FireflyManager extends ApiManager {
                                       source: { url: structureUrl },
                                   },
                               },
-                        //prettier-ignore
-                        style: { 
-                            imageReference: {
-                                source: { uploadId: styleUrl }
-                            },
-                            presets: styles 
+                        // prettier-ignore
+                        style: {
+                            presets: styles,
+                            ...(styleUrl && {
+                                imageReference: {
+                                    source: { uploadId: styleUrl },
+                                },
+                            }),
                         }
                     }),
                 })
@@ -57,7 +59,7 @@ export default class FireflyManager extends ApiManager {
                         console.error('Error:', error);
                         return '';
                     })
-            )
+            })
         );
 
     uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
@@ -239,8 +241,8 @@ export default class FireflyManager extends ApiManager {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
-            secureHandler: async ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.styleRef)
+            secureHandler: async ({ req, res }) => {
+                (req.body.styleRef ? this.uploadImageToDropbox(req.body.styleRef) : Promise.resolve(undefined))
                     .then(styleUrl => {
                         if (styleUrl instanceof Error) {
                             _invalid(res, styleUrl.message);
@@ -255,13 +257,14 @@ export default class FireflyManager extends ApiManager {
                         });
                     })
                     .then(uploads =>
-                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl).then(fire =>
+                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, req.body.styleUrl).then(fire => {
                             DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                                 if (info instanceof Error) _invalid(res, info.message);
                                 else _success(res, info);
-                            })
-                        )
-                    ),
+                            });
+                        })
+                    );
+            },
         });
         register({
             method: Method.POST,
-- 
cgit v1.2.3-70-g09d2

