aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/apis/gpt/GPT.ts30
-rw-r--r--src/client/util/DragManager.ts4
-rw-r--r--src/client/views/MainView.tsx13
-rw-r--r--src/client/views/collections/TabDocView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx18
-rw-r--r--src/client/views/nodes/ImageBox.tsx11
-rw-r--r--src/client/views/nodes/WebBox.tsx5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx102
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.scss97
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx559
-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.ts45
-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.tsx75
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss64
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx193
-rw-r--r--src/client/views/pdf/PDFViewer.tsx4
23 files changed, 1295 insertions, 174 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b3960902..6b4106f56 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,7 +14,7 @@ type GPTCallOpts = {
};
const callTypeMap: { [type: string]: GPTCallOpts } = {
- summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text briefly: ' },
+ summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' },
};
@@ -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);
@@ -47,7 +47,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
}
};
-const gptImageCall = async (prompt: string) => {
+const gptImageCall = async (prompt: string, n?: number) => {
try {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY,
@@ -55,33 +55,15 @@ const gptImageCall = async (prompt: string) => {
const openai = new OpenAIApi(configuration);
const response = await openai.createImage({
prompt: prompt,
- n: 1,
+ n: n ?? 1,
size: '1024x1024',
});
- return response.data.data[0].url;
+ return response.data.data.map(data => data.url);
+ // return response.data.data[0].url;
} catch (err) {
console.error(err);
return;
}
};
-// const gptEditCall = async (selectedText: string, fullText: string) => {
-// try {
-// const configuration = new Configuration({
-// apiKey: process.env.OPENAI_KEY,
-// });
-// const openai = new OpenAIApi(configuration);
-// const response = await openai.createCompletion({
-// model: 'text-davinci-003',
-// max_tokens: 256,
-// temperature: 0.1,
-// prompt: `Replace the phrase ${selectedText} inside of ${fullText}.`,
-// });
-// return response.data.choices[0].text.trim();
-// } catch (err) {
-// console.log(err);
-// return 'Error connecting with API.';
-// }
-// };
-
export { gptAPICall, gptImageCall, GPTCallType };
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f4ff38515..489c9df4a 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -195,7 +195,7 @@ export namespace DragManager {
}
// drag a document and drop it (or make an embed/copy on drop)
- export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) {
+ export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => any) {
const addAudioTag = (dropDoc: any) => {
dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
@@ -203,7 +203,7 @@ export namespace DragManager {
};
const finishDrag = async (e: DragCompleteEvent) => {
const docDragData = e.docDragData;
- dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
+ onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
docDragData.droppedDocuments = (
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index dc236b603..60eb64caa 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -65,6 +65,8 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -72,6 +74,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;
@@ -1012,6 +1023,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/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index d787f5262..71032ff68 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -137,7 +137,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
setupMoveUpEvents(
this,
e,
- e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY),
+ e =>
+ !e.defaultPrevented &&
+ DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY, undefined, () => {
+ CollectionDockingView.CloseSplit(doc);
+ }),
returnFalse,
action(e => {
if (this.view) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index fb8ec93b2..747ab9249 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,11 +237,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
- // pt1,
- // pt2,
+ // fully connected
+ pt1,
+ pt2,
// this code adds space between links
- pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
- pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+ // pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
+ // pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
};
}
@@ -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/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e1455525e..29122cb91 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -237,7 +237,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
elementFunc = () => this._layoutElements;
- shrinkWrap = () => {
+ fitContentOnce = () => {
if (this.props.DocumentView?.().nativeWidth) return;
const vals = this.fitToContentVals;
this.layoutDoc._freeform_panX = vals.bounds.cx;
@@ -1594,6 +1594,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
elements => (this._layoutElements = elements || []),
{ fireImmediately: true, name: 'doLayout' }
);
+
+ this._disposers.fitContent = reaction(
+ () => this.rootDoc.fitContentOnce,
+ fitContentOnce => {
+ if (fitContentOnce) this.fitContentOnce();
+ },
+ { fireImmediately: true, name: 'fitContent' }
+ );
})
);
}
@@ -1784,6 +1792,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!Doc.noviceMode &&
optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' });
this.props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor)), icon: 'palette' });
+ this.props.renderDepth &&
+ optionItems.push({
+ description: 'Fit Content Once',
+ event: () => {
+ this.fitContentOnce();
+ },
+ icon: 'object-group',
+ });
if (!Doc.noviceMode) {
optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' });
}
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/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f5df42161..07b931312 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -358,6 +358,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale);
// Changing which document to add the annotation to (the currently selected WebBox)
GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`);
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ console.log(anchor);
+ GPTPopup.Instance.setTargetAnchor(anchor);
+ }
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1dcc445e8..332f0f467 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -70,6 +70,7 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
const translateGoogleApi = require('translate-google-api');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -903,57 +904,86 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._downX = this._downY = Number.NaN;
};
- animateRes = (resIndex: number) => {
- if (resIndex < this.gptRes.length) {
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ animateRes = (resIndex: number, newText: string) => {
+ if (resIndex < newText.length) {
+ const marks = this._editorView?.state.storedMarks ?? [];
+ // if (!marks) return;
+ this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => {
- this.animateRes(resIndex + 1);
+ this.animateRes(resIndex + 1, newText);
}, 20);
}
};
+ // breakupdictiation for reference
+ // prepareForTyping
+ // stored marks - userMark and timestamp, setStoredMarks
+ // the._editorView.dispatch(state.tr.setSelection(updated).insertText('\n, to))
+
askGPT = action(async () => {
+ // const state = this._editorView?.state;
+ // if (!state) return;
+ // const to = state.selection.to;
+ // const updated = TextSelection.create(state.doc, to, to);
+ // this._editorView?.dispatch(state.tr.setSelection(updated).insertText('\n', to));
+ // this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText('\nTesting').setStoredMarks(marks));
+ // console.log('After ', this._editorView?.state.storedMarks);
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
- if (res) {
- this.gptRes = res;
- this.animateRes(0);
+ if (!res) {
+ console.error('GPT call failed');
+ this.animateRes(0, 'Something went wrong.');
+ } else {
+ this.animateRes(0, res);
}
} catch (err) {
- console.log(err);
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ console.error('GPT call failed');
+ this.animateRes(0, 'Something went wrong.');
}
});
generateImage = async () => {
console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text);
+ GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
+ GPTPopup.Instance?.setImgTargetDoc(this.rootDoc);
+ GPTPopup.Instance.setImgUrls([]);
+ GPTPopup.Instance.setMode(GPTPopupMode.IMAGE);
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.addToCollection = this.props.addDocument;
+ GPTPopup.Instance.setLoading(true);
+
try {
- let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
- if (image_url) {
- const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] });
- const source = result.accessPaths.agnostic.client;
- const newDoc = Docs.Create.ImageDocument(source, {
- x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
- y: NumCast(this.rootDoc.y),
- _height: 200,
- _width: 200,
- data_nativeWidth: result.nativeWidth,
- data_nativeHeight: result.nativeHeight,
- });
- if (Doc.IsInMyOverlay(this.rootDoc)) {
- newDoc.overlayX = this.rootDoc.x;
- newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddToMyOverlay(newDoc);
- } else {
- this.props.addDocument?.(newDoc);
- }
- // Create link between prompt and image
- DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
+ // make this support multiple images
+ let image_urls = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
+ console.log(image_urls);
+ if (image_urls) {
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+ GPTPopup.Instance.setImgUrls([source]);
+
+ // const newDoc = Docs.Create.ImageDocument(source, {
+ // x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
+ // y: NumCast(this.rootDoc.y),
+ // _height: 200,
+ // _width: 200,
+ // data_nativeWidth: result.nativeWidth,
+ // data_nativeHeight: result.nativeHeight,
+ // });
+ // if (Doc.IsInMyOverlay(this.rootDoc)) {
+ // newDoc.overlayX = this.rootDoc.x;
+ // newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ // Doc.AddToMyOverlay(newDoc);
+ // } else {
+ // this.props.addDocument?.(newDoc);
+ // }
+ // // Create link between prompt and image
+ // DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
}
} catch (err) {
console.log(err);
return '';
}
+ GPTPopup.Instance.setLoading(false);
};
breakupDictation = () => {
@@ -1248,11 +1278,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
setTimeout(this.autoLink, 20);
}
- // Accessing editor and text doc for gpt assisted text edits
- if (this._editorView && selected) {
- AnchorMenu.Instance?.setEditorView(this._editorView);
- AnchorMenu.Instance?.setTextDoc(this.dataDoc);
- }
+ // // Accessing editor and text doc for gpt assisted text edits
+ // if (this._editorView && selected) {
+ // console.log('Setting');
+ // GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
+ // // AnchorMenu.Instance?.setEditorView(this._editorView);
+ // // AnchorMenu.Instance?.setTextDoc(this.dataDoc);
+ // }
}),
{ fireImmediately: true }
);
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss
new file mode 100644
index 000000000..c2669a950
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss
@@ -0,0 +1,97 @@
+$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;
+ color: #000000;
+ 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..f8f9fe077
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,559 @@
+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 { Box, IconButton, Slider, 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 { BsEraser, BsX } from 'react-icons/bs';
+import { CiUndo, CiRedo } from 'react-icons/ci';
+import Buttons from './GenerativeFillButtons';
+import { MainView } from '../../MainView';
+import { Doc } from '../../../../fields/Doc';
+import { Networking } from '../../../Network';
+import { Utils } from '../../../../Utils';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { Cast, 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.
+ *
+ * add a branch from image directly checkbox
+ */
+
+/**
+ *
+ *
+ * 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<Set<string>>(new Set());
+
+ // references to keep track of tree structure
+ const newCollectionRef = useRef<Doc | null>(null);
+ const parentDoc = useRef<Doc | null>(null);
+ const childrenDocs = useRef<Doc[]>([]);
+ const addToExistingCollection = useRef<boolean>(false);
+
+ // 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,
+ }));
+ };
+
+ // 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 (addToExistingCollection.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;
+ 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) {
+ newCollectionRef.current.fitContentOnce = true;
+ 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>AI Image Editor</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>
+ {saveLoading && (
+ <span style={{ height: '100%', display: 'flex', alignItems: 'center', gap: '8px' }}>
+ Saving image... <Oval height={20} width={20} color="#000000" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />
+ </span>
+ )}
+ </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">
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.ADD);
+ }}>
+ <BsEraser 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>
+ <Box
+ sx={{
+ height: 225,
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ }}>
+ <Slider
+ sx={{
+ '& input[type="range"]': {
+ WebkitAppearance: 'slider-vertical',
+ },
+ }}
+ orientation="vertical"
+ min={10}
+ max={500}
+ defaultValue={150}
+ onChange={(e, val) => {
+ setCursorData(prev => ({ ...prev, width: val as number }));
+ }}
+ />
+ </Box>
+ </div>
+ {/* Edits thumbnails*/}
+ <div className="editsBox">
+ {edits.map((edit, i) => (
+ <img
+ key={i}
+ width={100}
+ height={100}
+ src={edit}
+ onClick={async () => {
+ // if (savedSrcs.current.has(edit)) return;
+ const img = new Image();
+ img.src = edit;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ savedSrcs.current.add(edit);
+ await 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>
+ )}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ InputLabelProps={{ style: { fontSize: '16px' } }}
+ inputProps={{ style: { fontSize: '16px' } }}
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '16px',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 64px)',
+ }}
+ />
+ </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..f84f04190
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -0,0 +1,45 @@
+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..45cf7196b
--- /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', '512x512');
+ 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..412a4d238
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
@@ -0,0 +1,9 @@
+// constants
+export const canvasSize = 512;
+export const freeformRenderSize = 300;
+export const offsetDistanceY = freeformRenderSize + 200;
+export const offsetX = 200;
+export const newCollectionSize = 500;
+
+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 e35e011e2..7404650d6 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -49,9 +49,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;
@@ -60,25 +57,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;
}
@@ -131,19 +114,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);
}
);
@@ -154,23 +130,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.setMode(GPTPopupMode.SUMMARY);
+ 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);
};
/**
@@ -185,9 +160,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);
@@ -197,16 +172,20 @@ 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);
+ };
+
+ gptImage = async () => {
+ console.log(this.GetAnchor(undefined, false));
};
/**
@@ -325,17 +304,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
color={StrCast(Doc.UserDoc().userColor)}
/>
)}
- <GPTPopup
- key="gptpopup"
- visible={this.showGPTPopup}
- text={this.GPTpopupText}
- highlightRange={this.highlightRange}
- loading={this.loadingGPT}
- callSummaryApi={this.gptSummarize}
- callEditApi={this.gptEdit}
- replaceText={this.replaceText}
- mode={this.GPTMode}
- />
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<IconButton
tooltip="Click to Record Annotation" //
@@ -344,14 +312,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
color={StrCast(Doc.UserDoc().userColor)}
/>
)}
- {this.canEdit() && (
- <IconButton
- tooltip="AI edit suggestions" //
- onPointerDown={this.gptEdit}
- icon={<FontAwesomeIcon icon="pencil-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
- )}
+ {/* Removed text editing for now, not quite ready */}
+ {/* {this.canEdit() && <IconButton tooltip={'AI edit suggestions'} onPointerDown={this.gptEdit} icon={<FontAwesomeIcon icon="pencil-alt" />} color={StrCast(Doc.UserDoc().userColor)} />} */}
+ {<IconButton tooltip={'Generate DALL-E Image'} onPointerDown={this.gptImage} icon={<FontAwesomeIcon icon="image" />} color={StrCast(Doc.UserDoc().userColor)} />}
<Popup
tooltip="Find document to link to selected text" //
type={Type.PRIM}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 44413ede7..478b7d4ba 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,9 +15,16 @@ $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;
+ justify-content: space-between;
align-items: center;
border-bottom: 1px solid $greyborder;
padding-bottom: 5px;
@@ -110,6 +111,57 @@ $highlightedText: #82e0ff;
}
}
+.image-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding-bottom: 16px;
+
+ .img-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .img-container {
+ position: relative;
+
+ img {
+ position: relative;
+ }
+ }
+
+ .img-container::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .btn-container {
+ position: absolute;
+ right: 8px;
+ bottom: 8px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ .img-container::after {
+ opacity: 1;
+ }
+
+ .btn-container {
+ opacity: 1;
+ }
+ }
+ }
+}
+
// Typist CSS
.Typist .Cursor {
display: inline-block;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 8bd060d4f..fc6fc1af8 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,82 +1,192 @@
import React = require('react');
+import './GPTPopup.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
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 './GPTPopup.scss';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { Button, IconButton, Type } from 'browndash-components';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { CgClose } from 'react-icons/cg';
export enum GPTPopupMode {
SUMMARY,
EDIT,
+ IMAGE,
}
-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 imageUrls: string[] = [];
+ @action
+ public setImgUrls = (imgs: string[]) => {
+ this.imageUrls = imgs;
+ };
+ @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;
};
+
+ // change what can be a ref into a ref
+ @observable
+ private sidebarId: string = '';
@action
public setSidebarId = (id: string) => {
this.sidebarId = id;
};
+ // pdfs and webpages
+ @observable
+ private targetAnchor: Doc | undefined;
+ @action
+ public setTargetAnchor = (anchor: Doc) => {
+ this.targetAnchor = anchor;
+ };
+
+ @observable
+ private imgTargetDoc: Doc | undefined;
+ @action
+ public setImgTargetDoc = (anchor: Doc) => {
+ this.imgTargetDoc = anchor;
+ };
+
+ @observable
+ private textAnchor: Doc | undefined;
+ @action
+ public setTextAnchor = (anchor: Doc) => {
+ this.textAnchor = anchor;
+ };
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+ public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
/**
* 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.targetAnchor) {
+ DocUtils.MakeLink(newDoc, this.targetAnchor, {
+ link_relationship: 'GPT Summary',
+ });
+ }
};
+ /**
+ * Transfers the image urls to actual image docs
+ */
+ private transferToImage = (source: string) => {
+ const textAnchor = this.imgTargetDoc;
+ if (!textAnchor) return;
+ const newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
+ y: NumCast(textAnchor.y),
+ _height: 200,
+ _width: 200,
+ data_nativeWidth: 1024,
+ data_nativeHeight: 1024,
+ });
+ if (Doc.IsInMyOverlay(textAnchor)) {
+ newDoc.overlayX = textAnchor.x;
+ newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
+ Doc.AddToMyOverlay(newDoc);
+ } else {
+ this.addToCollection?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
+ };
+
+ private getPreviewUrl = (source: string) => source.split('.').join('_m.');
+
constructor(props: GPTPopupProps) {
super(props);
GPTPopup.Instance = this;
}
componentDidUpdate = () => {
- if (this.props.loading) {
+ if (this.loading) {
this.setDone(false);
}
};
+ imageBox = () => {
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ {this.heading('GENERATED IMAGE')}
+ <div className="image-content-wrapper">
+ {this.imageUrls.map(rawSrc => (
+ <div className="img-wrapper">
+ <div className="img-container">
+ <img key={rawSrc} src={this.getPreviewUrl(rawSrc)} width={150} height={150} alt="dalle generation" />
+ </div>
+ <div className="btn-container">
+ <Button text="Save Image" onClick={() => this.transferToImage(rawSrc)} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ };
+
summaryBox = () => (
<>
<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,39 +194,32 @@ 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)}>
- <FontAwesomeIcon icon="redo-alt" size="lg" />
- </button>
- <button
- className="text-btn"
- onClick={e => {
- this.transferToText();
- }}>
- Transfer to Text
- </button>
+ <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
</>
) : (
<div className="summarizing">
<span>Summarizing</span>
<ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
- <button
- className="btn-secondary"
- onClick={e => {
+ <Button
+ text="Stop Animation"
+ onClick={() => {
this.setDone(true);
- }}>
- Stop Animation
- </button>
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ />
</div>
)}
</div>
@@ -125,7 +228,7 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
);
editBox = () => {
- const hr = this.props.highlightRange;
+ const hr = this.highlightRange;
return (
<>
<div>
@@ -133,22 +236,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 +277,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} /> : <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
</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.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.editBox()}
</div>
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1319a236d..c9fee4813 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -424,6 +424,10 @@ 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) {
+ GPTPopup.Instance.setTargetAnchor(anchor);
+ }
GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};