From 3b17868560090756caf8b9b0f043ea163f2320e8 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 20 Oct 2024 12:44:15 -0400
Subject: changes

---
 src/client/views/MainView.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7779d339f..7bc211036 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -73,7 +73,7 @@ import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { TaskCompletionBox } from './nodes/TaskCompletedBox';
 import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
 import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
-import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import ImageEditorBox from './nodes/imageEditor/ImageEditor';
 import { PresBox } from './nodes/trails';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
@@ -446,6 +446,7 @@ export class MainView extends ObservableReactComponent<object> {
                 fa.faAlignRight,
                 fa.faHeading,
                 fa.faRulerCombined,
+                fa.faFill,
                 fa.faFillDrip,
                 fa.faLink,
                 fa.faUnlink,
@@ -1146,7 +1147,7 @@ export class MainView extends ObservableReactComponent<object> {
                 <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
                 <GPTPopup key="gptpopup" />
                 <SchemaCSVPopUp key="schemacsvpopup" />
-                <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
+                <ImageEditorBox imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 89424e0a8efc6cf3364a2fd1ffc85c9d0d837453 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 22 Nov 2024 10:27:33 -0500
Subject: added initial Firefly endpoint and hanged smartDrawHandler to
 generate an image and an svg.

---
 src/client/util/bezierFit.ts                       |  3 +-
 src/client/views/MainView.tsx                      | 32 ++++++++++++--
 .../nodes/chatbot/chatboxcomponents/ChatBox.tsx    |  3 +-
 src/client/views/pdf/AnchorMenu.tsx                |  9 ++--
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 29 ++++++------
 src/server/ApiManagers/DataVizManager.ts           |  2 +-
 src/server/ApiManagers/FireflyManager.ts           | 51 ++++++++++++++++++++++
 src/server/DashUploadUtils.ts                      |  3 +-
 src/server/index.ts                                |  3 +-
 webpack.config.js                                  |  7 ++-
 10 files changed, 113 insertions(+), 29 deletions(-)
 create mode 100644 src/server/ApiManagers/FireflyManager.ts

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index d52460023..84b27e84c 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -703,7 +703,6 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
                     coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
-                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
                     lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
                 } else if (match[0].startsWith('C')) {
                     coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
@@ -720,7 +719,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 }
             });
             const hasZ = attributes.d.match(/Z/);
