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

---
 .../views/nodes/imageEditor/ImageEditorButtons.tsx | 69 ++++++++++++++++++++++
 1 file changed, 69 insertions(+)
 create mode 100644 src/client/views/nodes/imageEditor/ImageEditorButtons.tsx

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

diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
new file mode 100644
index 000000000..e90babf9b
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -0,0 +1,69 @@
+import './GenerativeFillButtons.scss';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { Button, IconButton, Type } from 'browndash-components';
+import { AiOutlineInfo } from 'react-icons/ai';
+import { bgColor } from './imageEditorUtils/imageEditorConstants';
+import { ImageEditTool, ImageToolType } from './imageEditorUtils/imageEditorInterfaces';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SettingsManager } from '../../../util/SettingsManager';
+
+interface ButtonContainerProps {
+    onClick: () => Promise<void>;
+    loading: boolean;
+    onReset: () => void;
+    btnText: string;
+}
+
+export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }: ButtonContainerProps) {
+    return (
+        <div className="generativeFillBtnContainer">
+            <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} />
+            {loading ? (
+                <Button
+                    text={btnText}
+                    type={Type.TERT}
+                    color={SettingsManager.userVariantColor}
+                    icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
+                    iconPlacement="right"
+                    onClick={() => {
+                        if (!loading) getEdit();
+                    }}
+                />
+            ) : (
+                <Button
+                    text={btnText}
+                    type={Type.TERT}
+                    color={SettingsManager.userVariantColor}
+                    onClick={() => {
+                        if (!loading) getEdit();
+                    }}
+                />
+            )}
+            <IconButton
+                type={Type.SEC}
+                color={SettingsManager.userVariantColor}
+                tooltip="Open Documentation"
+                icon={<AiOutlineInfo size="16px" />}
+                onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')}
+            />
+        </div>
+    );
+}
+
+export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) {
+    return (
+        <div className="imageEditorButtonContainer">
+            <Button
+                style={{ width: '100%', border: '1px' }}
+                text={tool.name}
+                type={Type.TERT}
+                color={isActive ? SettingsManager.userVariantColor : bgColor}
+                icon={<FontAwesomeIcon icon={tool.icon} />}
+                onClick={() => {
+                    selectTool(tool.type);
+                }}
+            />
+        </div>
+    );
+}
-- 
cgit v1.2.3-70-g09d2


From e8661840c39219fd848e664c990943450c41bbee Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 28 Oct 2024 23:10:24 -0400
Subject: basic structure of 4 cut modes implemented, many bugs to fix

---
 .../views/nodes/imageEditor/ImageEditor.scss       |  59 +++--
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 265 +++++++++++----------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   2 +-
 .../imageEditorUtils/imageEditorInterfaces.ts      |   3 +-
 4 files changed, 180 insertions(+), 149 deletions(-)

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

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index 49146aa23..c691e6a18 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -75,38 +75,47 @@ $scale: 0.5;
             }
         }
 
