aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json54
-rw-r--r--package.json1
-rw-r--r--src/client/apis/gpt/GPT.ts2
-rw-r--r--src/client/views/MainView.tsx17
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx4
-rw-r--r--src/client/views/nodes/ImageBox.tsx11
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx6
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.scss96
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx560
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.scss4
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx48
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts87
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts11
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts128
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts15
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts9
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts16
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx51
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss12
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx102
-rw-r--r--src/client/views/pdf/PDFViewer.tsx5
21 files changed, 1161 insertions, 78 deletions
diff --git a/package-lock.json b/package-lock.json
index 1f3255512..4b03c0d99 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22264,6 +22264,55 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-loader-spinner": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz",
+ "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==",
+ "requires": {
+ "react-is": "^18.2.0",
+ "styled-components": "^5.3.5",
+ "styled-tools": "^1.7.2"
+ },
+ "dependencies": {
+ "css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "styled-components": {
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
+ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ }
+ }
+ },
"react-loading": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz",
@@ -24670,6 +24719,11 @@
}
}
},
+ "styled-tools": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz",
+ "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg=="
+ },
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
diff --git a/package.json b/package.json
index e76388898..af6148e11 100644
--- a/package.json
+++ b/package.json
@@ -293,6 +293,7 @@
"react-grid-layout": "^1.3.4",
"react-icons": "^4.3.1",
"react-jsx-parser": "^1.29.0",
+ "react-loader-spinner": "^5.3.4",
"react-loading": "^2.0.3",
"react-markdown": "^8.0.3",
"react-measure": "^2.5.2",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b3960902..18222b32a 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -39,7 +39,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
temperature: opts.temp,
prompt: `${opts.prompt}${inputText}`,
});
- console.log(response.data.choices[0]);
+ // console.log(response.data.choices[0]);
return response.data.choices[0].text;
} catch (err) {
console.log(err);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2b6e7cb68..7c1abc232 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -66,8 +66,8 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
-import { ThemeProvider } from '@mui/material';
-import { theme } from '../theme';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -75,6 +75,15 @@ export class MainView extends React.Component {
public static Instance: MainView;
public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ // for ai image editor
+ @observable public imageEditorOpen: boolean = false;
+ @action public setImageEditorOpen = (open: boolean) => (this.imageEditorOpen = open);
+ @observable public imageEditorSource: string = '';
+ @action public setImageEditorSource = (source: string) => (this.imageEditorSource = source);
+ @observable public imageRootDoc: Doc | undefined;
+ @action public setImageRootDoc = (doc: Doc) => (this.imageRootDoc = doc);
+ @observable public addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+
@observable public LastButton: Opt<Doc>;
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
@@ -816,7 +825,7 @@ export class MainView extends React.Component {
{this.dockingContent}
{this._hideUI ? null : (
- <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background : 'linen' }}>
+ <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background: 'linen' }}>
<FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" />
</div>
)}
@@ -1023,6 +1032,8 @@ export class MainView extends React.Component {
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ <GPTPopup key="gptpopup" />
+ <GenerativeFill imageEditorOpen={this.imageEditorOpen} imageEditorSource={this.imageEditorSource} imageRootDoc={this.imageRootDoc} addDoc={this.addDoc} />
{/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index fb8ec93b2..db000d5de 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -21,6 +21,8 @@ export interface CollectionFreeFormLinkViewProps {
LinkDocs: Doc[];
}
+// props.screentolocatransform
+
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
@observable _opacity: number = 0;
@@ -235,6 +237,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
+ // fully connected
// pt1,
// pt2,
// this code adds space between links
@@ -297,6 +300,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
className="collectionfreeformlinkview-linkLine"
style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
onClick={this.onClickLine}
+ // d={`M ${pt1[0]} ${pt1[1]} L ${pt2[0]} ${pt2[1]}`}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
/>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d763753a5..4ce359f3f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,6 +38,7 @@ import './ImageBox.scss';
import { PinProps, PresBox } from './trails';
import React = require('react');
import Color = require('color');
+import { MainView } from '../MainView';
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -248,6 +249,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
+ funcs.push({
+ description: 'Open Image Editor',
+ event: action(() => {
+ MainView.Instance.setImageEditorOpen(true);
+ MainView.Instance.setImageEditorSource(this.choosePath(field.url));
+ MainView.Instance.addDoc = this.props.addDocument;
+ MainView.Instance.imageRootDoc = this.rootDoc;
+ }),
+ icon: 'pencil-alt',
+ });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0eb328a1..212d24165 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -897,6 +897,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
animateRes = (resIndex: number) => {
+ console.log(this.dataDoc.text);
if (resIndex < this.gptRes.length) {
this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
setTimeout(() => {
@@ -905,6 +906,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
};
+ // breakupdictiation for reference
+ // prepareForTyping
+ // stored marks - userMark and timestamp, setStoredMarks
+ // the._editorView.dispatch(state.tr.setSelection(updated).insertText('\n, to))
+
askGPT = action(async () => {
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss
new file mode 100644
index 000000000..b1e570cf1
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss
@@ -0,0 +1,96 @@
+$navHeight: 5rem;
+$canvasSize: 1024px;
+$scale: 0.5;
+
+.generativeFillContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .generativeFillControls {
+ flex-shrink: 0;
+ height: $navHeight;
+ background-color: #ffffff;
+ z-index: 999;
+ width: 100%;
+ display: flex;
+ gap: 3rem;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #c7cdd0;
+ padding: 0 2rem;
+
+ h1 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .drawingArea {
+ cursor: none;
+ touch-action: none;
+ position: relative;
+ flex-grow: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background-color: #f0f4f6;
+
+ canvas {
+ display: block;
+ position: absolute;
+ transform-origin: 50% 50%;
+ }
+
+ .pointer {
+ pointer-events: none;
+ position: absolute;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ border: 1px solid #ffffff;
+ transform: translate(-50%, -50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .innerPointer {
+ width: 100%;
+ height: 100%;
+ border: 1px solid #000000;
+ border-radius: 50%;
+ }
+ }
+
+ .iconContainer {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ .editsBox {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ img {
+ transition: all 0.2s ease-in-out;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
new file mode 100644
index 000000000..2b3cfb920
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,560 @@
+import './GenerativeFill.scss';
+import React = require('react');
+import { useEffect, useRef, useState } from 'react';
+import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
+import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { IconButton, TextField } from '@mui/material';
+import { CursorData, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
+import { PointerHandler } from './generativeFillUtils/PointerHandler';
+import { BsBrush, BsEraser, BsX } from 'react-icons/bs';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { CiUndo, CiRedo } from 'react-icons/ci';
+import Buttons from './GenerativeFillButtons';
+import { EditableText } from 'browndash-components';
+import { MainView } from '../../MainView';
+import { Doc } from '../../../../fields/Doc';
+import { Networking } from '../../../Network';
+import { Utils } from '../../../../Utils';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { DocCast, NumCast } from '../../../../fields/Types';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { OpenWhere, OpenWhereMod } from '../DocumentView';
+import { Oval } from 'react-loader-spinner';
+
+/**
+ * For images not 1024x1024 fill in the rest in solid black, or a
+ * reflected version of the image.
+ */
+
+/**
+ * TODO: Look into img onload, sometimes the canvas doesn't update properly
+ *
+ * Ref:
+ *
+ *
+ * CollectionDockingView.AddSplit(Doc.MakeCopy(DocCast(Doc.UserDoc().emptyPane)), OpenWhereMod.right);
+ * CollectionDockingView.AddSplit(newCollection,OpenWhere.inParent)
+ * mind mapping
+ * this.props.addDocTab();
+ */
+
+enum BrushStyle {
+ ADD,
+ SUBTRACT,
+ MARQUEE,
+}
+
+interface ImageEdit {
+ imgElement: HTMLImageElement;
+ parent: ImageEdit;
+ children: ImageEdit[];
+}
+
+interface GenerativeFillProps {
+ imageEditorOpen: boolean;
+ imageEditorSource: string;
+ imageRootDoc: Doc | undefined;
+ addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+}
+
+const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+ const drawingAreaRef = useRef<HTMLDivElement>(null);
+ const fileRef = useRef<HTMLInputElement>(null);
+ const [cursorData, setCursorData] = useState<CursorData>({
+ x: 0,
+ y: 0,
+ width: 150,
+ });
+ const [isBrushing, setIsBrushing] = useState(false);
+ const [canvasScale, setCanvasScale] = useState(0.5);
+ const [edits, setEdits] = useState<string[]>([]);
+ const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [saveLoading, setSaveLoading] = useState(false);
+ // the current image in the main canvas
+ const currImg = useRef<HTMLImageElement | null>(null);
+ // the unedited version of each generation (parent)
+ const originalImg = useRef<HTMLImageElement | null>(null);
+ // stores history of data urls
+ const undoStack = useRef<string[]>([]);
+ // stores redo stack
+ const redoStack = useRef<string[]>([]);
+
+ // early stage properly, likely will get rid of
+ const freeformPosition = useRef<number[]>([0, 0]);
+
+ // which urls were already saved to canvas
+ const savedSrcs = useRef<string[]>([]);
+
+ // references to keep track of tree structure
+ const newCollectionRef = useRef<Doc | null>(null);
+ const parentDoc = useRef<Doc | null>(null);
+ const childrenDocs = useRef<Doc[]>([]);
+
+ // Undo and Redo
+ const handleUndo = () => {
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx || !currImg.current || !canvasRef.current) return;
+
+ const target = undoStack.current[undoStack.current.length - 1];
+ if (!target) {
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef);
+ } else {
+ redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()];
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ undoStack.current = undoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleRedo = () => {
+ // TODO: handle undo as well
+ const target = redoStack.current[redoStack.current.length - 1];
+ if (!target) {
+ } else {
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ redoStack.current = redoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleReset = () => {
+ if (!canvasRef.current || !currImg.current) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ undoStack.current = [];
+ redoStack.current = [];
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, true);
+ };
+
+ // initiate brushing
+ const handlePointerDown = (e: React.PointerEvent) => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ undoStack.current = [...undoStack.current, canvasRef.current.toDataURL()];
+ redoStack.current = [];
+
+ setIsBrushing(true);
+ const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+
+ BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ // stop brushing, push to undo stack
+ const handlePointerUp = (e: React.PointerEvent) => {
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ if (!isBrushing) return;
+ setIsBrushing(false);
+ };
+
+ // handles brushing on pointer movement
+ useEffect(() => {
+ if (!isBrushing) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ const handlePointerMove = (e: PointerEvent) => {
+ const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+ const lastPoint: Point = {
+ x: currPoint.x - e.movementX / canvasScale,
+ y: currPoint.y - e.movementY / canvasScale,
+ };
+ BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+ return () => {
+ drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+ };
+ }, [isBrushing]);
+
+ // first load
+ useEffect(() => {
+ console.log('first load');
+ if (!imageEditorSource || imageEditorSource === '') return;
+ const img = new Image();
+ img.src = imageEditorSource;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ originalImg.current = img;
+ freeformPosition.current = [0, 0];
+
+ return () => {
+ console.log('cleanup');
+ newCollectionRef.current = null;
+ parentDoc.current = null;
+ childrenDocs.current = [];
+ currImg.current = null;
+ originalImg.current = null;
+ freeformPosition.current = [0, 0];
+ undoStack.current = [];
+ redoStack.current = [];
+ };
+ }, [canvasRef, imageEditorSource]);
+
+ // handles brush sizing
+ useEffect(() => {
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => ({ ...data, width: data.width + 5 }));
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data));
+ }
+ };
+ window.addEventListener('keydown', handleKeyPress);
+ return () => window.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ // handle pinch zoom
+ useEffect(() => {
+ const handlePinch = (e: WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const delta = e.deltaY;
+ const scaleFactor = delta > 0 ? 0.98 : 1.02; // Adjust the scale factor as per your requirement
+ setCanvasScale(prevScale => prevScale * scaleFactor);
+ };
+
+ drawingAreaRef.current?.addEventListener('wheel', handlePinch, {
+ passive: false,
+ });
+ return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch);
+ }, [drawingAreaRef]);
+
+ // updates the current position of the cursor
+ const updateCursorData = (e: React.PointerEvent) => {
+ const drawingArea = drawingAreaRef.current;
+ if (!drawingArea) return;
+ const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1);
+ setCursorData(data => ({
+ ...data,
+ x,
+ y,
+ }));
+ };
+
+ // File upload
+ const uploadImg = (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (e.target.files) {
+ const file = e.target.files[0];
+ const image = new Image();
+ const imgUrl = URL.createObjectURL(file);
+ image.src = imgUrl;
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ }
+ };
+
+ // Get AI Edit
+ const getEdit = async () => {
+ const img = currImg.current;
+ if (!img) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ setLoading(true);
+ // need to adjust later
+ try {
+ const maskBlob = await ImageUtility.canvasToBlob(canvas);
+ const imgBlob = await ImageUtility.canvasToBlob(ImageUtility.getCanvasImg(img));
+ 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.mockGetEdit(img.src);
+
+ // create first image
+ if (!newCollectionRef.current) {
+ if (!(originalImg.current && imageRootDoc)) return;
+ console.log('creating first image');
+ // create new collection and add it to the view
+ newCollectionRef.current = Docs.Create.FreeformDocument([], {
+ x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+ y: NumCast(imageRootDoc.y),
+ _width: newCollectionSize,
+ _height: newCollectionSize,
+ title: 'Image edit collection',
+ });
+ DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false });
+ // add the doc to the main freeform
+ addDoc?.(newCollectionRef.current);
+ await createNewImgDoc(originalImg.current, true);
+ } else {
+ parentDoc.current = childrenDocs.current[childrenDocs.current.length - 1];
+ childrenDocs.current = [];
+ }
+
+ originalImg.current = currImg.current;
+
+ const { urls } = res as APISuccess;
+ const image = new Image();
+ image.src = urls[0];
+ setEdits(urls);
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ onSave();
+ freeformPosition.current[0] += 1;
+ freeformPosition.current[1] = 0;
+ } catch (err) {
+ console.log(err);
+ }
+ setLoading(false);
+ };
+
+ // adjusts all the img positions to be aligned
+ const adjustImgPositions = () => {
+ if (!parentDoc.current) return;
+ const startY = NumCast(parentDoc.current.y);
+ const len = childrenDocs.current.length;
+ console.log(len);
+ let initialYPositions: number[] = [];
+ for (let i = 0; i < len; i++) {
+ initialYPositions.push(startY + i * offsetDistanceY);
+ }
+ childrenDocs.current.forEach((doc, i) => {
+ if (len % 2 === 1) {
+ doc.y = initialYPositions[i] - Math.floor(len / 2) * offsetDistanceY;
+ } else {
+ doc.y = initialYPositions[i] - (len / 2 - 1 / 2) * offsetDistanceY;
+ }
+ });
+ };
+
+ // creates a new image document and returns its reference
+ const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => {
+ if (!newCollectionRef.current || !imageRootDoc) return;
+ const src = img.src;
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+
+ if (firstDoc) {
+ const x = 0;
+ const initialY = 0;
+ console.log('first doc');
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ parentDoc.current = newImg;
+ return newImg;
+ } else {
+ if (!parentDoc.current) return;
+ const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+ // dummy position
+ console.log('creating child elements');
+ const initialY = 0;
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ childrenDocs.current.push(newImg);
+ DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: 'Image Edit', link_displayLine: true });
+ adjustImgPositions();
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ return newImg;
+ }
+ };
+
+ // need to maybe call on every img click, not just when the save btn is clicked
+ const onSave = async () => {
+ setSaveLoading(true);
+ if (!currImg.current || !originalImg.current || !imageRootDoc) return;
+ try {
+ console.log('creating another image');
+ await createNewImgDoc(currImg.current, false);
+ } catch (err) {
+ console.log(err);
+ }
+ setSaveLoading(false);
+ };
+
+ const handleViewClose = () => {
+ // if (newCollectionRef.current) {
+ // CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+ // }
+ MainView.Instance.setImageEditorOpen(false);
+ MainView.Instance.setImageEditorSource('');
+ setEdits([]);
+ };
+
+ return (
+ <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
+ <div className="generativeFillControls">
+ <h1>Generative Fill</h1>
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
+ <Buttons canvasRef={canvasRef} currImg={currImg} getEdit={getEdit} loading={loading} onSave={onSave} onReset={handleReset} />
+ <IconButton onClick={handleViewClose}>
+ <BsX color={activeColor} />
+ </IconButton>
+ </div>
+ </div>
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer"></div>
+ </div>
+ {/* Icons */}
+ <div className="iconContainer">
+ <input ref={fileRef} type="file" accept="image/*" onChange={uploadImg} style={{ display: 'none' }} />
+ <IconButton
+ onClick={() => {
+ if (fileRef.current) {
+ fileRef.current.click();
+ }
+ }}>
+ <AiOutlineUpload />
+ </IconButton>
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.ADD);
+ }}>
+ <BsBrush color={brushStyle === BrushStyle.ADD ? activeColor : 'inherit'} />
+ </IconButton>
+ {/* Undo and Redo */}
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleUndo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiUndo />
+ </IconButton>
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleRedo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiRedo />
+ </IconButton>
+ </div>
+ {/* Edits thumbnails*/}
+ <div className="editsBox">
+ {edits.map((edit, i) => (
+ <img
+ key={i}
+ width={100}
+ height={100}
+ src={edit}
+ onClick={() => {
+ const img = new Image();
+ img.src = edit;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ onSave();
+ }}
+ />
+ ))}
+ {/* Original img thumbnail */}
+ {edits.length > 0 && (
+ <div style={{ position: 'relative' }}>
+ <label
+ style={{
+ position: 'absolute',
+ bottom: 10,
+ left: 10,
+ color: '#ffffff',
+ fontSize: '0.8rem',
+ letterSpacing: '1px',
+ textTransform: 'uppercase',
+ }}>
+ Original
+ </label>
+ <img
+ width={100}
+ height={100}
+ src={originalImg.current?.src}
+ style={{ cursor: 'pointer' }}
+ onClick={() => {
+ if (!originalImg.current) return;
+ const img = new Image();
+ img.src = originalImg.current.src;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ }}
+ />
+ <div
+ style={{
+ position: 'absolute',
+ top: 10,
+ right: 10,
+ }}>
+ {saveLoading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ InputLabelProps={{ style: { fontSize: '1.5rem' } }}
+ inputProps={{ style: { fontSize: '1.5rem' } }}
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '1rem',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 4rem)',
+ scale: 1.2,
+ }}
+ />
+ </div>
+ </div>
+ );
+};
+
+export default GenerativeFill;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
new file mode 100644
index 000000000..0180ef904
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
@@ -0,0 +1,4 @@
+.generativeFillBtnContainer {
+ display: flex;
+ gap: 1rem;
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
new file mode 100644
index 000000000..53c6cec84
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
@@ -0,0 +1,48 @@
+import { Button } from '@mui/material';
+import { ImageUtility } from './generativeFillUtils/ImageHandler';
+import { canvasSize } from './generativeFillUtils/generativeFillConstants';
+import { Oval } from 'react-loader-spinner';
+import './GenerativeFillButtons.scss';
+import React = require('react');
+import { Doc } from '../../../../fields/Doc';
+
+interface ButtonContainerProps {
+ canvasRef: React.RefObject<HTMLCanvasElement>;
+ currImg: React.MutableRefObject<HTMLImageElement | null>;
+ getEdit: () => Promise<void>;
+ loading: boolean;
+ onSave: () => Promise<void>;
+ onReset: () => void;
+}
+
+const Buttons = ({ canvasRef, currImg, loading, getEdit, onSave, onReset }: ButtonContainerProps) => {
+ const handleSave = () => {
+ onSave();
+ };
+
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button onClick={onReset}>Reset</Button>
+ {/* <Button onClick={handleSave}>Save</Button> */}
+ {/* <Button
+ onClick={() => {
+ if (!canvasRef.current) return;
+ ImageUtility.downloadImageCanvas('/assets/firefly.png');
+ }}>
+ Download Original
+ </Button> */}
+ <Button
+ variant="contained"
+ onClick={() => {
+ getEdit();
+ }}>
+ <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
+ Get Edit
+ {loading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </span>
+ </Button>
+ </div>
+ );
+};
+
+export default Buttons;
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
new file mode 100644
index 000000000..c2716e083
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -0,0 +1,87 @@
+import { GenerativeFillMathHelpers } from "./GenerativeFillMathHelpers";
+import { eraserColor } from "./generativeFillConstants";
+import { Point } from "./generativeFillInterfaces";
+
+export class BrushHandler {
+ static brushCircle = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.shadowColor = "#ffffffeb";
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static brushCircleOverlay = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ // ctx.globalCompositeOperation = erase ? "destination-out" : "source-over";
+ ctx.fillStyle = fillColor;
+ ctx.shadowColor = eraserColor;
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static createBrushPath = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircle(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx
+ );
+ }
+ };
+
+ static createBrushPathOverlay = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircleOverlay(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx,
+ fillColor,
+ erase
+ );
+ }
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
new file mode 100644
index 000000000..027b99a52
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
@@ -0,0 +1,11 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class GenerativeFillMathHelpers {
+ // math helpers
+ static distanceBetween = (p1: Point, p2: Point) => {
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+ };
+ static angleBetween = (p1: Point, p2: Point) => {
+ return Math.atan2(p2.x - p1.x, p2.y - p1.y);
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
new file mode 100644
index 000000000..48055903c
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
@@ -0,0 +1,128 @@
+import { RefObject } from 'react';
+import { canvasSize } from './generativeFillConstants';
+
+export interface APISuccess {
+ status: 'success';
+ urls: string[];
+}
+
+export interface APIError {
+ status: 'error';
+ message: string;
+}
+
+export class ImageUtility {
+ static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ }
+ }, 'image/png');
+ });
+ };
+
+ static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
+ const apiUrl = 'https://api.openai.com/v1/images/edits';
+ const fd = new FormData();
+ fd.append('image', imgBlob, 'image.png');
+ fd.append('mask', maskBlob, 'mask.png');
+ fd.append('prompt', prompt);
+ fd.append('size', '1024x1024');
+ fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('response_format', 'b64_json');
+
+ try {
+ const res = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
+ },
+ body: fd,
+ });
+ const data = await res.json();
+ console.log(data.data);
+ return {
+ status: 'success',
+ urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`),
+ };
+ } catch (err) {
+ console.log(err);
+ return { status: 'error', message: 'API error.' };
+ }
+ };
+
+ static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => {
+ return {
+ status: 'success',
+ urls: [mockSrc, mockSrc, mockSrc],
+ };
+ };
+
+ static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
+ if (!canvasRef.current) return null;
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) return null;
+ return ctx;
+ };
+
+ static downloadCanvas = (canvas: HTMLCanvasElement) => {
+ const url = canvas.toDataURL();
+ const downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.download = 'canvas';
+
+ downloadLink.click();
+ downloadLink.remove();
+ };
+
+ static downloadImageCanvas = (imgUrl: string) => {
+ const img = new Image();
+ img.src = imgUrl;
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ this.downloadCanvas(canvas);
+ };
+ };
+
+ static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, loaded?: boolean) => {
+ if (loaded) {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ } else {
+ img.onload = () => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ };
+ }
+ };
+
+ // The image must be loaded!
+ static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ return canvas;
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
new file mode 100644
index 000000000..9e620ad11
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
@@ -0,0 +1,15 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class PointerHandler {
+ static getPointRelativeToElement = (
+ element: HTMLElement,
+ e: React.PointerEvent | PointerEvent,
+ scale: number
+ ): Point => {
+ const boundingBox = element.getBoundingClientRect();
+ return {
+ x: (e.clientX - boundingBox.x) / scale,
+ y: (e.clientY - boundingBox.y) / scale,
+ };
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
new file mode 100644
index 000000000..5a8d33742
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
@@ -0,0 +1,9 @@
+// constants
+export const canvasSize = 1024;
+export const freeformRenderSize = 300;
+export const offsetDistanceY = freeformRenderSize + 200;
+export const offsetX = 200;
+export const newCollectionSize = 1000;
+
+export const activeColor = '#1976d2';
+export const eraserColor = '#e1e9ec';
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
new file mode 100644
index 000000000..9b9b9d3c2
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
@@ -0,0 +1,16 @@
+// interfaces
+export interface CursorData {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export enum BrushMode {
+ ADD,
+ SUBTRACT,
+}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 17a3d0a03..e6b9cb382 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -48,9 +48,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Status: 'marquee' | 'annotation' | '' = '';
// GPT additions
- @observable private GPTpopupText: string = '';
- @observable private loadingGPT: boolean = false;
- @observable private showGPTPopup: boolean = false;
@observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY;
@observable private selectedText: string = '';
@observable private editorView?: EditorView;
@@ -59,25 +56,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
private selectionRange: number[] | undefined;
@action
- setGPTPopupVis = (vis: boolean) => {
- this.showGPTPopup = vis;
- };
- @action
setGPTMode = (mode: GPTPopupMode) => {
this.GPTMode = mode;
};
@action
- setGPTPopupText = (txt: string) => {
- this.GPTpopupText = txt;
- };
-
- @action
- setLoading = (loading: boolean) => {
- this.loadingGPT = loading;
- };
-
- @action
setHighlightRange(r: number[] | undefined) {
this.highlightRange = r;
}
@@ -130,19 +113,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentDidMount() {
this._disposer2 = reaction(
() => this._opacity,
- opacity => {
- if (!opacity) {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
- }
- },
+ opacity => {},
{ fireImmediately: true }
);
this._disposer = reaction(
() => SelectionManager.Views().slice(),
selected => {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
}
);
@@ -153,23 +129,22 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
+ GPTPopup.Instance.setVisible(true);
this.setHighlightRange(undefined);
- this.setGPTPopupVis(true);
this.setGPTMode(GPTPopupMode.SUMMARY);
- this.setLoading(true);
+ GPTPopup.Instance.setLoading(true);
try {
const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
if (res) {
- this.setGPTPopupText(res);
+ GPTPopup.Instance.setText(res);
} else {
- this.setGPTPopupText('Something went wrong.');
+ GPTPopup.Instance.setText('Something went wrong.');
}
} catch (err) {
console.error(err);
}
-
- this.setLoading(false);
+ GPTPopup.Instance.setLoading(false);
};
/**
@@ -184,9 +159,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
const fullText = state.doc.textBetween(0, this.editorView.state.doc.content.size, ' \n');
const selectedText = state.doc.textBetween(sel.from, sel.to);
- this.setGPTPopupVis(true);
+ GPTPopup.Instance.setVisible(true);
this.setGPTMode(GPTPopupMode.EDIT);
- this.setLoading(true);
+ GPTPopup.Instance.setLoading(true);
try {
let res = await gptAPICall(selectedText, GPTCallType.EDIT);
@@ -196,16 +171,16 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1);
if (res) {
- this.setGPTPopupText(resultText);
+ GPTPopup.Instance.setText(resultText);
this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]);
} else {
- this.setGPTPopupText('Something went wrong.');
+ GPTPopup.Instance.setText('Something went wrong.');
}
} catch (err) {
console.error(err);
}
- this.setLoading(false);
+ GPTPopup.Instance.setLoading(false);
};
/**
@@ -318,7 +293,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
</button>
</Tooltip>
)}
- <GPTPopup
+ {/* <GPTPopup
key="gptpopup"
visible={this.showGPTPopup}
text={this.GPTpopupText}
@@ -328,7 +303,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
callEditApi={this.gptEdit}
replaceText={this.replaceText}
mode={this.GPTMode}
- />
+ /> */}
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<IconButton tooltip={'Click to Record Annotation'} onPointerDown={this.audioDown} icon={<FontAwesomeIcon icon="microphone" />} color={StrCast(Doc.UserDoc().userColor)} />
)}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 44413ede7..2f0ff83e2 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -6,12 +6,6 @@ $button: #5b97ff;
$highlightedText: #82e0ff;
.summary-box {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- background-color: #ffffff;
- box-shadow: 0 2px 5px #7474748d;
- color: $textgrey;
position: fixed;
bottom: 10px;
right: 10px;
@@ -21,6 +15,12 @@ $highlightedText: #82e0ff;
padding: 15px;
padding-bottom: 0;
z-index: 999;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
.summary-heading {
display: flex;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 8bd060d4f..9f28cb5d1 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -5,56 +5,96 @@ import { observer } from 'mobx-react';
import ReactLoading from 'react-loading';
import Typist from 'react-typist';
import { Doc } from '../../../../fields/Doc';
-import { Docs } from '../../../documents/Documents';
+import { DocUtils, Docs } from '../../../documents/Documents';
import './GPTPopup.scss';
+import { Button, IconButton } from 'browndash-components';
+import { StrCast } from '../../../../fields/Types';
export enum GPTPopupMode {
SUMMARY,
EDIT,
}
-interface GPTPopupProps {
- visible: boolean;
- text: string;
- loading: boolean;
- mode: GPTPopupMode;
- callSummaryApi: (e: React.PointerEvent) => Promise<void>;
- callEditApi: (e: React.PointerEvent) => Promise<void>;
- replaceText: (replacement: string) => void;
- highlightRange?: number[];
-}
+interface GPTPopupProps {}
@observer
export class GPTPopup extends React.Component<GPTPopupProps> {
static Instance: GPTPopup;
@observable
- private done: boolean = false;
+ public visible: boolean = false;
+ @action
+ public setVisible = (vis: boolean) => {
+ this.visible = vis;
+ };
@observable
- private sidebarId: string = '';
+ public loading: boolean = false;
+ @action
+ public setLoading = (loading: boolean) => {
+ this.loading = loading;
+ };
+ @observable
+ public text: string = '';
+ @action
+ public setText = (text: string) => {
+ this.text = text;
+ };
+ @observable
+ public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action
+ public setMode = (mode: GPTPopupMode) => {
+ this.mode = mode;
+ };
+ @observable
+ public highlightRange: number[] = [];
+ @action callSummaryApi = () => {};
+ @action callEditApi = () => {};
+ @action replaceText = (replacement: string) => {};
+
+ @observable
+ private done: boolean = false;
@action
public setDone = (done: boolean) => {
this.done = done;
};
+ @observable
+ private sidebarId: string = '';
@action
public setSidebarId = (id: string) => {
this.sidebarId = id;
};
+ // reference
+ // if (focusNode) {
+ // const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
+ // anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
+ // }
+ @observable
+ private pdfAnchor: Doc | undefined;
+ @action
+ public setPdfAnchor = (anchor: Doc) => {
+ this.pdfAnchor = anchor;
+ };
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
private transferToText = () => {
- const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ const newDoc = Docs.Create.TextDocument(this.text.trim(), {
_width: 200,
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
});
this.addDoc(newDoc, this.sidebarId);
+ if (this.pdfAnchor) {
+ DocUtils.MakeLink(newDoc, this.pdfAnchor, {
+ link_relationship: 'GPT Summary',
+ });
+ }
};
constructor(props: GPTPopupProps) {
@@ -63,7 +103,7 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
}
componentDidUpdate = () => {
- if (this.props.loading) {
+ if (this.loading) {
this.setDone(false);
}
};
@@ -73,10 +113,10 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
<div>
{this.heading('SUMMARY')}
<div className="content-wrapper">
- {!this.props.loading &&
+ {!this.loading &&
(!this.done ? (
<Typist
- key={this.props.text}
+ key={this.text}
avgTypingDelay={15}
cursor={{ hideWhenDone: true }}
onTypingDone={() => {
@@ -84,18 +124,18 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
this.setDone(true);
}, 500);
}}>
- {this.props.text}
+ {this.text}
</Typist>
) : (
- this.props.text
+ this.text
))}
</div>
</div>
- {!this.props.loading && (
+ {!this.loading && (
<div className="btns-wrapper">
{this.done ? (
<>
- <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
+ {/* <button className="icon-btn" onClick={this.callSummaryApi}>
<FontAwesomeIcon icon="redo-alt" size="lg" />
</button>
<button
@@ -104,7 +144,9 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
this.transferToText();
}}>
Transfer to Text
- </button>
+ </button> */}
+ <IconButton onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ <Button text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} />
</>
) : (
<div className="summarizing">
@@ -125,7 +167,7 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
);
editBox = () => {
- const hr = this.props.highlightRange;
+ const hr = this.highlightRange;
return (
<>
<div>
@@ -133,22 +175,22 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
<div className="content-wrapper">
{hr && (
<div>
- {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
+ {this.text.slice(0, hr[0])} <span className="highlighted-text">{this.text.slice(hr[0], hr[1])}</span> {this.text.slice(hr[1])}
</div>
)}
</div>
</div>
- {hr && !this.props.loading && (
+ {hr && !this.loading && (
<>
<div className="btns-wrapper">
<>
- <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
+ <button className="icon-btn" onPointerDown={this.callEditApi}>
<FontAwesomeIcon icon="redo-alt" size="lg" />
</button>
<button
className="text-btn"
onClick={e => {
- this.props.replaceText(this.props.text);
+ this.replaceText(this.text);
}}>
Replace Text
</button>
@@ -174,14 +216,14 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ {this.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
</div>
);
render() {
return (
- <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
- {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
+ {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
</div>
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 0fd93868a..4fc31ffe3 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -423,6 +423,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
// Changing which document to add the annotation to (the currently selected PDF)
GPTPopup.Instance.setSidebarId('data_sidebar');
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ console.log(anchor);
+ GPTPopup.Instance.setPdfAnchor(anchor);
+ }
GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};