-            if (hasZ) {
+            if (hasZ || attributes.fill) {
                 coordList.push(lastPt);
                 coordList.push(startPt);
                 coordList.push(startPt);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7779d339f..0d071fe4f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
 import '../../../node_modules/browndash-components/dist/styles/global.min.css';
-import { ClientUtils, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
+import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, GetDocFromUrl, Opt, returnEmptyDoclist } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
@@ -1023,10 +1023,36 @@ export class MainView extends ObservableReactComponent<object> {
                 <svg style={{ width: '100%', height: '100%' }}>
                     {[
                         ...SnappingManager.HorizSnapLines.map(l => (
-                            <line key={'horiz' + l} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                            <line
+                                key={'horiz' + l}
+                                x1="0"
+                                y1={l}
+                                x2="2000"
+                                y2={l}
+                                stroke={
+                                    SnappingManager.userVariantColor
+                                    /* lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))*/
+                                }
+                                opacity={0.3}
+                                strokeWidth={3}
+                                strokeDasharray="2 2"
+                            />
                         )),
                         ...SnappingManager.VertSnapLines.map(l => (
-                            <line key={'vert' + l} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                            <line
+                                key={'vert' + l}
+                                y1={this.topOfMainDocContent.toString()}
+                                x1={l}
+                                y2="2000"
+                                x2={l}
+                                stroke={
+                                    SnappingManager.userVariantColor
+                                    /* lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))*/
+                                }
+                                opacity={0.3}
+                                strokeWidth={3}
+                                strokeDasharray="2 2"
+                            />
                         )),
                     ]}
                 </svg>
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index a61705250..3ef6bdd8b 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -431,7 +431,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 doc = Docs.Create.FunctionPlotDocument([], options);
                 break;
             case 'dataviz':
-            case 'data_viz':
+            case 'data_viz': {
                 const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
                     filename: (options.title as string).replace(/\s+/g, '') + '.csv',
                     data: data,
@@ -439,6 +439,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 doc = Docs.Create.DataVizDocument(fileUrl, { ...options, text: RTFCast(data) });
                 this.addCSVForAnalysis(doc, id);
                 break;
+            }
             case 'chat':
                 doc = Docs.Create.ChatDocument(options);
                 break;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 5ab9b556c..fe03f32a5 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -131,12 +131,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     /**
      * Creates a GPT drawing based on selected text.
      */
-    gptDraw = async (e: React.PointerEvent) => {
+    gptDraw = (e: React.PointerEvent) => {
         try {
             SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
             runInAction(() => (this._isLoading = true));
-            await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
-            runInAction(() => (this._isLoading = false));
+            SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true)?.then(
+                action(() => {
+                    this._isLoading = false;
+                })
+            );
         } catch (err) {
             console.error(err);
         }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index d0f6566a5..342b91bd9 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -13,6 +13,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { Networking } from '../../Network';
 import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { SettingsManager } from '../../util/SettingsManager';
@@ -21,7 +22,8 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/OpenWhere';
 import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
@@ -230,20 +232,21 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * Calls GPT API to create a drawing based on user input.
      */
     @action
-    drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
-        if (input === '') return;
-        this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
-        const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
-        if (!res) {
-            console.error('GPT call failed');
-            return;
-        }
-        const strokeData = await this.parseSvg(res, startPt, false, autoColor);
-        const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-        drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+    drawWithGPT = (startPt: { X: number; Y: number }, prompt: string, complexity: number, size: number, autoColor: boolean) => {
+        if (prompt === '') return;
+        this._lastInput = { text: prompt, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+
+        Networking.PostToServer('/queryFireflyImage', { prompt }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, { title: prompt }), OpenWhere.addRight));
+
+        const result = gptAPICall(`"${prompt}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true).then(res =>
+            this.parseSvg(res, startPt, false, autoColor).then(strokeData => {
+                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+            })
+        );
 
         this._errorOccurredOnce = false;
-        return strokeData;
+        return result;
     };
 
     /**
diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts
index 88f22992d..d2028f23b 100644
--- a/src/server/ApiManagers/DataVizManager.ts
+++ b/src/server/ApiManagers/DataVizManager.ts
@@ -9,7 +9,7 @@ export default class DataVizManager extends ApiManager {
         register({
             method: Method.GET,
             subscription: '/csvData',
-            secureHandler: async ({ req, res }) => {
+            secureHandler: ({ req, res }) => {
                 const uri = req.query.uri as string;
 
                 return new Promise<void>(resolve => {
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
new file mode 100644
index 000000000..04fa8f065
--- /dev/null
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -0,0 +1,51 @@
+import { DashUploadUtils } from '../DashUploadUtils';
+import { _invalid, _success, Method } from '../RouteManager';
+import ApiManager, { Registration } from './ApiManager';
+
+export default class FireflyManager extends ApiManager {
+    askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => {
+        const fetched = fetch('https://ims-na1.adobelogin.com/ims/token/v3', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded',
+            },
+            body: `grant_type=client_credentials&client_id=${process.env._CLIENT_FIREFLY_CLIENT_ID}&client_secret=${process.env._CLIENT_FIREFLY_SECRET}&scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis`,
+        })
+            .then(response => response.json())
+            .then((data: { access_token: string }) =>
+                fetch('https://firefly-api.adobe.io/v3/images/generate', {
+                    method: 'POST',
+                    headers: [
+                        ['Content-Type', 'application/json'],
+                        ['Accept', 'application/json'],
+                        ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+                        ['Authorization', `Bearer ${data.access_token}`],
+                    ],
+                    body: `{  "prompt": "${prompt}" }`,
+                })
+                    .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
+                    .catch(error => {
+                        console.error('Error:', error);
+                        return '';
+                    })
+            )
+            .catch(error => {
+                console.error('Error:', error);
+                return '';
+            });
+        return fetched;
+    };
+    protected initialize(register: Registration): void {
+        register({
+            method: Method.POST,
+            subscription: '/queryFireflyImage',
+            secureHandler: ({ req, res }) =>
+                this.askFirefly(req.body.prompt).then(fire =>
+                    DashUploadUtils.UploadImage(JSON.parse(fire).url).then(info => {
+                        if (info instanceof Error) _invalid(res, info.message);
+                        else _success(res, info.accessPaths.agnostic.client);
+                    })
+                ),
+        });
+    }
+}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 1e55a885a..032d13d43 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -369,7 +369,8 @@ export namespace DashUploadUtils {
      */
     export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
+        const dfltSuffix = remaining.contentType.split('/')[1].toLowerCase();
+        const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${dfltSuffix === 'xml' ? 'jpg' : dfltSuffix}`;
         const { images } = Directory;
         const information: Upload.ImageInformation = {
             accessPaths: {
diff --git a/src/server/index.ts b/src/server/index.ts
index 88dbd232d..1f9af9ee0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -7,6 +7,7 @@ import AssistantManager from './ApiManagers/AssistantManager';
 import DataVizManager from './ApiManagers/DataVizManager';
 import DeleteManager from './ApiManagers/DeleteManager';
 import DownloadManager from './ApiManagers/DownloadManager';
+import FireflyManager from './ApiManagers/FireflyManager';
 import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager';
 import SessionManager from './ApiManagers/SessionManager';
 import UploadManager from './ApiManagers/UploadManager';
@@ -71,6 +72,7 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage
         new GeneralGoogleManager(),
         /* new GooglePhotosManager(), */ new DataVizManager(),
         new AssistantManager(),
+        new FireflyManager(),
     ];
 
     // initialize API Managers
@@ -112,7 +114,6 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage
     });
 
     const serve: PublicHandler = ({ req, res }) => {
-        // eslint-disable-next-line new-cap
         const detector = new mobileDetect(req.headers['user-agent'] || '');
         const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
         res.sendFile(path.join(__dirname, '../../deploy/' + filename));
diff --git a/webpack.config.js b/webpack.config.js
index e1afc64e5..67417fb02 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable @typescript-eslint/no-require-imports */
 const path = require('path');
 const webpack = require('webpack');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -36,7 +36,6 @@ function transferEnvironmentVariables() {
     }
     const resolvedClientSide = Object.keys(parsed).reduce((mapping, envKey) => {
         if (envKey.startsWith(prefix)) {
-            // eslint-disable-next-line
             mapping[`process.env.${envKey.replace(prefix, '')}`] = JSON.stringify(parsed[envKey]);
         }
         return mapping;
@@ -112,7 +111,7 @@ module.exports = {
                 test: /\.scss|css$/,
                 exclude: /\.module\.scss$/i,
                 use: [
-                    { loader: 'style-loader' }, // eslint-disable-next-line prettier/prettier
+                    { loader: 'style-loader' }, //
                     { loader: 'css-loader' },
                     { loader: 'sass-loader' },
                 ],
@@ -127,7 +126,7 @@ module.exports = {
             {
                 test: /\.module\.scss$/i,
                 use: [
-                    { loader: 'style-loader' }, // eslint-disable-next-line prettier/prettier
+                    { loader: 'style-loader' }, //
                     { loader: 'css-loader', options: { modules: true } },
                     { loader: 'sass-loader' },
                 ],
-- 
cgit v1.2.3-70-g09d2


From cffb72e762d989eb1219a5736f082f5b656bee37 Mon Sep 17 00:00:00 2001
From: Geireann Lindfield Roberts <geireann_lindfield_roberts@brown.edu>
Date: Wed, 1 Jan 2025 22:45:48 -0800
Subject: Update MainView.tsx

---
 src/client/views/MainView.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0d071fe4f..65d90a5a0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,7 +7,7 @@ import { action, computed, configure, makeObservable, observable, reaction, runI
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
-import '../../../node_modules/browndash-components/dist/styles/global.min.css';
+import '@dash/components/src/global/globalCssVariables.scss';
 import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, GetDocFromUrl, Opt, returnEmptyDoclist } from '../../fields/Doc';
-- 
cgit v1.2.3-70-g09d2


From 6fa61343a3e63a61747bc7123810190057d0bfe4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Jan 2025 12:14:12 -0500
Subject: cleaned up filter icons.

---
 src/client/util/CurrentUserUtils.ts                | 38 +++++++++++++++++-----
 src/client/views/MainView.tsx                      | 25 +++-----------
 .../views/collections/CollectionCardDeckView.tsx   | 20 +++---------
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 34 +++++++------------
 4 files changed, 48 insertions(+), 69 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index e806675aa..306914450 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -40,6 +40,7 @@ import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
 import { DocumentView } from "../views/nodes/DocumentView";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
 
 export interface Button {
     // DocumentOptions fields a button can set
@@ -67,6 +68,21 @@ export interface Button {
     subMenu?: Button[];
 }
 
+// Not really necessary, but for now, all tags should start with a capital first letter
+export type TagName<T extends string> =
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        T extends `${infer First}${infer Rest}`
+          ? First extends Uppercase<First> 
+            ? First extends Lowercase<First> 
+              ? never  // If it's the same when uppercased and lowercased, it's not a letter.
+              : T      // Otherwise, it's a valid capitalized string.
+            : never
+          : never;
+export function ToTagName(key: string):"Tag"{
+    return ((str => str[0].toUpperCase() + str.slice(1))(key.startsWith('#') ? key.substring(1) : key)) as "Tag";
+}
+
+
 export let resolvedPorts: { server: number, socket: number };
 export class CurrentUserUtils {
 
@@ -703,18 +719,22 @@ pie title Minerals in my tap water
             ]},
          ]
     }
+
+    static filterBtnDesc<T extends string>(tag:TagName<T>|"Tag", icon:IconProp):Button {
+        return { title: tag, isSystem: false, icon: icon.toString(),  toolTip:`Click to toggle visibility of ${tag} tagged Docs`,  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:`#${tag.toLowerCase()}`, funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}}
+    }
   
     static filterTools(): Button[] {
-        const defaultTagButtonDescs = [
-            { title: "Star", isSystem: false,icon: "star",  toolTip:"Click to toggle visibility of Star tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#star", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
-            { title: "Like", isSystem: false,icon: "heart", toolTip:"Click to toggle visibility of Like tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#like", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
-            { title: "Todo", isSystem: false,icon: "bolt",  toolTip:"Click to toggle visibility of Todo tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#todo", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
-            { title: "Idea", isSystem: false,icon: "cloud", toolTip:"Click to toggle visibility of Idea tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#idea", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
+        // If there's no active dashboard, then a default set of tags are added, otherwise, the user controls which tags are kept
+        const tagButtonDescs = Doc.UserDoc().activeDashboard ? [] : [
+            this.filterBtnDesc("Star", "star"),
+            this.filterBtnDesc("Like", "heart"),
+            this.filterBtnDesc("Todo", "bolt"),
+            this.filterBtnDesc("Idea", "cloud")
         ];
-        // hack: if there's no dashboard, create default filters.  otherwise, just make sure that the Options button is preserved
         return [
-            { title:"Options",isSystem: true,icon: "gear",  toolTip:"Click to customize list of filter buttons",       btnType: ButtonType.ClickButton,  expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}},
-            ...(Doc.UserDoc().activeDashboard ? [] : defaultTagButtonDescs)
+            { title:"Options",isSystem: true,icon: "gear",  toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}},
+            ...tagButtonDescs
         ]
     }
     static viewTools(): Button[] {
@@ -866,7 +886,7 @@ pie title Minerals in my tap water
             childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick,
             linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
         
-        const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) );
+        const items = (menuBtn?:Doc) => !menuBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtn) );
         const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList;
         const btnDoc = DocUtils.AssignScripts( DocUtils.AssignDocField(menuDoc, StrCast(params.title), 
                 (opts) => creator(opts, items(menuBtnDoc)), reqdSubMenuOpts, items(menuBtnDoc)), params.scripts, params.funcs);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7abca5197..d748b70ae 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -20,7 +20,7 @@ import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { CalendarManager } from '../util/CalendarManager';
 import { CaptureManager } from '../util/CaptureManager';
-import { Button, CurrentUserUtils } from '../util/CurrentUserUtils';
+import { CurrentUserUtils, ToTagName } from '../util/CurrentUserUtils';
 import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
@@ -63,7 +63,6 @@ import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu';
 import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
 import { DocButtonState } from './nodes/DocumentLinksButton';
 import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
-import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
 import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
@@ -874,25 +873,9 @@ export class MainView extends ObservableReactComponent<object> {
      * @param hotKey tite of the new hotkey
      */
     addHotKey = (hotKey: string) => {
-        const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
-        const filter = DocCast(buttons.Filter);
-        const title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey;
-
-        const newKey: Button = {
-            title,
-            icon: 'question',
-            toolTip: `Click to toggle the ${title}'s group's visibility`,
-            btnType: ButtonType.ToggleButton,
-            expertMode: false,
-            toolType: '#' + title,
-            funcs: {},
-            scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
-        };
-
-        const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
-        newBtn.isSystem = newBtn[DocData].isSystem = undefined;
-
-        Doc.AddToFilterHotKeys(newBtn);
+        const filterIcons = DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter);
+        const menuDoc = CurrentUserUtils.setupContextMenuBtn(CurrentUserUtils.filterBtnDesc(ToTagName(hotKey), 'question'), filterIcons);
+        Doc.AddToFilterHotKeys(menuDoc);
     };
 
     @computed get mainInnerContent() {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 366d0f448..b40aa37de 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -462,24 +462,13 @@ export class CollectionCardView extends CollectionSubView() {
                     case '6':
                         doc.chatIndex = index;
                         break;
-                    case '1': {
-                        const allHotKeys = Doc.MyFilterHotKeys;
-                        let myTag = '';
+                    case '1':
                         if (tag) {
-                            for (let i = 0; i < allHotKeys.length; i++) {
-                                const keyTag = StrCast(allHotKeys[i].toolType);
-                                if (keyTag.includes(tag)) {
-                                    myTag = keyTag;
-                                    break;
-                                }
-                            }
-
-                            if (myTag) {
-                                TagItem.addTagToDoc(doc, myTag);
-                            }
+                            const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
+                            const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+                            TagItem.addTagToDoc(doc, filterTag);
                         }
                         break;
-                    }
                     case '2':
                     case '4':
                         doc.chatFilter = true;
@@ -584,7 +573,6 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        console.log(this.docDraggedIndex, this.childDocs[0].title, this.childDocs[1].title);
         // Map sorted documents to their rendered components
         return this.childDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 4028ef479..c33b81eb4 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -188,8 +188,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
 
         const selected = DocumentView.SelectedDocs().lastElement();
 
-        const questionText = 'Question: ' + StrCast(selected['gptInputText']);
-        const rubricText = 'Rubric: ' + StrCast(selected['gptRubric']);
+        const questionText = 'Question: ' + StrCast(selected.gptInputText);
+        const rubricText = 'Rubric: ' + StrCast(selected.gptRubric);
         const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText;
 
         try {
@@ -262,27 +262,15 @@ export class GPTPopup extends ObservableReactComponent<object> {
 
         try {
             const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
-            const questionNumber = questionType.split(' ')[0];
-            let res = '';
-
-            switch (questionNumber) {
-                case '1':
-                case '2':
-                case '4':
-                    res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
-                    break;
-                case '6':
-                    res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
-                    break;
-                default:
-                    {
-                        const selected = DocumentView.SelectedDocs().lastElement();
-                        const questionText = StrCast(selected?.gptInputText);
-
-                        res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt);
-                    }
-                    break;
-            }
+            const questionNumber = questionType.split(' ')[0][0];
+            const res = await (() => {
+                switch (questionNumber) {
+                    case '1':
+                    case '2':
+                    case '4': return gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
+                    case '6': return gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
+                    default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, this.chatSortPrompt);
+                }})(); // prettier-ignore
 
             // Trigger the callback with the result
             if (this.onSortComplete) {
-- 
cgit v1.2.3-70-g09d2

