aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/DocumentDecorations.tsx7
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/OverlayView.scss14
-rw-r--r--src/client/views/OverlayView.tsx18
-rw-r--r--src/client/views/PreviewCursor.tsx4
-rw-r--r--src/client/views/ScriptBox.tsx1
-rw-r--r--src/client/views/StyleProviderQuiz.tsx4
-rw-r--r--src/client/views/ViewBoxInterface.ts2
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx162
-rw-r--r--src/client/views/collections/CollectionSubView.tsx34
-rw-r--r--src/client/views/collections/CollectionView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx1
-rw-r--r--src/client/views/global/globalScripts.ts29
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx4
-rw-r--r--src/client/views/nodes/ImageBox.scss7
-rw-r--r--src/client/views/nodes/ImageBox.tsx82
-rw-r--r--src/client/views/nodes/WebBox.tsx4
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx44
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts3
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx5
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts38
-rw-r--r--src/client/views/nodes/chatbot/tools/ImageCreationTool.ts36
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx15
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditor.tsx83
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditorButtons.tsx4
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts1
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts5
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx21
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss111
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx864
-rw-r--r--src/client/views/pdf/PDFViewer.tsx2
32 files changed, 691 insertions, 927 deletions
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 54ff3904d..d9b6bdf1a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -36,6 +36,7 @@ import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { TagsView } from './TagsView';
+import { ImageField } from '../../fields/URLField';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -284,8 +285,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
embedding.y = -NumCast(embedding._height) / 2;
CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
} else if (e.altKey) {
- // open same document in new tab
- CollectionDockingView.ToggleSplit(selView.Document, OpenWhereMod.right);
+ // open same document in new tab or in custom editor
+ selView.ComponentView?.docEditorView?.() ?? CollectionDockingView.ToggleSplit(selView.Document, OpenWhereMod.right);
} else {
let openDoc = selView.Document;
if (openDoc.layout_fieldKey === 'layout_icon') {
@@ -821,7 +822,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
{hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')}
{hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')}
{titleArea}
- {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
+ {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection, opption: in editor view)')}
</div>
{hideResizers ? null : (
<>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d748b70ae..195b1c572 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -78,6 +78,7 @@ import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
import { TopBar } from './topbar/TopBar';
+import { OverlayView } from './OverlayView';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@@ -168,6 +169,7 @@ export class MainView extends ObservableReactComponent<object> {
mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight();
componentDidMount() {
+ OverlayView.Instance.addWindow(<GPTPopup />, { x: 400, y: 200, width: 500, height: 400, title: 'GPT', backgroundColor: 'transparent', isHidden: () => !SnappingManager.ChatVisible, onClick: () => SnappingManager.SetChatVisible(false) });
// Utils.TraceConsoleLog();
reaction(
// when a multi-selection occurs, remove focus from all active elements to allow keyboad input to go only to global key manager to act upon selection
@@ -1154,7 +1156,6 @@ export class MainView extends ObservableReactComponent<object> {
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
- <GPTPopup key="gptpopup" />
<SchemaCSVPopUp key="schemacsvpopup" />
<ImageEditorBox imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
</div>
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 33a297fd4..2e8621b5b 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -4,7 +4,7 @@
top: 0;
width: 100vw;
height: 100vh;
- z-index: 1001; // shouold be greater than LightboxView's z-index so that link lines and the presentation mini player appear
+ z-index: 2002; // shouold be greater than LightboxView's z-index so that link lines and the presentation mini player appear
/* background-color: pink; */
user-select: none;
}
@@ -17,6 +17,7 @@
top: 0;
left: 0;
pointer-events: all;
+ box-shadow: black 5px 5px 5px;
}
.overlayWindow-outerDiv,
@@ -26,27 +27,30 @@
}
.overlayWindow-titleBar {
- flex: 0 1 30px;
+ flex: 0 1 20px;
background: darkslategray;
color: whitesmoke;
text-align: center;
cursor: move;
+ z-index: 1;
}
.overlayWindow-content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
+ z-index: 0;
}
.overlayWindow-closeButton {
float: right;
- height: 30px;
- width: 30px;
+ height: 20px;
+ width: 20px;
+ padding: 0;
+ background-color: inherit;
}
.overlayWindow-resizeDragger {
- background-color: rgb(0, 0, 0);
position: absolute;
right: 0px;
bottom: 0px;
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 5e9677b45..20931fc3d 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -18,6 +18,7 @@ import { ObservableReactComponent } from './ObservableReactComponent';
import './OverlayView.scss';
import { DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { SnappingManager } from '../util/SnappingManager';
export type OverlayDisposer = () => void;
@@ -27,12 +28,17 @@ export type OverlayElementOptions = {
width?: number;
height?: number;
title?: string;
+ onClick?: (e: React.MouseEvent) => void;
+ isHidden?: () => boolean;
+ backgroundColor?: string;
};
export interface OverlayWindowProps {
children: JSX.Element;
overlayOptions: OverlayElementOptions;
- onClick: () => void;
+ onClick: (e: React.MouseEvent) => void;
+ isHidden?: () => boolean;
+ backgroundColor?: string;
}
@observer
@@ -93,15 +99,17 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
render() {
return (
- <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
- <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown}>
+ <div
+ className="overlayWindow-outerDiv"
+ style={{ display: this.props.isHidden?.() ? 'none' : undefined, backgroundColor: this._props.backgroundColor, transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+ <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} style={{ backgroundColor: SnappingManager.userVariantColor, color: SnappingManager.userColor }}>
{this._props.overlayOptions.title || 'Untitled'}
<button type="button" onClick={this._props.onClick} className="overlayWindow-closeButton">
X
</button>
</div>
<div className="overlayWindow-content">{this.props.children}</div>
- <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown} />
+ <div className="overlayWindow-resizeDragger" style={{ backgroundColor: SnappingManager.userVariantColor }} onPointerDown={this.onResizerPointerDown} />
</div>
);
}
@@ -166,7 +174,7 @@ export class OverlayView extends ObservableReactComponent<object> {
if (index !== -1) this._elements.splice(index, 1);
});
const wincontents = (
- <OverlayWindow onClick={() => remove(wincontents)} key={Utils.GenerateGuid()} overlayOptions={options}>
+ <OverlayWindow isHidden={options.isHidden} backgroundColor={options.backgroundColor} onClick={options.onClick ?? (() => remove(wincontents))} key={Utils.GenerateGuid()} overlayOptions={options}>
{contents}
</OverlayWindow>
);
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 7e597879d..eb4e75f74 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -46,8 +46,6 @@ export class PreviewCursor extends ObservableReactComponent<object> {
this.Visible = false;
});
- // tests for URL and makes web document
- const re = /^https?:\/\//g;
const plain = e.clipboardData.getData('text/plain');
if (plain && newPoint) {
// tests for youtube and makes video document
@@ -62,7 +60,7 @@ export class PreviewCursor extends ObservableReactComponent<object> {
y: newPoint[1],
};
this._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, this._addDocument ?? returnFalse).then(batch.end);
- } else if (re.test(plain)) {
+ } else if ((/^https?:\/\//g).test(plain)) { // tests for URL and makes web document
const url = plain;
if (!url.startsWith(window.location.href)) {
undoable(
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 9c36e6d26..d05b0a6b6 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index b3fb8c930..db9ab831a 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -125,7 +125,7 @@ export namespace styleProviderQuiz {
try {
const hrefBase64 = await createCanvas(img);
const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
- AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc['x']), NumCast(img.layoutDoc['y']));
+ AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc.x), NumCast(img.layoutDoc.y));
} catch (error) {
console.log('Error', error);
}
@@ -265,7 +265,7 @@ export namespace styleProviderQuiz {
'. ' +
rubricText +
'. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
- const response = await gptAPICall(queryText, GPTCallType.QUIZ);
+ const response = await gptAPICall(queryText, GPTCallType.QUIZDOC);
const hexSent = extractHexAndSentences(response);
doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
doc.backgroundColor = '#' + hexSent.hexNumber;
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index a66a20cf6..b943259ff 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -22,6 +22,8 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
return ''; //
}
promoteCollection?: () => void; // moves contents of collection to parent
+ hasChildDocs?: () => Doc[];
+ docEditorView?: () => void;
updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away. see PresBox.restoreTargetDocView
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 43464e50c..d7f4251f3 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,22 +1,22 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CSS from 'csstype';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
-import * as CSS from 'csstype';
-import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Animation, DocData } from '../../../fields/DocSymbols';
+import { Animation } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { URLField } from '../../../fields/URLField';
-import { gptImageLabel } from '../../apis/gpt/GPT';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
+import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable, UndoManager } from '../../util/UndoManager';
@@ -25,11 +25,8 @@ import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
-import { CollectionSubView, docSortings, SubCollectionViewProps } from './CollectionSubView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../util/SettingsManager';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
/**
* New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -42,7 +39,6 @@ import { SettingsManager } from '../../util/SettingsManager';
export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [key: string]: IReactionDisposer } = {};
- private _textToDoc = new Map<string, Doc>();
private _oldWheel: HTMLElement | null = null;
private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@@ -74,22 +70,7 @@ export class CollectionCardView extends CollectionSubView() {
return Math.ceil(this.cardDeckWidth / this.cardWidth);
}
- /**
- * update's gpt's doc-text list and initializes callbacks
- */
- childPairStringListAndUpdateSortDesc = () =>
- this.childPairStringList().then(sortDesc => {
- GPTPopup.Instance.setSortDesc(sortDesc.join());
- GPTPopup.Instance.onSortComplete = this.processGptOutput;
- GPTPopup.Instance.onQuizRandom = this.quizMode;
- });
-
componentDidMount() {
- this._disposers.chatVis = reaction(
- () => GPTPopup.Instance.Visible,
- vis => !vis && this.onGptHide()
- );
- GPTPopup.Instance.setRegenerateCallback(this.Document, this.childPairStringListAndUpdateSortDesc);
this._props.setContentViewBox?.(this);
// if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
// when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the
@@ -110,12 +91,7 @@ export class CollectionCardView extends CollectionSubView() {
);
}
- onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove');
componentWillUnmount() {
- GPTPopup.Instance.setSortDesc('');
- GPTPopup.Instance.onSortComplete = undefined;
- GPTPopup.Instance.onQuizRandom = undefined;
- GPTPopup.Instance.setRegenerateCallback(undefined, null);
Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
this._dropDisposer?.();
}
@@ -130,7 +106,7 @@ export class CollectionCardView extends CollectionSubView() {
* Circle arc size, in radians, to layout cards
*/
@computed get archAngle() {
- return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+ return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.length / this._maxRowCount : 1);
}
/**
* Spacing card rows as a percent of Doc size. 100 means rows spread out to fill 100% of the Doc vertically. Default is 60%
@@ -142,7 +118,7 @@ export class CollectionCardView extends CollectionSubView() {
/**
* The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's)
*/
- @computed get childCards() {
+ @computed get childDocsNoInk() {
return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
}
@@ -150,7 +126,7 @@ export class CollectionCardView extends CollectionSubView() {
* how much to scale down the contents of the view so that everything will fit
*/
@computed get fitContentScale() {
- const length = Math.min(this.childCards.length, this._maxRowCount);
+ const length = Math.min(this.childDocsNoInk.length, this._maxRowCount);
return (this.childPanelWidth() * length) / this._props.PanelWidth();
}
@@ -166,19 +142,12 @@ export class CollectionCardView extends CollectionSubView() {
return this._props.PanelWidth() - 2 * this.xMargin;
}
- /**
- * When in quiz mode, randomly selects a document
- */
- quizMode = () => {
- this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)];
- };
-
setHoveredNodeIndex = action((index: number) => {
if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
});
isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
- childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
+ childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.length) / this.nativeScaling));
childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
@@ -322,10 +291,10 @@ export class CollectionCardView extends CollectionSubView() {
* @returns number of cards in row that contains index
*/
cardsInRowThatIncludesCardIndex = (index: number) => {
- if (this.childCards.length < this._maxRowCount) {
- return this.childCards.length;
+ if (this.childDocsNoInk.length < this._maxRowCount) {
+ return this.childDocsNoInk.length;
}
- const totalCards = this.childCards.length;
+ const totalCards = this.childDocsNoInk.length;
if (index < totalCards - (totalCards % this._maxRowCount)) {
return this._maxRowCount;
}
@@ -389,103 +358,6 @@ export class CollectionCardView extends CollectionSubView() {
: this.translateY(index);
};
- /**
- * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
- * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
- * inputted into the gpt prompt to sort everything together
- * @returns
- */
- childPairStringList = () => {
- const docToText = (doc: Doc) => {
- switch (doc.type) {
- case DocumentType.PDF: return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
- case DocumentType.IMG: return this.getImageDesc(doc);
- case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text);
- default: return StrCast(doc.title);
- } // prettier-ignore
- };
- const docTextPromises = this.childCards
- .map(pair => pair.layout)
- .map(async doc => {
- const docText = (await docToText(doc)) ?? '';
- doc.gptInputText = docText;
- this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
- return `======${docText.replace(/\n/g, ' ').trim()}======`;
- });
- return Promise.all<string>(docTextPromises);
- };
-
- /**
- * Calls the gpt API to generate descriptions for the images in the view
- * @param image
- * @returns
- */
- getImageDesc = async (image: Doc) => {
- if (StrCast(image.description)) return StrCast(image.description); // Return existing description
- const { href } = (image.data as URLField).url;
- const hrefParts = href.split('.');
- const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
- try {
- const hrefBase64 = await imageUrlToBase64(hrefComplete);
- const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
- image[DocData].description = response.trim();
- return response; // Return the response from gptImageLabel
- } catch (error) {
- console.log(error);
- }
- return '';
- };
-
- /**
- * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
- * usable code
- * @param gptOutput
- * @param questionType
- * @param tag
- */
- processGptOutput = (gptOutput: string, questionType: string, tag?: string) =>
- undoable(() => {
- // Split the string into individual list items
- const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
- if (questionType === '2' || questionType === '4') {
- this.childDocs.forEach(d => {
- TagItem.removeTagFromDoc(d, '#chat');
- });
- }
-
- if (questionType === '6') {
- this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
- }
-
- listItems.forEach((item, index) => {
- const normalizedItem = item.trim();
- // find the corresponding Doc in the textToDoc map
- const doc = this._textToDoc.get(normalizedItem);
- if (doc) {
- switch (questionType) {
- case '6':
- doc.chatIndex = index;
- break;
- case '1':
- if (tag) {
- const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
- const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
- TagItem.addTagToDoc(doc, filterTag);
- }
- break;
- case '2':
- case '4':
- TagItem.addTagToDoc(doc, '#chat');
- Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
- break;
- }
- } else {
- console.warn(`No matching document found for item: ${normalizedItem}`);
- }
- });
- }, '')();
-
childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
// need to explicitly trigger an invalidation since we're reading everything from the Dom
this._forceChildXf;
@@ -625,7 +497,7 @@ export class CollectionCardView extends CollectionSubView() {
curDoc = () => DocCast(this.layoutDoc._card_curDoc);
render() {
- const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
+ const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale;
return (
<div
className="collectionCardView-outer"
@@ -657,7 +529,7 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="collectionCardView-flashcardUI"
style={{
- pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+ pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none',
height: `${100 / this.nativeScaling / fitContentScale}%`,
width: `${100 / this.nativeScaling / fitContentScale}%`,
transform: `scale(${this.nativeScaling * fitContentScale})`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5e99bec39..b40cd2761 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -37,6 +37,9 @@ export enum docSortings {
Tag = 'tag',
None = '',
}
+
+export const ChatSortField = 'chat_sortIndex';
+
export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -121,6 +124,7 @@ export function CollectionSubView<X>() {
return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout. hopefully nothing depending on the previous code.
}
+ hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
@computed get childLayoutPairs(): { layout: Doc; data: Doc }[] {
const { Document, TemplateDataDocument } = this._props;
const validPairs = this.childDocs
@@ -228,23 +232,21 @@ export function CollectionSubView<X>() {
childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
- const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+ const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']);
const docs = docsIn.slice();
- if (sortType) {
- docs.sort((docA, docB) => {
- const [typeA, typeB] = (() => {
- switch (sortType) {
- default:
- case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)];
- case docSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)];
- case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
- case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
- case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")];
- }
- })(); //prettier-ignore
- return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
- });
- }
+ sortType && docs.sort((docA, docB) => {
+ const [typeA, typeB] = (() => {
+ switch (sortType) {
+ default:
+ case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)];
+ case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999), NumCast(docB[ChatSortField], 9999)];
+ case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
+ case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+ case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")];
+ }
+ })();
+ return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1);
+ }); //prettier-ignore
if (dragIndex !== -1) {
const draggedDoc = DragManager.docsBeingDragged[0];
const originalIndex = docs.findIndex(doc => doc === draggedDoc);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6f0833a22..a4900e9d7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -89,6 +89,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
TraceMobx();
if (type === undefined) return null;
switch (type) {
+ default:
+ case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />;
case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />;
case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
@@ -105,8 +107,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />;
case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />;
case CollectionViewType.Card: return <CollectionCardView key="collview" {...props} />;
- case CollectionViewType.Freeform:
- default: return <CollectionFreeFormView key="collview" {...props} />;
}
};
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 5524fedb3..9cfb0416c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -461,6 +461,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
const newColDim = 900;
for (const label of labelGroups) {
const newCollection = MarqueeView.getCollection([], undefined, false, this.Bounds);
+ newCollection[DocData].title = label + ' Collection';
newCollection._x = this.Bounds.left + x_offset;
newCollection._y = this.Bounds.top + y_offset;
newCollection._width = newColDim;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index b44292164..79873ed8f 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -152,7 +152,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function showFreeform(
- attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'toggle-chat' | 'toggle-tags' | 'tag',
+ attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse' | 'toggle-chat' | 'toggle-tags' | 'tag',
checkResult?: boolean,
persist?: boolean
) {
@@ -163,7 +163,7 @@ ScriptingGlobals.add(function showFreeform(
}
// prettier-ignore
- const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down'| 'toggle-chat' | 'toggle-tags' | 'tag',
+ const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse'| 'toggle-chat' | 'toggle-tags' | 'tag',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
@@ -214,29 +214,20 @@ ScriptingGlobals.add(function showFreeform(
checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
}],
- ['up', {
- checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
- setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
- }],
- ['down', {
- checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
- setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
+ ['reverse', {
+ checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_reverse"]),
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"] = !doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"]; },
}],
['toggle-chat', {
- checkResult: (doc: Doc) => GPTPopup.Instance.Visible,
+ checkResult: (doc: Doc) => SnappingManager.ChatVisible,
setDoc: (doc: Doc, dv: DocumentView) => {
- if (GPTPopup.Instance.Visible){
+ if (SnappingManager.ChatVisible){
doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
- GPTPopup.Instance.setVisible(false);
-
+ SnappingManager.SetChatVisible(false);
} else {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.CARD);
- GPTPopup.Instance.setCardsDoneLoading(true);
-
+ SnappingManager.SetChatVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.GPT_MENU);
}
-
-
},
}],
['toggle-tags', {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index cb0831d3c..5315612e1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -291,7 +291,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this.askGPTPhonemes(this._inputValue);
this._renderSide = this.backKey;
this._outputValue = '';
- } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+ } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC);
};
onPointerMove = ({ movementX }: PointerEvent) => {
@@ -511,7 +511,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
*/
askGPT = async (callType: GPTCallType) => {
const questionText = this.frontText;
- const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
+ const queryText = questionText + (callType == GPTCallType.QUIZDOC ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
this.loading = true;
const res = !this.frontText
@@ -522,7 +522,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
case GPTCallType.CHATCARD:
DocCast(this.dataDoc[this.backKey])[DocData].text = resp;
break;
- case GPTCallType.QUIZ:
+ case GPTCallType.QUIZDOC:
this._renderSide = this.backKey;
this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
break;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index b874d077b..fa3ab73a7 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -489,7 +489,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
// Changing which document to add the annotation to (the currently selected PDF)
- GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
};
@@ -523,7 +523,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
askGPT = action(async () => {
- GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
GPTPopup.Instance.setDataJson('');
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index fe4f0b1a2..59e093683 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,7 +40,7 @@
max-height: 100%;
pointer-events: inherit;
background: transparent;
- z-index: -10000;
+ // z-index: -10000; // bcz: not sure why this was here. it broke dropping images on the image box alternate bullseye icon.
img {
height: auto;
@@ -129,7 +129,12 @@
right: 0;
bottom: 0;
z-index: 2;
+ transform-origin: bottom right;
cursor: default;
+ > svg {
+ width: 100%;
+ height: 100%;
+ }
}
.imageBox-fader img {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index caefbf542..f76e10a0e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,11 +1,12 @@
+import { Button, Colors, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Slider, Tooltip } from '@mui/material';
import axios from 'axios';
-import { Colors, Button, Type, Size } from '@dash/components';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
import ReactLoading from 'react-loading';
import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
@@ -16,12 +17,14 @@ import { ObjectField } from '../../../fields/ObjectField';
import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
+import { Upload } from '../../../server/SharedMediaTypes';
import { emptyFunction } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { undoable, undoBatch } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -32,6 +35,9 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { OverlayView } from '../OverlayView';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { PinDocView, PinProps } from '../PinFuncs';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
+import { FireflyImageData } from '../smartdraw/FireflyConstants';
+import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import { StickerPalette } from '../smartdraw/StickerPalette';
import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
@@ -39,12 +45,6 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
-import { Upload } from '../../../server/SharedMediaTypes';
-import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
-import { SettingsManager } from '../../util/SettingsManager';
-import { AiOutlineSend } from 'react-icons/ai';
-import { FireflyImageData } from '../smartdraw/FireflyConstants';
-import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
export class ImageEditorData {
// eslint-disable-next-line no-use-before-define
@@ -83,7 +83,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private _disposers: { [name: string]: IReactionDisposer } = {};
private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
private _overlayIconRef = React.createRef<HTMLDivElement>();
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _mainCont: HTMLDivElement | null = null;
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
imageRef: HTMLImageElement | null = null; // <video> ref
marqueeref = React.createRef<MarqueeAnnotator>();
@@ -108,6 +108,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
protected createDropTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
};
@@ -147,7 +148,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._disposers.path = reaction(
() => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
({ nativeSize, width }) => {
- if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
+ if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) {
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
@@ -157,8 +158,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
() => this.layoutDoc.layout_scrollTop,
sTop => {
this._forcedScroll = true;
- !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(sTop));
- this._mainCont.current?.scrollTo({ top: NumCast(sTop) });
+ !this._ignoreScroll && this._mainCont && (this._mainCont.scrollTop = NumCast(sTop));
+ this._mainCont?.scrollTo({ top: NumCast(sTop) });
this._forcedScroll = false;
},
{ fireImmediately: true }
@@ -315,6 +316,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return cropping;
};
+ docEditorView = action(() => {
+ const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+ if (field) {
+ ImageEditorData.Open = true;
+ ImageEditorData.Source = this.choosePath(field.url);
+ ImageEditorData.AddDoc = this._props.addDocument;
+ ImageEditorData.RootDoc = this.Document;
+ }
+ });
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -352,16 +363,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
icon: 'expand-arrows-alt',
});
funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
- funcs.push({
- description: 'Open Image Editor',
- event: action(() => {
- ImageEditorData.Open = true;
- ImageEditorData.Source = this.choosePath(field.url);
- ImageEditorData.AddDoc = this._props.addDocument;
- ImageEditorData.RootDoc = this.Document;
- }),
- icon: 'pencil-alt',
- });
+ funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' });
this.layoutDoc.ai &&
funcs.push({
description: 'Regenerate AI Image',
@@ -381,7 +383,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// updateIcon = () => new Promise<void>(res => res());
updateIcon = (usePanelDimensions?: boolean) => {
- const contentDiv = this._mainCont.current;
+ const contentDiv = this._mainCont;
return !contentDiv
? new Promise<void>(res => res())
: UpdateIcon(
@@ -423,6 +425,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
+ private _sideBtnWidth = 35;
+ /**
+ * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+ */
+ @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore
+ /**
+ * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+ */
+ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore
+ /**
+ * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+ */
+ @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore
+
@computed get overlayImageIcon() {
const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
return (
@@ -451,9 +467,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
})
}
style={{
- display: (this._props.isContentActive() !== false && SnappingManager.CanEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none',
- width: 'min(10%, 25px)',
- height: 'min(10%, 25px)',
+ display: this._props.isContentActive() && (SnappingManager.CanEmbed || this.dataDoc[this.fieldKey + '_alternates']) ? 'block' : 'none',
+ transform: `scale(${this.uiBtnScaling})`,
+ width: this._sideBtnWidth,
+ height: this._sideBtnWidth,
background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
color: usePath === undefined ? 'black' : 'white',
}}>
@@ -510,7 +527,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._isHovering = false;
})}
key={this.layoutDoc[Id]}
- ref={this.createDropTarget}
onPointerDown={this.marqueeDown}>
<div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img
@@ -531,7 +547,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
)}
</div>
- {this.overlayImageIcon}
</div>
);
}
@@ -739,12 +754,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div
className="imageBox"
onContextMenu={this.specificContextMenu}
- ref={this._mainCont}
+ ref={this.createDropTarget}
onScroll={action(() => {
if (!this._forcedScroll) {
- if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop;
+ this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
this._ignoreScroll = false;
}
}
@@ -786,8 +801,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<ReactLoading type="spin" height={50} width={50} color={'blue'} />
</div>
) : null}
+ {this.overlayImageIcon}
{this.annotationLayer}
- {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
+ {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
<MarqueeAnnotator
Document={this.Document}
ref={this.marqueeref}
@@ -802,7 +818,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
savedAnnotations={this.savedAnnotations}
selectionText={returnEmptyString}
annotationLayer={this._annotationLayer.current}
- marqueeContainer={this._mainCont.current}
+ marqueeContainer={this._mainCont}
highlightDragSrcColor=""
anchorMenuCrop={this.crop}
// anchorMenuFlashcard={() => this.getImageDesc()}
@@ -839,5 +855,5 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
Docs.Prototypes.TemplateMap.set(DocumentType.IMG, {
layout: { view: ImageBox, dataField: 'data' },
- options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' },
+ options: { acl: '', freeform: '', _layout_nativeDimEditable: true, systemIcon: 'BsFileEarmarkImageFill' },
});
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 6026d9ca7..e7a10cc29 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -383,7 +383,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined);
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 ? this._urlHash + '_' : ''}sidebar`);
+ GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
} else {
@@ -446,7 +446,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange);
(!sel.isCollapsed || this.marqueeing) && AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
// Changing which document to add the annotation to (the currently selected WebBox)
- GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
+ GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
};
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index d38cb5423..009eb82cd 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -1,4 +1,4 @@
-import { Calendar, EventClickArg, EventSourceInput } from '@fullcalendar/core';
+import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import multiMonthPlugin from '@fullcalendar/multimonth';
import timeGrid from '@fullcalendar/timegrid';
@@ -17,6 +17,7 @@ import { DocumentView } from '../DocumentView';
import { OpenWhere } from '../OpenWhere';
import { DragManager } from '../../../util/DragManager';
import { DocData } from '../../../../fields/DocSymbols';
+import { ContextMenu } from '../../ContextMenu';
type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay';
@@ -104,32 +105,44 @@ export class CalendarBox extends CollectionSubView() {
}
// TODO: Return a different color based on the event type
- eventToColor(event: Doc): string {
+ eventToColor = (event: Doc): string => {
return 'red';
- }
+ };
- internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) {
+ internalDocDrop = (e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) => {
if (!super.onInternalDrop(e, de)) return false;
de.complete.docDragData?.droppedDocuments.forEach(doc => {
const today = new Date().toISOString();
if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`;
});
return true;
- }
+ };
onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => {
if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
return false;
};
+ handleEventDrop = (arg: EventDropArg) => {
+ const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
+ doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString());
+ };
+
handleEventClick = (arg: EventClickArg) => {
const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
- DocumentView.DeselectAll();
if (doc) {
DocumentView.showDocument(doc, { openLocation: OpenWhere.lightboxAlways });
arg.jsEvent.stopPropagation();
}
};
+ handleEventContextMenu = (pageX: number, pageY: number, docid: string) => {
+ const doc = DocServer.GetCachedRefField(docid ?? '');
+ if (doc) {
+ const cm = ContextMenu.Instance;
+ cm.addItem({ description: 'Show Metadata', event: () => this._props.addDocTab(doc, OpenWhere.addRightKeyvalue), icon: 'table-columns' });
+ cm.displayMenu(pageX - 15, pageY - 15, undefined, undefined);
+ }
+ };
// https://fullcalendar.io
renderCalendar = () => {
@@ -157,6 +170,25 @@ export class CalendarBox extends CollectionSubView() {
aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height),
events: this.calendarEvents,
eventClick: this.handleEventClick,
+ eventDrop: this.handleEventDrop,
+ eventDidMount: arg => {
+ arg.el.addEventListener('pointerdown', ev => {
+ ev.button && ev.stopPropagation();
+ });
+ if (navigator.userAgent.includes('Macintosh')) {
+ arg.el.addEventListener('pointerup', ev => {
+ ev.button && ev.stopPropagation();
+ ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
+ });
+ }
+ arg.el.addEventListener('contextmenu', ev => {
+ if (!navigator.userAgent.includes('Macintosh')) {
+ this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
+ }
+ ev.stopPropagation();
+ ev.preventDefault();
+ });
+ },
}));
cal?.render();
setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end));
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 19fd6ae36..e93fb87db 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -22,6 +22,7 @@ import { ChatCompletionMessageParam } from 'openai/resources';
import { Doc } from '../../../../../fields/Doc';
import { parsedDoc } from '../chatboxcomponents/ChatBox';
import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
import { RAGTool } from '../tools/RAGTool';
//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
@@ -62,7 +63,7 @@ export class Agent {
history: () => string,
csvData: () => { filename: string; id: string; text: string }[],
addLinkedUrlDoc: (url: string, id: string) => void,
- createImage: (result: any, options: DocumentOptions) => void,
+ createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void,
addLinkedDoc: (doc: parsedDoc) => Doc | undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
createCSVInDash: (url: string, title: string, id: string, data: string) => void
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index f8fe531ab..6e9307d37 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -42,6 +42,7 @@ import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { ProgressBar } from './ProgressBar';
import { OpenWhere } from '../../OpenWhere';
+import { Upload } from '../../../../../server/SharedMediaTypes';
dotenv.config();
@@ -412,7 +413,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
@action
- createImageInDash = async (result: any, options: DocumentOptions) => {
+ createImageInDash = async (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => {
const newImgSrc =
result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
? ClientUtils.prepend(result.accessPaths.agnostic.client)
@@ -1046,5 +1047,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, {
layout: { view: ChatBox, dataField: 'data' },
- options: { acl: '', chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
+ options: { acl: '', _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
});
diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
index 6dc36b0d1..284879a4a 100644
--- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
@@ -263,79 +263,79 @@ const standardOptions = ['title', 'backgroundColor'];
* Description of document options and data field for each type.
*/
const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = {
- [supportedDocTypes.comparison]: {
+ comparison: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of two documents of any kind that can be compared.',
},
- [supportedDocTypes.deck]: {
+ deck: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of flashcard docs',
},
- [supportedDocTypes.flashcard]: {
+ flashcard: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer',
},
- [supportedDocTypes.text]: {
+ text: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'The text content of the document.',
},
- [supportedDocTypes.web]: {
+ web: {
options: [],
dataDescription: 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University',
},
- [supportedDocTypes.html]: {
+ html: {
options: [],
dataDescription: 'The HTML-formatted text content of the document.',
},
- [supportedDocTypes.equation]: {
+ equation: {
options: [...standardOptions, 'fontColor'],
dataDescription: 'The equation content represented as a MathML string.',
},
- [supportedDocTypes.functionplot]: {
+ functionplot: {
options: [...standardOptions, 'function_definition'],
dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.',
},
- [supportedDocTypes.dataviz]: {
+ dataviz: {
options: [...standardOptions, 'chartType'],
dataDescription: 'A string of comma-separated values representing the CSV data.',
},
- [supportedDocTypes.notetaking]: {
+ notetaking: {
options: standardOptions,
dataDescription: 'An array of related text documents with small amounts of text.',
},
- [supportedDocTypes.rtf]: {
+ rtf: {
options: standardOptions,
dataDescription: 'The rich text content in RTF format.',
},
- [supportedDocTypes.image]: {
+ image: {
options: standardOptions,
dataDescription: `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`,
},
- [supportedDocTypes.pdf]: {
+ pdf: {
options: standardOptions,
dataDescription: 'the pdf content as a PDF file url.',
},
- [supportedDocTypes.audio]: {
+ audio: {
options: standardOptions,
dataDescription: 'The audio content as a file url.',
},
- [supportedDocTypes.video]: {
+ video: {
options: standardOptions,
dataDescription: 'The video content as a file url.',
},
- [supportedDocTypes.message]: {
+ message: {
options: standardOptions,
dataDescription: 'The message content of the document.',
},
- [supportedDocTypes.diagram]: {
+ diagram: {
options: standardOptions,
dataDescription: 'diagram content as a text string in Mermaid format.',
},
- [supportedDocTypes.script]: {
+ script: {
options: standardOptions,
dataDescription: 'The compilable JavaScript code. Use this for creating scripts.',
},
- [supportedDocTypes.collection]: {
+ collection: {
options: [...standardOptions, 'type_collection'],
dataDescription: 'A collection of Docs represented as an array.',
},
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index 177552c5c..dc6140871 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -1,10 +1,10 @@
-import { v4 as uuidv4 } from 'uuid';
import { RTFCast } from '../../../../../fields/Types';
import { DocumentOptions } from '../../../../documents/Documents';
import { Networking } from '../../../../Network';
import { ParametersType, ToolInfo } from '../types/tool_types';
import { Observation } from '../types/types';
import { BaseTool } from './BaseTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
const imageCreationToolParams = [
{
@@ -25,8 +25,8 @@ const imageCreationToolInfo: ToolInfo<ImageCreationToolParamsType> = {
};
export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
- private _createImage: (result: any, options: DocumentOptions) => void;
- constructor(createImage: (result: any, options: DocumentOptions) => void) {
+ private _createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void;
+ constructor(createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void) {
super(imageCreationToolInfo);
this._createImage = createImage;
}
@@ -42,23 +42,19 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
});
console.log('Image generation result:', result);
this._createImage(result, { text: RTFCast(image_prompt) });
- if (url) {
- const id = uuidv4();
-
- return [
- {
- type: 'image_url',
- image_url: { url },
- },
- ];
- } else {
- return [
- {
- type: 'text',
- text: `An error occurred while generating image.`,
- },
- ];
- }
+ return url
+ ? [
+ {
+ type: 'image_url',
+ image_url: { url },
+ },
+ ]
+ : [
+ {
+ type: 'text',
+ text: `An error occurred while generating image.`,
+ },
+ ];
} catch (error) {
console.log(error);
return [
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index eb1f9d07b..3abb39ff2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -135,7 +135,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
/**
* ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database.
- * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to
+ * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to
* the prototype or other external edits
*/
public ApplyingChange: string = '';
@@ -977,7 +977,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
},
icon: 'star',
});
- optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
+ optionItems.push({ description: `Generate Dall-E Image`, event: this.generateImage, icon: 'star' });
// optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' });
optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' });
this._props.renderDepth &&
@@ -1043,7 +1043,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
askGPT = action(async () => {
try {
- GPTPopup.Instance.setSidebarId(this.sidebarKey);
+ GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
@@ -1061,12 +1061,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
- generateImage = async () => {
+ generateImage = () => {
GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
- GPTPopup.Instance?.setImgTargetDoc(this.Document);
- GPTPopup.Instance.addToCollection = this._props.addDocument;
- GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text);
- GPTPopup.Instance.generateImage();
+ GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument);
};
breakupDictation = () => {
@@ -1660,7 +1657,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
onSelectEnd = () => {
- GPTPopup.Instance.setSidebarId(this.sidebarKey);
+ GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
document.removeEventListener('pointerup', this.onSelectEnd);
};
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 6b1d05031..3c0ab3da5 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -90,18 +90,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
* @param type The new tool type we are changing to
*/
const changeTool = (type: ImageToolType) => {
- switch (type) {
- case ImageToolType.GenerativeFill:
- setCurrTool(genFillTool);
- setCursorData(prev => ({ ...prev, width: genFillTool.sliderDefault as number }));
- break;
- case ImageToolType.Cut:
- setCurrTool(cutTool);
- setCursorData(prev => ({ ...prev, width: cutTool.sliderDefault as number }));
- break;
- default:
- break;
- }
+ setCurrToolType(type);
+ setCursorData(prev => ({ ...prev, width: currTool().sliderDefault as number }));
};
// Undo and Redo
const handleUndo = () => {
@@ -171,9 +161,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
// handles brushing on pointer movement
useEffect(() => {
- if (!isBrushing) return undefined;
const canvas = canvasRef.current;
- if (!canvas) return undefined;
+ if (!isBrushing || !canvas) return undefined;
const ctx = ImageUtility.getCanvasContext(canvasRef);
if (!ctx) return undefined;
@@ -188,33 +177,29 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
};
drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
- return () => {
- drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
- };
+ return () => drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
}, [isBrushing]);
// first load
useEffect(() => {
- const loadInitial = async () => {
- if (!imageEditorSource || imageEditorSource === '') return;
- const img = new Image();
- const res = await ImageUtility.urlToBase64(imageEditorSource);
- if (!res) return;
- img.src = `data:image/png;base64,${res}`;
-
- img.onload = () => {
- currImg.current = img;
- originalImg.current = img;
- const imgWidth = img.naturalWidth;
- const imgHeight = img.naturalHeight;
- const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
- const width = imgWidth * scale;
- const height = imgHeight * scale;
- setCanvasDims({ width, height });
- };
- };
-
- loadInitial();
+ if (imageEditorSource && imageEditorSource) {
+ ImageUtility.urlToBase64(imageEditorSource).then(res => {
+ if (res) {
+ const img = new Image();
+ img.src = `data:image/png;base64,${res}`;
+ img.onload = () => {
+ currImg.current = img;
+ originalImg.current = img;
+ const imgWidth = img.naturalWidth;
+ const imgHeight = img.naturalHeight;
+ const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
+ const width = imgWidth * scale;
+ const height = imgHeight * scale;
+ setCanvasDims({ width, height });
+ };
+ }
+ });
+ }
// cleanup
return () => {
@@ -300,7 +285,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
if (!canvasMask) return;
const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
- const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+ const res = await ImageUtility.getEdit(imgBlob, maskBlob, input || 'Fill in the image in the same style', 2);
// create first image
if (!newCollectionRef.current) {
@@ -569,11 +554,15 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
setIsFirstDoc(true);
};
+ function currTool() {
+ return imageEditTools.find(tool => tool.type === currToolType) ?? genFillTool;
+ }
+
// defines the tools and sets current tool
- const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, name: 'Generative Fill', btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 };
- const cutTool: ImageEditTool = { type: ImageToolType.Cut, name: 'Cut', btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 };
+ const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 };
+ const cutTool: ImageEditTool = { type: ImageToolType.Cut, btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 };
const imageEditTools: ImageEditTool[] = [genFillTool, cutTool];
- const [currTool, setCurrTool] = useState<ImageEditTool>(genFillTool);
+ const [currToolType, setCurrToolType] = useState<ImageToolType>(ImageToolType.GenerativeFill);
// the top controls for making a new collection, resetting, and applying edits,
function renderControls() {
@@ -595,7 +584,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
labelPlacement="end"
sx={{ whiteSpace: 'nowrap' }}
/>
- <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+ <ApplyFuncButtons onClick={() => currTool().applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool().btnText} />
<IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
</div>
</div>
@@ -607,8 +596,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
return (
<div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
<div className="sideControls">
- <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div>
- {currTool.type == ImageToolType.Cut && (
+ <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool().type, changeTool))}</div>
+ {currTool().type == ImageToolType.Cut && (
<div className="cutToolsContainer">
<Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
<Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
@@ -617,7 +606,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
</div>
)}
<div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
- {currTool.type === ImageToolType.GenerativeFill && (
+ {currTool().type === ImageToolType.GenerativeFill && (
<Slider
sx={{
'& input[type="range"]': {
@@ -633,7 +622,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
/>
)}
- {currTool.type === ImageToolType.Cut && (
+ {currTool().type === ImageToolType.Cut && (
<Slider
sx={{
'& input[type="range"]': {
@@ -780,7 +769,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
{renderSideIcons()}
{renderEditThumbnails()}
</div>
- {currTool.type === ImageToolType.GenerativeFill && renderPromptBox()}
+ {currTool().type === ImageToolType.GenerativeFill && renderPromptBox()}
</div>
);
};
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index 985dc914f..3eaa251f2 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -53,10 +53,10 @@ export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }
export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) {
return (
- <div key={tool.name} className="imageEditorButtonContainer">
+ <div key={tool.type} className="imageEditorButtonContainer">
<Button
style={{ width: '100%' }}
- text={tool.name}
+ text={tool.type}
type={Type.TERT}
color={isActive ? SettingsManager.userVariantColor : bgColor}
icon={<FontAwesomeIcon icon={tool.icon} />}
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
index ece0f4d7f..1c6a38a24 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
@@ -87,7 +87,6 @@ export class ImageUtility {
body: fd,
});
const data = await res.json();
- console.log(data.data);
return {
status: 'success',
urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`),
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index a14b55439..02dbc0312 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -13,8 +13,8 @@ export interface Point {
}
export enum ImageToolType {
- GenerativeFill = 'genFill',
- Cut = 'cut',
+ GenerativeFill = 'Generative Fill',
+ Cut = 'Cut',
}
export enum CutMode {
@@ -26,7 +26,6 @@ export enum CutMode {
export interface ImageEditTool {
type: ImageToolType;
- name: string;
btnText: string;
icon: IconProp;
// this is the function that the image tool applies, so it can be defined depending on the tool
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 11f2f7988..f7070c780 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,5 +1,5 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from '@dash/components';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -9,16 +9,15 @@ import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtil
import { emptyFunction, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { SettingsManager } from '../../util/SettingsManager';
import { undoBatch } from '../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
+import { ComparisonBox } from '../nodes/ComparisonBox';
import { DocumentView } from '../nodes/DocumentView';
import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import './AnchorMenu.scss';
-import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import { ComparisonBox } from '../nodes/ComparisonBox';
+import { GPTPopup } from './GPTPopup/GPTPopup';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -98,19 +97,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* Invokes the API with the selected text and stores it in the summarized text.
* @param e pointer down event
*/
- gptSummarize = async () => {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
- GPTPopup.Instance.setLoading(true);
-
- try {
- const res = await gptAPICall(this._selectedText, GPTCallType.SUMMARY);
- GPTPopup.Instance.setText(res || 'Something went wrong.');
- } catch (err) {
- console.error(err);
- }
- GPTPopup.Instance.setLoading(false);
- };
+ gptSummarize = () => GPTPopup.Instance.generateSummary(this._selectedText);
/*
* Transfers the flashcard text generated by GPT on flashcards and creates a collection out them.
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 0247dc10c..c8903e09f 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -4,19 +4,23 @@ $greyborder: #d3d3d3;
$lightgrey: #ececec;
$button: #5b97ff;
$highlightedText: #82e0ff;
+$inputHeight: 60px;
+$headingHeight: 32px;
-.summary-box {
+.gptPopup-summary-box {
position: fixed;
top: 115px;
left: 75px;
- width: 250px;
- height: 200px;
- min-height: 200px;
- min-width: 180px;
-
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ border-top: solid gray 20px;
border-radius: 16px;
padding: 16px;
padding-bottom: 0;
+ padding-top: 0px;
z-index: 999;
display: flex;
flex-direction: column;
@@ -24,25 +28,20 @@ $highlightedText: #82e0ff;
background-color: #ffffff;
box-shadow: 0 2px 5px #7474748d;
color: $textgrey;
- resize: both; /* Allows resizing */
- overflow: auto;
-
- .resize-handle {
- width: 10px;
- height: 10px;
- background: #ccc;
- position: absolute;
- right: 0;
- bottom: 0;
- cursor: se-resize;
- }
+
+ .gptPopup-sortBox {
+ display: flex;
+ flex-direction: column;
+ height: calc(100% - $inputHeight - $headingHeight);
+ pointer-events: all;
+ }
.summary-heading {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid $greyborder;
- padding-bottom: 5px;
+ height: $headingHeight;
.summary-text {
font-size: 12px;
@@ -63,95 +62,77 @@ $highlightedText: #82e0ff;
cursor: pointer;
}
- .content-wrapper {
+ .gptPopup-content-wrapper {
padding-top: 10px;
min-height: 50px;
- // max-height: 150px;
- overflow-y: auto;
- height: 100%
+ height: calc(100% - 32px);
}
- .btns-wrapper-gpt {
- height: 100%;
+ .inputWrapper {
display: flex;
justify-content: center;
align-items: center;
- flex-direction: column;
+ height: $inputHeight;
+ background-color: white;
+ width: 100%;
+ pointer-events: all;
- .inputWrapper{
- display: flex;
- justify-content: center;
- align-items: center;
- height: 60px;
- position: absolute;
- bottom: 0;
- width: 100%;
- background-color: white;
-
-
- }
-
- .searchBox-input{
+ .searchBox-input {
height: 40px;
border-radius: 10px;
- position: absolute;
- bottom: 10px;
+ position: relative;
border-color: #5b97ff;
- width: 90%
+ width: 90%;
}
+ }
+ .btns-wrapper-gpt {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
.chat-wrapper {
display: flex;
flex-direction: column;
width: 100%;
- max-height: calc(100vh - 80px);
+ height: 100%;
overflow-y: auto;
- padding-bottom: 60px;
+ padding-right: 5px;
}
-
+
.chat-bubbles {
margin-top: 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
-
+
.chat-bubble {
padding: 10px;
margin-bottom: 10px;
border-radius: 10px;
max-width: 60%;
}
-
+
.user-message {
background-color: #283d53;
align-self: flex-end;
color: whitesmoke;
}
-
+
.chat-message {
background-color: #367ae7;
align-self: flex-start;
- color:whitesmoke;
+ color: whitesmoke;
}
-
-
-
.summarizing {
display: flex;
align-items: center;
}
-
-
-
-
-
-
}
-
-
.text-btn {
&:hover {
background-color: $button;
@@ -198,22 +179,16 @@ $highlightedText: #82e0ff;
color: #666;
}
-
-
-
-
@keyframes spin {
to {
transform: rotate(360deg);
}
}
-
-
.image-content-wrapper {
display: flex;
flex-direction: column;
- align-items: flex-start;
+ align-items: center;
gap: 8px;
padding-bottom: 16px;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index f5a9f9e6a..2cf39bec4 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,340 +1,300 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
+import { CgCornerUpLeft } from 'react-icons/cg';
import ReactLoading from 'react-loading';
import { TypeAnimation } from 'react-type-animation';
import { ClientUtils } from '../../../../ClientUtils';
import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
-import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
import { DocUtils } from '../../../documents/DocUtils';
import { Docs } from '../../../documents/Documents';
import { SettingsManager } from '../../../util/SettingsManager';
import { SnappingManager } from '../../../util/SnappingManager';
+import { undoable } from '../../../util/UndoManager';
import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { TagItem } from '../../TagsView';
+import { ChatSortField, docSortings } from '../../collections/CollectionSubView';
import { DocumentView } from '../../nodes/DocumentView';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
+import { DictationButton } from '../../DictationButton';
+import { AiOutlineSend } from 'react-icons/ai';
export enum GPTPopupMode {
- SUMMARY,
- EDIT,
- IMAGE,
- FLASHCARD,
+ SUMMARY, // summary of seleted document text
+ IMAGE, // generate image from image description
DATA,
- CARD,
- SORT,
- QUIZ,
-}
-
-export enum GPTQuizType {
- CURRENT = 0,
- CHOOSE = 1,
- MULTIPLE = 2,
+ GPT_MENU, // menu for choosing type of prompts user will provide
+ USER_PROMPT, // user prompts for sorting,filtering and asking about docs
+ QUIZ_RESPONSE, // user definitions or explanations to be evaluated by GPT
}
@observer
export class GPTPopup extends ObservableReactComponent<object> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
- private messagesEndRef: React.RefObject<HTMLDivElement>;
-
- @observable private chatMode: boolean = false;
- private correlatedColumns: string[] = [];
+ static ChatTag = '#chat'; // tag used by GPT popup to filter docs
+ private _askDictation: DictationButton | null = null;
+ private _messagesEndRef: React.RefObject<HTMLDivElement>;
+ private _correlatedColumns: string[] = [];
+ private _dataChatPrompt: string | undefined = undefined;
+ private _imgTargetDoc: Doc | undefined;
+ private _textAnchor: Doc | undefined;
+ private _dataJson: string = '';
+ private _documentDescriptions: Promise<string> | undefined; // a cache of the descriptions of all docs in the selected collection. makes it more efficient when asking GPT multiple questions about the collection.
+ private _sidebarFieldKey: string = '';
+ private _textToSummarize: string = '';
+ private _imageDescription: string = '';
+ private _textToDocMap = new Map<string, Doc>(); // when GPT answers with a doc's content, this helps us find the Doc
+ private _addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
- @observable public Visible: boolean = false;
- @action public setVisible = (vis: boolean) => {
- this.Visible = vis;
- };
- @observable 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 selectedText: string = '';
- @action public setSelectedText = (text: string) => {
- this.selectedText = text;
- };
- @observable public dataJson: string = '';
- public dataChatPrompt: string | undefined = undefined;
- @action public setDataJson = (text: string) => {
- if (text === '') this.dataChatPrompt = '';
- this.dataJson = text;
- };
-
- @observable public imgDesc: string = '';
- @action public setImgDesc = (text: string) => {
- this.imgDesc = text;
- };
-
- @observable public imgUrls: string[][] = [];
- @action public setImgUrls = (imgs: string[][]) => {
- this.imgUrls = imgs;
- };
-
- @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
- @action public setMode = (mode: GPTPopupMode) => {
- this.mode = mode;
- };
-
- @observable public highlightRange: number[] = [];
- @action callSummaryApi = () => {};
-
- @observable private done: boolean = false;
- @action public setDone = (done: boolean) => {
- this.done = done;
- this.chatMode = false;
- };
-
- // change what can be a ref into a ref
- @observable private sidebarId: string = '';
- @action public setSidebarId = (id: string) => {
- this.sidebarId = id;
- };
-
- @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;
- };
+ constructor(props: object) {
+ super(props);
+ makeObservable(this);
+ GPTPopup.Instance = this;
+ this._messagesEndRef = React.createRef();
+ }
- @observable public sortDesc: string = '';
- @action public setSortDesc = (t: string) => {
- this.sortDesc = t;
+ public addDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
+ public createFilteredDoc: (axes?: string[]) => boolean = () => false;
+ public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id);
+ public setImgTargetDoc = (anchor: Doc) => (this._imgTargetDoc = anchor);
+ public setTextAnchor = (anchor: Doc) => (this._textAnchor = anchor);
+ public setDataJson = (text: string) => {
+ if (text === '') this._dataChatPrompt = '';
+ this._dataJson = text;
};
- onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
- onQuizRandom?: () => void;
- @observable cardsDoneLoading = false;
-
- @observable collectionDoc: Doc | undefined = undefined;
- @action setCollectionDoc(doc: Doc | undefined) {
- this.collectionDoc = doc;
+ componentDidUpdate() {
+ this._gptProcessing && this.setStopAnimatingResponse(false);
}
-
- @action setCardsDoneLoading(done: boolean) {
- this.cardsDoneLoading = done;
- }
-
- @observable sortRespText: string = '';
-
- @action setSortRespText(resp: string) {
- this.sortRespText = resp;
+ componentDidMount(): void {
+ reaction(
+ () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }),
+ ({ selDoc, visible }) => {
+ const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs;
+ if (hasChildDocs) {
+ this._textToDocMap.clear();
+ this.setCollectionContext(selDoc.Document);
+ this.onGptResponse = (sortResult: string, questionType: GPTDocCommand, args?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, args);
+ this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+ this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+ Doc.getDescription(doc).then(text => this._textToDocMap.set(text.trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+ )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ }
+ },
+ { fireImmediately: true }
+ );
}
- @observable chatSortPrompt: string = '';
-
- sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.chatSortPrompt = e.target.value;
- });
-
- @observable quizAnswer: string = '';
-
- quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.quizAnswer = e.target.value;
- });
+ @observable private _conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+ @observable private _chatEnabled: boolean = false;
+ @action private setChatEnabled = (start: boolean) => (this._chatEnabled = start);
+ @observable private _gptProcessing: boolean = false;
+ @action private setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
+ @observable private _responseText: string = '';
+ @action private setResponseText = (text: string) => (this._responseText = text);
+ @observable private _imgUrls: string[][] = [];
+ @action private setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs);
+ @observable private _collectionContext: Doc | undefined = undefined;
+ @action setCollectionContext = (doc: Doc | undefined) => (this._collectionContext = doc);
+ @observable private _userPrompt: string = '';
+ @action setUserPrompt = (e: string) => (this._userPrompt = e);
+ @observable private _quizAnswer: string = '';
+ @action setQuizAnswer = (e: string) => (this._quizAnswer = e);
+ @observable private _stopAnimatingResponse: boolean = false;
+ @action private setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
+
+ @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
- @observable conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+ onQuizRandom?: () => void;
+ onGptResponse?: (sortResult: string, questionType: GPTDocCommand, args?: string) => void;
+ NumberToCommandType = (questionType: string) => +questionType.split(' ')[0][0];
/**
- * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
- * @returns
+ * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
+ * usable code
+ * @param gptOutput
+ * @param questionType
+ * @param tag
*/
- generateQuiz = async () => {
- this.setLoading(true);
-
- await this.regenerateCallback?.();
-
- const selected = DocumentView.SelectedDocs().lastElement();
- if (!StrCast(selected.gptRubric)) {
- await this.generateRubric(StrCast(selected.gptInputText), selected);
- }
-
- try {
- const res = await gptAPICall('Question: ' + StrCast(selected.gptInputText) + ' UserAnswer: ' + this.quizAnswer + '. Rubric: ' + StrCast(selected.gptRubric), GPTCallType.QUIZ);
- if (res) {
- this.setQuizResp(res);
- this.conversationArray.push(res);
-
- this.setLoading(false);
- this.onQuizRandom?.();
- } else {
- console.error('GPT provided no response');
- }
- } catch (err) {
- console.error('GPT call failed', err);
- }
- };
+ processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand, args?: string) =>
+ undoable(() => {
+ switch (questionType) { // reset collection based on question typefc
+ case GPTDocCommand.Sort:
+ docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
+ break;
+ case GPTDocCommand.Filter:
+ docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag));
+ break;
+ } // prettier-ignore
+
+ gptOutput.split('======').filter(item => item.trim() !== '') // Split output into individual document contents
+ .map(docContentRaw => textToDocMap.get(docContentRaw.replace(/\n/g, ' ').trim())) // the find the corresponding Doc using textToDoc map
+ .filter(doc => doc).map(doc => doc!) // filter out undefined values
+ .forEach((doc, index) => {
+ switch (questionType) {
+ case GPTDocCommand.Sort:
+ doc[ChatSortField] = index;
+ break;
+ case GPTDocCommand.AssignTags:
+ if (args) {
+ const hashTag = args.startsWith('#') ? args : '#' + args[0].toLowerCase() + args.slice(1);
+ const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(args)) ?? hashTag;
+ TagItem.addTagToDoc(doc, filterTag);
+ }
+ break;
+ case GPTDocCommand.Filter:
+ TagItem.addTagToDoc(doc, GPTPopup.ChatTag);
+ Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check');
+ break;
+ }
+ }); // prettier-ignore
+ }, '')();
/**
- * Generates a rubric by which to compare the user's answer to
- * @param inputText user's answer
+ * When in quiz mode, randomly selects a document
+ */
+ randomlyChooseDoc = (doc: Doc, childDocs: Doc[]) => DocumentView.getDocumentView(childDocs[Math.floor(Math.random() * childDocs.length)])?.select(false);
+ /**
+ * Generates a rubric for evaluating the user's description of the document's text
* @param doc the doc the user is providing info about
- * @returns gpt's response
+ * @returns gpt's response rubric
*/
- generateRubric = async (inputText: string, doc: Doc) => {
- try {
- const res = await gptAPICall(inputText, GPTCallType.RUBRIC);
- doc.gptRubric = res;
- return res;
- } catch (err) {
- console.error('GPT call failed', err);
- }
- };
-
- @observable private regenerateCallback: (() => Promise<void>) | null = null;
+ generateRubric = (doc: Doc) =>
+ StrCast(doc.gptRubric)
+ ? Promise.resolve(StrCast(doc.gptRubric))
+ : Doc.getDescription(doc).then(desc =>
+ gptAPICall(desc, GPTCallType.MAKERUBRIC)
+ .then(res => (doc.gptRubric = res))
+ .catch(err => console.error('GPT call failed', err))
+ );
/**
- * Callback function that causes the card view to update the childpair string list
- * @param callback
+ * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
+ * @param doc the doc the user is providing info about
+ * @param quizAnswer the user's answer/description for the document
+ * @returns
*/
- @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
- this.setCollectionDoc(collectionDoc);
- this.regenerateCallback = callback;
- }
-
- public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
- public createFilteredDoc: (axes?: string[]) => boolean = () => false;
- public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
-
- @observable quizRespText: string = '';
-
- @action setQuizResp(resp: string) {
- this.quizRespText = resp;
- }
+ generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) =>
+ this.generateRubric(doc).then(() =>
+ Doc.getDescription(doc).then(desc =>
+ gptAPICall(
+ `Question: ${desc};
+ UserAnswer: ${quizAnswer};
+ Rubric: ${StrCast(doc.gptRubric)}`,
+ GPTCallType.QUIZDOC
+ ).then(res => {
+ this._conversationArray.push(res || 'GPT provided no answer');
+ this.onQuizRandom?.();
+ })
+ .catch(err => console.error('GPT call failed', err))
+ )) // prettier-ignore
/**
- * Generates a response to the user's question depending on the type of their question
+ * Generates a response to the user's question about the docs in the collection.
+ * The type of response depends on the chat's analysis of the type of their question
+ * @param userPrompt the user's input that chat will respond to
*/
- generateCard = async () => {
- this.setLoading(true);
-
- await this.regenerateCallback?.();
-
- try {
- const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
- const questionNumber = questionType.split(' ')[0][0];
- const res = await (() => {
- switch (questionNumber) {
- case '1':
- case '2':
- case '4': return gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
- case '6': return gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
- default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, this.chatSortPrompt);
- }})(); // prettier-ignore
-
- // Trigger the callback with the result
- if (this.onSortComplete) {
- this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' '));
-
- let explanation = res;
-
- if (questionType != '5' && questionType != '3') {
- // Extract explanation surrounded by ------ at the top or both at the top and bottom
- const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || [];
- explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found';
- }
-
- // Set the extracted explanation to sortRespText
- this.setSortRespText(explanation);
- runInAction(() => this.conversationArray.push(this.sortRespText));
- this.scrollToBottom();
-
- console.log(res);
- }
- } catch (err) {
- console.error(err);
- }
-
- this.setLoading(false);
- };
+ generateUserPromptResponse = (userPrompt: string) =>
+ gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then((commandType, args = commandType.split(' ').slice(1).join(' ')) =>
+ (async () => {
+ switch (this.NumberToCommandType(commandType)) {
+ case GPTDocCommand.AssignTags:
+ case GPTDocCommand.Filter: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SUBSETDOCS, descs)) ?? "";
+ case GPTDocCommand.Sort: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SORTDOCS, descs)) ?? "";
+ default: return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(userPrompt, GPTCallType.DOCINFO, desc));
+ } // prettier-ignore
+ })().then(
+ action(res => {
+ // Trigger the callback with the result
+ this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType), args);
+ this._conversationArray.push(
+ this.NumberToCommandType(commandType) === GPTDocCommand.GetInfo ? res:
+ // Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom
+ (res.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`)) ?? [])[1]?.trim() ?? 'No explanation found'
+ );
+ })
+ ).catch(err => console.log(err))
+ ).catch(err => console.log(err)); // prettier-ignore
/**
* Generates a Dalle image and uploads it to the server.
*/
- generateImage = async () => {
- if (this.imgDesc === '') return undefined;
+ generateImage = (imgDesc: string, imgTarget: Doc, addToCollection?: (doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) => {
+ this._imgTargetDoc = imgTarget;
+ SnappingManager.SetChatVisible(true);
+ this.addDoc = addToCollection;
this.setImgUrls([]);
this.setMode(GPTPopupMode.IMAGE);
- this.setVisible(true);
- this.setLoading(true);
-
- try {
- const imageUrls = await gptImageCall(this.imgDesc);
- if (imageUrls && imageUrls[0]) {
- const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] });
- const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
- this.setImgUrls([[imageUrls[0], source]]);
- }
- } catch (err) {
- console.error(err);
- }
- this.setLoading(false);
- return undefined;
+ this.setGptProcessing(true);
+ this._imageDescription = imgDesc;
+
+ return gptImageCall(imgDesc)
+ .then(imageUrls =>
+ imageUrls?.[0]
+ ? Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }).then(res => {
+ const source = ClientUtils.prepend(res[0].accessPaths.agnostic.client);
+ return this.setImgUrls([[imageUrls[0]!, source]]);
+ })
+ : undefined
+ )
+ .catch(err => console.error(err))
+ .finally(() => this.setGptProcessing(false));
};
/**
- * Completes an API call to generate a summary of
- * this.selectedText in the popup.
+ * Completes an API call to generate a summary of the specified text
+ *
+ * @param text the text to summarizz
*/
- generateSummary = async () => {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
- GPTPopup.Instance.setLoading(true);
-
- try {
- const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
- GPTPopup.Instance.setText(res || 'Something went wrong.');
- } catch (err) {
- console.error(err);
- }
- GPTPopup.Instance.setLoading(false);
+ generateSummary = (text: string) => {
+ SnappingManager.SetChatVisible(true);
+ this._textToSummarize = text;
+ this.setMode(GPTPopupMode.SUMMARY);
+ this.setGptProcessing(true);
+ return gptAPICall(text, GPTCallType.SUMMARY)
+ .then(res => this.setResponseText(res || 'Something went wrong.'))
+ .catch(err => console.error(err))
+ .finally(() => this.setGptProcessing(false));
};
/**
* Completes an API call to generate an analysis of
* this.dataJson in the popup.
*/
- generateDataAnalysis = async () => {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setLoading(true);
- try {
- const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt);
- const json = JSON.parse(res! as string);
- const keys = Object.keys(json);
- this.correlatedColumns = [];
- this.correlatedColumns.push(json[keys[0]]);
- this.correlatedColumns.push(json[keys[1]]);
- GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.');
- } catch (err) {
- console.error(err);
- }
- GPTPopup.Instance.setLoading(false);
+ generateDataAnalysis = () => {
+ this.setGptProcessing(true);
+ return gptAPICall(this._dataJson, GPTCallType.DATA, this._dataChatPrompt)
+ .then(res => {
+ const json = JSON.parse(res! as string);
+ const keys = Object.keys(json);
+ this._correlatedColumns = [];
+ this._correlatedColumns.push(json[keys[0]]);
+ this._correlatedColumns.push(json[keys[1]]);
+ this.setResponseText(json[keys[2]] || 'Something went wrong.');
+ })
+ .catch(err => console.error(err))
+ .finally(() => this.setGptProcessing(false));
};
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
private transferToText = () => {
- const newDoc = Docs.Create.TextDocument(this.text.trim(), {
+ const newDoc = Docs.Create.TextDocument(this._responseText.trim(), {
_width: 200,
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
});
- this.addDoc(newDoc, this.sidebarId);
- // newDoc.data = 'Hello world';
+ this.addDoc?.(newDoc, this._sidebarFieldKey);
const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
if (anchor) {
DocUtils.MakeLink(newDoc, anchor, {
@@ -346,80 +306,42 @@ export class GPTPopup extends ObservableReactComponent<object> {
/**
* Creates a histogram to show the correlation relationship that was found
*/
- private createVisualization = () => {
- this.createFilteredDoc(this.correlatedColumns);
- };
+ private createVisualization = () => this.createFilteredDoc(this._correlatedColumns);
/**
* Transfers the image urls to actual image docs
*/
private transferToImage = (source: string) => {
- const textAnchor = this.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' });
- };
-
- /**
- * Creates a chatbox for analyzing data so that users can ask specific questions.
- */
- private chatWithAI = () => {
- this.chatMode = true;
- };
- dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.dataChatPrompt = e.target.value;
- });
-
- private getPreviewUrl = (source: string) => source.split('.').join('_m.');
-
- constructor(props: object) {
- super(props);
- makeObservable(this);
- GPTPopup.Instance = this;
- this.messagesEndRef = React.createRef();
- }
-
- scrollToBottom = () => {
- setTimeout(() => {
- // Code to execute after 1 second (1000 ms)
- if (this.messagesEndRef.current) {
- this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
+ const textAnchor = this._textAnchor ?? this._imgTargetDoc;
+ if (textAnchor) {
+ 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.addDoc?.(newDoc);
}
- }, 50);
- };
-
- componentDidUpdate = () => {
- if (this.loading) {
- this.setDone(false);
+ // Create link between prompt and image
+ DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
}
};
- @observable quizMode: GPTQuizType = GPTQuizType.CURRENT;
- @action setQuizMode(g: GPTQuizType) {
- this.quizMode = g;
- }
+ scrollToBottom = () => setTimeout(() => this._messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }), 50);
- cardMenu = () => (
+ gptMenu = () => (
<div className="btns-wrapper-gpt">
<Button
- tooltip="Have ChatGPT sort, tag, define, or filter your cards for you!"
- text="Modify/Sort Cards!"
- onClick={() => this.setMode(GPTPopupMode.SORT)}
+ tooltip="Ask GPT to sort, tag, define, or filter your Docs!"
+ text="Ask GPT"
+ onClick={() => this.setMode(GPTPopupMode.USER_PROMPT)}
color={StrCast(Doc.UserDoc().userVariantColor)}
type={Type.TERT}
style={{
@@ -432,11 +354,11 @@ export class GPTPopup extends ObservableReactComponent<object> {
}}
/>
<Button
- tooltip="Test your knowledge with ChatGPT!"
- text="Quiz Cards!"
+ tooltip="Test your knowledge by verifying answers with ChatGPT"
+ text="Take Quiz"
onClick={() => {
- this.conversationArray = ['Define the selected card!'];
- this.setMode(GPTPopupMode.QUIZ);
+ this._conversationArray = ['Define the selected card!'];
+ this.setMode(GPTPopupMode.QUIZ_RESPONSE);
this.onQuizRandom?.();
}}
color={StrCast(Doc.UserDoc().userVariantColor)}
@@ -452,149 +374,143 @@ export class GPTPopup extends ObservableReactComponent<object> {
</div>
);
+ callGpt = (isUserPrompt: boolean) => {
+ this.setGptProcessing(true);
+ if (isUserPrompt) {
+ this._conversationArray.push(this._userPrompt);
+ return this.generateUserPromptResponse(this._userPrompt).then(action(() => (this._userPrompt = '')));
+ }
+ this._conversationArray.push(this._quizAnswer);
+ return this.generateQuizAnswerAnalysis(DocumentView.SelectedDocs().lastElement(), this._quizAnswer).then(action(() => (this._quizAnswer = '')));
+ };
+
@action
- handleKeyPress = (e: React.KeyboardEvent, isSort: boolean) => {
+ handleKeyPress = async (e: React.KeyboardEvent, isUserPrompt: boolean) => {
+ this._askDictation?.stopDictation();
if (e.key === 'Enter') {
e.stopPropagation();
- if (isSort) {
- this.conversationArray.push(this.chatSortPrompt);
- this.generateCard().then(
- action(() => {
- this.chatSortPrompt = '';
- })
- );
- } else {
- this.conversationArray.push(this.quizAnswer);
- this.generateQuiz().then(
- action(() => {
- this.quizAnswer = '';
- })
- );
- }
-
- this.scrollToBottom();
+ this.callGpt(isUserPrompt).then(() => {
+ this.setGptProcessing(false);
+ this.scrollToBottom();
+ });
}
};
- cardActual = (opt: GPTPopupMode) => {
- const isSort = opt === GPTPopupMode.SORT;
- return (
- <div className="btns-wrapper-gpt">
- <div className="chat-wrapper">
- <div className="chat-bubbles">
- {this.conversationArray.map((message, index) => (
- <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}>
- {message}
- </div>
- ))}
- {(!this.cardsDoneLoading || this.loading) && <div className={`chat-bubble chat-message`}>...</div>}
- </div>
-
- <div ref={this.messagesEndRef} style={{ height: '100px' }} />
+ gptUserInput = () => (
+ <div className="btns-wrapper-gpt">
+ <div className="chat-wrapper">
+ <div className="chat-bubbles">
+ {this._conversationArray.map((message, index) => (
+ <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}>
+ {message}
+ </div>
+ ))}
+ {this._gptProcessing && <div className="chat-bubble chat-message">...</div>}
</div>
- <div className="inputWrapper">
- <input
- className="searchBox-input"
- defaultValue=""
- value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input
- autoComplete="off"
- onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged}
- onKeyDown={e => {
- this.handleKeyPress(e, isSort);
- }}
- type="text"
- placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`}
- />
- </div>
+ <div ref={this._messagesEndRef} style={{ height: '100px' }} />
</div>
- );
- };
+ </div>
+ );
- sortBox = () => (
- <div className="gptPopup-sortBox" style={{ height: '80%' }}>
- {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
- <>
- {
- !this.cardsDoneLoading ? (
- <div className="content-wrapper">
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
- </div>
- </div>
- ) : this.mode === GPTPopupMode.CARD ? (
- this.cardMenu()
- ) : (
- this.cardActual(this.mode)
- ) // Call the functions to render JSX
- }
- </>
+ promptBox = (isUserPrompt: boolean) => (
+ <>
+ <div className="gptPopup-sortBox">
+ {this.heading(isUserPrompt ? 'ASK' : 'QUIZ')}
+ {this.gptUserInput()}
+ </div>
+ <div className="inputWrapper">
+ <input
+ className="searchBox-input"
+ value={isUserPrompt ? this._userPrompt : this._quizAnswer} // Controlled input
+ autoComplete="off"
+ onChange={e => (isUserPrompt ? this.setUserPrompt : this.setQuizAnswer)(e.target.value)}
+ onKeyDown={e => this.handleKeyPress(e, isUserPrompt)}
+ type="text"
+ placeholder={`${isUserPrompt ? 'Have ChatGPT sort, tag, define, or filter your documents for you!' : 'Describe/answer the selected document!'}`}
+ />
+ <Button //
+ text="Send"
+ type={Type.TERT}
+ icon={<AiOutlineSend />}
+ iconPlacement="right"
+ color={SnappingManager.userVariantColor}
+ onClick={() => this.callGpt(isUserPrompt)}
+ />
+ <DictationButton ref={r => (this._askDictation = r)} setInput={isUserPrompt ? this.setUserPrompt : this.setQuizAnswer} />
+ </div>
+ </>
+ );
+
+ menuBox = () => (
+ <div className="gptPopup-sortBox">
+ {this.heading('CHOOSE')}
+ {this.gptMenu()}
</div>
);
imageBox = () => (
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', overflow: 'auto', height: '100%', pointerEvents: 'all' }}>
{this.heading('GENERATED IMAGE')}
<div className="image-content-wrapper">
- {this.imgUrls.map((rawSrc, i) => (
- <div key={rawSrc[0] + i} className="img-wrapper">
- <div className="img-container">
- <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
+ {this._imgUrls.map((rawSrc, i) => (
+ <>
+ <div key={rawSrc[0] + i} className="img-wrapper">
+ <div className="img-container">
+ <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
+ </div>
</div>
<div className="btn-container">
<Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
</div>
- </div>
+ </>
))}
</div>
- {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+ {this._gptProcessing ? null : (
+ <IconButton
+ tooltip="Generate Again"
+ onClick={() => this._imgTargetDoc && this.generateImage(this._imageDescription, this._imgTargetDoc, this._addToCollection)}
+ icon={<FontAwesomeIcon icon="redo-alt" size="lg" />}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ />
+ )}
</div>
);
summaryBox = () => (
<>
- <div>
+ <div style={{ height: 'calc(100% - 60px)', overflow: 'auto' }}>
{this.heading('SUMMARY')}
- <div className="content-wrapper">
- {!this.loading &&
- (!this.done ? (
+ <div className="gptPopup-content-wrapper">
+ {!this._gptProcessing &&
+ (!this._stopAnimatingResponse ? (
<TypeAnimation
speed={50}
sequence={[
- this.text,
+ this._responseText,
() => {
- setTimeout(() => {
- this.setDone(true);
- }, 500);
+ setTimeout(() => this.setStopAnimatingResponse(true), 500);
},
]}
/>
) : (
- this.text
+ this._responseText
))}
</div>
</div>
- {!this.loading && (
- <div className="btns-wrapper">
- {this.done ? (
+ {!this._gptProcessing && (
+ <div className="btns-wrapper" style={{ position: 'absolute', bottom: 0, width: 'calc(100% - 32px)' }}>
+ {this._stopAnimatingResponse ? (
<>
- <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
+ <IconButton tooltip="Generate Again" onClick={() => this.generateSummary(this._textToSummarize + ' ')} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
<Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
</>
) : (
<div className="summarizing">
<span>Summarizing</span>
<ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
- <Button
- text="Stop Animation"
- onClick={() => {
- this.setDone(true);
- }}
- color={StrCast(SettingsManager.userVariantColor)}
- type={Type.TERT}
- />
+ <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
</div>
)}
</div>
@@ -606,33 +522,31 @@ export class GPTPopup extends ObservableReactComponent<object> {
<>
<div>
{this.heading('ANALYSIS')}
- <div className="content-wrapper">
- {!this.loading &&
- (!this.done ? (
+ <div className="gptPopup-content-wrapper">
+ {!this._gptProcessing &&
+ (!this._stopAnimatingResponse ? (
<TypeAnimation
speed={50}
sequence={[
- this.text,
+ this._responseText,
() => {
- setTimeout(() => {
- this.setDone(true);
- }, 500);
+ setTimeout(() => this.setStopAnimatingResponse(true), 500);
},
]}
/>
) : (
- this.text
+ this._responseText
))}
</div>
</div>
- {!this.loading && (
+ {!this._gptProcessing && (
<div className="btns-wrapper">
- {this.done ? (
- this.chatMode ? (
+ {this._stopAnimatingResponse ? (
+ this._chatEnabled ? (
<input
defaultValue=""
autoComplete="off"
- onChange={this.dataPromptChanged}
+ onChange={e => (this._dataChatPrompt = e.target.value)}
onKeyDown={e => {
e.key === 'Enter' ? this.generateDataAnalysis() : null;
e.stopPropagation();
@@ -646,21 +560,14 @@ export class GPTPopup extends ObservableReactComponent<object> {
) : (
<>
<Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
- <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
+ <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
</>
)
) : (
<div className="summarizing">
<span>Summarizing</span>
<ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
- <Button
- text="Stop Animation"
- onClick={() => {
- this.setDone(true);
- }}
- color={StrCast(SnappingManager.userVariantColor)}
- type={Type.TERT}
- />
+ <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
</div>
)}
</div>
@@ -669,71 +576,52 @@ export class GPTPopup extends ObservableReactComponent<object> {
);
aiWarning = () =>
- this.done ? (
+ !this._stopAnimatingResponse ? null : (
<div className="ai-warning">
<FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
AI generated responses can contain inaccurate or misleading content.
</div>
- ) : null;
+ );
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.loading ? (
+ {this._gptProcessing ? (
<ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />
) : (
<>
- {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
- <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
- )}
<Toggle
tooltip="Clear Chat filter"
toggleType={ToggleType.BUTTON}
type={Type.PRIM}
- toggleStatus={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat')}
- text={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat') ? 'filtered' : ''}
- color="red"
- onClick={() => this.collectionDoc && Doc.setDocFilter(this.collectionDoc, 'tags', '#chat', 'remove')}
- />
- <IconButton
- color={StrCast(SettingsManager.userVariantColor)}
- tooltip="close"
- icon={<CgClose size="16px" />}
- onClick={() => {
- this.setVisible(false);
- }}
+ toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag)}
+ text={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'filtered' : ''}
+ color={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'red' : 'transparent'}
+ onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')}
/>
+ {(this._mode === GPTPopupMode.USER_PROMPT || this._mode === GPTPopupMode.QUIZ_RESPONSE) && (
+ <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} />
+ )}
</>
)}
</div>
);
render() {
- let content;
-
- switch (this.mode) {
- case GPTPopupMode.SUMMARY:
- content = this.summaryBox();
- break;
- case GPTPopupMode.DATA:
- content = this.dataAnalysisBox();
- break;
- case GPTPopupMode.IMAGE:
- content = this.imageBox();
- break;
- case GPTPopupMode.SORT:
- case GPTPopupMode.CARD:
- case GPTPopupMode.QUIZ:
- content = this.sortBox();
- break;
- default:
- content = null;
- }
-
return (
- <div className="summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
- {content}
- <div className="resize-handle" />
+ <div className="gptPopup-summary-box" style={{ display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}>
+ {(() => {
+ //prettier-ignore
+ switch (this._mode) {
+ case GPTPopupMode.USER_PROMPT:
+ case GPTPopupMode.QUIZ_RESPONSE: return this.promptBox(this._mode === GPTPopupMode.USER_PROMPT);
+ case GPTPopupMode.GPT_MENU: return this.menuBox();
+ case GPTPopupMode.SUMMARY: return this.summaryBox();
+ case GPTPopupMode.DATA: return this.dataAnalysisBox();
+ case GPTPopupMode.IMAGE: return this.imageBox();
+ default: return null;
+ }
+ })()}
</div>
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 8728ce99c..167421a4a 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -437,7 +437,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
}
- GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
// allows for creating collection
AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;