-        .iconContainer {
+        .sideControlsContainer {
+            width: 160px;
             position: absolute;
-            top: 3rem;
-            left: 2rem;
-            display: flex;
-            flex-direction: column;
-            gap: 4rem;
+            left: 0;
+            height: 100%;
 
-            .imageToolsContainer {
+            .sideControls {
+                position: absolute;
+                width: 120px;
+                top: 3rem;
+                left: 2rem;
                 display: flex;
                 flex-direction: column;
-                gap: 10px;
-            }
+                gap: 1rem;
 
-            .cutToolsContainer {
-                display: grid;
-                gap: 5px;
-                grid-template-columns: 1fr 1fr;
-            }
+                .imageToolsContainer {
+                    display: flex;
+                    flex-direction: column;
+                    gap: 10px;
+                }
 
-            .undoRedoContainer {
-                justify-content: center;
-                display: flex;
-                flex-direction: row;
-            }
+                .cutToolsContainer {
+                    display: grid;
+                    gap: 5px;
+                    grid-template-columns: 1fr 1fr;
+                }
 
-            .sliderContainer {
-                height: 225px;
-                width: 100%;
-                display: flex;
-                justify-content: center;
-                cursor: pointer;
+                .undoRedoContainer {
+                    justify-content: center;
+                    display: flex;
+                    flex-direction: row;
+                }
+
+                .sliderContainer {
+                    margin: 3rem 0;
+                    height: 225px;
+                    width: 100%;
+                    display: flex;
+                    justify-content: center;
+                    cursor: pointer;
+                }
             }
         }
 
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index d9f46876e..980f3e566 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -4,7 +4,7 @@
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable react/function-component-definition */
 import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material';
-import { IconButton } from 'browndash-components';
+import { Button, IconButton, Type } from 'browndash-components';
 import * as React from 'react';
 import { useEffect, useRef, useState } from 'react';
 import { CgClose } from 'react-icons/cg';
@@ -85,7 +85,6 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // constants for image cutting
     const cutPts = useRef<Point[]>([]);
 
-
     /**
      *
      * @param type The new tool type we are changing to
@@ -356,7 +355,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         setLoading(false);
     };
 
-    const cutImage = async () => {
+    const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => {
         const img = currImg.current;
         const canvas = canvasRef.current;
         if (!canvas || !img) return;
@@ -381,21 +380,22 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 maxX = Math.max(cutPts.current[i].x, maxX);
                 maxY = Math.max(cutPts.current[i].y, maxY);
             }
-            ctx.closePath();
-            switch (cutType) { // may need to move this before the drawing at all for the line cases
+            switch (currCutType) {
                 case BrushMode.IN:
+                    ctx.closePath();
                     ctx.globalCompositeOperation = 'destination-in';
                     ctx.fill();
                     break;
                 case BrushMode.OUT:
+                    ctx.closePath();
                     ctx.globalCompositeOperation = 'destination-out';
                     ctx.fill();
                     break;
-                case BrushMode.LINE_OUT:
-                    ctx.globalCompositeOperation = 'destination-out';
-                    break;
                 case BrushMode.LINE_IN:
                     ctx.globalCompositeOperation = 'destination-in';
+                    ctx.lineWidth = brushWidth + 20;
+                    ctx.stroke();
+                    break;
             }
         }
 
@@ -423,31 +423,41 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         const image = new Image();
         image.src = url;
         image.onload = async () => {
-            const croppedCanvas = document.createElement('canvas');
-            const croppedCtx = croppedCanvas.getContext('2d');
-            if (!croppedCtx) return;
-            croppedCanvas.width = maxX - minX;
-            croppedCanvas.height = maxY - minY;
-            croppedCtx.globalCompositeOperation = 'source-over';
-            croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
-            croppedCtx.drawImage(image, -minX, -minY);
-            const croppedURL = croppedCanvas.toDataURL();
-            const croppedImage = new Image();
-            croppedImage.src = croppedURL;
-
-            currImg.current = croppedImage;
-            const newImgDoc = await createNewImgDoc(croppedImage, isFirstDoc.current);
-            if (isFirstDoc.current) isFirstDoc.current = false;
+            let finalImg: HTMLImageElement | undefined = image;
+            let finalImgURL: string = url;
+            if (currCutType == BrushMode.IN) {
+                const croppedData = cropImage(image, minX, maxY, minY, maxY);
+                finalImg = croppedData;
+                finalImgURL = croppedData.src;
+            }
+            currImg.current = finalImg;
+            const newImgDoc = await createNewImgDoc(finalImg, isFirstDoc.current);
             if (newImgDoc) {
                 const docData = newImgDoc[DocData];
                 docData.backgroundColor = 'transparent';
+                if (isFirstDoc.current) isFirstDoc.current = false;
+                setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
             }
-            setEdits(prevEdits => [...prevEdits, { url: croppedURL, saveRes: undefined }]);
             setLoading(false);
             cutPts.current.length = 0;
         };
     };
 
+    const cropImage = (image: HTMLImageElement, minX: number, maxX: number, minY: number, maxY: number) => {
+        const croppedCanvas = document.createElement('canvas');
+        const croppedCtx = croppedCanvas.getContext('2d');
+        if (!croppedCtx) return image;
+        croppedCanvas.width = maxX - minX;
+        croppedCanvas.height = maxY - minY;
+        croppedCtx.globalCompositeOperation = 'source-over';
+        croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
+        croppedCtx.drawImage(image, -minX, -minY);
+        const croppedURL = croppedCanvas.toDataURL();
+        const croppedImage = new Image();
+        croppedImage.src = croppedURL;
+        return croppedImage;
+    };
+
     // adjusts all the img positions to be aligned
     const adjustImgPositions = () => {
         if (!parentDoc.current) return;
@@ -577,7 +587,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         labelPlacement="end"
                         sx={{ whiteSpace: 'nowrap' }}
                     />
-                    <ApplyFuncButtons onClick={currTool.applyFunc} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+                    <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits)} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
                     <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
                 </div>
             </div>
@@ -587,109 +597,120 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // the side icons including tool type, the slider, and undo/redo
     function renderSideIcons() {
         return (
-            <div className="iconContainer">
-                <div className="imageToolsContainer">
-                    {imageEditTools.map(tool => {
-                        return ImageToolButton(tool, tool.type === currTool.type, changeTool);
-                    })}
-                </div>
-                {currTool.type == ImageToolType.Cut && 
-                    <div className="cutToolContainer"> 
-                        <Button 
-                            text="Keep in"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.IN ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.IN);
-                            }}/>
-                        <Button 
-                            text="Keep out"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.OUT ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.OUT);
-                            }}/>
-                        <Button 
-                            text="Draw in"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.LINE_IN ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.LINE_IN);
-                            }}/>
-                        <Button 
-                            text="Erase"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.LINE_OUT ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.LINE_OUT);
-                            }}/>
-                    </div>}
-                <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
-                    {currTool.type === ImageToolType.GenerativeFill && (
-                        <Slider
-                            sx={{
-                                '& input[type="range"]': {
-                                    WebkitAppearance: 'slider-vertical',
-                                },
+            <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
+                <div className="sideControls">
+                    <div className="imageToolsContainer">
+                        {imageEditTools.map(tool => {
+                            return ImageToolButton(tool, tool.type === currTool.type, changeTool);
+                        })}
+                    </div>
+                    {currTool.type == ImageToolType.Cut && (
+                        <div className="cutToolsContainer">
+                            <Button
+                                style={{ width: '100%' }}
+                                text="Keep in"
+                                type={Type.TERT}
+                                color={cutType == BrushMode.IN ? SettingsManager.userColor : bgColor}
+                                onClick={() => {
+                                    setCutType(BrushMode.IN);
+                                }}
+                            />
+                            <Button
+                                style={{ width: '100%' }}
+                                text="Keep out"
+                                type={Type.TERT}
+                                color={cutType == BrushMode.OUT ? SettingsManager.userColor : bgColor}
+                                onClick={() => {
+                                    setCutType(BrushMode.OUT);
+                                }}
+                            />
+                            <Button
+                                style={{ width: '100%' }}
+                                text="Draw in"
+                                type={Type.TERT}
+                                color={cutType == BrushMode.LINE_IN ? SettingsManager.userColor : bgColor}
+                                onClick={() => {
+                                    setCutType(BrushMode.LINE_IN);
+                                }}
+                            />
+                            <Button
+                                style={{ width: '100%' }}
+                                text="Erase"
+                                type={Type.TERT}
+                                color={cutType == BrushMode.LINE_OUT ? SettingsManager.userColor : bgColor}
+                                onClick={() => {
+                                    setCutType(BrushMode.LINE_OUT);
+                                }}
+                            />
+                        </div>
+                    )}
+                    <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
+                        {currTool.type === ImageToolType.GenerativeFill && (
+                            <Slider
+                                sx={{
+                                    '& input[type="range"]': {
+                                        WebkitAppearance: 'slider-vertical',
+                                    },
+                                }}
+                                orientation="vertical"
+                                min={genFillTool.sliderMin}
+                                max={genFillTool.sliderMax}
+                                defaultValue={genFillTool.sliderDefault}
+                                size="small"
+                                valueLabelDisplay="auto"
+                                onChange={(e, val) => {
+                                    setCursorData(prev => ({ ...prev, width: val as number }));
+                                }}
+                            />
+                        )}
+                        {currTool.type === ImageToolType.Cut && (
+                            <Slider
+                                sx={{
+                                    '& input[type="range"]': {
+                                        WebkitAppearance: 'slider-vertical',
+                                    },
+                                }}
+                                orientation="vertical"
+                                min={cutTool.sliderMin}
+                                max={cutTool.sliderMax}
+                                defaultValue={cutTool.sliderDefault}
+                                size="small"
+                                valueLabelDisplay="auto"
+                                onChange={(e, val) => {
+                                    setCursorData(prev => ({ ...prev, width: val as number }));
+                                }}
+                            />
+                        )}
+                    </div>
+                    {/* Undo and Redo */}
+                    <div className="undoRedoContainer">
+                        <IconButton
+                            style={{ cursor: 'pointer' }}
+                            onPointerDown={e => {
+                                e.stopPropagation();
+                                handleUndo();
                             }}
-                            orientation="vertical"
-                            min={genFillTool.sliderMin}
-                            max={genFillTool.sliderMax}
-                            defaultValue={genFillTool.sliderDefault}
-                            size="small"
-                            valueLabelDisplay="auto"
-                            onChange={(e, val) => {
-                                setCursorData(prev => ({ ...prev, width: val as number }));
+                            onPointerUp={e => {
+                                e.stopPropagation();
                             }}
+                            color={activeColor}
+                            tooltip="Undo"
+                            icon={<IoMdUndo />}
                         />
-                    )}
-                    {currTool.type === ImageToolType.Cut && (
-                        <Slider
-                            sx={{
-                                '& input[type="range"]': {
-                                    WebkitAppearance: 'slider-vertical',
-                                },
+                        <IconButton
+                            style={{ cursor: 'pointer' }}
+                            onPointerDown={e => {
+                                e.stopPropagation();
+                                handleRedo();
                             }}
-                            orientation="vertical"
-                            min={cutTool.sliderMin}
-                            max={cutTool.sliderMax}
-                            defaultValue={cutTool.sliderDefault}
-                            size="small"
-                            valueLabelDisplay="auto"
-                            onChange={(e, val) => {
-                                setCursorData(prev => ({ ...prev, width: val as number }));
+                            onPointerUp={e => {
+                                e.stopPropagation();
                             }}
+                            color={activeColor}
+                            tooltip="Redo"
+                            icon={<IoMdRedo />}
                         />
-                    )}
-                </div>
-                {/* Undo and Redo */}
-                <div className="undoRedoContainer">
-                    <IconButton
-                        style={{ cursor: 'pointer' }}
-                        onPointerDown={e => {
-                            e.stopPropagation();
-                            handleUndo();
-                        }}
-                        onPointerUp={e => {
-                            e.stopPropagation();
-                        }}
-                        color={activeColor}
-                        tooltip="Undo"
-                        icon={<IoMdUndo />}
-                    />
-                    <IconButton
-                        style={{ cursor: 'pointer' }}
-                        onPointerDown={e => {
-                            e.stopPropagation();
-                            handleRedo();
-                        }}
-                        onPointerUp={e => {
-                            e.stopPropagation();
-                        }}
-                        color={activeColor}
-                        tooltip="Redo"
-                        icon={<IoMdRedo />}
-                    />
+                    </div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index e90babf9b..cb963616b 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -55,7 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo
     return (
         <div className="imageEditorButtonContainer">
             <Button
-                style={{ width: '100%', border: '1px' }}
+                style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
                 color={isActive ? SettingsManager.userVariantColor : bgColor}
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index bd2fac775..660af7c42 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -1,4 +1,5 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Doc } from '../../../../../fields/Doc';
 
 export interface CursorData {
     x: number;
@@ -21,7 +22,7 @@ export interface ImageEditTool {
     name: string;
     btnText: string;
     icon: IconProp;
-    applyFunc: () => Promise<void>;
+    applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => Promise<void>;
     sliderMin?: number;
     sliderMax?: number;
     sliderDefault?: number;
-- 
cgit v1.2.3-70-g09d2


From 2f7d1f0073943e1eb9e0f34c4459bc0176377697 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Jan 2025 10:39:13 -0500
Subject: from last

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

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

diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index cb963616b..de2116253 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -1,7 +1,7 @@
 import './GenerativeFillButtons.scss';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
-import { Button, IconButton, Type } from 'browndash-components';
+import { Button, IconButton, Type } from '@dash/components';
 import { AiOutlineInfo } from 'react-icons/ai';
 import { bgColor } from './imageEditorUtils/imageEditorConstants';
 import { ImageEditTool, ImageToolType } from './imageEditorUtils/imageEditorInterfaces';
-- 
cgit v1.2.3-70-g09d2


From a496c583f375fe5e118f786b1244c42bc8a34fec Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Jan 2025 14:20:40 -0500
Subject: fixed flashcard practice mode.  moved doc sorting from cardDeck into
 collectionSubView to apply it to all collections.

---
 .../src/components/Dropdown/Dropdown.tsx           |  30 ++----
 src/client/util/CurrentUserUtils.ts                |  10 +-
 .../views/collections/CollectionCardDeckView.tsx   | 119 ++++-----------------
 src/client/views/collections/CollectionSubView.tsx |  49 ++++++++-
 .../views/collections/FlashcardPracticeUI.tsx      |   4 +-
 src/client/views/global/globalScripts.ts           |  35 +++---
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx |  69 ++----------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   1 +
 9 files changed, 108 insertions(+), 211 deletions(-)

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

diff --git a/packages/components/src/components/Dropdown/Dropdown.tsx b/packages/components/src/components/Dropdown/Dropdown.tsx
index 0953f412c..b9b6f01b8 100644
--- a/packages/components/src/components/Dropdown/Dropdown.tsx
+++ b/packages/components/src/components/Dropdown/Dropdown.tsx
@@ -99,22 +99,14 @@ export const Dropdown = (props: IDropdownProps) => {
         background: background ?? color,
     };
 
-    const getCaretDirection = (active: boolean, placement: Placement = 'left'): JSX.Element => {
-        if (iconProvider) return iconProvider(active, placement);
-        switch (placement) {
-            case 'bottom':
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-            case 'right':
-                if (active) return <FaCaretLeft />;
-                return <FaCaretRight />;
-            case 'top':
-                if (active) return <FaCaretDown />;
-                return <FaCaretUp />;
-            default:
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-        }
+    const getCaretDirection = (isActive: boolean, caretPlacement: Placement = 'left'): JSX.Element => {
+        if (iconProvider) return iconProvider(isActive, caretPlacement);
+        switch (caretPlacement) {
+            default:      
+            case 'bottom':return isActive ?  <FaCaretUp />: <FaCaretDown />;
+            case 'right': return isActive ? <FaCaretLeft /> : <FaCaretRight />;
+            case 'top':   return isActive ? <FaCaretDown />: <FaCaretUp />;
+        } // prettier-ignore
     };
 
     const getToggle = () => {
@@ -143,9 +135,9 @@ export const Dropdown = (props: IDropdownProps) => {
         }
     };
 
-    const setActiveChanged = (active: boolean) => {
-        setActive(active);
-        props.activeChanged?.(active);
+    const setActiveChanged = (isActive: boolean) => {
+        setActive(isActive);
+        props.activeChanged?.(isActive);
     };
 
     const dropdown: JSX.Element = (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7c36a82f2..e806675aa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -688,7 +688,7 @@ pie title Minerals in my tap water
             { title: "Center",    icon: "align-center",  toolTip: "Center Align Stack",  btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
-    static cardTools(): Button[] {
+    static sortTools(): Button[] {
         return [
             { title: "Time",      icon:"hourglass-half", toolTip:"Sort by most recent document creation",          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
@@ -704,7 +704,7 @@ pie title Minerals in my tap water
          ]
     }
   
-    static tagGroupTools(): Button[] {
+    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_);}'}},
@@ -723,7 +723,6 @@ pie title Minerals in my tap water
             { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Fit All",   icon: "object-group",        toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
             { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            { title: "Cards",     icon: "brain",               toolTip: "Flashcards",          btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
@@ -822,15 +821,14 @@ pie title Minerals in my tap water
             { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             
-            { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+            { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true,  toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+            { title: "Sort",    icon: "Sort",   toolTip: "Sort Documents",       subMenu: CurrentUserUtils.sortTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Ink",     icon: "Ink",    toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {hidden: `IsExploreMode()`,                                                   linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
             { title: "Doc",     icon: "Doc",    toolTip: "Freeform Doc tools",   subMenu: CurrentUserUtils.freeTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "View",    icon: "View",   toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Stack",   icon: "View",   toolTip: "Stacking tools",       subMenu: CurrentUserUtils.stackTools(),  expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             
-            { title: "Card",    icon: "Card",   toolTip: "Card View Tools",      subMenu: CurrentUserUtils.cardTools(),   expertMode: false, toolType:CollectionViewType.Card,     funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-         // { title: "Create",  icon: "Create", toolTip: "Assign card labels",   subMenu: CurrentUserUtils.labelTools(),  expertMode: false, toolType:CollectionViewType.Card,     funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Web",     icon: "Web",    toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
             { title: "Video",   icon: "Video",  toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
             { title: "Image",   icon: "Image",  toolTip: "Image functions",      subMenu: CurrentUserUtils.imageTools(),  expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 1bca68846..aaf33c3a1 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,16 +1,16 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import * as CSS from 'csstype';
-import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { URLField } from '../../../fields/URLField';
 import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,21 +25,12 @@ import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { SettingsManager } from '../../util/SettingsManager';
 
-enum cardSortings {
-    Time = 'time',
-    Type = 'type',
-    Color = 'color',
-    Chat = 'chat',
-    Tag = 'tag',
-    None = '',
-}
-
 /**
  * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
  * sort and filter using presets, and customize your experience with chat gpt.
@@ -60,7 +51,6 @@ export class CollectionCardView extends CollectionSubView() {
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
-    @observable _docDraggedIndex: number = -1;
     @observable _cursor: CSS.Property.Cursor = 'ew-resize';
 
     constructor(props: SubCollectionViewProps) {
@@ -102,18 +92,8 @@ export class CollectionCardView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._disposers.sort = reaction(
-            () => GPTPopup.Instance.visible,
-            isVis => {
-                if (isVis) {
-                    this.openChatPopup();
-                } else {
-                    this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
-                }
-            }
-        );
         // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
-        // when inquired from the dom (below in childScreenToLocal).  When the doc is actually renders, we need to act like the
+        // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
         // dash data just changed and trigger a React involidation with the correct data (read from the dom).
         this._disposers.child = reaction(
             () => [this.Document.x, this.Document.y],
@@ -136,14 +116,11 @@ export class CollectionCardView extends CollectionSubView() {
         this._dropDisposer?.();
     }
 
-    @computed get cardSort() {
-        return StrCast(this.Document.card_sort) as cardSortings;
-    }
     /**
      * Number of rows of cards to be rendered
      */
     @computed get numRows() {
-        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+        return Math.ceil(this.childDocs.length / this._maxRowCount);
     }
     /**
      * Circle arc size, in radians, to layout cards
@@ -211,7 +188,7 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns the card's new index
      */
     findCardDropIndex = (mouseX: number, mouseY: number) => {
-        const cardCount = this.sortedDocs.length;
+        const cardCount = this.childDocs.length;
         let index = 0;
         const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
 
@@ -245,8 +222,8 @@ export class CollectionCardView extends CollectionSubView() {
      */
     @action
     onPointerMove = (x: number, y: number) => {
-        if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
-            this._docDraggedIndex = this.findCardDropIndex(x, y);
+        if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) {
+            this.docDraggedIndex = this.findCardDropIndex(x, y);
         }
     };
 
@@ -259,11 +236,11 @@ export class CollectionCardView extends CollectionSubView() {
     onInternalDrop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
             if (de.complete.docDragData) {
-                const dragIndex = this._docDraggedIndex;
+                const dragIndex = this.docDraggedIndex;
                 const draggedDoc = DragManager.docsBeingDragged[0];
                 if (dragIndex > -1 && draggedDoc) {
-                    this._docDraggedIndex = -1;
-                    const sorted = this.sortedDocs;
+                    this.docDraggedIndex = -1;
+                    const sorted = this.childDocs;
                     const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
 
                     this.Document.card_sort = '';
@@ -295,50 +272,6 @@ export class CollectionCardView extends CollectionSubView() {
             .map(({ i }) => i)
             .join('.');
 
-    /**
-     * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
-     * front, latter cards to the back
-     * @param docs
-     * @param sortType
-     * @param isDesc
-     * @returns
-     */
-    sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
-        const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
-        sortType &&
-            docs.sort((docA, docB) => {
-                const [typeA, typeB] = (() => {
-                    switch (sortType) {
-                        default:                
-                        case cardSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
-                        case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
-                        case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
-                        case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
-                        case cardSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
-                    }
-                })(); //prettier-ignore
-                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
-            });
-        if (dragIndex !== -1) {
-            const draggedDoc = DragManager.docsBeingDragged[0];
-            const originalIndex = docs.findIndex(doc => doc === draggedDoc);
-
-            originalIndex !== -1 && docs.splice(originalIndex, 1);
-            draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
-        }
-
-        return docs;
-    };
-
-    @computed get sortedDocs() {
-        return this.sort(
-            this.childCards.map(card => card.layout),
-            this.cardSort,
-            BoolCast(this.Document.card_sort_isDesc),
-            this._docDraggedIndex
-        );
-    }
-
     isChildContentActive = computedFn(
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
@@ -560,16 +493,6 @@ export class CollectionCardView extends CollectionSubView() {
         });
     }, '');
 
-    /**
-     * Opens up the chat popup and starts the process for smart sorting.
-     */
-    openChatPopup = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.CARD);
-        GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
-        await this.childPairStringListAndUpdateSortDesc();
-    };
-
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
@@ -615,7 +538,7 @@ export class CollectionCardView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
+            (emove: PointerEvent, down: number[], delta: number[]) => {
                 this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0]));
                 return false;
             },
@@ -662,9 +585,9 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        trace();
+        console.log(this.docDraggedIndex, this.childDocs[0].title, this.childDocs[1].title);
         // Map sorted documents to their rendered components
-        return this.sortedDocs.map((doc, index) => {
+        return this.childDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
 
             const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
@@ -674,7 +597,7 @@ export class CollectionCardView extends CollectionSubView() {
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
             const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
                                     (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
-            const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
+            const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
             return (
                 <div
                     key={doc[Id]}
@@ -710,17 +633,13 @@ export class CollectionCardView extends CollectionSubView() {
     curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
-        trace();
         const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
                 ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
-                onPointerDown={action(e => {
-                    if (e.button === 2 || e.ctrlKey) return;
-                    this.releaseCurDoc();
-                })}
-                onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+                onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()}
+                onPointerLeave={action(() => (this.docDraggedIndex = -1))}
                 onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
                 onDrop={this.onExternalDrop.bind(this)}
                 style={{
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 0c059f729..5e99bec39 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import * as rp from 'request-promise';
-import { ClientUtils, returnFalse } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
 import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
 import { AclPrivate, DocData } from '../../../fields/DocSymbols';
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -29,6 +29,14 @@ import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
+export enum docSortings {
+    Time = 'time',
+    Type = 'type',
+    Color = 'color',
+    Chat = 'chat',
+    Tag = 'tag',
+    None = '',
+}
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
     isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -150,6 +158,8 @@ export function CollectionSubView<X>() {
         unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
         searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);
+
+        @observable docDraggedIndex = -1;
         @computed.struct get childDocs() {
             TraceMobx();
             let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -166,8 +176,10 @@ export function CollectionSubView<X>() {
                 const templateRoot = this._props.TemplateDataDocument;
                 rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : [];
             }
-
-            const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
+            const childDocs = this.childSortedDocs(
+                rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc),
+                this.docDraggedIndex
+            );
 
             const childDocFilters = this.childDocFilters();
             const childFiltersByRanges = this.childDocRangeFilters();
@@ -214,6 +226,35 @@ export function CollectionSubView<X>() {
             return docsforFilter;
         }
 
+        childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
+            const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
+            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+            const docs = docsIn.slice();
+            if (sortType) {
+                docs.sort((docA, docB) => {
+                    const [typeA, typeB] = (() => {
+                        switch (sortType) {
+                            default:                
+                            case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                            case docSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
+                            case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                            case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+                            case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
+                        }
+                    })(); //prettier-ignore
+                    return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
+                });
+            }
+            if (dragIndex !== -1) {
+                const draggedDoc = DragManager.docsBeingDragged[0];
+                const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+                originalIndex !== -1 && docs.splice(originalIndex, 1);
+                draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
+            }
+            return docs;
+        };
+
         @action
         protected async setCursorPosition(position: [number, number]) {
             let ind;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 77f1db9ad..c071c5fb8 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     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.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? 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));
@@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             </div>
         );
     }
-    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
             <div className="FlashcardPracticeUI">
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 542417531..b02eabab0 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -40,6 +40,7 @@ import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { docSortings } from '../collections/CollectionSubView';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
@@ -197,43 +198,35 @@ ScriptingGlobals.add(function showFreeform(
             checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false),
             setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
         }],
-        ['flashcards', {
-            checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
-            setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
-        }],
         ['time', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore
         }],
         ['docType', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore
         }],
         ['color', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore
         }],
         ['tag', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
         }],
         ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = false;
-            },
+            checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
         }],
         ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = true;
-            },
+            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => GPTPopup.Instance.visible,
             setDoc: (doc: Doc, dv: DocumentView) => {
                 if (GPTPopup.Instance.visible){
-                    doc.card_sort = ''
+                    doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
                     GPTPopup.Instance.setVisible(false);
    
                 } else {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e0c360132..f5291a4c1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 2ae6ee1dd..6b1d05031 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -551,8 +551,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         img.src = src;
         if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
         try {
-            const res = await createNewImgDoc(img, false);
-            return res;
+            return await createNewImgDoc(img, false);
         } catch (err) {
             console.log(err);
         }
@@ -589,9 +588,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 // disable once edited has been clicked (doesn't make sense to change after first edit)
                                 disabled={edited}
                                 checked={isNewCollection}
-                                onChange={() => {
-                                    setIsNewCollection(prev => !prev);
-                                }}
+                                onChange={() => setIsNewCollection(prev => !prev)}
                             />
                         }
                         label="Create New Collection"
@@ -610,49 +607,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         return (
             <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
                 <div className="sideControls">
-                    <div className="imageToolsContainer">
-                        {imageEditTools.map(tool => {
-                            return ImageToolButton(tool, tool.type === currTool.type, changeTool);
-                        })}
-                    </div>
+                    <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div>
                     {currTool.type == ImageToolType.Cut && (
                         <div className="cutToolsContainer">
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep out"
-                                type={Type.TERT}
-                                color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.OUT);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Draw in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.DRAW_IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Erase"
-                                type={Type.TERT}
-                                color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.ERASE);
-                                }}
-                            />
+                            <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
+                            <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
+                            <Button style={{ width: '100%' }} text="Draw in" type={Type.TERT} color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.DRAW_IN)} />
+                            <Button style={{ width: '100%' }} text="Erase" type={Type.TERT} color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.ERASE)} />
                         </div>
                     )}
                     <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
@@ -669,9 +630,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={genFillTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                         {currTool.type === ImageToolType.Cut && (
@@ -687,9 +646,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={cutTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                     </div>
@@ -701,9 +658,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleUndo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Undo"
                             icon={<IoMdUndo />}
@@ -714,9 +669,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleRedo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Redo"
                             icon={<IoMdRedo />}
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index de2116253..e810881a5 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -55,6 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo
     return (
         <div className="imageEditorButtonContainer">
             <Button
+                key={tool.name}
                 style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
-- 
cgit v1.2.3-70-g09d2


From 68bf2f9e4141d8a99407ff4c6c02d44795c638de Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 4 Feb 2025 14:08:38 -0500
Subject: fixing up gpt and animation panel in presbox.

---
 src/client/apis/gpt/PresCustomization.ts           | 115 ++++----
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   3 +-
 src/client/views/nodes/trails/PresBox.tsx          | 295 +++++++--------------
 src/client/views/nodes/trails/SpringUtils.ts       |   9 +-
 4 files changed, 170 insertions(+), 252 deletions(-)

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

diff --git a/src/client/apis/gpt/PresCustomization.ts b/src/client/apis/gpt/PresCustomization.ts
index 2262886a2..c465f098f 100644
--- a/src/client/apis/gpt/PresCustomization.ts
+++ b/src/client/apis/gpt/PresCustomization.ts
@@ -1,3 +1,5 @@
+import { PresEffect, PresEffectDirection } from '../../views/nodes/trails/PresEnums';
+import { AnimationSettingsProperties, easeItems } from '../../views/nodes/trails/SpringUtils';
 import { openai } from './setup';
 
 export enum CustomizationType {
@@ -10,15 +12,17 @@ interface PromptInfo {
 }
 const prompts: { [key: string]: PromptInfo } = {
     trails: {
-        description:
-            'We are customizing the properties and transition of a slide in a presentation. You are given the current properties of the slide in a json with the fields [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection], as well as the prompt for how the user wants to change it. Return a json with the required fields: [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection] by applying the changes in the prompt to the current state of the slide.',
+        description: `We are customizing the properties and transition of a slide in a presentation.
+             You are given the current properties of the slide in a json with the fields [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection], 
+                 as well as the prompt for how the user wants to change it. 
+             Return a json with the required fields: [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection] by applying the changes in the prompt to the current state of the slide.`,
         features: [],
     },
 };
 
 // Allows you to register properties that are customizable
 export const addCustomizationProperty = (type: CustomizationType, name: string, description: string, values?: string[]) => {
-    values ? prompts[type].features.push({ name, description, values }) : prompts[type].features.push({ name, description });
+    prompts[type].features.push({ name, description, ...(values ? { values } : {}) });
 };
 
 // All the registered fields, make sure to update during registration, this
@@ -41,35 +45,34 @@ export const gptSlideProperties = [
 
 // Registers slide properties
 const setupPresSlideCustomization = () => {
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'title', 'is the title/name of the slide.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_transition', 'is a number in milliseconds for how long it should take to transition/move to a slide.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_easeFunc', 'is the easing function for the movement to the slide.', ['Ease', 'Ease In', 'Ease Out', 'Ease Out', 'Ease In Out', 'Linear']);
-
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effect', 'is an effect applied to the slide when we transition to it.', ['None', 'Expand', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']);
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effectDirection', 'is what direction the effect is applied.', ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']);
-    addCustomizationProperty(
-        CustomizationType.PRES_TRAIL_SLIDE,
-        'presentation_effectTiming',
-        "is a json object of the format: {type: string, stiffness: number, damping: number, mass: number}. Type is always “custom”. Controls the spring-based timing of the presentation effect animation. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to match the user's description of how they want to animate the effect."
-    );
-
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'config_zoom', 'is a number from 0 to 1.0 indicating the percentage we should zoom into the slide.');
-
-    // boolean values
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_playAudio', 'is a boolean value indicating if we should play audio when we go to the slide.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_zoomText', 'is a boolean value indicating if we should zoom into text selections when we go to the slide.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideBefore', 'is a boolean value indicating if we should hide the slide before going to it.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hide', 'is a boolean value indicating if we should hide the slide during the presentation.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideAfter', 'is a boolean value indicating if we should hide the slide after going to it.');
-    addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_openInLightbox', 'is a boolean value indicating if we should open the slide in an overlay or lightbox view during the presentation.');
-};
+    const add = (name: string, val:string, opts?:string[]) => addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, name, val, opts);
+    const addBool = (name: string, val:string) => add(name, 'is a boolean value indicating if we should ' + val);
+    add('title',                       'is the title/name of the slide.');
+    add('config_zoom',                 'is a number from 0 to 1.0 indicating the percentage we should zoom into the slide.');
+    add('presentation_transition',     'is a number in milliseconds for how long it should take to transition/move to a slide.');
+    add('presentation_easeFunc',       'is the easing function for the movement to the slide.', easeItems.filter(val => val.text !== 'Custom').map(val => val.text))
+    add('presentation_effect',         'is an effect applied to the slide when we transition to it.', Object.keys(PresEffect));
+    add('presentation_effectDirection','is what direction the effect is applied.', Object.keys(PresEffectDirection).filter(key => key !== PresEffectDirection.None));
+    add('presentation_effectTiming',   `is a json object of the format: {type: string, ${AnimationSettingsProperties.stiffness}: number, ${AnimationSettingsProperties.damping}: number, ${AnimationSettingsProperties.mass}: number}. 
+                                        Type is always “custom”. Controls the spring-based timing of the presentation effect animation. 
+                                        Stiffness, damping, and mass control the physics-based properties of spring animations. 
+                                        This is used to create a more natural looking timing, bouncy effects, etc.
+                                        Use spring physics to adjust these parameters to match the user's description of how they want to animate the effect.`);
+
+
+    addBool('presentation_playAudio',      'play audio when we go to the slide.');
+    addBool('presentation_zoomText',       'zoom into text selections when we go to the slide.');
+    addBool('presentation_hideBefore',     'hide the slide before going to it.');
+    addBool('presentation_hide',           'hide the slide during the presentation.');
+    addBool('presentation_hideAfter',      'hide the slide after going to it.');
+    addBool('presentation_openInLightbox', 'open the slide in an overlay or lightbox view during the presentation.');
+}; // prettier-ignore
 
 setupPresSlideCustomization();
 
-export const getSlideTransitionSuggestions = async (inputText: string) => {
+export const getSlideTransitionSuggestions = (inputText: string) => {
     /**
-     * Prompt: Generate an entrance animations from slower and gentler
-     * to bouncier and more high energy
+     * Prompt: Generate entrance animations from slower and gentler to bouncier and more high energy
      *
      * Format:
      * {
@@ -81,13 +84,19 @@ export const getSlideTransitionSuggestions = async (inputText: string) => {
      * }
      */
 
-    const prompt =
-        "I want to generate four distinct types of slide effect animations. Return a json of the form {effect: string, direction: string, stiffness: number, damping: number, mass: number}[] with four elements.  Effect is the type of animation; its only possible values are ['Expand', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']. Direction is the direction that the animation starts from; its only possible values are ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural-looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to animate the effect.";
+    const prompt = `I want to generate four distinct types of slide effect animations. 
+         Return a json of the form { ${AnimationSettingsProperties.effect}: string, ${AnimationSettingsProperties.direction}: string, ${AnimationSettingsProperties.stiffness}: number, ${AnimationSettingsProperties.damping}: number, ${AnimationSettingsProperties.mass}: number}[] with four elements.  
+         ${AnimationSettingsProperties.effect} is the type of animation; its only possible values are [${Object.keys(PresEffect).filter(key => key !== PresEffect.None).join(',')}]. 
+         ${AnimationSettingsProperties.direction} is the direction that the animation starts from; 
+            its only possible values are [${Object.values(PresEffectDirection).filter(key => key !== PresEffectDirection.None).join(',')}]. 
+         ${AnimationSettingsProperties.stiffness}, ${AnimationSettingsProperties.damping}, and ${AnimationSettingsProperties.mass} control the physics-based properties of spring animations. 
+         This is used to create a more natural-looking timing, bouncy effects, etc. 
+         Use spring physics to adjust these parameters to animate the effect.`; // prettier-ignore
 
     const customInput = inputText ?? 'Make them as contrasting as possible with different effects and timings ranging from gentle to energetic.';
 
-    try {
-        const response = await openai.chat.completions.create({
+    return openai.chat.completions
+        .create({
             model: 'gpt-4',
             messages: [
                 { role: 'system', content: prompt },
@@ -95,39 +104,33 @@ export const getSlideTransitionSuggestions = async (inputText: string) => {
             ],
             temperature: 0,
             max_tokens: 1000,
+        })
+        .then(response => response.choices[0].message?.content ?? '')
+        .catch(err => {
+            console.log(err);
+            return 'Error connecting with API.';
         });
-        return response.choices[0].message?.content;
-    } catch (err) {
-        console.log(err);
-        return 'Error connecting with API.';
-    }
 };
 
-export const gptTrailSlideCustomization = async (inputText: string, properties: any | any[]) => {
-    let prompt = prompts.trails.description;
-
-    prompts.trails.features.forEach(feature => {
-        prompt += feature.name + ' ' + feature.description;
-        if (feature.values) {
-            prompt += `Its only possible values are [${feature.values.join(', ')}].`;
-        }
-    });
+export const gptTrailSlideCustomization = (inputText: string, properties: string) => {
+    const preamble = prompts.trails.description + prompts.trails.features.map(feature => feature.name + ' ' + feature.description + (feature.values ? `Its only possible values are [${feature.values.join(', ')}]` : '')).join('. ');
 
-    prompt += 'Set unchanged values to null and make sure you include new properties if they are specified in the prompt even if they do not exist in current properties. Please only return the json with the keys described and their values.';
+    const prompt = `Set unchanged values to null and make sure you include new properties if they are specified in the prompt even if they do not exist in current properties. 
+                    Please only return the json with the keys described and their values.`;
 
-    try {
-        const response = await openai.chat.completions.create({
+    return openai.chat.completions
+        .create({
             model: 'gpt-4',
             messages: [
-                { role: 'system', content: prompt },
-                { role: 'user', content: `Prompt: ${inputText}, Current properties: ${JSON.stringify(properties)}` },
+                { role: 'system', content: preamble + prompt },
+                { role: 'user', content: `Prompt: ${inputText}, Current properties: ${properties}` },
             ],
             temperature: 0,
             max_tokens: 1000,
+        })
+        .then(response => response.choices[0].message?.content ?? '')
+        .catch(err => {
+            console.log(err);
+            return 'Error connecting with API.';
         });
-        return response.choices[0].message?.content;
-    } catch (err) {
-        console.log(err);
-        return 'Error connecting with API.';
-    }
 };
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index e810881a5..985dc914f 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -53,9 +53,8 @@ export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }
 
 export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) {
     return (
-        <div className="imageEditorButtonContainer">
+        <div key={tool.name} className="imageEditorButtonContainer">
             <Button
-                key={tool.name}
                 style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e0de8e1fd..3919ac3a8 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,4 +1,4 @@
-import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components';
+import { Button, Dropdown, DropdownType, IconButton, Size, Toggle, ToggleType, Type } from '@dash/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import Slider from '@mui/material/Slider';
@@ -61,6 +61,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
     static navigateToDocScript: ScriptField;
 
+    public static PanelName = 'PRESBOX'; // name of dockingview tab where presentations get added
+
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
@@ -72,6 +74,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
     public selectedArray = new ObservableSet<Doc>();
+    public slideToModify: Doc | null = null;
     _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves
     _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
     _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
@@ -97,14 +100,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @observable _presKeyEvents: boolean = false;
     @observable _forceKeyEvents: boolean = false;
 
+    @observable _showBezierEditor = false;
+    @observable _showSpringEditor = true;
+    @observable _showPreview = true;
+    @observable _easeDropdownVal = 'ease';
+
     // GPT
-    private _inputref: HTMLTextAreaElement | null = null;
-    private _inputref2: HTMLTextAreaElement | null = null;
-    @observable chatActive: boolean = false;
-    @observable chatInput: string = '';
-    public slideToModify: Doc | null = null;
-    @observable isRecording: boolean = false;
-    @observable isLoading: boolean = false;
+    @observable _chatActive: boolean = false;
+    @observable _animationChat: string = '';
+    @observable _chatInput: string = '';
+    @observable _isRecording: boolean = false;
+    @observable _isLoading: boolean = false;
 
     @observable generatedAnimations: AnimationSettings[] = [
         // default presets
@@ -138,54 +144,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         },
     ];
 
-    @action
-    setGeneratedAnimations = (settings: AnimationSettings[]) => {
-        this.generatedAnimations = settings;
-    };
-
-    @observable animationChat: string = '';
-
-    @action
-    setChatInput = (input: string) => {
-        this.chatInput = input;
-    };
-
-    @action
-    setAnimationChat = (input: string) => {
-        this.animationChat = input;
-    };
-
-    @action
-    setIsLoading = (isLoading: boolean) => {
-        this.isLoading = isLoading;
-    };
-
-    @action
-    public setIsRecording = (isRecording: boolean) => {
-        this.isRecording = isRecording;
-    };
-
-    @observable showBezierEditor = false;
-    @action setBezierEditorVisibility = (visible: boolean) => {
-        this.showBezierEditor = visible;
-    };
-    @observable showSpringEditor = true;
-    @action setSpringEditorVisibility = (visible: boolean) => {
-        this.showSpringEditor = visible;
-    };
-
-    // Easing function variables
-
-    @observable easeDropdownVal = 'ease';
-
-    @action setBezierControlPoints = (newPoints: { p1: number[]; p2: number[] }) => {
+    setGeneratedAnimations =    action((input: AnimationSettings[]) => { this.generatedAnimations = input; }) // prettier-ignore
+    setChatInput =              action((input: string) => { this._chatInput = input; }); // prettier-ignore
+    setAnimationChat =          action((input: string) => { this._animationChat = input; }); // prettier-ignore
+    setIsLoading =              action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore
+    setIsRecording =            action((input: boolean) => { this._isRecording = input; }); // prettier-ignore
+    setBezierEditorVisibility = action((visible: boolean) => { this._showBezierEditor = visible; }); // prettier-ignore
+    setSpringEditorVisibility = action((visible: boolean) => { this._showSpringEditor = visible; }); // prettier-ignore
+    setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => {
         this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`);
-    };
+    });
 
     @computed
     get currCPoints() {
-        const strPoints = this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease';
-        return EaseFuncToPoints(strPoints);
+        return EaseFuncToPoints(this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease');
     }
 
     @computed
@@ -221,25 +193,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true;
         return false;
     }
-    @computed get panable() {
-        if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
-        return false;
-    }
     @computed get selectedDocumentView() {
         if (DocumentView.Selected().length) return DocumentView.Selected()[0];
         if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document);
         return undefined;
     }
-    @computed get isPres() {
-        return this.selectedDoc === this.Document;
-    }
-    @computed get selectedDoc() {
-        return this.selectedDocumentView?.Document;
-    }
-    isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentation_targetDoc === layoutDoc;
-    clearSelectedArray = () => this.selectedArray.clear();
-    addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
-    removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
 
     componentWillUnmount() {
         this._unmounting = true;
@@ -256,7 +214,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             () => this.pauseAutoPres()
         );
         this._disposers.keyboard = reaction(
-            () => this.selectedDoc,
+            () => this.selectedDocumentView?.Document,
             selected => {
                 document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
                 (this._presKeyEvents = selected === this.Document) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
@@ -293,13 +251,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
+    clearSelectedArray = () => this.selectedArray.clear();
+    addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
+    removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
+
     @action
     updateCurrentPresentation = (pres?: Doc) => {
         Doc.ActivePresentation = pres ?? this.Document;
         PresBox.Instance = this;
     };
 
-    _mediaTimer!: [NodeJS.Timeout, Doc];
     // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
     startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
         const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart);
@@ -318,7 +279,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     // Recording for GPT customization
-
     recordDictation = () => {
         this.setIsRecording(true);
         this.setChatInput('');
@@ -336,64 +296,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         DictationManager.Controls.stop();
     };
 
-    setDictationContent = (value: string) => {
-        console.log('Dictation value', value);
-        this.setChatInput(value);
-    };
+    setDictationContent = (value: string) => this.setChatInput(value);
 
-    @action
-    customizeAnimations = async () => {
+    customizeAnimations = action(() => {
         this.setIsLoading(true);
-        try {
-            const res = await getSlideTransitionSuggestions(this.animationChat);
-            if (typeof res === 'string') {
-                const resObj = JSON.parse(res);
-                console.log('Parsed GPT Result ', resObj);
-                this.setGeneratedAnimations(resObj as AnimationSettings[]);
-            }
-        } catch (err) {
-            console.error(err);
-        }
-        this.setIsLoading(false);
-    };
+        getSlideTransitionSuggestions(this._animationChat)
+            .then(res => this.setGeneratedAnimations(JSON.parse(res) as AnimationSettings[]))
+            .catch(err => console.error(err))
+            .finally(this.setIsLoading);
+    });
 
-    @action
-    customizeWithGPT = async (input: string) => {
+    customizeWithGPT = action((input: string) => {
         // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect';
         this.setIsRecording(false);
         this.setIsLoading(true);
-
-        const currSlideProperties: { [key: string]: FieldResult } = {};
-        gptSlideProperties.forEach(key => {
-            if (this.activeItem[key]) {
-                currSlideProperties[key] = this.activeItem[key];
-            }
-            // default values
-            else if (key === 'presentation_transition') {
-                currSlideProperties[key] = 500;
-            } else if (key === 'config_zoom') {
-                currSlideProperties[key] = 1.0;
-            }
-        });
-        console.log('current slide props ', currSlideProperties);
-
-        try {
-            const res = await gptTrailSlideCustomization(input, currSlideProperties);
-            if (typeof res === 'string') {
-                const resObj = JSON.parse(res);
-                console.log('Parsed GPT Result ', resObj);
-                for (const key in resObj) {
-                    if (resObj[key]) {
-                        console.log('typeof property', typeof resObj[key]);
-                        this.activeItem[key] = resObj[key];
-                    }
-                }
-            }
-        } catch (err) {
-            console.error(err);
-        }
-        this.setIsLoading(false);
-    };
+        const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 };
+        const currSlideProperties = gptSlideProperties.reduce(
+            (prev, key) => { prev[key] = Field.toString(this.activeItem[key]) ?? prev[key]; return prev; }, 
+            slideDefaults); // prettier-ignore
+
+        gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties))
+            .then(res =>
+                (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => {
+                    this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key]);
+                })
+            )
+            .catch(e => console.error(e))
+            .finally(this.setIsLoading);
+    });
 
     // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
     // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
@@ -847,8 +777,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
     };
 
-    public static PanelName = 'PRESBOX';
-
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) {
         if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
@@ -1842,10 +1770,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <ReactTextareaAutosize
                                     placeholder="Describe how you would like to modify the slide properties."
                                     className="pres-chatbox"
-                                    value={this.chatInput}
-                                    onChange={e => {
-                                        this.setChatInput(e.target.value);
-                                    }}
+                                    value={this._chatInput}
+                                    onChange={e => this.setChatInput(e.target.value)}
                                     onKeyDown={e => {
                                         this.stopDictation();
                                         e.stopPropagation();
@@ -1853,11 +1779,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 />
                                 <IconButton
                                     type={Type.TERT}
-                                    color={this.isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
+                                    color={this._isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
                                     tooltip="Record"
                                     icon={<BiMicrophone size="16px" />}
                                     onClick={() => {
-                                        if (!this.isRecording) {
+                                        if (!this._isRecording) {
                                             this.recordDictation();
                                         } else {
                                             this.stopDictation();
@@ -1869,12 +1795,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 style={{ alignSelf: 'flex-end' }}
                                 text="Send"
                                 type={Type.TERT}
-                                icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+                                icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 color={SnappingManager.userVariantColor}
                                 onClick={() => {
                                     this.stopDictation();
-                                    this.customizeWithGPT(this.chatInput);
+                                    this.customizeWithGPT(this._chatInput);
                                 }}
                             />
                         </div>
@@ -1952,16 +1878,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 style={{ alignSelf: 'flex-start' }}
                                 onClick={e => {
                                     e.stopPropagation();
-                                    this.setBezierEditorVisibility(!this.showBezierEditor);
+                                    this.setBezierEditorVisibility(!this._showBezierEditor);
                                 }}>
-                                {`${this.showBezierEditor ? 'Hide' : 'Show'} Timing Editor`}
-                                <FontAwesomeIcon icon={this.showBezierEditor ? 'chevron-up' : 'chevron-down'} />
+                                {`${this._showBezierEditor ? 'Hide' : 'Show'} Timing Editor`}
+                                <FontAwesomeIcon icon={this._showBezierEditor ? 'chevron-up' : 'chevron-down'} />
                             </div>
                         </div>
                     </div>
 
                     {/* Cubic bezier editor */}
-                    {this.showBezierEditor && (
+                    {this._showBezierEditor && (
                         <div className="presBox-option-block" style={{ paddingTop: 0 }}>
                             <p className="presBox-submenu-label" style={{ alignSelf: 'flex-start' }}>
                                 Custom Timing Function
@@ -1978,7 +1904,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <ReactTextareaAutosize
                                     placeholder="Customize prompt for effect suggestions. Leave blank for random results."
                                     className="pres-chatbox"
-                                    value={this.animationChat}
+                                    value={this._animationChat}
                                     onChange={e => {
                                         this.setAnimationChat(e.target.value);
                                     }}
@@ -1992,7 +1918,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 style={{ alignSelf: 'flex-end' }}
                                 text="Send"
                                 type={Type.TERT}
-                                icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+                                icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 color={SnappingManager.userVariantColor}
                                 onClick={this.customizeAnimations}
@@ -2107,12 +2033,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         closeOnSelect
                                         items={effectTimings}
                                         selectedVal={timingConfig.type}
-                                        setSelectedVal={val => {
-                                            this.updateEffectTiming(activeItem, {
-                                                type: val as SpringType,
-                                                ...springMappings[val],
-                                            });
-                                        }}
+                                        setSelectedVal={val => this.updateEffectTiming(activeItem, { type: val as SpringType, ...springMappings[val] })}
                                         dropdownType={DropdownType.SELECT}
                                         type={Type.TERT}
                                     />
@@ -2120,73 +2041,61 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         className="presBox-show-hide-dropdown"
                                         onClick={e => {
                                             e.stopPropagation();
-                                            this.setSpringEditorVisibility(!this.showSpringEditor);
+                                            this.setSpringEditorVisibility(!this._showSpringEditor);
                                         }}>
-                                        {`${this.showSpringEditor ? 'Hide' : 'Show'} Spring Settings`}
-                                        <FontAwesomeIcon icon={this.showSpringEditor ? 'chevron-up' : 'chevron-down'} />
+                                        {`${this._showSpringEditor ? 'Hide' : 'Show'} Spring Settings`}
+                                        <FontAwesomeIcon icon={this._showSpringEditor ? 'chevron-up' : 'chevron-down'} />
                                     </div>
-                                    {this.showSpringEditor && (
+                                    {this._showSpringEditor && (
                                         <>
                                             <div>Tension</div>
-                                            <div
-                                                onPointerDown={e => {
-                                                    e.stopPropagation();
-                                                }}>
-                                                <Slider
-                                                    min={1}
-                                                    max={1000}
-                                                    step={5}
-                                                    size="small"
+                                            <div onPointerDown={e => e.stopPropagation()}>
+                                                {/* prettier-ignore */}
+                                                <Slider min={1} max={1000} step={5} size="small" 
                                                     value={timingConfig.stiffness}
-                                                    onChange={(e, val) => {
-                                                        if (!timingConfig) return;
-                                                        this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number });
-                                                    }}
+                                                    onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number })}
                                                     valueLabelDisplay="auto"
                                                 />
                                             </div>
                                             <div>Damping</div>
-                                            <div
-                                                onPointerDown={e => {
-                                                    e.stopPropagation();
-                                                }}>
-                                                <Slider
-                                                    min={1}
-                                                    max={100}
-                                                    step={1}
-                                                    size="small"
+                                            <div onPointerDown={e => e.stopPropagation()}>
+                                                {/* prettier-ignore */}
+                                                <Slider min={1} max={100} step={1} size="small"
                                                     value={timingConfig.damping}
-                                                    onChange={(e, val) => {
-                                                        if (!timingConfig) return;
-                                                        this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number });
-                                                    }}
+                                                    onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number })}
                                                     valueLabelDisplay="auto"
                                                 />
                                             </div>
                                             <div>Mass</div>
-                                            <div
-                                                onPointerDown={e => {
-                                                    e.stopPropagation();
-                                                }}>
-                                                <Slider
-                                                    min={1}
-                                                    max={10}
-                                                    step={1}
-                                                    size="small"
+                                            <div onPointerDown={e => e.stopPropagation()}>
+                                                {/* prettier-ignore */}
+                                                <Slider min={1} max={10} step={1} size="small"
                                                     value={timingConfig.mass}
-                                                    onChange={(e, val) => {
-                                                        if (!timingConfig) return;
-                                                        this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number });
-                                                    }}
+                                                    onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number })}
                                                     valueLabelDisplay="auto"
                                                 />
                                             </div>
-                                            Preview Effect
-                                            <div className="presBox-option-block presBox-option-center">
-                                                <div className="presBox-effect-container">
-                                                    <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig} infinite>
-                                                        <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} />
-                                                    </SlideEffect>
+                                            <div style={{ display: 'flex', alignItems: 'center'}}>
+                                                <Button
+                                                    type={Type.TERT}
+                                                    tooltip="show preview of slide animation effect"
+                                                    size={Size.SMALL}
+                                                    color={SnappingManager.userColor}
+                                                    background={'transparent'}
+                                                    onClick={action(() => {
+                                                        this._showPreview = false;
+                                                        setTimeout(action(() => { this._showPreview = true; }) );// prettier-ignore
+                                                    })}
+                                                    text={'Preview of: ' + effect + ', ' + direction}
+                                                />
+                                                <div className="presBox-option-block presBox-option-center">
+                                                    <div className="presBox-effect-container">
+                                                        {!this._showPreview ? null : (
+                                                            <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig}>
+                                                                <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} />
+                                                            </SlideEffect>
+                                                        )}
+                                                    </div>
                                                 </div>
                                             </div>
                                         </>
@@ -3078,7 +2987,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     } */}
                 </div>
                 {/* presbox chatbox */}
-                {this.chatActive && <div className="presBox-chatbox" />}
+                {this._chatActive && <div className="presBox-chatbox" />}
             </div>
         );
     }
diff --git a/src/client/views/nodes/trails/SpringUtils.ts b/src/client/views/nodes/trails/SpringUtils.ts
index 73e1e14f1..044afbeb1 100644
--- a/src/client/views/nodes/trails/SpringUtils.ts
+++ b/src/client/views/nodes/trails/SpringUtils.ts
@@ -22,7 +22,14 @@ export interface SpringSettings {
 }
 
 // Overall config
-
+// Keeps these settings in sync with the AnimationSettings interface (used by gpt);
+export enum AnimationSettingsProperties {
+    effect = 'effect',
+    direction = 'direction',
+    stiffness = 'stiffness',
+    damping = 'damping',
+    mass = 'mass',
+}
 export interface AnimationSettings {
     effect: PresEffect;
     direction: PresEffectDirection;
-- 
cgit v1.2.3-70-g09d2


From 30ec70722678eba318fb7521a8b2bd642328bf54 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 23 Feb 2025 16:34:40 -0500
Subject: added option+maximize to open in doc editor view (currently only for
 images).  fixed imageEditor to generate edits the first time through.

---
 src/client/views/DocumentDecorations.tsx           |  7 +-
 src/client/views/StyleProviderQuiz.tsx             |  2 +-
 src/client/views/ViewBoxInterface.ts               |  1 +
 src/client/views/nodes/ImageBox.tsx                | 21 +++---
 src/client/views/nodes/calendarBox/CalendarBox.tsx |  2 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 83 ++++++++++------------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |  4 +-
 .../imageEditor/imageEditorUtils/ImageHandler.ts   |  1 -
 .../imageEditorUtils/imageEditorInterfaces.ts      |  5 +-
 9 files changed, 58 insertions(+), 68 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 54ff3904d..d9b6bdf1a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -36,6 +36,7 @@ import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { TagsView } from './TagsView';
+import { ImageField } from '../../fields/URLField';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -284,8 +285,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 embedding.y = -NumCast(embedding._height) / 2;
                 CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
             } else if (e.altKey) {
-                // open same document in new tab
-                CollectionDockingView.ToggleSplit(selView.Document, OpenWhereMod.right);
+                // open same document in new tab or in custom editor
+                selView.ComponentView?.docEditorView?.() ?? CollectionDockingView.ToggleSplit(selView.Document, OpenWhereMod.right);
             } else {
                 let openDoc = selView.Document;
                 if (openDoc.layout_fieldKey === 'layout_icon') {
@@ -821,7 +822,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                 {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')}
                                 {titleArea}
-                                {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
+                                {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection, opption: in editor view)')}
                             </div>
                             {hideResizers ? null : (
                                 <>
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index d8eeb3490..db9ab831a 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -125,7 +125,7 @@ export namespace styleProviderQuiz {
         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']));
+            AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc.x), NumCast(img.layoutDoc.y));
         } catch (error) {
             console.log('Error', error);
         }
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index 30da8c616..b943259ff 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -23,6 +23,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     }
     promoteCollection?: () => void; // moves contents of collection to parent
     hasChildDocs?: () => Doc[];
+    docEditorView?: () => void;
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away.  see PresBox.restoreTargetDocView
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ff6baf199..dd28832f9 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -316,6 +316,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return cropping;
     };
 
+    docEditorView = action(() => {
+        const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+        if (field) {
+            ImageEditorData.Open = true;
+            ImageEditorData.Source = this.choosePath(field.url);
+            ImageEditorData.AddDoc = this._props.addDocument;
+            ImageEditorData.RootDoc = this.Document;
+        }
+    });
+
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
@@ -353,16 +363,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 icon: 'expand-arrows-alt',
             });
             funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
-            funcs.push({
-                description: 'Open Image Editor',
-                event: action(() => {
-                    ImageEditorData.Open = true;
-                    ImageEditorData.Source = this.choosePath(field.url);
-                    ImageEditorData.AddDoc = this._props.addDocument;
-                    ImageEditorData.RootDoc = this.Document;
-                }),
-                icon: 'pencil-alt',
-            });
+            funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' });
             this.layoutDoc.ai &&
                 funcs.push({
                     description: 'Regenerate AI Image',
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index c1f6f5859..009eb82cd 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -6,7 +6,7 @@ import interactionPlugin from '@fullcalendar/interaction';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { dateRangeStrToDates, simulateMouseClick } from '../../../../ClientUtils';
+import { dateRangeStrToDates } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
 import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView';
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 6b1d05031..3c0ab3da5 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -90,18 +90,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
      * @param type The new tool type we are changing to
      */
     const changeTool = (type: ImageToolType) => {
-        switch (type) {
-            case ImageToolType.GenerativeFill:
-                setCurrTool(genFillTool);
-                setCursorData(prev => ({ ...prev, width: genFillTool.sliderDefault as number }));
-                break;
-            case ImageToolType.Cut:
-                setCurrTool(cutTool);
-                setCursorData(prev => ({ ...prev, width: cutTool.sliderDefault as number }));
-                break;
-            default:
-                break;
-        }
+        setCurrToolType(type);
+        setCursorData(prev => ({ ...prev, width: currTool().sliderDefault as number }));
     };
     // Undo and Redo
     const handleUndo = () => {
@@ -171,9 +161,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
 
     // handles brushing on pointer movement
     useEffect(() => {
-        if (!isBrushing) return undefined;
         const canvas = canvasRef.current;
-        if (!canvas) return undefined;
+        if (!isBrushing || !canvas) return undefined;
         const ctx = ImageUtility.getCanvasContext(canvasRef);
         if (!ctx) return undefined;
 
@@ -188,33 +177,29 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         };
 
         drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
-        return () => {
-            drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
-        };
+        return () => drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
     }, [isBrushing]);
 
     // first load
     useEffect(() => {
-        const loadInitial = async () => {
-            if (!imageEditorSource || imageEditorSource === '') return;
-            const img = new Image();
-            const res = await ImageUtility.urlToBase64(imageEditorSource);
-            if (!res) return;
-            img.src = `data:image/png;base64,${res}`;
-
-            img.onload = () => {
-                currImg.current = img;
-                originalImg.current = img;
-                const imgWidth = img.naturalWidth;
-                const imgHeight = img.naturalHeight;
-                const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
-                const width = imgWidth * scale;
-                const height = imgHeight * scale;
-                setCanvasDims({ width, height });
-            };
-        };
-
-        loadInitial();
+        if (imageEditorSource && imageEditorSource) {
+            ImageUtility.urlToBase64(imageEditorSource).then(res => {
+                if (res) {
+                    const img = new Image();
+                    img.src = `data:image/png;base64,${res}`;
+                    img.onload = () => {
+                        currImg.current = img;
+                        originalImg.current = img;
+                        const imgWidth = img.naturalWidth;
+                        const imgHeight = img.naturalHeight;
+                        const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
+                        const width = imgWidth * scale;
+                        const height = imgHeight * scale;
+                        setCanvasDims({ width, height });
+                    };
+                }
+            });
+        }
 
         // cleanup
         return () => {
@@ -300,7 +285,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             if (!canvasMask) return;
             const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
             const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
-            const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+            const res = await ImageUtility.getEdit(imgBlob, maskBlob, input || 'Fill in the image in the same style', 2);
 
             // create first image
             if (!newCollectionRef.current) {
@@ -569,11 +554,15 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         setIsFirstDoc(true);
     };
 
+    function currTool() {
+        return imageEditTools.find(tool => tool.type === currToolType) ?? genFillTool;
+    }
+
     // defines the tools and sets current tool
-    const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, name: 'Generative Fill', btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 };
-    const cutTool: ImageEditTool = { type: ImageToolType.Cut, name: 'Cut', btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 };
+    const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 };
+    const cutTool: ImageEditTool = { type: ImageToolType.Cut, btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 };
     const imageEditTools: ImageEditTool[] = [genFillTool, cutTool];
-    const [currTool, setCurrTool] = useState<ImageEditTool>(genFillTool);
+    const [currToolType, setCurrToolType] = useState<ImageToolType>(ImageToolType.GenerativeFill);
 
     // the top controls for making a new collection, resetting, and applying edits,
     function renderControls() {
@@ -595,7 +584,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         labelPlacement="end"
                         sx={{ whiteSpace: 'nowrap' }}
                     />
-                    <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+                    <ApplyFuncButtons onClick={() => currTool().applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool().btnText} />
                     <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
                 </div>
             </div>
@@ -607,8 +596,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         return (
             <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
                 <div className="sideControls">
-                    <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div>
-                    {currTool.type == ImageToolType.Cut && (
+                    <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool().type, changeTool))}</div>
+                    {currTool().type == ImageToolType.Cut && (
                         <div className="cutToolsContainer">
                             <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
                             <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
@@ -617,7 +606,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         </div>
                     )}
                     <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
-                        {currTool.type === ImageToolType.GenerativeFill && (
+                        {currTool().type === ImageToolType.GenerativeFill && (
                             <Slider
                                 sx={{
                                     '& input[type="range"]': {
@@ -633,7 +622,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
-                        {currTool.type === ImageToolType.Cut && (
+                        {currTool().type === ImageToolType.Cut && (
                             <Slider
                                 sx={{
                                     '& input[type="range"]': {
@@ -780,7 +769,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 {renderSideIcons()}
                 {renderEditThumbnails()}
             </div>
-            {currTool.type === ImageToolType.GenerativeFill && renderPromptBox()}
+            {currTool().type === ImageToolType.GenerativeFill && renderPromptBox()}
         </div>
     );
 };
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index 985dc914f..3eaa251f2 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -53,10 +53,10 @@ export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }
 
 export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) {
     return (
-        <div key={tool.name} className="imageEditorButtonContainer">
+        <div key={tool.type} className="imageEditorButtonContainer">
             <Button
                 style={{ width: '100%' }}
-                text={tool.name}
+                text={tool.type}
                 type={Type.TERT}
                 color={isActive ? SettingsManager.userVariantColor : bgColor}
                 icon={<FontAwesomeIcon icon={tool.icon} />}
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
index ece0f4d7f..1c6a38a24 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
@@ -87,7 +87,6 @@ export class ImageUtility {
                 body: fd,
             });
             const data = await res.json();
-            console.log(data.data);
             return {
                 status: 'success',
                 urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`),
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index a14b55439..02dbc0312 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -13,8 +13,8 @@ export interface Point {
 }
 
 export enum ImageToolType {
-    GenerativeFill = 'genFill',
-    Cut = 'cut',
+    GenerativeFill = 'Generative Fill',
+    Cut = 'Cut',
 }
 
 export enum CutMode {
@@ -26,7 +26,6 @@ export enum CutMode {
 
 export interface ImageEditTool {
     type: ImageToolType;
-    name: string;
     btnText: string;
     icon: IconProp;
     // this is the function that the image tool applies, so it can be defined depending on the tool
-- 
cgit v1.2.3-70-g09d2

