aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ClientUtils.ts2
-rw-r--r--src/client/apis/gpt/GPT.ts88
-rw-r--r--src/client/util/CurrentUserUtils.ts16
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts12
-rw-r--r--src/client/util/SnappingManager.ts3
-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
-rw-r--r--src/fields/Doc.ts24
-rw-r--r--src/fields/RichTextUtils.ts2
-rw-r--r--src/fields/Types.ts4
40 files changed, 787 insertions, 982 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 8c9dd0a32..c5b61477b 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -1,4 +1,4 @@
-import * as Color from 'color';
+import Color from 'color';
import * as React from 'react';
import { ColorResult } from 'react-color';
import * as rp from 'request-promise';
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 1894bb4df..6d9bc1d06 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,14 +1,22 @@
import { ChatCompletionMessageParam, Image } from 'openai/resources';
import { openai } from './setup';
+export enum GPTDocCommand {
+ AssignTags = 1,
+ Filter = 2,
+ GetInfo = 3,
+ Sort = 4,
+}
+
+export const DescriptionSeperator = '======';
+export const DocSeperator = '------';
+
enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
CHATCARD = 'chatcard', // a single flashcard style response to a question
FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic
- QUIZ = 'quiz',
- SORT = 'sort',
DESCRIBE = 'describe',
MERMAID = 'mermaid',
DATA = 'data',
@@ -16,15 +24,17 @@ enum GPTCallType {
PRONUNCIATION = 'pronunciation',
DRAW = 'draw',
COLOR = 'color',
- RUBRIC = 'rubric', // needs to be filled in below
- TYPE = 'type', // needs to be filled in below
- SUBSET = 'subset', // needs to be filled in below
- INFO = 'info', // needs to be filled in below
TEMPLATE = 'template',
VIZSUM = 'vizsum',
VIZSUM2 = 'vizsum2',
FILL = 'fill',
COMPLETEPROMPT = 'completeprompt',
+ QUIZDOC = 'quiz_doc',
+ MAKERUBRIC = 'make_rubric', // create a definition rubric for a document to be used when quizzing the user
+ COMMANDTYPE = 'command_type', // Determine the type of command being made (GPTQueryType - eg., AssignTags, Sort, Filter, DocInfo, GenInfo) and possibly some parameters (eg, Tag type for Tags)
+ SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions
+ DOCINFO = 'doc_info', // provide information about a document
+ SORTDOCS = 'sort_docs',
}
type GPTCallOpts = {
@@ -34,7 +44,7 @@ type GPTCallOpts = {
prompt: string;
};
-const callTypeMap: { [type: string]: GPTCallOpts } = {
+const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
// newest model: gpt-4
summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
@@ -58,11 +68,17 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
temp: 0.5,
prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ",
},
- sort: {
+ sort_docs: {
model: 'gpt-4o',
maxTokens: 2048,
temp: 0.25,
- prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
+ prompt: `The user is going to give you a list of descriptions.
+ Each one is separated by '${DescriptionSeperator}' on either side.
+ Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'.
+ Sort them by the user's specifications.
+ Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'.
+ Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description).
+ It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
flashcard: {
@@ -72,7 +88,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Create a title for each question and asnwer that is labeled as "title". Do not label each flashcard and do not include asterisks: ',
},
chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
- quiz: {
+ quiz_doc: {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
@@ -121,38 +137,45 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
temp: 0.5,
prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
},
- type: {
+ command_type: {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
prompt: `I'm going to provide you with a question.
Based on the question, is the user asking you to
- 1. Assigns docs with tags(like star / heart etc)/labels,
- 2. Filter docs,
- 3. Provide information about a specific doc
- 4. Provide a specific doc based on a question/information
- 5. Provide general information
- 6. Put cards in a specific order.
- Answer with only the number for 2-6. For number one, provide the number (1) and the appropriate tag`,
+ ${GPTDocCommand.AssignTags}. Assigns docs with tags(like star / heart etc)/labels.
+ ${GPTDocCommand.GetInfo}. Provide information about a specific doc.
+ ${GPTDocCommand.Filter}. Filter docs based on a question/information.
+ ${GPTDocCommand.Sort}. Put docs in a specific order.
+ Answer with only the number for ${GPTDocCommand.GetInfo}-${GPTDocCommand.Sort}.
+ For number one, provide the number (${GPTDocCommand.AssignTags}) and the appropriate tag`,
},
- subset: {
+ subset_docs: {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: "I'm going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and no commas",
+ prompt: `I'm going to give you a list of descriptions.
+ Each one is separated by '${DescriptionSeperator}' on either side.
+ Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'.
+ Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications.
+ Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'.
+ Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description).
+ It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
},
- info: {
+ doc_info: {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)",
+ prompt: `Answer the user's question with a short (<100 word) response.
+ If a particular document is selected I will provide that information (which may help with your response)`,
},
- rubric: {
+ make_rubric: {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: "BRIEFLY (<25 words) provide a definition for the following term. It will be used as a rubric to evaluate the user's understanding of the topic",
+ prompt: `BRIEFLY (<25 words) provide a definition for the following term.
+ It will be used as a rubric to evaluate the user's understanding of the topic`,
},
};
let lastCall = '';
@@ -164,17 +187,15 @@ let lastResp = '';
* @returns AI Output
*/
const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
- const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? '.' : '');
+ const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZDOC, GPTCallType.STACK].includes(callType) ? '.' : '');
const opts = callTypeMap[callType];
if (!opts) {
console.log('The query type:' + callType + ' requires a configuration.');
return 'Error connecting with API.';
}
- if (lastCall === inputText && dontCache !== true) return lastResp;
+ if (lastCall === inputText && dontCache !== true && lastResp) return lastResp;
try {
- lastCall = inputText;
-
- const usePrompt = prompt ? prompt + opts.prompt : opts.prompt;
+ const usePrompt = prompt ? prompt + '.' + opts.prompt : opts.prompt;
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
@@ -186,9 +207,12 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: s
temperature: opts.temp,
max_tokens: opts.maxTokens,
});
- lastResp = response.choices[0].message.content ?? '';
- console.log('RESP:' + lastResp);
- return lastResp;
+ const result = response.choices[0].message.content ?? '';
+ if (!dontCache) {
+ lastResp = result;
+ lastCall = inputText;
+ }
+ return result;
} catch (err) {
console.log(err);
return 'Error connecting with API.';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 0783bb80e..fb349abd9 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -402,8 +402,8 @@ pie title Minerals in my tap water
{key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }},
{key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
- {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
- {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, }},
+ {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300 }},
+ {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}},
{key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
{key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
@@ -710,11 +710,7 @@ pie title Minerals in my tap water
{ title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ title: "Tags", icon:"bolt", toolTip:"Sort by document's tags", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Sort", icon: "sort" , toolTip: "Manage sort order / lock status", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
- subMenu: [
- { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up", toolType:"up", ignoreClick: true, scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
- { title: "Descending",toolTip: "Sort the cards in descending order",btnType: ButtonType.ToggleButton, icon: "sort-down",toolType:"down",ignoreClick: true, scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
- ]},
+ { title: "Reverse", icon: "sort-up", toolTip: "Sort the cards in reverse order", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"reverse", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
]
}
@@ -832,7 +828,7 @@ pie title Minerals in my tap water
CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear,
CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Schema, CollectionViewType.Stacking,
CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
- title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, shiftKey, _readOnly_); }'}},
+ title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, shiftKey, _readOnly_); }'}},
{ title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
{ title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} },
{ title: "Template",icon: "scroll", toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF, scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
@@ -844,8 +840,8 @@ pie title Minerals in my tap water
{ title: "Chat", icon:"lightbulb", toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, width: 30, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
{ title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
{ title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: CurrentUserUtils.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
{ title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 8d4eefa7e..f73149fdc 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -4,22 +4,16 @@ import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
+import { Upload } from '../../../server/SharedMediaTypes';
import { Networking } from '../../Network';
export namespace ImageUtils {
- export type imgInfo = {
- contentSize: number;
- nativeWidth: number;
- nativeHeight: number;
- source: string;
- exifData: { error: string | undefined; data: string };
- };
- export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => {
+ export const ExtractImgInfo = async (document: Doc): Promise<Upload.InspectionResults | undefined> => {
const field = Cast(document.data, ImageField);
return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
};
- export const AssignImgInfo = (document: Doc, data?: imgInfo) => {
+ export const AssignImgInfo = (document: Doc, data?: Upload.InspectionResults) => {
if (data) {
data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth);
const proto = document[DocData];
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 2a150dc5a..9d8a41844 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -32,6 +32,7 @@ export class SnappingManager {
@observable _hideDecorations: boolean = false;
@observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode
@observable _inkShape: Gestures | undefined = undefined;
+ @observable _chatVisible: boolean = false;
private constructor() {
SnappingManager._manager = this;
@@ -66,6 +67,7 @@ export class SnappingManager {
public static get HideDecorations(){ return this.Instance._hideDecorations; } // prettier-ignore
public static get KeepGestureMode(){ return this.Instance._keepGestureMode; } // prettier-ignore
public static get InkShape() { return this.Instance._inkShape; } // prettier-ignore
+ public static get ChatVisible() { return this.Instance._chatVisible; } // prettier-ignore
public static SetLongPress = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore
public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
@@ -85,6 +87,7 @@ export class SnappingManager {
public static SetHideDecorations= (state:boolean) =>runInAction(() => {this.Instance._hideDecorations = state}); // prettier-ignore
public static SetKeepGestureMode= (state:boolean) =>runInAction(() => {this.Instance._keepGestureMode = state}); // prettier-ignore
public static SetInkShape = (shape?:Gestures)=>runInAction(() => {this.Instance._inkShape = shape}); // prettier-ignore
+ public static SetChatVisible = (vis:boolean) =>runInAction(() => {this.Instance._chatVisible = vis}); // prettier-ignore
public static userColor: string | undefined;
public static userVariantColor: string | undefined;
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;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e62ca4bb8..bdd41b0bb 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -7,7 +7,7 @@ import { CollectionViewType, DocumentType } from '../client/documents/DocumentTy
import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
import { undoable, UndoManager } from '../client/util/UndoManager';
-import { ClientUtils, incrementTitleCopy } from '../ClientUtils';
+import { ClientUtils, imageUrlToBase64, incrementTitleCopy } from '../ClientUtils';
import {
AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks,
DocAcl, DocCss, DocData, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight,
@@ -22,8 +22,9 @@ import { FieldId, RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types';
+import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types';
import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
+import { gptImageLabel } from '../client/apis/gpt/GPT';
export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -1467,6 +1468,25 @@ export namespace Doc {
});
}
+ /**
+ * text description of a Doc. 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.
+ * @param doc
+ * @returns
+ */
+ export function getDescription(doc: Doc) {
+ const curDescription = StrCast(doc[DocData][Doc.LayoutFieldKey(doc) + '_description']);
+ const docText = (async (tdoc:Doc) => {
+ switch (tdoc.type) {
+ case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
+ case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '')
+ .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.'));
+ case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text;
+ default: return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
+ }}); // prettier-ignore
+ return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text));
+ }
+
// prettier-ignore
export function toIcon(doc?: Doc, isOpen?: Opt<boolean>) {
if (isOpen) return doc?.isFolder ? 'chevron-down' : 'folder-open';
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index 8c073c87b..42dd0d432 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-use-before-define */
import { AssertionError } from 'assert';
-import * as Color from 'color';
+import Color from 'color';
import { docs_v1 as docsV1 } from 'googleapis';
import { Fragment, Mark, Node, Schema } from 'prosemirror-model';
import { sinkListItem } from 'prosemirror-schema-list';
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index e19673665..474882959 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -134,6 +134,10 @@ export function PDFCast(field: FieldResult, defaultVal: PdfField | null = null)
export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) {
return Cast(field, ImageField, defaultVal);
}
+export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) {
+ const href = ImageCast(field, defaultVal)?.url.href;
+ return href ? `${href.split('.')[0]}${suffix}.${href.split('.')[1]}` : null;
+}
export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
// eslint-disable-next-line no-redeclare