aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-02-10 19:07:20 -0500
committerbobzel <zzzman@gmail.com>2025-02-10 19:07:20 -0500
commitc9686eaebffb3547b7e0f20aec64754627af76ce (patch)
tree7ebf1c38323a8d7af554ba564acf95cfe79b7709 /src
parentb72d018698ad1d2e713f0fcbef392d23bf1cf545 (diff)
parente93ca53af693fa1ec2186ca9417af122bb5e8e09 (diff)
updated from master
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/ClientUtils.ts45
-rw-r--r--src/client/Network.ts2
-rw-r--r--src/client/apis/gpt/GPT.ts95
-rw-r--r--src/client/apis/gpt/PresCustomization.ts115
-rw-r--r--src/client/documents/DocUtils.ts86
-rw-r--r--src/client/documents/DocumentTypes.ts2
-rw-r--r--src/client/documents/Documents.ts37
-rw-r--r--src/client/util/CalendarManager.tsx2
-rw-r--r--src/client/util/CurrentUserUtils.ts174
-rw-r--r--src/client/util/DictationManager.ts5
-rw-r--r--src/client/util/DocumentManager.ts37
-rw-r--r--src/client/util/GroupManager.tsx2
-rw-r--r--src/client/util/GroupMemberView.tsx2
-rw-r--r--src/client/util/InteractionUtils.tsx6
-rw-r--r--src/client/util/LinkFollower.ts2
-rw-r--r--src/client/util/SelectionManager.ts3
-rw-r--r--src/client/util/SettingsManager.tsx28
-rw-r--r--src/client/util/SharingManager.tsx2
-rw-r--r--src/client/util/SnappingManager.ts7
-rw-r--r--src/client/util/bezierFit.ts11
-rw-r--r--src/client/util/reportManager/ReportManager.tsx2
-rw-r--r--src/client/views/ComponentDecorations.tsx6
-rw-r--r--src/client/views/ContextMenu.tsx1
-rw-r--r--src/client/views/ContextMenuItem.tsx10
-rw-r--r--src/client/views/DashboardView.scss33
-rw-r--r--src/client/views/DashboardView.tsx2
-rw-r--r--src/client/views/DictationButton.tsx57
-rw-r--r--src/client/views/DictationOverlay.tsx1
-rw-r--r--src/client/views/DocumentButtonBar.tsx15
-rw-r--r--src/client/views/DocumentDecorations.tsx28
-rw-r--r--src/client/views/FilterPanel.tsx2
-rw-r--r--src/client/views/GestureOverlay.tsx203
-rw-r--r--src/client/views/GlobalKeyHandler.ts17
-rw-r--r--src/client/views/InkingStroke.tsx19
-rw-r--r--src/client/views/LightboxView.tsx17
-rw-r--r--src/client/views/Main.tsx4
-rw-r--r--src/client/views/MainView.tsx64
-rw-r--r--src/client/views/MainViewModal.tsx2
-rw-r--r--src/client/views/MarqueeAnnotator.tsx8
-rw-r--r--src/client/views/PinFuncs.ts23
-rw-r--r--src/client/views/PropertiesButtons.tsx2
-rw-r--r--src/client/views/PropertiesView.scss2
-rw-r--r--src/client/views/PropertiesView.tsx135
-rw-r--r--src/client/views/SidebarAnnos.tsx4
-rw-r--r--src/client/views/StyleProp.ts4
-rw-r--r--src/client/views/StyleProvider.tsx18
-rw-r--r--src/client/views/StyleProviderQuiz.tsx4
-rw-r--r--src/client/views/TagsView.scss23
-rw-r--r--src/client/views/TagsView.tsx24
-rw-r--r--src/client/views/UndoStack.tsx2
-rw-r--r--src/client/views/ViewBoxInterface.ts4
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx14
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss5
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx362
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx44
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx28
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx4
-rw-r--r--src/client/views/collections/CollectionMenu.tsx2
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx11
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx5
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx13
-rw-r--r--src/client/views/collections/CollectionSubView.tsx66
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx4
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx8
-rw-r--r--src/client/views/collections/TabDocView.tsx3
-rw-r--r--src/client/views/collections/TreeView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss62
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx265
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx6
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx12
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx2
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx80
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx12
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx2
-rw-r--r--src/client/views/global/globalScripts.ts228
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx2
-rw-r--r--src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx6
-rw-r--r--src/client/views/newlightbox/Header/LightboxHeader.tsx2
-rw-r--r--src/client/views/newlightbox/RecommendationList/RecommendationList.tsx2
-rw-r--r--src/client/views/newlightbox/components/EditableText/EditableText.tsx2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx9
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx66
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx9
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx10
-rw-r--r--src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx2
-rw-r--r--src/client/views/nodes/DiagramBox.tsx6
-rw-r--r--src/client/views/nodes/DocumentView.scss28
-rw-r--r--src/client/views/nodes/DocumentView.tsx286
-rw-r--r--src/client/views/nodes/EquationBox.scss4
-rw-r--r--src/client/views/nodes/EquationBox.tsx100
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/FocusViewOptions.ts11
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.scss7
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx197
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx43
-rw-r--r--src/client/views/nodes/IconTagBox.scss2
-rw-r--r--src/client/views/nodes/ImageBox.scss86
-rw-r--r--src/client/views/nodes/ImageBox.tsx262
-rw-r--r--src/client/views/nodes/LabelBox.scss1
-rw-r--r--src/client/views/nodes/LabelBox.tsx168
-rw-r--r--src/client/views/nodes/LinkBox.tsx40
-rw-r--r--src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx14
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx4
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx2
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx7
-rw-r--r--src/client/views/nodes/PDFBox.scss11
-rw-r--r--src/client/views/nodes/PDFBox.tsx46
-rw-r--r--src/client/views/nodes/VideoBox.tsx3
-rw-r--r--src/client/views/nodes/WebBox.tsx19
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts50
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx442
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts170
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts415
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts17
-rw-r--r--src/client/views/nodes/chatbot/tools/ImageCreationTool.ts10
-rw-r--r--src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts11
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx9
-rw-r--r--src/client/views/nodes/formattedText/EquationEditor.tsx11
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx285
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx90
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts4
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts20
-rw-r--r--src/client/views/nodes/imageEditor/GenerativeFillButtons.scss (renamed from src/client/views/nodes/generativeFill/GenerativeFillButtons.scss)0
-rw-r--r--src/client/views/nodes/imageEditor/GenerativeFillButtons.tsx (renamed from src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx)4
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditor.scss (renamed from src/client/views/nodes/generativeFill/GenerativeFill.scss)68
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditor.tsx (renamed from src/client/views/nodes/generativeFill/GenerativeFill.tsx)555
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditorButtons.tsx69
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts29
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/GenerativeFillMathHelpers.ts (renamed from src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts)2
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts312
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/PointerHandler.ts (renamed from src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts)2
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts (renamed from src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts)1
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts43
-rw-r--r--src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts (renamed from src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts)6
-rw-r--r--src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts (renamed from src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts)2
-rw-r--r--src/client/views/nodes/trails/CubicBezierEditor.tsx154
-rw-r--r--src/client/views/nodes/trails/PresBox.scss117
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx1132
-rw-r--r--src/client/views/nodes/trails/SpringUtils.ts9
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx22
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx250
-rw-r--r--src/client/views/pdf/PDFViewer.scss12
-rw-r--r--src/client/views/pdf/PDFViewer.tsx66
-rw-r--r--src/client/views/selectedDoc/SelectedDocView.tsx2
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx361
-rw-r--r--src/client/views/smartdraw/DrawingFillHandler.tsx79
-rw-r--r--src/client/views/smartdraw/FireflyConstants.ts49
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.scss87
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx424
-rw-r--r--src/client/views/smartdraw/StickerPalette.scss (renamed from src/client/views/smartdraw/AnnotationPalette.scss)2
-rw-r--r--src/client/views/smartdraw/StickerPalette.tsx352
-rw-r--r--src/client/views/topbar/TopBar.tsx2
-rw-r--r--src/extensions/ExtensionsTypings.ts8
-rw-r--r--src/extensions/Extensions_Array.ts10
-rw-r--r--src/fields/Doc.ts8
-rw-r--r--src/fields/InkField.ts33
-rw-r--r--src/fields/ScriptField.ts15
-rw-r--r--src/fields/documentSchemas.ts7
-rw-r--r--src/pen-gestures/GestureTypes.ts1
-rw-r--r--src/pen-gestures/ndollar.ts96
-rw-r--r--src/server/ApiManagers/AssistantManager.ts58
-rw-r--r--src/server/ApiManagers/DataVizManager.ts2
-rw-r--r--src/server/ApiManagers/FireflyManager.ts407
-rw-r--r--src/server/ApiManagers/UploadManager.ts35
-rw-r--r--src/server/DashUploadUtils.ts57
-rw-r--r--src/server/RouteManager.ts3
-rw-r--r--src/server/authentication/DashUserModel.ts5
-rw-r--r--src/server/flashcard/labels.py285
-rw-r--r--src/server/flashcard/requirements.txt12
-rw-r--r--src/server/flashcard/venv/pyvenv.cfg3
-rw-r--r--src/server/index.ts3
-rw-r--r--src/server/server_Initialization.ts10
189 files changed, 7104 insertions, 3849 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 9b66f8d8e..1ef749033 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index e7aee1c2a..8c9dd0a32 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -107,12 +107,12 @@ export namespace ClientUtils {
default: return type.charAt(0).toUpperCase() + type.substring(1,3);
} // prettier-ignore
}
- export function cleanDocumentType(type: DocumentType, colType: CollectionViewType) {
+ export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) {
switch (type) {
case DocumentType.PDF: return 'PDF';
case DocumentType.IMG: return 'Image';
case DocumentType.AUDIO: return 'Audio';
- case DocumentType.COL: return 'Collection:'+colType;
+ case DocumentType.COL: return 'Collection:'+ (colType ?? "");
case DocumentType.RTF: return 'Text';
default: return type.charAt(0).toUpperCase() + type.slice(1);
} // prettier-ignore
@@ -144,15 +144,20 @@ export namespace ClientUtils {
export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) {
try {
const posting = ClientUtils.prepend('/uploadURI');
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix,
- replaceRootFilename,
- },
- json: true,
- });
+ const returnedUri = await rp
+ .post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename,
+ },
+ json: true,
+ })
+ .catch(e => {
+ alert('Data URI Error: ' + e.toString());
+ return undefined;
+ });
return returnedUri;
} catch (e) {
console.log('ConvertDataURI :' + e);
@@ -165,7 +170,7 @@ export namespace ClientUtils {
return { scale: 0, translateX: 1, translateY: 1 };
}
const rect = ele.getBoundingClientRect();
- const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth;
+ const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / (ele.offsetWidth || 1);
const translateX = rect.left;
const translateY = rect.top;
@@ -212,7 +217,7 @@ export namespace ClientUtils {
return { r: r, g: g, b: b, a: a };
}
- const isTransparentFunctionHack = 'isTransparent(__value__)';
+ export const isTransparentFunctionHack = 'isTransparent(__value__)';
export const noRecursionHack = '__noRecursion';
// special case filters
@@ -223,11 +228,6 @@ export namespace ClientUtils {
export function IsRecursiveFilter(val: string) {
return !val.includes(noRecursionHack);
}
- export function HasFunctionFilter(val: string) {
- if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
- // add other function filters here...
- return undefined;
- }
export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
@@ -365,6 +365,15 @@ export namespace ClientUtils {
}
}
+/**
+ * Removes specified keys from an object and returns the result in the 'omit' field of the return value.
+ * The keys that were removed ared retuned in the 'extract' field of the return value.
+ * @param obj - object to remove keys from
+ * @param keys - list of key field names to remove
+ * @param pattern - optional pattern to specify keys to removed
+ * @param addKeyFunc - optional function to call with object after keys have been removed
+ * @returns a tuple object containint 'omit' (oject after keys have been removed) and 'extact' (object containing omitted fields)
+ */
export function OmitKeys(obj: object, keys: string[], pattern?: string, addKeyFunc?: (dup: object) => void): { omit: { [key: string]: unknown }; extract: { [key: string]: unknown } } {
const omit: { [key: string]: unknown } = { ...obj };
const extract: { [key: string]: unknown } = {};
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 9afdc844f..3b0406141 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -15,7 +15,7 @@ export namespace Networking {
return (await fetch(relativeRoute)).text();
}
- export async function PostToServer(relativeRoute: string, body?: unknown) {
+ export function PostToServer(relativeRoute: string, body?: unknown) {
const options = {
uri: ClientUtils.prepend(relativeRoute),
method: 'POST',
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 8a2c91269..1894bb4df 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -16,10 +16,10 @@ enum GPTCallType {
PRONUNCIATION = 'pronunciation',
DRAW = 'draw',
COLOR = 'color',
- RUBRIC = 'rubric',
- TYPE = 'type',
- SUBSET = 'subset',
- INFO = 'info',
+ 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',
@@ -42,7 +42,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
model: 'gpt-4o',
maxTokens: 2048,
temp: 0.7,
- prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
+ prompt: 'Create a stack of at least 10 flashcards out of this text with each question and answer labeled as question and answer. Each flashcard should have a title that represents the question in just a few words and label it "title". For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
},
completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
@@ -65,7 +65,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
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",
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
- flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' },
+ flashcard: {
+ model: 'gpt-4-turbo',
+ maxTokens: 512,
+ temp: 0.5,
+ 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: {
model: 'gpt-4-turbo',
@@ -116,6 +121,39 @@ 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: {
+ 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`,
+ },
+ subset: {
+ 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",
+ },
+
+ 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)",
+ },
+ 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",
+ },
};
let lastCall = '';
let lastResp = '';
@@ -126,8 +164,12 @@ let lastResp = '';
* @returns AI Output
*/
const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
- const inputText = [GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? inputTextIn + '.' : inputTextIn;
+ const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, 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;
try {
lastCall = inputText;
@@ -244,6 +286,41 @@ const gptHandwriting = async (src: string): Promise<string> => {
}
};
+const gptDescribeImage = async (image: string): Promise<string> => {
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o',
+ temperature: 0,
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: `Very briefly identify what this drawing is and list all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `${image}`,
+ detail: 'low',
+ },
+ },
+ ],
+ },
+ ],
+ });
+ if (response.choices[0].message.content) {
+ console.log('GPT DESCRIPTION', response.choices[0].message.content);
+ return response.choices[0].message.content;
+ }
+ return 'Unknown drawing';
+ } catch (err) {
+ console.log(err);
+ return 'Error connecting with API';
+ }
+};
+
const gptDrawingColor = async (image: string, coords: string[]): Promise<string> => {
try {
const response = await openai.chat.completions.create({
@@ -271,11 +348,11 @@ const gptDrawingColor = async (image: string, coords: string[]): Promise<string>
if (response.choices[0].message.content) {
return response.choices[0].message.content;
}
- return 'Missing labels';
+ return 'Unknown drawing';
} catch (err) {
console.log(err);
return 'Error connecting with API';
}
};
-export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor };
+export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDescribeImage, gptDrawingColor };
diff --git a/src/client/apis/gpt/PresCustomization.ts b/src/client/apis/gpt/PresCustomization.ts
index 2262886a2..c465f098f 100644
--- a/src/client/apis/gpt/PresCustomization.ts
+++ b/src/client/apis/gpt/PresCustomization.ts
@@ -1,3 +1,5 @@
+import { PresEffect, PresEffectDirection } from '../../views/nodes/trails/PresEnums';
+import { AnimationSettingsProperties, easeItems } from '../../views/nodes/trails/SpringUtils';
import { openai } from './setup';
export enum CustomizationType {
@@ -10,15 +12,17 @@ interface PromptInfo {
}
const prompts: { [key: string]: PromptInfo } = {
trails: {
- description:
- 'We are customizing the properties and transition of a slide in a presentation. You are given the current properties of the slide in a json with the fields [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection], as well as the prompt for how the user wants to change it. Return a json with the required fields: [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection] by applying the changes in the prompt to the current state of the slide.',
+ description: `We are customizing the properties and transition of a slide in a presentation.
+ You are given the current properties of the slide in a json with the fields [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection],
+ as well as the prompt for how the user wants to change it.
+ Return a json with the required fields: [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection] by applying the changes in the prompt to the current state of the slide.`,
features: [],
},
};
// Allows you to register properties that are customizable
export const addCustomizationProperty = (type: CustomizationType, name: string, description: string, values?: string[]) => {
- values ? prompts[type].features.push({ name, description, values }) : prompts[type].features.push({ name, description });
+ prompts[type].features.push({ name, description, ...(values ? { values } : {}) });
};
// All the registered fields, make sure to update during registration, this
@@ -41,35 +45,34 @@ export const gptSlideProperties = [
// Registers slide properties
const setupPresSlideCustomization = () => {
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'title', 'is the title/name of the slide.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_transition', 'is a number in milliseconds for how long it should take to transition/move to a slide.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_easeFunc', 'is the easing function for the movement to the slide.', ['Ease', 'Ease In', 'Ease Out', 'Ease Out', 'Ease In Out', 'Linear']);
-
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effect', 'is an effect applied to the slide when we transition to it.', ['None', 'Expand', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']);
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effectDirection', 'is what direction the effect is applied.', ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']);
- addCustomizationProperty(
- CustomizationType.PRES_TRAIL_SLIDE,
- 'presentation_effectTiming',
- "is a json object of the format: {type: string, stiffness: number, damping: number, mass: number}. Type is always “custom”. Controls the spring-based timing of the presentation effect animation. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to match the user's description of how they want to animate the effect."
- );
-
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'config_zoom', 'is a number from 0 to 1.0 indicating the percentage we should zoom into the slide.');
-
- // boolean values
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_playAudio', 'is a boolean value indicating if we should play audio when we go to the slide.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_zoomText', 'is a boolean value indicating if we should zoom into text selections when we go to the slide.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideBefore', 'is a boolean value indicating if we should hide the slide before going to it.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hide', 'is a boolean value indicating if we should hide the slide during the presentation.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideAfter', 'is a boolean value indicating if we should hide the slide after going to it.');
- addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_openInLightbox', 'is a boolean value indicating if we should open the slide in an overlay or lightbox view during the presentation.');
-};
+ const add = (name: string, val:string, opts?:string[]) => addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, name, val, opts);
+ const addBool = (name: string, val:string) => add(name, 'is a boolean value indicating if we should ' + val);
+ add('title', 'is the title/name of the slide.');
+ add('config_zoom', 'is a number from 0 to 1.0 indicating the percentage we should zoom into the slide.');
+ add('presentation_transition', 'is a number in milliseconds for how long it should take to transition/move to a slide.');
+ add('presentation_easeFunc', 'is the easing function for the movement to the slide.', easeItems.filter(val => val.text !== 'Custom').map(val => val.text))
+ add('presentation_effect', 'is an effect applied to the slide when we transition to it.', Object.keys(PresEffect));
+ add('presentation_effectDirection','is what direction the effect is applied.', Object.keys(PresEffectDirection).filter(key => key !== PresEffectDirection.None));
+ add('presentation_effectTiming', `is a json object of the format: {type: string, ${AnimationSettingsProperties.stiffness}: number, ${AnimationSettingsProperties.damping}: number, ${AnimationSettingsProperties.mass}: number}.
+ Type is always “custom”. Controls the spring-based timing of the presentation effect animation.
+ Stiffness, damping, and mass control the physics-based properties of spring animations.
+ This is used to create a more natural looking timing, bouncy effects, etc.
+ Use spring physics to adjust these parameters to match the user's description of how they want to animate the effect.`);
+
+
+ addBool('presentation_playAudio', 'play audio when we go to the slide.');
+ addBool('presentation_zoomText', 'zoom into text selections when we go to the slide.');
+ addBool('presentation_hideBefore', 'hide the slide before going to it.');
+ addBool('presentation_hide', 'hide the slide during the presentation.');
+ addBool('presentation_hideAfter', 'hide the slide after going to it.');
+ addBool('presentation_openInLightbox', 'open the slide in an overlay or lightbox view during the presentation.');
+}; // prettier-ignore
setupPresSlideCustomization();
-export const getSlideTransitionSuggestions = async (inputText: string) => {
+export const getSlideTransitionSuggestions = (inputText: string) => {
/**
- * Prompt: Generate an entrance animations from slower and gentler
- * to bouncier and more high energy
+ * Prompt: Generate entrance animations from slower and gentler to bouncier and more high energy
*
* Format:
* {
@@ -81,13 +84,19 @@ export const getSlideTransitionSuggestions = async (inputText: string) => {
* }
*/
- const prompt =
- "I want to generate four distinct types of slide effect animations. Return a json of the form {effect: string, direction: string, stiffness: number, damping: number, mass: number}[] with four elements. Effect is the type of animation; its only possible values are ['Expand', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']. Direction is the direction that the animation starts from; its only possible values are ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural-looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to animate the effect.";
+ const prompt = `I want to generate four distinct types of slide effect animations.
+ Return a json of the form { ${AnimationSettingsProperties.effect}: string, ${AnimationSettingsProperties.direction}: string, ${AnimationSettingsProperties.stiffness}: number, ${AnimationSettingsProperties.damping}: number, ${AnimationSettingsProperties.mass}: number}[] with four elements.
+ ${AnimationSettingsProperties.effect} is the type of animation; its only possible values are [${Object.keys(PresEffect).filter(key => key !== PresEffect.None).join(',')}].
+ ${AnimationSettingsProperties.direction} is the direction that the animation starts from;
+ its only possible values are [${Object.values(PresEffectDirection).filter(key => key !== PresEffectDirection.None).join(',')}].
+ ${AnimationSettingsProperties.stiffness}, ${AnimationSettingsProperties.damping}, and ${AnimationSettingsProperties.mass} control the physics-based properties of spring animations.
+ This is used to create a more natural-looking timing, bouncy effects, etc.
+ Use spring physics to adjust these parameters to animate the effect.`; // prettier-ignore
const customInput = inputText ?? 'Make them as contrasting as possible with different effects and timings ranging from gentle to energetic.';
- try {
- const response = await openai.chat.completions.create({
+ return openai.chat.completions
+ .create({
model: 'gpt-4',
messages: [
{ role: 'system', content: prompt },
@@ -95,39 +104,33 @@ export const getSlideTransitionSuggestions = async (inputText: string) => {
],
temperature: 0,
max_tokens: 1000,
+ })
+ .then(response => response.choices[0].message?.content ?? '')
+ .catch(err => {
+ console.log(err);
+ return 'Error connecting with API.';
});
- return response.choices[0].message?.content;
- } catch (err) {
- console.log(err);
- return 'Error connecting with API.';
- }
};
-export const gptTrailSlideCustomization = async (inputText: string, properties: any | any[]) => {
- let prompt = prompts.trails.description;
-
- prompts.trails.features.forEach(feature => {
- prompt += feature.name + ' ' + feature.description;
- if (feature.values) {
- prompt += `Its only possible values are [${feature.values.join(', ')}].`;
- }
- });
+export const gptTrailSlideCustomization = (inputText: string, properties: string) => {
+ const preamble = prompts.trails.description + prompts.trails.features.map(feature => feature.name + ' ' + feature.description + (feature.values ? `Its only possible values are [${feature.values.join(', ')}]` : '')).join('. ');
- prompt += 'Set unchanged values to null and make sure you include new properties if they are specified in the prompt even if they do not exist in current properties. Please only return the json with the keys described and their values.';
+ const prompt = `Set unchanged values to null and make sure you include new properties if they are specified in the prompt even if they do not exist in current properties.
+ Please only return the json with the keys described and their values.`;
- try {
- const response = await openai.chat.completions.create({
+ return openai.chat.completions
+ .create({
model: 'gpt-4',
messages: [
- { role: 'system', content: prompt },
- { role: 'user', content: `Prompt: ${inputText}, Current properties: ${JSON.stringify(properties)}` },
+ { role: 'system', content: preamble + prompt },
+ { role: 'user', content: `Prompt: ${inputText}, Current properties: ${properties}` },
],
temperature: 0,
max_tokens: 1000,
+ })
+ .then(response => response.choices[0].message?.content ?? '')
+ .catch(err => {
+ console.log(err);
+ return 'Error connecting with API.';
});
- return response.choices[0].message?.content;
- } catch (err) {
- console.log(err);
- return 'Error connecting with API.';
- }
};
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 19f3c89ef..23032b62e 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { saveAs } from 'file-saver';
import * as JSZip from 'jszip';
import { action, runInAction } from 'mobx';
-import { ClientUtils } from '../../ClientUtils';
+import { ClientUtils, DashColor } from '../../ClientUtils';
import * as JSZipUtils from '../../JSZipUtils';
import { decycle } from '../../decycler/decycler';
import { DateField } from '../../fields/DateField';
@@ -36,11 +36,16 @@ import { DocumentView } from '../views/nodes/DocumentView';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
export namespace DocUtils {
+ function HasFunctionFilter(val: string) {
+ if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1;
+ // add other function filters here...
+ return undefined;
+ }
function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean {
let value = valueIn;
- const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string);
+ const hasFunctionFilter = HasFunctionFilter(value as string);
if (hasFunctionFilter) {
- return hasFunctionFilter(StrCast(doc[key]));
+ return hasFunctionFilter(doc, StrCast(doc[key]));
}
if (key === LinkedTo) {
// links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("..."))
@@ -67,9 +72,9 @@ export namespace DocUtils {
}
const vals = StrListCast(fieldVal); // list typing is very imperfect. casting to a string list doesn't mean that the entries will actually be strings
if (vals.length) {
- return vals.some(v => typeof v === 'string' && v.includes(value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+ return vals.some(v => typeof v === 'string' && v === (value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
- return Field.toString(fieldVal as FieldType).includes(value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+ return Field.toString(fieldVal as FieldType) === (value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
/**
* @param docs
@@ -163,7 +168,7 @@ export namespace DocUtils {
return rangeFilteredDocs;
}
- export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
+ export function MakeLink(source: Doc, target: Doc, linkSettings: { layout_isSvg?: boolean; link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
if (target.doc === Doc.UserDoc()) return undefined;
@@ -215,6 +220,7 @@ export namespace DocUtils {
link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
link_relationship: linkSettings.link_relationship,
link_description: linkSettings.link_description,
+ layout_isSvg: linkSettings.layout_isSvg,
x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't
y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't
link_autoMoveAnchors: true,
@@ -352,6 +358,16 @@ export namespace DocUtils {
return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined;
}
+ /**
+ * Adds items to the doc creator (':') context menu for creating each document type
+ * @param docTextAdder
+ * @param docAdder
+ * @param x
+ * @param y
+ * @param simpleMenu
+ * @param pivotField
+ * @param pivotValue
+ */
export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string | number | boolean): void {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
.filter(btnDoc => !btnDoc.hidden)
@@ -365,6 +381,7 @@ export namespace DocUtils {
newDoc.author = ClientUtils.CurrentUserEmail();
newDoc.x = x;
newDoc.y = y;
+ newDoc[DocData].backgroundColor = Doc.UserDoc().textBackgroundColor;
Doc.SetSelectOnLoad(newDoc);
if (pivotField) {
newDoc[pivotField] = pivotValue;
@@ -669,22 +686,47 @@ export namespace DocUtils {
export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout);
- const tbox = Docs.Create.TextDocument('', {
- annotationOn,
- backgroundColor,
- x,
- y,
- title,
- ...(defaultTextTemplate
- ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
- : {
- _width: width || 200,
- _height: 35,
- _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
- _layout_fitWidth: true,
- _layout_autoHeight: true,
- }),
- });
+ const tbox =
+ StrCast(Doc.UserDoc().fontFamily) === 'Math'
+ ? Docs.Create.EquationDocument('', {
+ //
+ annotationOn,
+ backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor),
+ x,
+ y,
+ title,
+ text_fontColor: StrCast(Doc.UserDoc().fontColor),
+ _width: 50,
+ _height: 50,
+ _yMargin: 10,
+ _xMargin: 10,
+ nativeWidth: 40,
+ nativeHeight: 40,
+ })
+ : Docs.Create.TextDocument('', {
+ annotationOn,
+ backgroundColor,
+ x,
+ y,
+ title,
+ ...(defaultTextTemplate
+ ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
+ : {
+ _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
+ _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
+ _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+ text_fitBox: BoolCast(Doc.UserDoc().fitBox),
+ text_align: StrCast(Doc.UserDoc().textAlign),
+ text_fontColor: StrCast(Doc.UserDoc().fontColor),
+ text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
+ text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
+ text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
+ }),
+ });
if (defaultTextTemplate) {
tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index efe73fbbe..8aa844c0b 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -26,7 +26,7 @@ export enum DocumentType {
SCRIPTING = 'script', // script editor
CHAT = 'chat', // chat with GPT about files
EQUATION = 'equation', // equation editor
- FUNCPLOT = 'funcplot', // function plotter
+ FUNCPLOT = 'function plot', // function plotter
MAP = 'map',
DATAVIZ = 'dataviz',
ANNOPALETTE = 'annopalette',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 52cd36401..891223952 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -197,8 +197,10 @@ export class DocumentOptions {
data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false);
data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false);
linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false);
- _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false);
- _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false);
+ _nativeWidth?: NUMt = new NumInfo('Deprecated: use nativeWidth. native width of document contents (e.g., the pixel width of an image)', false);
+ _nativeHeight?: NUMt = new NumInfo('Deprecated: use nativeHeight. native height of document contents (e.g., the pixel height of an image)', false);
+ nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false);
+ nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false);
acl?: STRt = new StrInfo('unused except as a display category in KeyValueBox');
acl_Guest?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions
@@ -281,7 +283,7 @@ export class DocumentOptions {
_layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
_layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false);
_layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button');
- _layout_isFlashcard?: BOOLt = new BoolInfo('whether comparison node should be displayed as a flashcard');
+ _layout_flashcardType?: STRt = new StrInfo('flashcard style to render in ComparisonBox. currently just "flashcard".');
_layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
_layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
@@ -294,7 +296,6 @@ export class DocumentOptions {
_yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false);
_xPadding?: NUMt = new NumInfo('x padding', false);
_yPadding?: NUMt = new NumInfo('y padding', false);
- _singleLine?: boolean; // whether label box is restricted to one line of text
_createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
_columnWidth?: NUMt = new NumInfo('width of table column', false);
_columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden');
@@ -302,10 +303,14 @@ export class DocumentOptions {
_caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true);
icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true);
icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true);
- _text_fontSize?: string;
- _text_fontFamily?: string;
- _text_fontWeight?: string;
- text_align?: STRt = new StrInfo('horizontal text alignment default');
+ text_fontSize?: string;
+ text_fontFamily?: string;
+ text_fontWeight?: string;
+ text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box");
+ text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']);
+ title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']);
+ title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
+ text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected');
fontSize?: string;
_pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
@@ -372,6 +377,8 @@ export class DocumentOptions {
config_panX?: NUMt = new NumInfo('panX saved as a view spec', false);
config_panY?: NUMt = new NumInfo('panY saved as a view spec', false);
config_zoom?: NUMt = new NumInfo('zoom saved as a view spec', false);
+ config_carousel_index?: NUMt = new NumInfo('saved carousel index', false);
+ config_card_curDoc?: DOCt = new DocInfo('current doc in a collection view, e.g., cardView');
config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false);
presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false);
presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
@@ -494,7 +501,6 @@ export class DocumentOptions {
sidebar_type_collection?: string; // collection type of text sidebar
data_dashboards?: List<FieldType>; // list of dashboards used in shareddocs;
- textTransform?: string;
letterSpacing?: string;
iconTemplate?: string; // name of icon template style
icon_fieldKey?: string; // specifies the icon template to use (e.g., icon_fieldKey='george', then the icon template's name is icon_george; otherwise, the template's name would be icon_<type> where type is the Doc's type(pdf,rich text, etc))
@@ -510,6 +516,10 @@ export class DocumentOptions {
card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+
+ ai?: string; // to mark items as ai generated
+ ai_firefly_seed?: number;
+ ai_firefly_prompt?: string;
}
export const DocOptions = new DocumentOptions();
@@ -821,7 +831,7 @@ export namespace Docs {
data_front: front ?? CenteredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', { text_placeholder: true, cloneOnCopy: true }, undefined),
data_back: back ?? CenteredTextCreator('answer', 'answer here', { text_placeholder: true, cloneOnCopy: true }, undefined),
_layout_fitWidth: true,
- _layout_isFlashcard: true,
+ _layout_flashcardType: 'flashcard',
title,
...options,
});
@@ -912,7 +922,7 @@ export namespace Docs {
const I = Doc.GetProto(ink);
// I.layout_hideOpenButton = true; // don't show open full screen button when selected
I.color = color;
- I.fillColor = fillColor;
+ I.fillColor = fillColor && fillColor !== 'transparent' ? fillColor : undefined;
I.stroke = new InkField(points);
I.stroke_width = strokeWidth;
I.stroke_bezier = strokeBezier;
@@ -922,8 +932,9 @@ export namespace Docs {
I.stroke_isInkMask = isInkMask;
I.text_align = 'center';
I.rotation = 0;
+ I.width_min = 1;
+ I.height_min = 1;
I.defaultDoubleClick = 'ignore';
- I.keepZWhenDragged = true;
I.author_date = new DateField();
I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
// I.acl_Override = SharingPermissions.Unset;
@@ -1083,7 +1094,7 @@ export namespace Docs {
}
export function AnnoPaletteDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyAnnos]), { ...(options || {}) });
+ return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyStickers]), { ...(options || {}) });
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx
index d0cd69273..d28b3a2c9 100644
--- a/src/client/util/CalendarManager.tsx
+++ b/src/client/util/CalendarManager.tsx
@@ -2,7 +2,7 @@ import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum';
import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { TextField } from '@mui/material';
-import { Button } from 'browndash-components';
+import { Button } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 30c75c659..0783bb80e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -4,7 +4,7 @@ import * as rp from 'request-promise';
import { ClientUtils, OmitKeys } from "../../ClientUtils";
import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
import { DocData } from "../../fields/DocSymbols";
-import { InkTool } from "../../fields/InkField";
+import { InkEraserTool, InkInkTool, InkProperty, InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
@@ -39,6 +39,8 @@ import { SelectionManager } from "./SelectionManager";
import { ColorScheme } from "./SettingsManager";
import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
+import { DocumentView } from "../views/nodes/DocumentView";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
export interface Button {
// DocumentOptions fields a button can set
@@ -66,6 +68,21 @@ export interface Button {
subMenu?: Button[];
}
+// Not really necessary, but for now, all tags should start with a capital first letter
+export type TagName<T extends string> =
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ T extends `${infer First}${infer Rest}`
+ ? First extends Uppercase<First>
+ ? First extends Lowercase<First>
+ ? never // If it's the same when uppercased and lowercased, it's not a letter.
+ : T // Otherwise, it's a valid capitalized string.
+ : never
+ : never;
+export function ToTagName(key: string):"Tag"{
+ return ((str => str[0].toUpperCase() + str.slice(1))(key.startsWith('#') ? key.substring(1) : key)) as "Tag";
+}
+
+
export let resolvedPorts: { server: number, socket: number };
export class CurrentUserUtils {
@@ -160,9 +177,9 @@ export class CurrentUserUtils {
return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts);
}
- static setupAnnoPalette(doc: Doc, field="myAnnos") {
+ static setupAnnoPalette(doc: Doc, field="myStickers") {
const reqdOpts:DocumentOptions = {
- title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true,
+ title: "Stickers", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true,
_dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true,
_layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
@@ -186,7 +203,7 @@ export class CurrentUserUtils {
const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
- layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+ layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
});
const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts });
const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
@@ -268,7 +285,7 @@ export class CurrentUserUtils {
MakeTemplate(Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }),
- Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
+ Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) })
], {...opts, title: "Slide View Template"}));
const plotlyApi = () => {
let plotly = Doc.MyPublishedDocs.find(fdoc => fdoc.title === "@plotly");
@@ -371,13 +388,13 @@ pie title Minerals in my tap water
{key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }},
{key: "Flashcard", creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}},
{key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }},
- {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 50, _height: 50, nativeWidth: 40, nativeHeight: 40, _xMargin: 10, _yMargin: 10}},
{key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
{key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }},
{key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _width: 300, _height: 300 }},
- {key: "Diagram", creator: Docs.Create.DiagramDocument, opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}},
+ {key: "Diagram", creator: opts => Docs.Create.DiagramDocument("", opts), opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, _layout_fitWidth: true, }},
{key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, }},
@@ -392,7 +409,7 @@ pie title Minerals in my tap water
{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" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
{key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
- treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ treeView_HasOverlay: true, text_fontSize: "20px", _layout_autoHeight: true,
dropAction:dropActionType.move, treeView_Type: TreeViewType.outline,
backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true
}, funcs: {title: 'this.text?.Text'}},
@@ -687,16 +704,12 @@ pie title Minerals in my tap water
{ title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
- static cardTools(): Button[] {
+ static sortTools(): Button[] {
return [
{ title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ 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: "Pile", icon:"layer-group", toolTip:"View the cards as a pile in the free form view", btnType: ButtonType.ClickButton, expertMode: false, toolType:"pile", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Chat Popup",icon:"lightbulb", toolTip:"Toggle the chat popup's visibility", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
- { title: "Show Tags", icon:"id-card", toolTip:"Toggle tag annotation panel", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-tags",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_);}'} },
@@ -704,38 +717,44 @@ pie title Minerals in my tap water
]},
]
}
+
+ static filterBtnDesc<T extends string>(tag:TagName<T>|"Tag", icon:IconProp):Button {
+ return { title: tag, isSystem: false, icon: icon.toString(), toolTip:`Click to toggle visibility of ${tag} tagged Docs`, btnType: ButtonType.ToggleButton, expertMode: false, toolType:`#${tag.toLowerCase()}`, funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}}
+ }
- static tagGroupTools(): Button[] {
- const defaultTagButtonDescs = [
- { title: "Star", isSystem: false,icon: "star", toolTip:"Click to toggle visibility of Star tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#star", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
- { title: "Like", isSystem: false,icon: "heart", toolTip:"Click to toggle visibility of Like tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#like", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
- { title: "Todo", isSystem: false,icon: "bolt", toolTip:"Click to toggle visibility of Todo tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#todo", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
- { title: "Idea", isSystem: false,icon: "cloud", toolTip:"Click to toggle visibility of Idea tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#idea", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
+ static filterTools(): Button[] {
+ // If there's no active dashboard, then a default set of tags are added, otherwise, the user controls which tags are kept
+ const tagButtonDescs = Doc.UserDoc().activeDashboard ? [] : [
+ this.filterBtnDesc("Star", "star"),
+ this.filterBtnDesc("Like", "heart"),
+ this.filterBtnDesc("Todo", "bolt"),
+ this.filterBtnDesc("Idea", "cloud"),
+ this.filterBtnDesc("Chat", "robot")
];
- // hack: if there's no dashboard, create default filters. otherwise, just make sure that the Options button is preserved
return [
- { title:"Options",isSystem: true,icon: "gear", toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}},
- ...(Doc.UserDoc().activeDashboard ? [] : defaultTagButtonDescs)
+ { title:"Options",isSystem: true,icon: "gear", toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}},
+ ...tagButtonDescs
]
- }
+ }
static viewTools(): Button[] {
return [
- { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Fit All", icon: "object-group", toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
- { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Show Tags", icon: "id-card", toolTip: "Toggle tags", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)",
+ btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
+ { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
static textTools():Button[] {
return [
{ title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
- btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
- { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
+ btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Math"]) },
+ { title: " Size", toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
{ title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
{ title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
{ title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
- { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italic", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -746,6 +765,7 @@ pie title Minerals in my tap water
{ title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
]},
+ { title: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
@@ -757,24 +777,27 @@ pie title Minerals in my tap water
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
- { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
+ { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },
+ subMenu: [
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: InkInkTool.Pen, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+ { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: InkInkTool.Write, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
+ ]},
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40},
+ { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
subMenu: [
- { title: "Stroke", toolTip: "Stroke Erase", btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
- { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark", toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
- { title: "Radius", toolTip: "Radius Erase", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+ { title: "Stroke", toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkEraserTool.Stroke, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+ { title: "Segment", toolTip: "Erase between intersections",btnType:ButtonType.ToggleButton,icon:"xmark", toolType:InkEraserTool.Segment, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+ { title: "Area", toolTip: "Erase like a pencil", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
]},
- { title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
- { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
- { title: "Labels", toolTip: "Labels", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
- { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
- { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
- { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
+ { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40},
+ { title: "Mask", toolTip: "Make Stroke a Stencil Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", toolType: InkProperty.Mask, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
+ { title: "Labels", toolTip: "Show Labels Inside Shapes", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: InkProperty.Labels, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
+ { title: "Smart Draw", toolTip: "Draw with AI", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
];
}
@@ -809,25 +832,24 @@ 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, _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_); }'} },
- { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
- { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
-
- { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+ title: "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_); }'} },
+ { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+ { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "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: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "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
- { title: "Card", icon: "Card", toolTip: "Card View Tools", subMenu: CurrentUserUtils.cardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- // { title: "Create", icon: "Create", toolTip: "Assign card labels", subMenu: CurrentUserUtils.labelTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
{ title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
{ title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
@@ -862,10 +884,10 @@ pie title Minerals in my tap water
}
// linear view
const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick,
+ childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick,
linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
- const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) );
+ const items = (menuBtn?:Doc) => !menuBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtn) );
const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList;
const btnDoc = DocUtils.AssignScripts( DocUtils.AssignDocField(menuDoc, StrCast(params.title),
(opts) => creator(opts, items(menuBtnDoc)), reqdSubMenuOpts, items(menuBtnDoc)), params.scripts, params.funcs);
@@ -986,15 +1008,23 @@ pie title Minerals in my tap water
Doc.noviceMode ?? (Doc.noviceMode = true);
doc._showLabel ?? (doc._showLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
+ doc.textBackgroundColor ?? (doc.textBackgroundColor = Colors.LIGHT_GRAY);
doc.activeTool = InkTool.None;
doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
- doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
- doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
- doc.activeInkWidth ?? (doc.activeInkWidth = 1);
- doc.activeInkBezier ?? (doc.activeInkBezier = "0");
- doc.activeFillColor ?? (doc.activeFillColor = "");
- doc.activeArrowStart ?? (doc.activeArrowStart = "");
- doc.activeArrowEnd ?? (doc.activeArrowEnd = "");
+ doc.activeHideTextLabels ?? (doc.activeHideTextLabels = false);
+ doc[`active${InkInkTool.Pen}Color`] ?? (doc[`active${InkInkTool.Pen}Color`] = "rgb(0, 0, 0)");
+ doc[`active${InkInkTool.Pen}Width`] ?? (doc[`active${InkInkTool.Pen}Width`] = 2);
+ doc[`active${InkInkTool.Pen}Bezier`] ?? (doc[`active${InkInkTool.Pen}Bezier`] = "0");
+ doc[`active${InkInkTool.Write}Color`] ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
+ doc[`active${InkInkTool.Write}Width`] ?? (doc[`active${InkInkTool.Write}Width`] = 1);
+ doc[`active${InkInkTool.Write}Bezier`] ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+ doc[`active${InkInkTool.Highlight}Color`] ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
+ doc[`active${InkInkTool.Highlight}Width`] ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
+ doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
+ doc[`active${InkInkTool.Highlight}Fill`] ?? (doc[`active${InkInkTool.Highlight}Fill`] = "rgba(0, 255, 255, 0.4)");
+ doc.activeInkTool ?? (doc.activeInkTool = InkInkTool.Pen);
+ doc.activeEraserTool ?? (doc.activeEraserTool = InkEraserTool.Stroke);
+ doc.activeEraserWidth ?? (doc.activeEraserWidth = 20);
doc.activeDash ?? (doc.activeDash === "0");
doc.fontSize ?? (doc.fontSize = "12px");
doc.fontFamily ?? (doc.fontFamily = "Arial");
@@ -1129,7 +1159,9 @@ pie title Minerals in my tap water
}
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser");
+ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs");
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 831afe538..897366757 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -71,7 +71,6 @@ export namespace DictationManager {
let current: string | undefined;
let sessionResults: string[] = [];
- // eslint-disable-next-line new-cap
const recognizer: Opt<SpeechRecognition> = webkitSpeechRecognition ? new webkitSpeechRecognition() : undefined;
export type InterimResultHandler = (results: string) => void;
@@ -257,7 +256,6 @@ export namespace DictationManager {
if (entry) {
let success = false;
const { restrictTo } = entry;
- // eslint-disable-next-line no-restricted-syntax
for (const target of targets) {
if (!restrictTo || validate(target, restrictTo)) {
// eslint-disable-next-line no-await-in-loop
@@ -268,7 +266,6 @@ export namespace DictationManager {
return success;
}
- // eslint-disable-next-line no-restricted-syntax
for (const depEntry of Dependent) {
const regex = depEntry.expression;
const matches = regex.exec(phrase);
@@ -276,7 +273,6 @@ export namespace DictationManager {
if (matches !== null) {
let success = false;
const { restrictTo } = depEntry;
- // eslint-disable-next-line no-restricted-syntax
for (const target of targets) {
if (!restrictTo || validate(target, restrictTo)) {
// eslint-disable-next-line no-await-in-loop
@@ -307,7 +303,6 @@ export namespace DictationManager {
};
const validate = (target: DocumentView, types: DocumentType[]) => {
- // eslint-disable-next-line no-restricted-syntax
for (const type of types) {
if (tryCast(target, type)) {
return true;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4ab2e8d05..acb35f7eb 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -8,7 +8,7 @@ import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
import { AudioField } from '../../fields/URLField';
import { CollectionViewType } from '../documents/DocumentTypes';
import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
-import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../views/nodes/FocusViewOptions';
import { OpenWhere } from '../views/nodes/OpenWhere';
import { PresBox } from '../views/nodes/trails';
@@ -256,7 +256,11 @@ export class DocumentManager {
Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
- if (DocumentView.activateTabView(docContextPath[0])) options.toggleTarget = false;
+ let activatedTab = false;
+ if (DocumentView.activateTabView(docContextPath[0])) {
+ options.toggleTarget = false;
+ activatedTab = true;
+ }
const rootContextView =
docContextPath.length &&
@@ -268,7 +272,7 @@ export class DocumentManager {
return;
}
options.didMove = true;
- (!DocumentView.LightboxDoc() && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+ (!DocumentView.LightboxDoc() && (activatedTab || docContextPath.some(doc => DocumentView.activateTabView(doc)))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
}));
if (options.openLocation?.includes(OpenWhere.lightbox)) {
@@ -348,21 +352,18 @@ export class DocumentManager {
// if there's an options.effect, it will be handled from linkFollowHighlight. We delay the start of
// the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
// bcz: should this delay be an options parameter?
- setTimeout(
- () => {
- Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
- if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
- // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
- contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
- DocumentManager._overlayViews.add(contextView);
- }
- Doc.AddUnHighlightWatcher(() => {
- docView.Document[Animation] = undefined;
- DocumentManager.removeOverlayViews();
- });
- },
- (options.zoomTime ?? 0) * 0.5
- );
+ setTimeout(() => {
+ Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
+ if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
+ // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+ contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
+ DocumentManager._overlayViews.add(contextView);
+ }
+ Doc.AddUnHighlightWatcher(() => {
+ docView.Document[Animation] = undefined;
+ DocumentManager.removeOverlayViews();
+ });
+ }, FocusEffectDelay(options));
if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.Document._layout_currentTimecode));
if (options.playAudio) DocumentManager.playAudioAnno(docView.Document);
if (options.toggleTarget && (!options.didMove || docView.Document.hidden)) docView.Document.hidden = !docView.Document.hidden;
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 9d0817a06..1ec85c9d9 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Size, Type } from 'browndash-components';
+import { Button, IconButton, Size, Type } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 88d73d742..cfeaf02d7 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Size, Type } from 'browndash-components';
+import { Button, IconButton, Size, Type } from '@dash/components';
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4231c2ca8..ca1cb8014 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -109,7 +109,7 @@ export namespace InteractionUtils {
dash: string | undefined,
scalexIn: number,
scaleyIn: number,
- shape: Gestures,
+ shape: Gestures | undefined,
pevents: Property.PointerEvents,
opacity: number,
nodefs: boolean,
@@ -136,7 +136,7 @@ export namespace InteractionUtils {
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
return (
- <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ <svg fill={color || 'transparent'} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
{' '}
{/* setting the svg fill sets the arrowStart fill */}
{nodefs ? null : (
@@ -186,7 +186,7 @@ export namespace InteractionUtils {
opacity: 1.0,
// opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents,
- stroke: color ?? 'rgb(0, 0, 0)',
+ stroke: (color ?? 'rgb(0, 0, 0)') || 'transparent',
strokeWidth,
strokeLinecap: strokeLineCap,
strokeDasharray: dashArray,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 0a3a0ba49..0e67dcfaa 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -113,12 +113,10 @@ export class LinkFollower {
}
const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
- // eslint-disable-next-line prefer-destructuring
target.x = moveTo[0];
movedTarget = true;
}
if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
- // eslint-disable-next-line prefer-destructuring
target.y = moveTo[1];
movedTarget = true;
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1ab84421c..a1f2849cd 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,7 +88,8 @@ ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean,
return DocumentView.Selected().lastElement()?._props.renderDepth === 0;
}
const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
- return selected?.type === type || selected?.type_collection === type || !type;
+ const matchOverlayFreeform = type === CollectionViewType.Freeform && DocumentView.Selected().lastElement()?.ComponentView?.annotationKey;
+ return matchOverlayFreeform || selected?.type === type || selected?.type_collection === type || !type;
});
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function deselectAll() {
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9200d68db..5d041f7b4 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Colors, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from '@dash/components';
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -45,7 +45,6 @@ export class SettingsManager extends React.Component<object> {
public closeMgr = action(() => {
this._isOpen = false;
});
- // eslint-disable-next-line react/no-unused-class-component-methods
public openMgr = action(() => {
this._isOpen = true;
});
@@ -269,9 +268,9 @@ export class SettingsManager extends React.Component<object> {
formLabelPlacement="right"
toggleType={ToggleType.SWITCH}
onClick={() => {
- Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels;
+ Doc.UserDoc().activeHideTextLabels = !Doc.UserDoc().activeHideTextLabels;
}}
- toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+ toggleStatus={BoolCast(Doc.UserDoc().activeHideTextLabels)}
size={Size.XSMALL}
color={SettingsManager.userColor}
/>
@@ -354,6 +353,27 @@ export class SettingsManager extends React.Component<object> {
Doc.UserDoc().fontSize = val + 'px';
}}
/>
+ <ColorPicker
+ color={SettingsManager.userColor}
+ type={Type.PRIM}
+ defaultPickerType="Classic"
+ selectedColor={StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY)}
+ background={SnappingManager.userBackgroundColor}
+ icon={<FontAwesomeIcon icon="palette" size="lg" />}
+ tooltip="default text background color"
+ label="background"
+ setSelectedColor={value => {
+ Doc.UserDoc().textBackgroundColor = value;
+ // if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+ // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ }}
+ setFinalColor={value => {
+ Doc.UserDoc().textBackgroundColor = value;
+ // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ // this.colorBatch?.end();
+ // this.colorBatch = undefined;
+ }}
+ />
<Dropdown
items={fontFamilies.map(val => ({
text: val,
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 117d7935e..efc8e79a6 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Size, Type } from 'browndash-components';
+import { Button, IconButton, Size, Type } from '@dash/components';
import { concat, intersection } from 'lodash';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 5f6c7d9ac..2a150dc5a 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
import { observable, action, runInAction, makeObservable } from 'mobx';
+import { Gestures } from '../../pen-gestures/GestureTypes';
export enum freeformScrollMode {
Pan = 'pan',
@@ -29,6 +30,8 @@ export class SnappingManager {
@observable _propertyWid: number = 0;
@observable _printToConsole: boolean = false;
@observable _hideDecorations: boolean = false;
+ @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode
+ @observable _inkShape: Gestures | undefined = undefined;
private constructor() {
SnappingManager._manager = this;
@@ -61,6 +64,8 @@ export class SnappingManager {
public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
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 SetLongPress = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore
public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
@@ -78,6 +83,8 @@ export class SnappingManager {
public static SetPropertiesWidth= (wid:number) =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
public static SetPrintToConsole = (state:boolean) =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
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 userColor: string | undefined;
public static userVariantColor: string | undefined;
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 4aef28e6b..7ef370d48 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -691,8 +691,9 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
}
case 'path': {
const coordList: Point[] = [];
- const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
- coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ const [startX, startY] = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/).slice(1);
+ const startPt = { X: parseInt(startX), Y: parseInt(startY) };
+ coordList.push(startPt);
const matches: RegExpMatchArray[] = Array.from(
attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
);
@@ -719,10 +720,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
}
});
const hasZ = attributes.d.match(/Z/);
- if (hasZ) {
+ if (hasZ || attributes.fill) {
coordList.push(lastPt);
- coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
- coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ coordList.push(startPt);
+ coordList.push(startPt);
} else {
coordList.pop();
}
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
index c969f9036..a6b5911f7 100644
--- a/src/client/util/reportManager/ReportManager.tsx
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/no-unused-class-component-methods */
import { Octokit } from '@octokit/core';
-import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
+import { Button, Dropdown, DropdownType, IconButton, Type } from '@dash/components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/ComponentDecorations.tsx b/src/client/views/ComponentDecorations.tsx
index 929b549e0..28e9d9792 100644
--- a/src/client/views/ComponentDecorations.tsx
+++ b/src/client/views/ComponentDecorations.tsx
@@ -5,11 +5,7 @@ import { DocumentView } from './nodes/DocumentView';
@observer
export class ComponentDecorations extends React.Component<{ boundsTop: number; boundsLeft: number }, { value: string }> {
- // eslint-disable-next-line no-use-before-define
- static Instance: ComponentDecorations;
-
render() {
- const seldoc = DocumentView.Selected().lastElement();
- return seldoc?.ComponentView?.componentUI?.(this.props.boundsLeft, this.props.boundsTop) ?? null;
+ return DocumentView.Selected().map(seldoc => seldoc?.ComponentView?.componentUI?.(this.props.boundsLeft, this.props.boundsTop) ?? null);
}
}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 1931d7c2a..eae45221c 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -142,6 +142,7 @@ export class ContextMenu extends ObservableReactComponent<{ noexpand?: boolean }
this.clearItems();
this._display = false;
this._shouldDisplay = false;
+ this._selectedIndex = -1;
return wasOpen;
};
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 6f8f41bdd..218718b18 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { SnappingManager } from '../util/SnappingManager';
@@ -26,7 +26,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
_hoverTimeout?: NodeJS.Timeout;
_overPosY = 0;
_overPosX = 0;
- @observable _items: ContextMenuProps[] = [];
+ @observable.shallow _items: ContextMenuProps[] = [];
@observable _overItem = false;
constructor(props: ContextMenuProps & { selected?: boolean }) {
@@ -34,8 +34,8 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
makeObservable(this);
}
- componentDidMount() {
- runInAction(() => this._items.push(...(this._props.subitems ?? [])));
+ @computed get items() {
+ return this._items.concat(this._props.subitems ?? []);
}
handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
@@ -91,7 +91,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
};
render() {
- const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
+ const submenu = this.items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>;
}
}
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index 90f64b393..25feca7bf 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -1,31 +1,39 @@
@import './global/globalCssVariables.module';
+$dashboard-left-menu-width: 250px;
+$dashboard-view-padding: 20px;
+$dashboard-container-height: 200px;
+$dashboard-container-width: 250px;
+
.dashboard-view {
- padding: 50px;
display: flex;
flex-direction: row;
width: 100%;
- position: absolute;
height: 100%;
- width: 100%;
- padding-right: 0px;
+ position: absolute;
overflow: auto;
.left-menu {
display: flex;
justify-content: flex-start;
flex-direction: column;
- width: 250px;
- min-width: 250px;
+ position: fixed;
+ min-width: $dashboard-left-menu-width;
gap: 5px;
+ padding: $dashboard-view-padding;
}
.all-dashboards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
- overflow-y: auto;
width: 100%;
+ height: fit-content;
+ justify-content: flex-start;
+ align-items: flex-start;
+ padding: $dashboard-view-padding 0px 0px $dashboard-left-menu-width;
+ gap: 10px;
+ margin-bottom: 60px;
}
}
@@ -48,13 +56,12 @@
.dashboard-container-new {
border-radius: 10px;
- width: 250px;
- height: 200px;
+ width: $dashboard-container-width;
+ height: $dashboard-container-height;
font-size: 120px;
font-weight: 100;
text-align: center;
border: solid 2px $light-gray;
- margin: 0 0px 30px 30px;
cursor: pointer;
color: $light-gray;
display: flex;
@@ -82,12 +89,12 @@
border-radius: 10px;
position: relative;
cursor: pointer;
- width: 250px;
- height: 200px;
+ width: $dashboard-container-width;
+ height: $dashboard-container-height;
outline: solid 2px $light-gray;
+ outline-offset: -2px;
display: flex;
flex-direction: column;
- margin: 0 0px 30px 30px;
overflow: hidden;
&:hover {
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 448178397..7f0118ed3 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components';
+import { Button, ColorPicker, EditableText, Size, Type } from '@dash/components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/DictationButton.tsx b/src/client/views/DictationButton.tsx
new file mode 100644
index 000000000..0ce586df4
--- /dev/null
+++ b/src/client/views/DictationButton.tsx
@@ -0,0 +1,57 @@
+import { IconButton, Type } from '@dash/components';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { BiMicrophone } from 'react-icons/bi';
+import { DictationManager } from '../util/DictationManager';
+import { SnappingManager } from '../util/SnappingManager';
+
+export interface DictationButtonProps {
+ setInput: (val: string) => void;
+ inputRef?: HTMLInputElement | null | undefined;
+}
+@observer
+export class DictationButton extends React.Component<DictationButtonProps> {
+ @observable private _isRecording = false;
+ constructor(props: DictationButtonProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ stopDictation = action(() => {
+ this._isRecording = false;
+ DictationManager.Controls.stop();
+ });
+
+ render() {
+ return (
+ <IconButton
+ type={Type.TERT}
+ color={this._isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
+ tooltip="Record"
+ icon={<BiMicrophone size="16px" />}
+ onClick={action(() => {
+ if (!this._isRecording) {
+ this._isRecording = true;
+ DictationManager.Controls.listen({
+ interimHandler: (value: string) => {
+ this.props.setInput(value);
+ if (this.props.inputRef) {
+ this.props.inputRef.focus();
+ this.props.inputRef.scrollLeft = 1000000;
+ }
+ },
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ });
+ } else {
+ this.stopDictation();
+ }
+ })}
+ />
+ );
+ }
+}
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
index e33049d3b..66831ec7f 100644
--- a/src/client/views/DictationOverlay.tsx
+++ b/src/client/views/DictationOverlay.tsx
@@ -14,7 +14,6 @@ export class DictationOverlay extends React.Component {
@observable private _dictationDisplayState = false;
@observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
- // eslint-disable-next-line react/no-unused-class-component-methods
public hasActiveModal = false;
constructor(props: object) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 32bf67df1..a9f03a658 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -2,7 +2,7 @@ import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { Popup } from 'browndash-components';
+import { Popup } from '@dash/components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -314,6 +314,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
);
}
+ @computed
+ get aiEditorButton() {
+ const targetDoc = this.view0?.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
+ <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
+ </div>
+ </Tooltip>
+ );
+ }
+
@observable _isRecording = false;
_stopFunc: () => void = emptyFunction;
@computed
@@ -484,6 +496,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
<div className="documentButtonBar-button">{this.calendarButton}</div>
+ <div className="documentButtonBar-button">{this.aiEditorButton}</div>
<div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 5a48b6c62..54ff3904d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -181,7 +181,13 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
};
onBackgroundDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(false, moveEv), emptyFunction, emptyFunction);
+ setupMoveUpEvents(
+ this,
+ e,
+ moveEv => this.onBackgroundMove(false, moveEv),
+ emptyFunction,
+ (clickEv, doubleTap) => doubleTap && DocumentView.Selected().some(dv => dv.Document.layout_isSvg) && (InkStrokeProperties.Instance._controlButton = true)
+ );
e.stopPropagation();
};
@action
@@ -232,9 +238,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (iconViewDoc.activeFrame) {
iconViewDoc.opacity = 0; // if in an animation collection, set opacity to 0 to allow inkMasks and other documents to remain in the collection and to smoothly animate when they are activated in a different animation frame
} else {
- // if Doc is in the annotation palette, remove the flag indicating that it's saved
+ // if Doc is in the sticker palette, remove the flag indicating that it's saved
const dragFactory = DocCast(iconView.Document.dragFactory);
- if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+ if (dragFactory && DocCast(dragFactory.cloneOf).savedAsSticker) DocCast(dragFactory.cloneOf).savedAsSticker = undefined;
// if this is a face Annotation doc, then just hide it.
if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
@@ -500,7 +506,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
// determines how much to resize, and determines the resize reference point
//
getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => {
- const [w, h] = [this.Bounds.r - this.Bounds.x, this.Bounds.b - this.Bounds.y];
+ const [w, h] = [Math.max(1, this.Bounds.r - this.Bounds.x), Math.max(1, this.Bounds.b - this.Bounds.y)];
const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y];
switch (dragHdl) {
case 'topLeft': return { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.Bounds.r, this.Bounds.b] };
@@ -554,14 +560,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
}
}
if (['bottom', 'top'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeHeight(doc)) {
- const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight;
+ const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && (!doc.layout_reflowVertical || opts.ctrlKey);
doc._nativeHeight = scale.y * Doc.NativeHeight(doc);
if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight));
}
- doc._width = Math.max(1, NumCast(doc._width) * scale.x);
- doc._height = Math.max(1, NumCast(doc._height) * scale.y);
- const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth, initHeight);
+ doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
+ doc._height = Math.max(NumCast(doc._height_min), NumCast(doc._height) * scale.y);
+ const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
doc.x = NumCast(doc.x) + deltaX;
doc.y = NumCast(doc.y) + deltaY;
@@ -791,7 +797,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
transformOrigin,
background: SnappingManager.ShiftKey ? undefined : 'yellow',
pointerEvents: SnappingManager.ShiftKey || SnappingManager.IsResizing ? 'none' : 'all',
- display: DocumentView.Selected().length <= 1 || hideDecorations ? 'none' : undefined,
+ display: DocumentView.Selected().length <= 1 || InkStrokeProperties.Instance._controlButton || hideDecorations ? 'none' : undefined,
transform: `rotate(${rotation}deg)`,
}}
onPointerDown={this.onBackgroundDown}
@@ -855,7 +861,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
className="documentDecorations-tagsView"
style={{
- top: `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ top: 30, // offset by height of documentButtonBar so that items can be clicked without overlap interference
transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
}}>
{DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null}
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 11425e477..99738052d 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -102,7 +102,7 @@ const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, sel
return (
<div
- className={`filterHotKey-button`}
+ className="filterHotKey-button"
onClick={e => {
e.stopPropagation();
state.startEditing();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index afeecaa63..777a34ebc 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,9 +2,9 @@ import * as fitCurve from 'fit-curve';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction, intersectRect } from '../../Utils';
-import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { NumCast } from '../../fields/Types';
import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -14,27 +14,25 @@ import { DocumentType } from '../documents/DocumentTypes';
import { Docs } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
+import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import './GestureOverlay.scss';
import { InkingStroke } from './InkingStroke';
import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import {
- ActiveArrowEnd,
- ActiveArrowScale,
- ActiveArrowStart,
- ActiveDash,
- ActiveFillColor,
- ActiveInkBezierApprox,
+ ActiveInkArrowEnd,
+ ActiveInkArrowScale,
+ ActiveInkArrowStart,
ActiveInkColor,
+ ActiveInkDash,
+ ActiveInkFillColor,
ActiveInkWidth,
DocumentView,
- SetActiveArrowStart,
- SetActiveDash,
- SetActiveFillColor,
+ SetActiveInkArrowStart,
SetActiveInkColor,
+ SetActiveInkDash,
+ SetActiveInkFillColor,
SetActiveInkWidth,
} from './nodes/DocumentView';
export enum ToolglassTools {
@@ -57,19 +55,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
// eslint-disable-next-line no-use-before-define
static Instances: GestureOverlay[] = [];
- @observable public InkShape: Opt<Gestures> = undefined;
@observable public SavedColor?: string = undefined;
@observable public SavedWidth?: number = undefined;
@observable public Tool: ToolglassTools = ToolglassTools.None;
- @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
@observable private _thumbX?: number = undefined;
@observable private _thumbY?: number = undefined;
@observable private _pointerY?: number = undefined;
@observable private _points: { X: number; Y: number }[] = [];
- @observable private _strokes: InkData[] = [];
- @observable private _palette?: JSX.Element = undefined;
@observable private _clipboardDoc?: JSX.Element = undefined;
+ @observable private _debugCusps: { X: number; Y: number }[] = [];
+ @observable private _debugGestures = false;
@computed private get height(): number {
return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
@@ -95,8 +91,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
@action
onPointerDown = (e: React.PointerEvent) => {
+ (document.activeElement as HTMLElement)?.blur();
if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
- if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (Doc.ActiveTool === InkTool.Ink) {
this._points.push({ X: e.clientX, Y: e.clientY });
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
}
@@ -122,13 +119,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
};
@action primCreated() {
- if (!this.KeepPrimitiveMode) {
- this.InkShape = undefined;
- // get out of ink mode after each stroke=
- // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+ if (!SnappingManager.KeepGestureMode) {
+ SnappingManager.SetInkShape(undefined);
Doc.ActiveTool = InkTool.None;
- // SetActiveArrowStart('none');
- // SetActiveArrowEnd('none');
}
}
/**
@@ -179,9 +172,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
* Determines if what the array of cusp/intersection data corresponds to a scribble.
* true if there are at least 4 cusps and either:
* 1) the initial and final quarters of the array contain objects
- * 2) or half of the cusps contain objects
+ * 2) or a declining percentage (ranges from 0.5 to 0.2 - based on the number of cusps) of cusp lines intersect strokes
* @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
- * @returns
+ * @returns truthy if it's a scribble
*/
determineIfScribble = (intersectArray: boolean[]) => {
const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps
@@ -191,7 +184,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}), { start: false, end: false }); // prettier-ignore
const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
- return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+ return intersectArray.length > 3 && (percentCuspsWithContent >= Math.max(0.2, 1 / (intersectArray.length - 1)) || (start && end));
};
/**
* determines if inks intersect
@@ -244,27 +237,29 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this.dispatchGesture(Gestures.Stroke);
};
@action
- onPointerUp = () => {
- const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
- DocumentView.DownDocView = undefined;
+ onPointerUp = (e: PointerEvent) => {
+ const ffView = CollectionFreeFormView.DownFfview;
+ CollectionFreeFormView.DownFfview = undefined;
if (this._points.length > 1) {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
const { Name, Score } =
- (this.InkShape
- ? new Result(this.InkShape, 1, Date.now)
+ (SnappingManager.InkShape
+ ? new Result(SnappingManager.InkShape, 1, Date.now)
: Doc.UserDoc().recognizeGestures && points.length > 2
? GestureUtils.GestureRecognizer.Recognize([points])
: undefined) ??
new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
const cuspArray = this.getCusps(points);
+ const rect = this._overlayRef.current?.getBoundingClientRect();
+ this._debugCusps = rect ? cuspArray.map(p => ({ X: p.X + B.left - rect?.left, Y: p.Y + B.top - rect.top })) : [];
// if any of the shape is activated in the CollectionFreeFormViewChrome
// need to decide when to turn gestures back on
const actionPerformed = ((name: Gestures) => {
switch (name) {
case Gestures.Line:
- if (cuspArray.length > 2) return undefined;
+ if (cuspArray.length > 2 && Score < 1) return undefined;
// eslint-disable-next-line no-fallthrough
case Gestures.Triangle:
case Gestures.Rectangle:
@@ -280,13 +275,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
if (!actionPerformed) {
const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+ this.dryInk();
if (scribbledOver) {
- undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
- } else {
- this.dryInk();
+ // can undo the erase without undoing the scribble, or undo a second time to undo the scribble
+ setTimeout(undoable(() => ffView.removeDocument(scribbledOver.concat([ffView.childDocs.lastElement()])), 'scribble erase'));
}
}
+ } else {
+ ffView?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
+ e.preventDefault();
}
+ this.primCreated();
this._points.length = 0;
};
/**
@@ -340,13 +339,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
getCusps(points: InkData) {
const arrayOfPoints: { X: number; Y: number }[] = [];
arrayOfPoints.push(points[0]);
- for (let i = 0; i < points.length - 2; i++) {
+ for (let i = 0; i < points.length - 4; i++) {
const point1 = points[i];
- const point2 = points[i + 1];
- const point3 = points[i + 2];
+ const point2 = points[i + 2];
+ const point3 = points[i + 4];
if (this.find_angle(point1, point2, point3) < 90) {
// NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles
arrayOfPoints.push(point2);
+ i += 2;
}
}
arrayOfPoints.push(points[points.length - 1]);
@@ -542,7 +542,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
get elements() {
- const selView = DocumentView.DownDocView;
+ const selView = CollectionFreeFormView.DownFfview;
const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1);
const rect = this._overlayRef.current?.getBoundingClientRect();
const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(this._points, true);
@@ -552,97 +552,38 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
B.bottom += width / 2;
B.width += width;
B.height += width;
- const fillColor = ActiveFillColor();
+ const fillColor = ActiveInkFillColor();
const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor();
return [
this.props.children,
- this._palette,
- [
- this._strokes.map((l, i) => {
- const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
- return (
- <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
- {InteractionUtils.CreatePolyline(
- l,
- b.left,
- b.top,
- strokeColor,
- width,
- width,
- 'miter',
- 'round',
- ActiveInkBezierApprox(),
- 'none' /* ActiveFillColor() */,
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveArrowScale(),
- ActiveDash(),
- 1,
- 1,
- this.InkShape as Gestures,
- 'none',
- 1.0,
- false
- )}
- </svg>
- );
- }),
- this._points.length <= 1 ? null : (
- <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
- {InteractionUtils.CreatePolyline(
- this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
- B.left,
- B.top,
- ActiveInkColor(),
- width,
- width,
- 'miter',
- 'round',
- '',
- 'none' /* ActiveFillColor() */,
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveArrowScale(),
- ActiveDash(),
- 1,
- 1,
- this.InkShape as Gestures,
- 'none',
- 1.0,
- false
- )}
- </svg>
- ),
- ],
+ this._points.length <= 1 ? null : (
+ <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
+ {InteractionUtils.CreatePolyline(
+ this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
+ B.left,
+ B.top,
+ strokeColor,
+ width,
+ width,
+ 'miter',
+ 'round',
+ '',
+ 'none' /* ActiveFillColor() */,
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkArrowScale(),
+ ActiveInkDash(),
+ 1,
+ 1,
+ SnappingManager.InkShape,
+ 'none',
+ 1.0,
+ false
+ )}
+ </svg>
+ ),
];
}
- screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1);
- return300 = () => 300;
- @action
- public openFloatingDoc = (doc: Doc) => {
- this._clipboardDoc = (
- <DocumentView
- Document={doc}
- addDocument={undefined}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- removeDocument={undefined}
- ScreenToLocalTransform={this.screenToLocalTransform}
- PanelWidth={this.return300}
- PanelHeight={this.return300}
- isDocumentActive={returnFalse}
- isContentActive={returnFalse}
- renderDepth={0}
- styleProvider={returnEmptyString}
- containerViewPath={returnEmptyDocViewList}
- focus={emptyFunction}
- whenChildContentsActiveChanged={emptyFunction}
- childFiltersByRanges={returnEmptyFilter}
- childFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- />
- );
- };
@action
public closeFloatingDoc = () => {
@@ -653,7 +594,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
return (
<div className="gestureOverlay-cont" style={{ pointerEvents: this._props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown}>
{this.elements}
-
+ {this._debugGestures && this._debugCusps.map(c => <div key={c.toString()} style={{ top: 0, left: 0, position: 'absolute', transform: `translate(${c.X}px, ${c.Y}px)`, width: 4, height: 4, background: 'red' }} />)}
<div
className="clipboardDoc-cont"
style={{
@@ -689,10 +630,10 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string,
SetActiveInkColor(color);
GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
SetActiveInkWidth(width);
- SetActiveFillColor(fill);
- SetActiveArrowStart(arrowStart);
- SetActiveArrowStart(arrowEnd);
- SetActiveDash(dash);
+ SetActiveInkFillColor(fill);
+ SetActiveInkArrowStart(arrowStart);
+ SetActiveInkArrowStart(arrowEnd);
+ SetActiveInkDash(dash);
});
});
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d7d8e9506..b200aff65 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -2,7 +2,7 @@ import { random } from 'lodash';
import { action } from 'mobx';
import { Doc, DocListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
-import { InkTool } from '../../fields/InkField';
+import { InkInkTool, InkTool } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, PromiseValue } from '../../fields/Types';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -169,10 +169,10 @@ export class KeyManager {
return { stopPropagation: true, preventDefault: true };
}
break;
- case 'arrowleft': return this.nudge(-1,0, 'nudge left')
- case 'arrowright': return this.nudge(1,0, 'nudge right');
- case 'arrowup': return this.nudge(0, -1, 'nudge up');
- case 'arrowdown': return this.nudge(0, 1, 'nudge down');
+ case 'arrowleft': return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
+ case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right');
+ case 'arrowup': return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up');
+ case 'arrowdown': return (e.target as any).type !== 'text' && this.nudge(0, 1, 'nudge down');
default:
} // prettier-ignore
@@ -280,10 +280,10 @@ export class KeyManager {
}
break;
case 'e':
- Doc.ActiveTool = [InkTool.StrokeEraser, InkTool.SegmentEraser, InkTool.RadiusEraser].includes(Doc.ActiveTool) ? InkTool.None : InkTool.StrokeEraser;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser;
break;
case 'p':
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
break;
case 'r':
preventDefault = false;
@@ -378,7 +378,8 @@ export class KeyManager {
UndoManager.Redo();
break;
case 'p':
- Doc.ActiveTool = InkTool.Write;
+ Doc.ActiveInk = InkInkTool.Write;
+ Doc.ActiveTool = InkTool.Ink;
break;
default:
}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 270266a94..f555808ef 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -21,7 +21,7 @@
Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
*/
import { Property } from 'csstype';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { DashColor, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
@@ -66,9 +66,14 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
private _disposers: { [key: string]: IReactionDisposer } = {};
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+
@observable _nearestSeg?: number = undefined; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
@observable _nearestT?: number = undefined; // nearest t value within the nearest Bezier segment "
- @observable _nearestScrPt?: { X: number; Y: number }; // nearst screen point on the ink stroke ""
+ @observable _nearestScrPt?: { X: number; Y: number } = { X: 0, Y: 0 }; // nearst screen point on the ink stroke ""
componentDidMount() {
this._props.setContentViewBox?.(this);
@@ -155,6 +160,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
const isEditing = InkStrokeProperties.Instance._controlButton && this._props.isSelected();
this.controlUndo = undefined;
+ this._nearestScrPt = undefined;
setupMoveUpEvents(
this,
e,
@@ -275,7 +281,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
.map(p => ({ X: p[0], Y: p[1] }));
const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
- if (distance < 40) {
+ if (distance < 40 && !e.buttons) {
this._nearestT = nearestT;
this._nearestSeg = nearestSeg;
this._nearestScrPt = nearestPt;
@@ -309,7 +315,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
componentUI = (boundsLeft: number, boundsTop: number): null | JSX.Element => {
const inkDoc = this.Document;
const { inkData, inkStrokeWidth } = this.inkScaledData();
- const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.ScreenToLocalBoxXf().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke
+ const screenSpaceCenterlineStrokeWidth = 3; //Math.min(3, inkStrokeWidth * this.ScreenToLocalBoxXf().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke
const screenInkWidth = this.ScreenToLocalBoxXf().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth);
@@ -427,7 +433,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
StrCast(this.layoutDoc.stroke_lineJoin) as Property.StrokeLinejoin,
StrCast(this.layoutDoc.stroke_lineCap) as Property.StrokeLinecap,
StrCast(this.layoutDoc.stroke_bezier),
- !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor,
+ closed && fillColor && DashColor(fillColor).alpha() ? fillColor : 'none',
startMarker,
endMarker,
markerScale,
@@ -470,14 +476,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
className="inkStroke"
style={{
transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
- // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
cursor: this._props.isSelected() ? 'default' : undefined,
}}
{...interactions}>
{clickableLine(this.onPointerDown, isInkMask)}
{isInkMask ? null : inkLine}
</svg>
- {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+ {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeHideTextLabels)) ? null : (
<div
className="inkStroke-text"
style={{
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index a543b4875..e3df01bbb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Toggle, ToggleType, Type } from 'browndash-components';
+import { Toggle, ToggleType, Type } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -21,7 +21,7 @@ import { OverlayView } from './OverlayView';
import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './StyleProvider';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
-import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { StickerPalette } from './smartdraw/StickerPalette';
interface LightboxViewProps {
PanelWidth: number;
@@ -35,7 +35,7 @@ type LightboxSavedState = { [key: string]: FieldResult; }; // prettier-ignore
@observer
export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
/**
- * Determines whether a DocumentView is descendant of the lightbox view (or any of its pop-ups like the annotationPalette)
+ * Determines whether a DocumentView is descendant of the lightbox view (or any of its pop-ups like the stickerPalette)
* @param view
* @returns true if a DocumentView is descendant of the lightbox view
*/
@@ -56,7 +56,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
}[] = [];
private _savedState: LightboxSavedState = {};
private _history: { doc: Doc; target?: Doc }[] = [];
- private _annoPaletteView: AnnotationPalette | null = null;
+ private _annoPaletteView: StickerPalette | null = null;
@observable private _future: Doc[] = [];
@observable private _layoutTemplate: Opt<Doc> = undefined;
@observable private _layoutTemplateString: Opt<string> = undefined;
@@ -211,10 +211,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
};
togglePalette = () => {
this._showPalette = !this._showPalette;
- // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
};
togglePen = () => {
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
};
toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
@@ -317,7 +316,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
</GestureOverlay>
</div>
- {this._showPalette && <AnnotationPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
+ {this._showPalette && <StickerPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
{this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)}
{this.renderNavBtn(
this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
@@ -331,8 +330,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
<LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} />
{toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)}
{toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
- {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
- {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
+ {toggleBtn('lightboxView-paletteBtn', 'toggle sticker palette', this._showPalette === true, 'palette', '', this.togglePalette)}
+ {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)}
{toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
</div>
);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 2a59f6dc3..dda543470 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -64,7 +64,7 @@ import { ImportElementBox } from './nodes/importBox/ImportElementBox';
import { PresBox, PresElementBox } from './nodes/trails';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { SearchBox } from './search/SearchBox';
-import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { StickerPalette } from './smartdraw/StickerPalette';
dotenv.config();
@@ -117,7 +117,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
KeyValueBox.Init();
PresBox.Init(TabDocView.AllTabDocs);
DocumentContentsView.Init(KeyValueBox.LayoutString(), {
- AnnotationPalette,
+ StickerPalette: StickerPalette,
FormattedTextBox,
ImageBox,
FontIconBox,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7779d339f..d748b70ae 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,8 +7,8 @@ import { action, computed, configure, makeObservable, observable, reaction, runI
import { observer } from 'mobx-react';
import * as React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
-import '../../../node_modules/browndash-components/dist/styles/global.min.css';
-import { ClientUtils, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
+import '@dash/components/src/global/globalCssVariables.scss';
+import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, GetDocFromUrl, Opt, returnEmptyDoclist } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
@@ -20,7 +20,7 @@ import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { Docs } from '../documents/Documents';
import { CalendarManager } from '../util/CalendarManager';
import { CaptureManager } from '../util/CaptureManager';
-import { Button, CurrentUserUtils } from '../util/CurrentUserUtils';
+import { CurrentUserUtils, ToTagName } from '../util/CurrentUserUtils';
import { DocumentManager } from '../util/DocumentManager';
import { DragManager } from '../util/DragManager';
import { dropActionType } from '../util/DropActionTypes';
@@ -63,7 +63,6 @@ import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu';
import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
import { DocButtonState } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
-import { ButtonType } from './nodes/FontIconBox/FontIconBox';
import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
@@ -73,7 +72,7 @@ import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { TaskCompletionBox } from './nodes/TaskCompletedBox';
import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
-import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import ImageEditorBox from './nodes/imageEditor/ImageEditor';
import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
@@ -446,6 +445,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faAlignRight,
fa.faHeading,
fa.faRulerCombined,
+ fa.faFill,
fa.faFillDrip,
fa.faLink,
fa.faUnlink,
@@ -873,25 +873,9 @@ export class MainView extends ObservableReactComponent<object> {
* @param hotKey tite of the new hotkey
*/
addHotKey = (hotKey: string) => {
- const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
- const filter = DocCast(buttons.Filter);
- const title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey;
-
- const newKey: Button = {
- title,
- icon: 'question',
- toolTip: `Click to toggle the ${title}'s group's visibility`,
- btnType: ButtonType.ToggleButton,
- expertMode: false,
- toolType: '#' + title,
- funcs: {},
- scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
- };
-
- const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
- newBtn.isSystem = newBtn[DocData].isSystem = undefined;
-
- Doc.AddToFilterHotKeys(newBtn);
+ const filterIcons = DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter);
+ const menuDoc = CurrentUserUtils.setupContextMenuBtn(CurrentUserUtils.filterBtnDesc(ToTagName(hotKey), 'question'), filterIcons);
+ Doc.AddToFilterHotKeys(menuDoc);
};
@computed get mainInnerContent() {
@@ -1023,10 +1007,36 @@ export class MainView extends ObservableReactComponent<object> {
<svg style={{ width: '100%', height: '100%' }}>
{[
...SnappingManager.HorizSnapLines.map(l => (
- <line key={'horiz' + l} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+ <line
+ key={'horiz' + l}
+ x1="0"
+ y1={l}
+ x2="2000"
+ y2={l}
+ stroke={
+ SnappingManager.userVariantColor
+ /* lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))*/
+ }
+ opacity={0.3}
+ strokeWidth={3}
+ strokeDasharray="2 2"
+ />
)),
...SnappingManager.VertSnapLines.map(l => (
- <line key={'vert' + l} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+ <line
+ key={'vert' + l}
+ y1={this.topOfMainDocContent.toString()}
+ x1={l}
+ y2="2000"
+ x2={l}
+ stroke={
+ SnappingManager.userVariantColor
+ /* lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))*/
+ }
+ opacity={0.3}
+ strokeWidth={3}
+ strokeDasharray="2 2"
+ />
)),
]}
</svg>
@@ -1146,7 +1156,7 @@ export class MainView extends ObservableReactComponent<object> {
<LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
<GPTPopup key="gptpopup" />
<SchemaCSVPopUp key="schemacsvpopup" />
- <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
+ <ImageEditorBox imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
</div>
);
}
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 4a35805fb..178eefe2c 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/require-default-props */
-import { isDark } from 'browndash-components';
+import { isDark } from '@dash/components';
import { observer } from 'mobx-react';
import * as React from 'react';
import { SnappingManager } from '../util/SnappingManager';
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 7266875c5..02516264c 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -15,18 +15,19 @@ import './MarqueeAnnotator.scss';
import { DocumentView } from './nodes/DocumentView';
import { ObservableReactComponent } from './ObservableReactComponent';
import { AnchorMenu } from './pdf/AnchorMenu';
+import { Transform } from '../util/Transform';
export interface MarqueeAnnotatorProps {
Document: Doc;
down?: number[];
scrollTop: number;
- isNativeScaled?: boolean;
scaling?: () => number;
annotationLayerScaling?: () => number;
annotationLayerScrollTop: number;
containerOffset?: () => number[];
marqueeContainer: HTMLDivElement;
docView: () => DocumentView;
+ screenTransform: () => Transform;
savedAnnotations: () => ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>;
selectionText: () => string;
annotationLayer: HTMLDivElement;
@@ -34,7 +35,7 @@ export interface MarqueeAnnotatorProps {
getPageFromScroll?: (top: number) => number;
finishMarquee: (x?: number, y?: number) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
- anchorMenuFlashcard?: () => Promise<String>;
+ anchorMenuFlashcard?: () => Promise<string>;
anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
highlightDragSrcColor?: string;
}
@@ -157,7 +158,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
// 4) reattach the vector to the center of the bounding box
getTransformedScreenPt = (down: number[]) => {
const { marqueeContainer } = this.props;
- const containerXf = this.props.isNativeScaled ? this.props.docView().screenToContentsTransform() : this.props.docView().screenToViewTransform();
+ const containerXf = this.props.screenTransform();
const boundingRect = marqueeContainer.getBoundingClientRect();
const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
const downVec = Utils.rotPt(down[0] - center.x,
@@ -220,7 +221,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
e.preventDefault();
e.stopPropagation();
let cropRegion: Doc | undefined;
- // eslint-disable-next-line no-return-assign
const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!;
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
index 430455644..ab02c2d07 100644
--- a/src/client/views/PinFuncs.ts
+++ b/src/client/views/PinFuncs.ts
@@ -1,4 +1,4 @@
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { Copy, Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
@@ -16,7 +16,7 @@ export interface pinDataTypes {
scrollable?: boolean;
dataviz?: number[];
pannable?: boolean;
- type_collection?: boolean;
+ collectionType?: boolean;
inkable?: boolean;
filters?: boolean;
pivot?: boolean;
@@ -39,9 +39,14 @@ export interface PinProps {
pinData?: pinDataTypes;
}
-/// copies values from the targetDoc (which is the prototype of the pinDoc) to
-/// reserved fields on the pinDoc so that those values can be restored to the
-/// target doc when navigating to it.
+/**
+ * copies values from the targetDoc (which is the prototype of the pinDoc) to
+ * reserved fields on the pinDoc so that those values can be restored to the
+ * target doc when navigating to it.
+ * @param pinDoc Doc that will store pinned metadata
+ * @param pinProps description of props to pin
+ * @param targetDoc Doc that is being pinned
+ */
export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
const pinDoc = pinDocIn;
pinDoc.presentation = true;
@@ -60,7 +65,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
pinProps.pinData.scrollable ||
pinProps.pinData.temporal ||
pinProps.pinData.pannable ||
- pinProps.pinData.type_collection ||
+ pinProps.pinData.collectionType ||
pinProps.pinData.clippable ||
pinProps.pinData.datarange ||
pinProps.pinData.dataview ||
@@ -69,7 +74,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
const fkey = Doc.LayoutFieldKey(targetDoc);
if (pinProps.pinData.dataview) {
pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
- pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
+ pinDoc.config_data = Field.Copy(targetDoc[fkey]);
}
if (pinProps.pinData.dataannos) {
const fieldKey = Doc.LayoutFieldKey(targetDoc);
@@ -113,8 +118,8 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
})
)
);
- if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
- if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
+ if (pinProps.pinData.collectionType) pinDoc.config_type_collection = targetDoc._type_collection;
+ if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField) ?? new List<string>();
if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
if (pinProps.pinData.pannable) {
pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index f96a4a255..606fb17ed 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/no-unused-class-component-methods */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components';
+import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from '@dash/components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index a5e60b831..7866e67e7 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -508,6 +508,7 @@
display: flex;
margin-bottom: 3px;
margin-left: 4px;
+ justify-content: space-evenly;
.arrows-head {
display: flex;
@@ -641,6 +642,7 @@
.smooth,
.color,
+.strength-slider,
.smooth-slider {
margin-top: 7px;
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index c539b1d0a..bed96f600 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -2,7 +2,7 @@ import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, Tooltip } from '@mui/material';
-import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from '@dash/components';
import { concat } from 'lodash';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
@@ -44,6 +44,7 @@ import { StyleProviderFuncType } from './nodes/FieldView';
import { OpenWhere } from './nodes/OpenWhere';
import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
+import { DrawingFillHandler } from './smartdraw/DrawingFillHandler';
interface PropertiesViewProps {
width: number;
@@ -110,6 +111,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@observable openContexts: boolean = true;
@observable openLinks: boolean = true;
@observable openAppearance: boolean = true;
+ @observable openFirefly: boolean = true;
@observable openTransform: boolean = true;
@observable openFilters: boolean = false;
@observable openStyling: boolean = true;
@@ -117,6 +119,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
// Pres Trails booleans:
@observable openPresTransitions: boolean = true;
@observable openPresProgressivize: boolean = false;
+ @observable openPresMedia: boolean = false;
@observable openPresVisibilityAndDuration: boolean = false;
@observable openAddSlide: boolean = false;
@observable openSlideOptions: boolean = false;
@@ -983,22 +986,24 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
);
return (
<div>
- {!targetDoc.layout_isSvg && this.containsInkDoc && (
- <div className="color">
- <Toggle
- text={'Color with GPT'}
- color={SettingsManager.userColor}
- icon={<FontAwesomeIcon icon="fill-drip" />}
- iconPlacement="left"
- align="flex-start"
- fillWidth
- toggleType={ToggleType.BUTTON}
- onClick={undoable(() => {
- SmartDrawHandler.Instance.colorWithGPT(targetDoc);
- }, 'smoothStrokes')}
- />
- </div>
- )}
+ <div>
+ {!targetDoc.layout_isSvg && this.containsInkDoc && (
+ <div className="color">
+ <Toggle
+ text={'Color with GPT'}
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="fill-drip" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ SmartDrawHandler.Instance.colorWithGPT(targetDoc);
+ }, 'colorWithGPT')}
+ />
+ </div>
+ )}
+ </div>
<div className="smooth">
<Toggle
text={'Smooth Ink Strokes'}
@@ -1037,6 +1042,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
doc[DocData].stroke_markerScale = Number(value);
});
}
+ @computed get refStrength() { return Number(this.getField('drawing_refStrength') || '50'); } // prettier-ignore
+ set refStrength(value) {
+ this.selectedDoc[DocData].drawing_refStrength = Number(value);
+ }
@computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore
set smoothAmt(value) {
this.selectedStrokes.forEach(doc => {
@@ -1090,6 +1099,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
this.openTransform = false;
this.openFields = false;
this.openSharing = false;
+ this.openAppearance = false;
+ this.openFirefly = false;
this.openLayout = false;
this.openFilters = false;
};
@@ -1300,11 +1311,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
}
@computed get inkSubMenu() {
+ const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+ !isNaN(val) && (this.refStrength = val);
+ });
+ const targetDoc = this.selectedLayoutDoc;
return (
<>
<PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
{this.selectedStrokes.length ? this.inkEditor : null}
</PropertiesSection>
+ <PropertiesSection title="Firefly" isOpen={this.openFirefly} setIsOpen={bool => { this.openFirefly = bool; }} onDoubleClick={this.CloseAll}>
+ <>
+ <div className="drawing-to-image">
+ <Toggle
+ text="Create Image"
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="fill-drip" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
+ />
+ </div>
+ <div className="strength-slider">{strength}</div>
+ </>
+ </PropertiesSection>
<PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
{this.transformEditor}
</PropertiesSection>
@@ -1905,74 +1937,74 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
</div>
</div>
{!selectedItem ? null : (
- <div className="propertiesView-presentationTrails">
+ <div className="propertiesView-section">
<div
- className="propertiesView-presentationTrails-title"
+ className="propertiesView-sectionTitle"
onPointerDown={action(() => {
- this.openPresTransitions = !this.openPresTransitions;
+ this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration;
})}
style={{
color: SnappingManager.userColor,
- backgroundColor: this.openPresTransitions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ backgroundColor: SnappingManager.userVariantColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Transitions
+ Visibility
<div className="propertiesView-presentationTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
+ <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
+ {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null}
</div>
)}
{!selectedItem ? null : (
- <div className="propertiesView-presentationTrails">
+ <div className="propertiesView-section">
<div
- className="propertiesView-presentationTrails-title"
+ className="propertiesView-sectionTitle"
onPointerDown={action(() => {
- this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration;
+ this.openPresProgressivize = !this.openPresProgressivize;
})}
style={{
color: SnappingManager.userColor,
- backgroundColor: this.openPresVisibilityAndDuration ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ backgroundColor: SnappingManager.userVariantColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Visibility
+ Progressivize
<div className="propertiesView-presentationTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
+ <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null}
+ {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
</div>
)}
{!selectedItem ? null : (
- <div className="propertiesView-presentationTrails">
+ <div className="propertiesView-section">
<div
- className="propertiesView-presentationTrails-title"
+ className="propertiesView-sectionTitle"
onPointerDown={action(() => {
- this.openPresProgressivize = !this.openPresProgressivize;
+ this.openPresMedia = !this.openPresMedia;
})}
style={{
color: SnappingManager.userColor,
- backgroundColor: this.openPresProgressivize ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ backgroundColor: SnappingManager.userVariantColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Progressivize
+ Media
<div className="propertiesView-presentationTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
+ <FontAwesomeIcon icon={this.openPresMedia ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
+ {this.openPresMedia ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaDropdown}</div> : null}
</div>
)}
{!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
- <div className="propertiesView-presentationTrails">
+ <div className="propertiesView-section">
<div
- className="propertiesView-presentationTrails-title"
+ className="propertiesView-sectionTitle"
onPointerDown={action(() => {
this.openSlideOptions = !this.openSlideOptions;
})}
style={{
color: SnappingManager.userColor,
- backgroundColor: this.openSlideOptions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ backgroundColor: SnappingManager.userVariantColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
+ {type === DocumentType.AUDIO ? 'file-audio' : 'file-video'}
<div className="propertiesView-presentationTrails-title-icon">
<FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
@@ -1980,6 +2012,25 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
{this.openSlideOptions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
</div>
)}
+ {!selectedItem ? null : (
+ <div className="propertiesView-section">
+ <div
+ className="propertiesView-sectionTitle"
+ onPointerDown={action(() => {
+ this.openPresTransitions = !this.openPresTransitions;
+ })}
+ style={{
+ color: SnappingManager.userColor,
+ backgroundColor: SnappingManager.userVariantColor,
+ }}>
+ Transitions
+ <div className="propertiesView-presentationTrails-title-icon">
+ <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
+ </div>
+ </div>
+ {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
+ </div>
+ )}
</div>
);
}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1f3ad8444..87076bf65 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -78,8 +78,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _text_fontSize: StrCast(Doc.UserDoc().fontSize),
- _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ text_fontSize: StrCast(Doc.UserDoc().fontSize),
+ text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
});
Doc.SetSelectOnLoad(target);
FormattedTextBox.DontSelectInitialText = true;
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index 44d3bf757..56367e70b 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -19,7 +19,9 @@ export enum StyleProp {
FontColor = 'fontColor', // color o tet
FontSize = 'fontSize', // size of text font
FontFamily = 'fontFamily', // font family of text
- FontWeight = 'fontWeight', // font weight of text
+ FontWeight = 'fontWeight', // font weight of text (eg bold)
+ FontStyle = 'fontStyle', // font style of text (eg italic)
+ FontDecoration = 'fontDecoration', // text decoration of text (eg underline)
Highlighting = 'highlighting', // border highlighting
ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
AnchorMenuItems = 'anchorMenuItems',
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8859f6464..bebc9a341 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,7 +1,7 @@
+import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from 'browndash-components';
import { action, untracked } from 'mobx';
import { extname } from 'path';
import * as React from 'react';
@@ -10,6 +10,7 @@ import { FaFilter } from 'react-icons/fa';
import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
import { Doc, Opt, StrListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
+import { InkInkTool } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
import { AudioAnnoState } from '../../server/SharedMediaTypes';
@@ -21,10 +22,9 @@ import { TreeSort } from './collections/TreeSort';
import { Colors } from './global/globalEnums';
import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
-import { styleProviderQuiz } from './StyleProviderQuiz';
import { StyleProp } from './StyleProp';
import './StyleProvider.scss';
-import { TagsView } from './TagsView';
+import { styleProviderQuiz } from './StyleProviderQuiz';
function toggleLockedPosition(doc: Doc) {
UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -171,7 +171,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize));
case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight));
- case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
+ case StyleProp.FontStyle: return StrCast(doc?.[fieldKey + 'fontStyle'], StrCast(Doc.UserDoc().fontStyle));
+ case StyleProp.FontDecoration:return StrCast(doc?.[fieldKey + 'fontDecoration'], StrCast(Doc.UserDoc().fontDecoration));
+ case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, StrCast(Doc.UserDoc()[Doc.ActiveInk === InkInkTool.Highlight ? "inkHighlighterColor": "inkFillColor"], 'transparent'))));
case StyleProp.ShowCaption: return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
case StyleProp.TitleHeight: return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
case StyleProp.ShowTitle: return (
@@ -251,10 +253,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
case DocumentType.PRES: docColor = docColor || 'transparent'; break;
case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break;
- case DocumentType.RTF: docColor = docColor || Colors.LIGHT_GRAY; break;
+ case DocumentType.RTF: docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break;
case DocumentType.LINK: docColor = (isAnchor ? docColor : undefined); break;
case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
- case DocumentType.EQUATION: docColor = docColor || 'transparent'; break;
+ case DocumentType.EQUATION: docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, 'transparent'); break;
case DocumentType.LABEL: docColor = docColor || Colors.LIGHT_GRAY; break;
case DocumentType.BUTTON: docColor = docColor || Colors.LIGHT_GRAY; break;
case DocumentType.IMG:
@@ -313,7 +315,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
? undefined // if it's a background & has a cluster color, make the shadow spread really big
: fieldKey.includes('_inline') // if doc is an inline document in a text box
? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}`
- : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline)
+ :doc.rootDocument !== doc.embedContainer && DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) and this isn't a simple text template (where the layoutDoc's rootDocument is its embed container)
? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
: StrCast(doc.layout_boxShadow, '');
}
@@ -391,7 +393,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> : null;
return (
<>
@@ -399,7 +400,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
{lock()}
{filter()}
{audio()}
- {tags()}
</>
);
}
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index 1f2ad1485..b3fb8c930 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -17,7 +17,7 @@ import { AnchorMenu } from './pdf/AnchorMenu';
import { DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
import { ImageBox } from './nodes/ImageBox';
-import { ImageUtility } from './nodes/generativeFill/generativeFillUtils/ImageHandler';
+import { ImageUtility } from './nodes/imageEditor/imageEditorUtils/ImageHandler';
import './StyleProviderQuiz.scss';
export namespace styleProviderQuiz {
@@ -66,7 +66,7 @@ export namespace styleProviderQuiz {
newCol.zIndex = 1000;
newCol.forceActive = true;
newCol.quiz = text;
- newCol[DocData].textTransform = 'none';
+ newCol[DocData][Doc.LayoutFieldKey(newCol) + '_transform'] = 'none';
Doc.AddDocToList(img.Document, '_quizBoxes', newCol);
img.addDocument(newCol);
// img._loading = false;
diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss
index 24f9e86bc..b21d303fb 100644
--- a/src/client/views/TagsView.scss
+++ b/src/client/views/TagsView.scss
@@ -4,13 +4,19 @@
flex-direction: column;
border: 1px solid;
border-radius: 4px;
-}
-
-.tagsView-list {
- display: flex;
- flex-wrap: wrap;
- .iconButton-container {
- min-height: unset !important;
+ width: 100%;
+ position: relative;
+ .tagsView-content {
+ width: 100%;
+ height: inherit;
+ .tagsView-list {
+ display: flex;
+ flex-wrap: wrap;
+ height: 1;
+ .iconButton-container {
+ min-height: unset !important;
+ }
+ }
}
}
@@ -52,13 +58,14 @@
}
.tagsView-editing-box {
- margin-top: 8px;
+ margin-top: 20px;
}
.tagsView-input-box {
margin: auto;
align-self: center;
width: 90%;
+ color: black;
}
.tagsView-buttons {
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 2615bc5fb..b70e21918 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Colors, IconButton } from 'browndash-components';
+import { Button, Colors, IconButton } from '@dash/components';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { DocCast, NumCast, StrCast } from '../../fields/Types';
+import { DocCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
@@ -146,7 +146,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
}
}
}
- doc[DocData].tags = new List<string>((doc[DocData].tags as List<string>).filter(label => label !== tag));
+ doc[DocData].tags = new List<string>(StrListCast(doc[DocData].tags).filter(label => label !== tag));
};
private _ref: React.RefObject<HTMLDivElement>;
@@ -295,15 +295,6 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
return this._props.Views.lastElement();
}
- // x: 1 => 1/vs 0 => 1 1/(vs - (1-x)*(vs-1))
- @computed get currentScale() {
- if (this._props.Views.length > 1) return 1;
- const x = NumCast(this.View.Document.height) / this.View.screenToContentsTransform().Scale / 80;
- const xscale = x >= 1 ? 0 : 1 / (1 + x * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
- const y = NumCast(this.View.Document.width) / this.View.screenToContentsTransform().Scale / 200;
- const yscale = y >= 1 ? 0 : 1 / (1 + y * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
- return Math.max(xscale, yscale, 1 / this.View.screenToLocalScale());
- }
@computed get isEditing() {
return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View)));
}
@@ -359,16 +350,11 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
style={{
display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
- transformOrigin: 'top left',
- maxWidth: `${100 * this.currentScale}%`,
- width: `${100 * this.currentScale}%`,
- transform: `scale(${1 / this.currentScale})`,
backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
- position: 'relative',
- top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+ height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined,
}}>
- <div className="tagsView-content" style={{ width: '100%' }}>
+ <div className="tagsView-content">
<div className="tagsView-list">
{this._props.Views.length === 1 && !this.View.showTags ? null : ( //
<IconButton
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 9b71d46ea..32b97b31a 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -1,5 +1,5 @@
import { Tooltip } from '@mui/material';
-import { Popup, Type } from 'browndash-components';
+import { Popup, Type } from '@dash/components';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StrCast } from '../../fields/Types';
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index f66f6062e..a66a20cf6 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -24,7 +24,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
promoteCollection?: () => void; // moves contents of collection to parent
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;
+ restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away. see PresBox.restoreTargetDocView
scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
@@ -60,4 +60,6 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
+ componentAIView?: () => JSX.Element;
+ componentAIViewHistory?: () => JSX.Element;
}
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index d9ff21035..15683ebf2 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -16,6 +16,7 @@ import { RegionHelpers } from './Region';
import './Timeline.scss';
import { TimelineOverview } from './TimelineOverview';
import { Track } from './Track';
+import { Id } from '../../../fields/FieldSymbols';
/**
* Timeline class controls most of timeline functions besides individual region and track mechanism. Main functions are
@@ -56,7 +57,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> {
private DEFAULT_CONTAINER_HEIGHT: number = 330;
private MIN_CONTAINER_HEIGHT: number = 205;
- constructor(props: any) {
+ constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
}
@@ -89,7 +90,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> {
*/
@computed
private get children(): Doc[] {
- const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this._props.Document.type) as any);
+ const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this._props.Document.type) as unknown as DocumentType);
if (annotatedDoc) {
return DocListCast(this._props.Document[Doc.LayoutFieldKey(this._props.Document) + '_annotations']);
}
@@ -272,9 +273,9 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> {
* for displaying time to standard min:sec
*/
@action
- toReadTime = (time: number): string => {
- time = time / 1000;
- const inSeconds = Math.round(time * 100) / 100;
+ toReadTime = (timeIn: number): string => {
+ const timeSecs = timeIn / 1000;
+ const inSeconds = Math.round(timeSecs * 100) / 100;
const min = Math.floor(inSeconds / 60);
const sec = Math.round((inSeconds % 60) * 100) / 100;
@@ -552,6 +553,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> {
<div key="timeline_trackbox" className="trackbox" ref={this._trackbox} style={{ width: `${this._totalLength}px` }}>
{[...this.children, this._props.Document].map(doc => (
<Track
+ key={doc[Id]}
ref={ref => this.mapOfTracks.push(ref)}
timeline={this}
animatedDoc={doc}
@@ -570,7 +572,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> {
<div className="currentTime">Current: {this.getCurrentTime()}</div>
<div key="timeline_title" className="title-container" ref={this._titleContainer}>
{[...this.children, this._props.Document].map(doc => (
- <div style={{ height: `${this._titleHeight}px` }} className="datapane" onPointerOver={() => Doc.BrushDoc(doc)} onPointerOut={() => Doc.UnBrushDoc(doc)}>
+ <div key={doc[Id]} style={{ height: `${this._titleHeight}px` }} className="datapane" onPointerOver={() => Doc.BrushDoc(doc)} onPointerOut={() => Doc.UnBrushDoc(doc)}>
<p>{StrCast(doc.title)}</p>
</div>
))}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 5283601bf..79c53db08 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -28,11 +28,14 @@
.collectionCardView-cardwrapper {
display: grid;
- grid-template-columns: repeat(10, 1fr);
transform-origin: left 50%;
align-items: center;
z-index: 0; // so that setting z-index of active card doesn't make it land on top of things outside of the card-wrapper
}
+.collectionCardView-cardSizeDragger {
+ position: absolute;
+ top: 0;
+}
.no-card-span {
position: relative;
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index b86dad9d7..43464e50c 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,39 +1,35 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
-import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils';
+import * as CSS from 'csstype';
+import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Animation, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoable } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
+import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
-import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
-import './CollectionCardDeckView.scss';
-import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
-
-enum cardSortings {
- Time = 'time',
- Type = 'type',
- Color = 'color',
- Chat = 'chat',
- Tag = 'tag',
- None = '',
-}
+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';
/**
* New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -49,19 +45,17 @@ export class CollectionCardView extends CollectionSubView() {
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 _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!;
+ private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
+ private _draggerRef = React.createRef<HTMLDivElement>();
@observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
- @observable _maxRowCount = 10;
- @observable _docDraggedIndex: number = -1;
- @observable _curDoc: Doc | undefined = undefined;
+ @observable _cursor: CSS.Property.Cursor = 'ew-resize';
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
- this.setRegenerateCallback();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
this._dropDisposer?.();
@@ -73,36 +67,32 @@ export class CollectionCardView extends CollectionSubView() {
// prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
};
- /**
- * Callback to ensure gpt's text versions of the child docs are updated
- */
- setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
+ @computed get cardWidth() {
+ return NumCast(this.layoutDoc._cardWidth, 50);
+ }
+ @computed get _maxRowCount() {
+ return Math.ceil(this.cardDeckWidth / this.cardWidth);
+ }
/**
* update's gpt's doc-text list and initializes callbacks
*/
- @action
- childPairStringListAndUpdateSortDesc = async () => {
- const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
- GPTPopup.Instance.setSortDesc(sortDesc.join());
- GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
- GPTPopup.Instance.onQuizRandom = () => this.quizMode();
- };
+ childPairStringListAndUpdateSortDesc = () =>
+ this.childPairStringList().then(sortDesc => {
+ GPTPopup.Instance.setSortDesc(sortDesc.join());
+ GPTPopup.Instance.onSortComplete = this.processGptOutput;
+ GPTPopup.Instance.onQuizRandom = this.quizMode;
+ });
componentDidMount() {
- this._props.setContentViewBox?.(this);
- this._disposers.sort = reaction(
- () => GPTPopup.Instance.visible,
- isVis => {
- if (isVis) {
- this.openChatPopup();
- } else {
- this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
- }
- }
+ 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 renders, we need to act like the
+ // when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the
// dash data just changed and trigger a React involidation with the correct data (read from the dom).
this._disposers.child = reaction(
() => [this.Document.x, this.Document.y],
@@ -112,21 +102,29 @@ export class CollectionCardView extends CollectionSubView() {
}
}
);
+ this._disposers.select = reaction(
+ () => this.childDocs.find(d => this._docRefs.get(d)?.IsSelected),
+ selected => {
+ selected && (this.layoutDoc._card_curDoc = selected);
+ }
+ );
}
+ 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?.();
}
- @computed get cardSort() {
- return StrCast(this.Document.card_sort) as cardSortings;
- }
/**
* Number of rows of cards to be rendered
*/
@computed get numRows() {
- return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+ return Math.ceil(this.childDocs.length / this._maxRowCount);
}
/**
* Circle arc size, in radians, to layout cards
@@ -160,13 +158,20 @@ export class CollectionCardView extends CollectionSubView() {
return this._props.NativeDimScaling?.() || 1;
}
+ @computed get xMargin() {
+ return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth()));
+ }
+
+ @computed get cardDeckWidth() {
+ return this._props.PanelWidth() - 2 * this.xMargin;
+ }
+
/**
* When in quiz mode, randomly selects a document
*/
- quizMode = action(() => {
- const randomIndex = Math.floor(Math.random() * this.childDocs.length);
- this._curDoc = this.childDocs[randomIndex];
- });
+ quizMode = () => {
+ this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)];
+ };
setHoveredNodeIndex = action((index: number) => {
if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
@@ -186,7 +191,7 @@ export class CollectionCardView extends CollectionSubView() {
* @returns the card's new index
*/
findCardDropIndex = (mouseX: number, mouseY: number) => {
- const cardCount = this.sortedDocs.length;
+ const cardCount = this.childDocs.length;
let index = 0;
const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
@@ -220,8 +225,8 @@ export class CollectionCardView extends CollectionSubView() {
*/
@action
onPointerMove = (x: number, y: number) => {
- if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
- this._docDraggedIndex = this.findCardDropIndex(x, y);
+ if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) {
+ this.docDraggedIndex = this.findCardDropIndex(x, y);
}
};
@@ -234,14 +239,14 @@ export class CollectionCardView extends CollectionSubView() {
onInternalDrop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- const dragIndex = this._docDraggedIndex;
+ const dragIndex = this.docDraggedIndex;
const draggedDoc = DragManager.docsBeingDragged[0];
if (dragIndex > -1 && draggedDoc) {
- this._docDraggedIndex = -1;
- const sorted = this.sortedDocs;
+ this.docDraggedIndex = -1;
+ const sorted = this.childDocs;
const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
- this.Document.card_sort = '';
+ this.Document[this._props.fieldKey + '_sort'] = '';
originalIndex !== -1 && sorted.splice(originalIndex, 1);
sorted.splice(dragIndex, 0, draggedDoc);
if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
@@ -270,54 +275,11 @@ export class CollectionCardView extends CollectionSubView() {
.map(({ i }) => i)
.join('.');
- /**
- * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
- * front, latter cards to the back
- * @param docs
- * @param sortType
- * @param isDesc
- * @returns
- */
- sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
- const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
- sortType &&
- docs.sort((docA, docB) => {
- const [typeA, typeB] = (() => {
- switch (sortType) {
- default:
- case cardSortings.Type: return [StrCast(docA.type), StrCast(docB.type)];
- case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)];
- case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
- case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
- }
- })(); //prettier-ignore
- return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
- });
- if (dragIndex !== -1) {
- const draggedDoc = DragManager.docsBeingDragged[0];
- const originalIndex = docs.findIndex(doc => doc === draggedDoc);
-
- originalIndex !== -1 && docs.splice(originalIndex, 1);
- draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
- }
-
- return docs;
- };
-
- @computed get sortedDocs() {
- return this.sort(
- this.childCards.map(card => card.layout),
- this.cardSort,
- BoolCast(this.Document.card_sort_isDesc),
- this._docDraggedIndex
- );
- }
-
isChildContentActive = computedFn(
(doc: Doc) => () =>
this._props.isContentActive?.() === false
? false
- : this._props.isDocumentActive?.() && this._curDoc === doc
+ : this._props.isDocumentActive?.() && this.curDoc() === doc
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
@@ -345,10 +307,10 @@ export class CollectionCardView extends CollectionSubView() {
scriptContext={this}
focus={this.focus}
onDoubleClickScript={this.onChildDoubleClick}
- onClickScript={this._curDoc === doc ? undefined : this._clickScript}
+ onClickScript={this.curDoc() === doc ? undefined : this._setCurDocScript}
dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
- showTags={BoolCast(this.layoutDoc.showChildTags)}
+ showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
dontHideOnDrag
/>
@@ -478,74 +440,51 @@ export class CollectionCardView extends CollectionSubView() {
* 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
*/
- @action
- processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
- // Split the string into individual list items
- const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
- if (questionType === '2' || questionType === '4') {
- this.childDocs.forEach(d => {
- d.chatFilter = false;
- });
- }
-
- if (questionType === '6') {
- this.Document.card_sort = 'chat';
- }
+ 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');
+ });
+ }
- 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': {
- const allHotKeys = Doc.MyFilterHotKeys;
-
- let myTag = '';
-
- if (tag) {
- for (let i = 0; i < allHotKeys.length; i++) {
- // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED
- const keyTag = StrCast(allHotKeys[i].toolType);
- if (tag.includes(keyTag)) {
- myTag = keyTag;
- break;
- }
- }
+ if (questionType === '6') {
+ this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
+ }
- if (myTag != '') {
- doc[myTag] = true;
+ 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;
+ break;
+ case '2':
+ case '4':
+ TagItem.addTagToDoc(doc, '#chat');
+ Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
+ break;
}
- case '2':
- case '4':
- doc.chatFilter = true;
- Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
- break;
+ } else {
+ console.warn(`No matching document found for item: ${normalizedItem}`);
}
- } else {
- console.warn(`No matching document found for item: ${normalizedItem}`);
- }
- });
- }, '');
-
- /**
- * Opens up the chat popup and starts the process for smart sorting.
- */
- openChatPopup = async () => {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.CARD);
- GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
- await this.childPairStringListAndUpdateSortDesc();
- };
+ });
+ }, '')();
childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
// need to explicitly trigger an invalidation since we're reading everything from the Dom
@@ -554,7 +493,7 @@ export class CollectionCardView extends CollectionSubView() {
const dref = this._docRefs.get(doc);
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
- if (!scale) return new Transform(0, 0, 0);
+ if (!scale) return new Transform(0, 0, 1);
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
-translateY + (dref?.centeringY || 0) * scale, 1)
@@ -570,8 +509,8 @@ export class CollectionCardView extends CollectionSubView() {
* @param doc doc that will be animated away from center focus
*/
releaseCurDoc = action(() => {
- const selDoc = this._curDoc;
- this._curDoc = undefined;
+ const selDoc = this.curDoc();
+ this.layoutDoc._card_curDoc = undefined;
const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.());
if (cardDocView && selDoc) {
DocumentView.DeselectView(cardDocView);
@@ -584,11 +523,31 @@ export class CollectionCardView extends CollectionSubView() {
}
});
+ cardSizerDown = (e: React.PointerEvent) => {
+ runInAction(() => {
+ this._cursor = 'grabbing';
+ });
+ const batch = UndoManager.StartBatch('card view size');
+ setupMoveUpEvents(
+ this,
+ e,
+ (emove: PointerEvent, down: number[], delta: number[]) => {
+ this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0]));
+ return false;
+ },
+ action(() => {
+ this._cursor = 'ew-resize';
+ batch.end();
+ }),
+ emptyFunction
+ );
+ };
+
/**
* turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked
*/
cardPointerUp = action((doc: Doc) => {
- if (this._curDoc === doc || this._dropped) {
+ if (this.curDoc() === doc || this._dropped) {
this._dropped = false;
} else {
this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
@@ -596,44 +555,53 @@ export class CollectionCardView extends CollectionSubView() {
});
focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => {
- const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
- if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
- options.didMove = true;
- const target = DocCast(anchor.annotationOn) ?? anchor;
- const index = docs.indexOf(target);
- index !== -1 && (this._curDoc = target);
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const foundDoc = DocCast(
+ anchor.config_card_curDoc,
+ docs.find(doc => doc === DocCast(anchor.annotationOn, anchor))
+ );
+ options.didMove = foundDoc !== this.curDoc() ? true : false;
+ options.didMove && (this.layoutDoc._card_curDoc = foundDoc);
+ }
return undefined;
});
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
+ PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
/**
* Actually renders all the cards
*/
@computed get renderCards() {
- console.log('CHILDPw = ' + this.childPanelWidth());
// Map sorted documents to their rendered components
- return this.sortedDocs.map((doc, index) => {
+ return this.childDocs.map((doc, index) => {
const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
- const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this._curDoc);
+ const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
- const translateToCenterIfActive = () => (doc === this._curDoc ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
+ const translateToCenterIfActive = () => (doc === this.curDoc() ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()),
(this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
- const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
+ const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
return (
<div
key={doc[Id]}
- className={`card-item${doc === this._curDoc ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
+ className={`card-item${doc === this.curDoc() ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
onPointerUp={() => this.cardPointerUp(doc)}
style={{
width: this.childPanelWidth(),
height: 'max-content',
- transform: `translateY(${this.adjustCardYtoFitArch(doc === this._curDoc, index)}px)
+ transform: `translateY(${this.adjustCardYtoFitArch(doc === this.curDoc(), index)}px)
translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px))
- rotate(${doc !== this._curDoc ? this.rotate(index) : 0}rad)
- scale(${doc === this._curDoc ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
+ rotate(${doc !== this.curDoc()? this.rotate(index) : 0}rad)
+ scale(${doc === this.curDoc()? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
}} // prettier-ignore
onPointerEnter={() => this.setHoveredNodeIndex(index)}
onPointerLeave={() => this.setHoveredNodeIndex(-1)}>
@@ -651,10 +619,10 @@ export class CollectionCardView extends CollectionSubView() {
isContentActive: emptyFunction,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
- answered = action(() => {
- this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
- });
- curDoc = () => this._curDoc;
+ answered = () => {
+ this.layoutDoc._card_curDoc = this.curDoc() ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+ };
+ curDoc = () => DocCast(this.layoutDoc._card_curDoc);
render() {
const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
@@ -662,11 +630,8 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="collectionCardView-outer"
ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
- onPointerDown={action(e => {
- if (e.button === 2 || e.ctrlKey) return;
- this.releaseCurDoc();
- })}
- onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+ onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()}
+ onPointerLeave={action(() => (this.docDraggedIndex = -1))}
onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
onDrop={this.onExternalDrop.bind(this)}
style={{
@@ -683,6 +648,7 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="collectionCardView-cardwrapper"
style={{
+ gridTemplateColumns: `repeat(${this._maxRowCount}, 1fr)`,
gridAutoRows: `${100 / this.numRows}%`,
height: `${this.cardSpacing}%`,
}}>
@@ -699,6 +665,14 @@ export class CollectionCardView extends CollectionSubView() {
{this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
</div>
</div>
+
+ <div
+ className="collectionCardView-cardSizeDragger"
+ onPointerDown={this.cardSizerDown}
+ ref={this._draggerRef}
+ style={{ display: this._props.isContentActive() ? undefined : 'none', cursor: this._cursor, color: SettingsManager.userColor, left: `${this.cardWidth + this.xMargin}px` }}>
+ <FontAwesomeIcon icon="arrows-alt-h" />
+ </div>
</div>
);
}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index a71cc43ba..9c8ef5519 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,6 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { returnZero } from '../../../ClientUtils';
import { Utils } from '../../../Utils';
@@ -8,14 +9,15 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
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 { Transform } from '../../util/Transform';
+import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
import './CollectionCarousel3DView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { computedFn } from 'mobx-utils';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -30,6 +32,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -70,33 +75,45 @@ export class CollectionCarousel3DView extends CollectionSubView() {
? false
: undefined
);
- contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().translate(0, (-(Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) / this.nativeScaling());
childScreenLeftToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ (-this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childScreenRightToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ -2 * this.panelWidth() - (this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childCenterScreenToLocal = () =>
this.contentScreenToLocalXf()
.translate(
-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
- -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
- ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
+ ((this.centerScale - 1) * this.panelHeight()) / 2
+ )
.scale(1 / this.centerScale);
focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
- const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
- if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
- options.didMove = true;
- const target = DocCast(anchor.annotationOn) ?? anchor;
- const index = docs.indexOf(target);
- index !== -1 && (this.layoutDoc._carousel_index = index);
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
return undefined;
};
-
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
+ PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
const displayDoc = (child: Doc, dxf: () => Transform) => (
@@ -119,6 +136,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
/>
);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 1f2bc908f..87c6e3e5c 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,12 +3,16 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
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 { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -27,6 +31,9 @@ export class CollectionCarouselView extends CollectionSubView() {
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -67,6 +74,24 @@ export class CollectionCarouselView extends CollectionSubView() {
curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
+ focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
+ return undefined;
+ };
+
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
+ PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
+
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
// first look for properties on the document in the carousel, then fallback to properties on the container
const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -113,6 +138,7 @@ export class CollectionCarouselView extends CollectionSubView() {
LayoutTemplateString={this._props.childLayoutString}
TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
childFilters={this.childDocFilters}
+ focus={this.focus}
hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
addDocument={this._props.addDocument}
ScreenToLocalTransform={this.contentScreenToLocalXf}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e1786d2c9..539b49c86 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -382,7 +382,7 @@ export class CollectionDockingView extends CollectionSubView() {
} catch {
/* empty */
}
- this._goldenLayout?.destroy();
+ setTimeout(() => this._goldenLayout?.destroy());
window.removeEventListener('resize', this.onResize);
window.removeEventListener('mouseup', this.onPointerUp);
@@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() {
}
}
}
- if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
e.stopPropagation();
}
};
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index dab1298d5..de999c91a 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -2,7 +2,7 @@
/* eslint-disable react/sort-comp */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { Toggle, ToggleType, Type } from 'browndash-components';
+import { Toggle, ToggleType, Type } from '@dash/components';
import { Lambda, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index ac8e37358..4c23d4c48 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils';
-import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Copy, Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
@@ -48,6 +48,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
@computed get notetakingCategoryField() {
return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column'));
}
+ toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset'));
public DividerWidth = 16;
@observable docsDraggedRowCol: number[] = [];
@observable _scroll = 0;
@@ -136,7 +137,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const rowCol = this.docsDraggedRowCol;
// this will sort the docs into the correct columns (minus the ones you're currently dragging)
docs.forEach(d => {
- const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
+ const sectionValue = this.toHeader(d);
// look for if header exists already
const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
if (existingHeader) {
@@ -161,7 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
};
@computed get allFieldValues() {
- return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField])));
+ return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField]))));
}
componentDidMount() {
@@ -313,7 +314,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// how to get the width of a document. Currently returns the width of the column (minus margins)
// if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
getDocWidth = (d: Doc) => {
- const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType);
+ const heading = this.toHeader(d);
const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0;
const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
@@ -427,7 +428,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
this.childDocs?.forEach(d => {
if (d instanceof Promise) return;
- const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+ const sectionValue = this.toHeader(d);
if (sectionValue.toString() === colHeader) {
docsMatchingHeader.push(d);
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 486c826b6..c3047e5fb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-use-before-define */
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -59,7 +58,6 @@ export enum TrimScope {
@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
- // eslint-disable-next-line no-use-before-define
public static SelectingRegions: Set<CollectionStackedTimeline> = new Set();
public static StopSelecting() {
this.SelectingRegions.forEach(
@@ -171,7 +169,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
makeDocUnfiltered = (doc: Doc) => this.childDocList?.some(item => item === doc);
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden) options.didMove = !(doc.hidden = false);
const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
@@ -600,7 +598,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
pointerEvents: 'none',
}}>
<StackedTimelineAnchor
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
mark={d.anchor}
containerViewPath={this._props.containerViewPath}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1ac0b6d70..a57256424 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -146,7 +146,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
// just getting the style
- const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` };
+ const style = this.isStackingView
+ ? {
+ //
+ margin: undefined,
+ transition: this.getDocTransition(d)(),
+ width: this.columnWidth,
+ marginTop: i ? this.gridGap : 0,
+ height: this.getDocHeight(d)(),
+ zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
+ }
+ : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
// So we're choosing whether we're going to render a column or a masonry doc
return (
<div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
@@ -344,6 +354,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
rootSelected={this.rootSelected}
showTitle={this._props.childlayout_showTitle}
+ showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType}
onClickScript={this.onChildClickHandler}
onDoubleClickScript={this.onChildDoubleClickHandler}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 48aac3a68..5e99bec39 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
import { action, computed, makeObservable, observable } from 'mobx';
import * as React from 'react';
import * as rp from 'request-promise';
-import { ClientUtils, returnFalse } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils';
import CursorField from '../../../fields/CursorField';
import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
import { AclPrivate, DocData } from '../../../fields/DocSymbols';
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -27,7 +27,16 @@ import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldViewProps } from '../nodes/FieldView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FlashcardPracticeUI } from './FlashcardPracticeUI';
-
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
+
+export enum docSortings {
+ Time = 'time',
+ Type = 'type',
+ Color = 'color',
+ Chat = 'chat',
+ Tag = 'tag',
+ None = '',
+}
export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -130,6 +139,17 @@ export function CollectionSubView<X>() {
@computed get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
+
+ addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+ const doc = toList(docsIn).lastElement();
+ const where = location.split(':')[0];
+ if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+ if (doc.hidden) doc.hidden = false;
+ if (!location.includes(OpenWhereMod.always)) return true;
+ }
+ return this._props.addDocTab(docsIn, location);
+ };
+
collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
// child filters apply to the descendants of the documents in this collection
@@ -138,6 +158,8 @@ export function CollectionSubView<X>() {
unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);
+
+ @observable docDraggedIndex = -1;
@computed.struct get childDocs() {
TraceMobx();
let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -154,8 +176,10 @@ export function CollectionSubView<X>() {
const templateRoot = this._props.TemplateDataDocument;
rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : [];
}
-
- const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
+ const childDocs = this.childSortedDocs(
+ rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc),
+ this.docDraggedIndex
+ );
const childDocFilters = this.childDocFilters();
const childFiltersByRanges = this.childDocRangeFilters();
@@ -202,6 +226,35 @@ export function CollectionSubView<X>() {
return docsforFilter;
}
+ childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
+ const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
+ const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+ const docs = docsIn.slice();
+ if (sortType) {
+ docs.sort((docA, docB) => {
+ const [typeA, typeB] = (() => {
+ switch (sortType) {
+ default:
+ case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)];
+ case docSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)];
+ case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
+ case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+ case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")];
+ }
+ })(); //prettier-ignore
+ return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
+ });
+ }
+ if (dragIndex !== -1) {
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+ originalIndex !== -1 && docs.splice(originalIndex, 1);
+ draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
+ }
+ return docs;
+ };
+
@action
protected async setCursorPosition(position: [number, number]) {
let ind;
@@ -519,6 +572,7 @@ export function CollectionSubView<X>() {
};
protected _sideBtnWidth = 35;
+ protected _sideBtnMaxPanelPct = 0.15;
@observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
/**
* How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
@@ -529,7 +583,7 @@ export function CollectionSubView<X>() {
* This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted
* size or a fraction of the collection view.
*/
- @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: 0.25) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
+ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
/**
* This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
* Note, the scale factor does not allow for elements to grow larger than their native screen space size.
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 8a24db330..d75c633ac 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -52,7 +52,7 @@ export class CollectionTimeView extends CollectionSubView() {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
annotationOn: this.Document,
});
- PinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
+ PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
if (addAsAnnotation) {
// when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -147,7 +147,6 @@ export class CollectionTimeView extends CollectionSubView() {
return (
<div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
fitContentsToBox={returnTrue}
@@ -257,7 +256,6 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
const pivotView = DocumentView.getDocumentView(pivotDoc);
if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
- // eslint-disable-next-line prefer-destructuring
pivotDoc._pivotField = filterVals[0];
}
}
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 9e9318c0a..c071c5fb8 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { MultiToggle, Type } from 'browndash-components';
+import { MultiToggle, Type } from '@dash/components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
@computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
- @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+ @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -127,7 +127,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'lightgray');
const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- return !this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? null : (
+ return !this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? null : (
<div
className="FlashcardPracticeUI-practiceModes"
style={{
@@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
</div>
);
}
- tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+ tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
render() {
return (
<div className="FlashcardPracticeUI">
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5bfdee1f5..cc56a8ff9 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Popup, Type } from 'browndash-components';
+import { Popup, Type } from '@dash/components';
import { clamp } from 'lodash';
import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -285,6 +285,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
static Activate = (tabDoc: Doc) => {
const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
+ if (tab && tab.header.parent._activeContentItem === tab.contentItem) return false;
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 9284a36a2..ab4d8b060 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton, Size } from 'browndash-components';
+import { IconButton, Size } from '@dash/components';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
index 6ad67a864..3838852dd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, observable, untracked } from 'mobx';
import { CollectionFreeFormView } from '.';
import { intersectRect } from '../../../../Utils';
import { Doc, Opt } from '../../../../fields/Doc';
@@ -179,18 +179,20 @@ export class CollectionFreeFormClusters {
};
styleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
- if (doc && this.childDocs?.includes(doc))
+ // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs.
+ // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway.
+ if (doc && untracked(() => this.childDocs)?.includes(doc))
switch (property.split(':')[0]) {
case StyleProp.BackgroundColor:
{
const cluster = NumCast(doc?.layout_cluster);
- if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
+ if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG && !doc.layout_isSvg) {
if (this._clusterSets.length <= cluster) {
setTimeout(() => doc && this.addDocument(doc));
} else {
const palette = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
- // override palette cluster color with an explicitly set cluster doc color
- return this._clusterSets[cluster]?.reduce((b, s) => StrCast(s.backgroundColor, b), palette[cluster % palette.length]);
+ // override palette cluster color with an explicitly set cluster doc color ONLY if doc color matches the current default text color
+ return this._clusterSets[cluster]?.reduce((b, s) => (s.backgroundColor !== Doc.UserDoc().textBackgroundColor ? StrCast(s.backgroundColor, b) : b), palette[cluster % palette.length]);
}
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
index 51add85a8..437888ef2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
@@ -1,4 +1,4 @@
-import { IconButton, Size, Type } from 'browndash-components';
+import { IconButton, Size, Type } from '@dash/components';
import { IReactionDisposer, action, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 5d8373fc7..8b9a3e0ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
}
_firstDocPos = { x: 0, y: 0 };
- constructor(props: any) {
+ constructor(props: CollectionFreeFormInfoUIProps) {
super(props);
makeObservable(this);
this._currState = this.setupStates();
@@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
return presentDocs;
}],
// eslint-disable-next-line no-use-before-define
- activePen: [() => activeTool() === InkTool.Pen, () => penMode],
+ activePen: [() => activeTool() === InkTool.Ink, () => penMode],
},
'documentation.png',
() => TopBar.Instance.FlipDocumentationIcon()
@@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', {
// activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode],
- activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink],
+ activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink],
}); // prettier-ignore
// const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 79aad0ef2..bebdbd731 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -44,6 +44,7 @@ export interface PoolData {
transition?: string;
highlight?: boolean;
pointerEvents?: string;
+ showTags?: boolean;
}
export interface ViewDefResult {
@@ -102,7 +103,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
replica: '',
});
});
- // eslint-disable-next-line no-use-before-define
return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
}
@@ -272,7 +272,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
payload: pivotColumnGroups.get(key)?.filters,
}));
groupNames.push(...dividers);
- // eslint-disable-next-line no-use-before-define
return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
}
@@ -347,7 +346,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
}
- // eslint-disable-next-line no-use-before-define
layoutDocsAtTime(keyDocs, key);
});
if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
@@ -428,6 +426,7 @@ function normalizeResults(
opacity: newPosRaw.opacity,
color: newPosRaw.color,
pair: ele[1].pair,
+ showTags: newPosRaw.showTags,
};
if (newPosRaw.transition) newPos.transition = newPosRaw.transition;
poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 2c94446fb..46bd37f6d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -304,3 +304,65 @@
display: none;
}
}
+
+.collectionFreeformView-aiView {
+ text-align: center;
+ font-weight: bold;
+ width: 100%;
+
+ .collectionfreeformview-aiView-prompt {
+ height: 25px;
+ width: 65%;
+ }
+
+ .collectionFreeFormView-aiView-strength {
+ text-align: center;
+ align-items: center;
+ display: flex;
+ width: 25%;
+ .collectionFreeFormView-aiView-similarity {
+ max-width: 65px;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .collectionFreeForView-aiView-send {
+ width: 10%;
+ .button-container {
+ width: 100% !important;
+ justify-content: left !important;
+ }
+ }
+
+ .collectionFreeformView-aiView-options-container,
+ .collectionFreeFormView-aiView-regenerate-container {
+ text-align: start;
+ font-weight: normal;
+ width: 100%;
+ display: flex;
+ .collectionFreeformView-aiView-subtitle {
+ margin: auto;
+ width: 40px;
+ }
+ }
+ .collectionFreeformView-aiView-options,
+ .collectionFreeFormView-aiView-regenerate {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ align-items: center;
+ width: 100%;
+ gap: 10px;
+ .collectionFreeformView-aiView-input {
+ width: 100%;
+ }
+ .collectionFreeFormView-aiView-regenBtn {
+ width: 10%;
+ .button-container {
+ width: 100% !important;
+ }
+ }
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d2bc8f2c2..3c31b584e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,5 +1,5 @@
import { Bezier } from 'bezier-js';
-import { Colors } from 'browndash-components';
+import { Button, Colors, Type } from '@dash/components';
import { Property } from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData, Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
+import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { RichTextField } from '../../../../fields/RichTextField';
import { listSpec } from '../../../../fields/Schema';
@@ -36,13 +36,26 @@ import { ContextMenu } from '../../ContextMenu';
import { InkingStroke } from '../../InkingStroke';
import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView';
+import {
+ ActiveInkArrowEnd,
+ ActiveInkArrowStart,
+ ActiveInkDash,
+ ActiveEraserWidth,
+ ActiveInkFillColor,
+ ActiveInkBezierApprox,
+ ActiveInkColor,
+ ActiveInkWidth,
+ ActiveIsInkMask,
+ DocumentView,
+ SetActiveInkColor,
+ SetActiveInkWidth,
+} from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
+import { OpenWhere } from '../../nodes/OpenWhere';
import { PinDocView, PinProps } from '../../PinFuncs';
-import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
+import { StickerPalette } from '../../smartdraw/StickerPalette';
import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
import { StyleProp } from '../../StyleProp';
import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
@@ -54,6 +67,11 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
+import ReactLoading from 'react-loading';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { Slider } from '@mui/material';
+import { AiOutlineSend } from 'react-icons/ai';
+import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
@observer
class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -85,6 +103,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const parent = CollectionFreeFormDocumentView.from(dv)?._props.reactParent;
return parent instanceof CollectionFreeFormView ? parent : undefined;
}
+ /**
+ * The Freeformview below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to.
+ */
+ // eslint-disable-next-line no-use-before-define
+ public static DownFfview: CollectionFreeFormView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to.
private _clusters = new CollectionFreeFormClusters(this);
private _oldWheel: HTMLDivElement | null = null;
@@ -209,7 +232,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
- return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true);
+ return DocumentView.SetViewTransition(docs, 'all', duration, timer, true);
}
changeKeyFrame = (back = false) => {
const currentFrame = Cast(this.Document._currentFrame, 'number', null);
@@ -389,7 +412,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return undefined;
};
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
if (doc === this.Document) {
@@ -464,11 +487,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// do nothing if link is dropped into any freeform view parent of dragged document
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' });
const added = !!this._props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' });
- if (de.complete.linkDocument) {
- de.complete.linkDocument.layout_isSvg = true;
- this.addDocument(de.complete.linkDocument);
- }
+ de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { layout_isSvg: true, link_relationship: 'annotated by:annotation of' });
+ de.complete.linkDocument && this.addDocument(de.complete.linkDocument);
e.stopPropagation();
!added && e.preventDefault();
return added;
@@ -487,6 +507,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
onPointerDown = (e: React.PointerEvent): void => {
+ if (!CollectionFreeFormView.DownFfview) CollectionFreeFormView.DownFfview = this;
+
this._downX = this._lastX = e.pageX;
this._downY = this._lastY = e.pageY;
this._downTime = Date.now();
@@ -497,13 +519,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// prettier-ignore
const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
switch (Doc.ActiveTool) {
- case InkTool.Highlighter:
- case InkTool.Write:
- case InkTool.Pen:
+ case InkTool.Ink:
break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
- case InkTool.StrokeEraser:
- case InkTool.SegmentEraser:
- case InkTool.RadiusEraser:
+ case InkTool.Eraser:
this._batch = UndoManager.StartBatch('collectionErase');
this._eraserPts.length = 0;
setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
@@ -543,7 +561,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const { points } = ge;
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = this.createInkDoc(points, B);
- if (Doc.ActiveTool === InkTool.Write) {
+ if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent';
+ if (Doc.ActiveInk === InkInkTool.Write) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
}
@@ -606,7 +625,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const currPoint = { X: e.clientX, Y: e.clientY };
this._eraserPts.push([currPoint.X, currPoint.Y]);
this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
- if (Doc.ActiveTool === InkTool.RadiusEraser) {
+ if (Doc.ActiveEraser === InkEraserTool.Radius) {
const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
strokeMap.forEach((intersects, stroke) => {
if (!this._deleteList.includes(stroke)) {
@@ -614,13 +633,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
const segments = this.radiusErase(stroke, intersects.sort());
- segments?.forEach(segment =>
- this.forceStrokeGesture(
- e,
- Gestures.Stroke,
- segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
- )
- );
+ segments?.forEach(segment => {
+ const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+ const bounds = InkField.getBounds(points);
+ const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkDoc = this.createInkDoc(points, B);
+ ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => {
+ inkDoc[DocData][field] = stroke.dataDoc[field];
+ });
+ this.addDocument(inkDoc);
+ });
}
stroke.layoutDoc.opacity = 0;
stroke.layoutDoc.dontIntersect = true;
@@ -632,7 +654,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
// create a new curve by appending all curves of the current segment together in order to render a single new stroke.
- if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+ if (Doc.ActiveEraser !== InkEraserTool.Stroke) {
// this._eraserLock++;
const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
const newStrokes = segments?.map(segment => {
@@ -1195,14 +1217,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
ActiveInkColor(),
ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ ActiveInkFillColor(),
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
};
@@ -1212,7 +1234,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
- SmartDrawHandler.Instance.displaySmartDrawHandler(x, y);
+ SmartDrawHandler.Instance.displaySmartDrawHandler(x, y, NumCast(this.layoutDoc[this.scaleFieldKey]));
};
_drawing: Doc[] = [];
@@ -1235,14 +1257,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
this._drawing.push(inkDoc);
@@ -1272,12 +1294,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
const docData = doc[DocData];
docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
- docData.width = opts.size;
- docData.drawingInput = opts.text;
- docData.drawingComplexity = opts.complexity;
- docData.drawingColored = opts.autoColor;
- docData.drawingSize = opts.size;
- docData.drawingData = gptRes;
+ docData._width = opts.size;
+ docData.ai_drawing_input = opts.text;
+ docData.ai_drawing_complexity = opts.complexity;
+ docData.ai_drawing_colored = opts.autoColor;
+ docData.ai_drawing_size = opts.size;
+ docData.ai_drawing_data = gptRes;
+ docData.ai = 'gpt';
this._drawingContainer = doc;
this.addDocument(doc);
this._batch?.end();
@@ -1307,6 +1330,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.Document[this.scaleFieldKey] = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, (this._props.originTopLeft ? undefined : NumCast(this.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale, undefined, allowScroll);
}
+ SmartDrawHandler.Instance.hideSmartDrawHandler();
};
@action
@@ -1596,21 +1620,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
case undefined:
case OpenWhere.lightbox:
- {
- const firstDoc = docs[0];
- if (this.layoutDoc._isLightbox) {
- this._lightboxDoc = firstDoc;
- return true;
- }
- if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) {
- if (firstDoc.hidden) firstDoc.hidden = false;
- if (!location.includes(OpenWhereMod.always)) return true;
- }
+ if (this.layoutDoc._isLightbox) {
+ this._lightboxDoc = docs[0];
+ return true;
}
- break;
+ return this.addLinkedDocTab(docsIn, location);
default:
}
- return this._props.addDocTab(docs, location);
+ return this._props.addDocTab(docsIn, location);
});
getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData {
const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
@@ -1641,6 +1658,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
width: _width,
height: _height,
transition: StrCast(childDocLayout.dataTransition),
+ showTags: BoolCast(childDocLayout.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags),
pointerEvents: Cast(childDoc.pointerEvents, 'string', null),
pair,
replica: '',
@@ -1750,7 +1768,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
PinDocView(
anchor,
- { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } },
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
this.Document
);
@@ -1854,14 +1872,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
updateIcon = (usePanelDimensions?: boolean) => {
- const contentDiv = this.DocumentView?.().ContentDiv;
+ const contentDiv = this._mainCont;
return !contentDiv
? new Promise<void>(res => res())
: UpdateIcon(
this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
contentDiv,
- usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
- usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+ usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+ usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
this._props.PanelWidth(),
this._props.PanelHeight(),
0,
@@ -1981,20 +1999,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}),
icon: 'eye',
});
+ this.layoutDoc.drawingData != undefined &&
+ optionItems.push({
+ description: 'Regenerate AI Drawing',
+ event: action(() => {
+ SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
+ }),
+ icon: 'pen-to-square',
+ });
optionItems.push({
- description: 'Show Drawing Editor',
- event: action(() => {
- SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
- SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
- SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
- !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
- }),
- icon: 'pen-to-square',
- });
- optionItems.push({
- description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette',
- event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')),
- icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down',
+ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
+ event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
+ icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
});
this._props.renderDepth &&
optionItems.push({
@@ -2173,6 +2192,102 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
</div>
);
}
+
+ @observable private _regenInput = '';
+ @observable private _drawingFillInput = '';
+ @observable private _regenLoading = false;
+ @observable private _drawingFillLoading = false;
+ @observable private _fireflyRefStrength = 50;
+
+ componentAIView = () => {
+ return (
+ <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
+ <div className="collectionfreeformview-aiView-options-container">
+ <span className="collectionfreeformview-aiView-subtitle">Firefly:</span>
+ <div className="collectionfreeformview-aiView-options">
+ <input
+ className="collectionfreeformview-aiView-prompt"
+ placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
+ type="text"
+ value={this._drawingFillInput}
+ onChange={action(e => {
+ this._drawingFillInput = e.target.value;
+ })}
+ />
+ <div className="collectionFreeFormView-aiView-strength">
+ <span className="collectionFreeFormView-aiView-similarity">Similarity</span>
+ <Slider
+ className="collectionfreeformview-aiView-slider"
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+ }}
+ min={1}
+ max={100}
+ step={1}
+ size="small"
+ value={this._fireflyRefStrength}
+ onChange={action((e, val) => (this._fireflyRefStrength = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div className="collectionFreeFormView-aiView-send">
+ <Button
+ text="Send"
+ type={Type.SEC}
+ icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ onClick={undoable(
+ action(() => {
+ this._drawingFillLoading = true;
+ DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+ action(() => {
+ this._drawingFillLoading = false;
+ })
+ );
+ }),
+ 'create image'
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ <div className="collectionfreeformview-aiView-regenerate-container">
+ <span className="collectionfreeformview-aiView-subtitle">Regenerate</span>
+ <div className="collectionfreeformview-aiView-regenerate">
+ <input
+ className="collectionfreeformview-aiView-input"
+ aria-label="Edit instructions input"
+ type="text"
+ value={this._regenInput}
+ onChange={action(e => {
+ this._regenInput = e.target.value;
+ })}
+ placeholder="..under development.."
+ />
+ <div className="collectionFreeFormView-aiView-regenBtn">
+ <Button
+ text="Regenerate"
+ type={Type.SEC}
+ icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ // onClick={action(async () => {
+ // this._regenLoading = true;
+ // SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ // SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ // SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ // await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+ // this._regenLoading = false;
+ // })}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ };
+
render() {
TraceMobx();
return (
@@ -2202,7 +2317,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
width: `${100 / this.nativeDimScaling}%`,
height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
}}>
- {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && (
+ {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
<div
onPointerMove={this.onCursorMove}
style={{
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 6d51ecac6..b9f8b13a7 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton, Size } from 'browndash-components';
+import { IconButton, Size } from '@dash/components';
import * as faceapi from 'face-api.js';
import { FaceMatcher } from 'face-api.js';
import 'ldrs/ring';
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index 583f2e656..a3d9641da 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors, IconButton } from 'browndash-components';
+import { Colors, IconButton } from '@dash/components';
import similarity from 'compute-cosine-similarity';
import { ring } from 'ldrs';
import 'ldrs/ring';
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
index 73befb205..f050b9846 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index de65b240f..abd828945 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c865c681d..5524fedb3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -586,7 +586,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
/**
* When this is called, returns the list of documents that have been selected by the marquee box.
*/
- marqueeSelect(selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) {
+ marqueeSelect = (selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) => {
const selection: Doc[] = [];
const selectFunc = (doc: Doc) => {
const layoutDoc = Doc.Layout(doc);
@@ -619,7 +619,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
.filter(doc => doc.z !== undefined)
.map(selectFunc);
return selection;
- }
+ };
@computed get marqueeDiv() {
const cpt = this._lassoFreehand || !this._visible ? [0, 0] : [this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY];
@@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
}}
style={{
overflow: StrCast(this._props.Document._overflow),
- cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
+ cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
}}
onDragOver={e => e.preventDefault()}
onScroll={e => {
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 5c41fee37..6dffb80f1 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -186,21 +186,23 @@ export class CollectionGridView extends CollectionSubView() {
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- setContentViewBox={emptyFunction}
Document={layout}
TemplateDataDocument={layout.resolvedDataDoc as Doc}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ fitWidth={this._props.childLayoutFitWidth}
+ containerViewPath={this.childContainerViewPath}
+ renderDepth={this._props.renderDepth + 1}
isContentActive={this.isChildContentActive}
PanelWidth={width}
PanelHeight={height}
ScreenToLocalTransform={dxf}
+ setContentViewBox={emptyFunction}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
onClickScript={this.onChildClickHandler}
- renderDepth={this._props.renderDepth + 1}
dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'}
+ showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
/>
);
}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index ceae43c04..80116dd2f 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { Toggle, ToggleType, Type } from 'browndash-components';
+import { Toggle, ToggleType, Type } from '@dash/components';
import { Property } from 'csstype';
import { IReactionDisposer, action, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index d67e10c0b..8aae24df0 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { Button, IconButton } from 'browndash-components';
+import { Button, IconButton } from '@dash/components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index aef97e723..5a5cc3622 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton, Size } from 'browndash-components';
+import { IconButton, Size } from '@dash/components';
import { IReactionDisposer, Lambda, ObservableMap, action, computed, makeObservable, observable, observe, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 9ffdd812f..81a2d8e64 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -17,7 +17,7 @@ import { DocCast } from '../../../../fields/Types';
import { computedFn } from 'mobx-utils';
import { CollectionSchemaView } from './CollectionSchemaView';
import { undoable } from '../../../util/UndoManager';
-import { IconButton, Size } from 'browndash-components';
+import { IconButton, Size } from '@dash/components';
export enum SchemaFieldType {
Header,
@@ -122,45 +122,53 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
@computed get editableView() {
const { color, fieldProps, pointerEvents } = this.renderProps(this._props);
- return <div className='schema-column-edit-wrapper' onPointerUp={() => {
- SchemaColumnHeader.isDefaultField(this.fieldKey) && this.openKeyDropdown();
- this._props.schemaView.deselectAllCells();
- }}
- style={{
- color,
- width: '100%',
- pointerEvents,
- }}>
- <EditableView
- ref={r => {this._inputRef = r; this._props.autoFocus && r?.setIsFocused(true)}}
- oneLine={true}
- allowCRs={false}
- contents={''}
- onClick={this.openKeyDropdown}
- fieldContents={fieldProps}
- editing={undefined}
- placeholder={'Add key'}
- updateAlt={this.updateAlt} // alternate title to display
- updateSearch={this.updateKeyDropdown}
- inputString={true}
- inputStringPlaceholder={'Add key'}
- GetValue={() => {
- if (SchemaColumnHeader.isDefaultField(this.fieldKey)) return '';
- else if (this._altTitle) return this._altTitle;
- else return this.fieldKey;
+ return (
+ <div
+ className="schema-column-edit-wrapper"
+ onPointerUp={() => {
+ SchemaColumnHeader.isDefaultField(this.fieldKey) && this.openKeyDropdown();
+ this._props.schemaView.deselectAllCells();
}}
- SetValue={undoable((value: string, shiftKey?: boolean, enterKey?: boolean) => {
- if (enterKey) {
- // if shift & enter, set value of each cell in column
- this.setColumnValues(value, '');
- this._altTitle = undefined;
+ style={{
+ color,
+ width: '100%',
+ pointerEvents,
+ }}>
+ <EditableView
+ ref={r => {
+ this._inputRef = r;
+ this._props.autoFocus && r?.setIsFocused(true);
+ }}
+ oneLine={true}
+ allowCRs={false}
+ contents={''}
+ onClick={this.openKeyDropdown}
+ fieldContents={fieldProps}
+ editing={undefined}
+ placeholder={'Add key'}
+ updateAlt={this.updateAlt} // alternate title to display
+ updateSearch={this.updateKeyDropdown}
+ inputString={true}
+ inputStringPlaceholder={'Add key'}
+ GetValue={() => {
+ if (SchemaColumnHeader.isDefaultField(this.fieldKey)) return '';
+ else if (this._altTitle) return this._altTitle;
+ else return this.fieldKey;
+ }}
+ SetValue={undoable((value: string, shiftKey?: boolean, enterKey?: boolean) => {
+ if (enterKey) {
+ // if shift & enter, set value of each cell in column
+ this.setColumnValues(value, '');
+ this._altTitle = undefined;
+ this._props.finishEdit?.();
+ return true;
+ }
this._props.finishEdit?.();
return true;
- }
- this._props.finishEdit?.();
- return true;
- }, 'edit column header')}/>
+ }, 'edit column header')}
+ />
</div>
+ );
}
public static isDefaultField = (key: string) => {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 6ffb0865a..da203abfa 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -1,4 +1,4 @@
-import { IconButton, Size } from 'browndash-components';
+import { IconButton, Size } from '@dash/components';
import { computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
@@ -100,9 +100,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
return infos;
}
- isolatedSelection = (doc: Doc) => {
- return this.schemaView?.selectionOverlap(doc);
- };
+ isolatedSelection = (doc: Doc) => this.schemaView?.selectionOverlap(doc);
setCursorIndex = (mouseY: number) => this.schemaView?.setRelCursorIndex(mouseY);
selectedCol = () => this.schemaView._selectedCol;
getFinfo = computedFn((fieldKey: string) => this.schemaView?.fieldInfos.get(fieldKey));
@@ -113,9 +111,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
columnWidth = computedFn((index: number) => () => this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth);
computeRowIndex = () => this.schemaView?.rowIndex(this.Document);
highlightCells = (text: string) => this.schemaView?.highlightCells(text);
- selectReference = (doc: Doc, col: number) => {
- this.schemaView.selectReference(doc, col);
- };
+ selectReference = (doc: Doc, col: number) => this.schemaView.selectReference(doc, col);
eqHighlightFunc = (text: string) => {
const info = this.schemaView.findCellRefs(text);
const cells: HTMLDivElement[] = [];
@@ -188,7 +184,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
selectedCells={this.selectedCells}
selectedCol={this.selectedCol}
setColumnValues={this.setColumnValues}
- oneLine={BoolCast(this.schemaDoc?._singleLine)}
+ oneLine={BoolCast(this.schemaDoc?._schema_singleLine)}
menuTarget={this.schemaView.MenuTarget}
transform={() => {
const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index;
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index f036ff843..cd46ae824 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,6 +1,6 @@
/* eslint-disable no-use-before-define */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Popup, Size, Type } from 'browndash-components';
+import { Popup, Size, Type } from '@dash/components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 954c79f7d..b44292164 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { Colors } from 'browndash-components';
+import { Colors } from '@dash/components';
import { runInAction } from 'mobx';
import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
-import { InkTool } from '../../../fields/InkField';
+import { InkEraserTool, InkInkTool, InkProperty, InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
@@ -16,21 +16,20 @@ import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager, undoable } from '../../util/UndoManager';
import { GestureOverlay } from '../GestureOverlay';
import { InkTranscription } from '../InkTranscription';
-import { InkingStroke } from '../InkingStroke';
import { PropertiesView } from '../PropertiesView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import {
ActiveEraserWidth,
- ActiveFillColor,
+ ActiveInkFillColor,
ActiveInkColor,
- ActiveInkHideTextLabels,
+ ActiveHideTextLabels,
ActiveInkWidth,
ActiveIsInkMask,
DocumentView,
- SetActiveFillColor,
+ SetActiveInkFillColor,
SetActiveInkColor,
- SetActiveInkHideTextLabels,
+ SetactiveHideTextLabels,
SetActiveInkWidth,
SetActiveIsInkMask,
SetEraserWidth,
@@ -41,6 +40,7 @@ import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import { OpenWhere } from '../nodes/OpenWhere';
+import { docSortings } from '../collections/CollectionSubView';
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function IsNoneSelected() {
@@ -49,10 +49,20 @@ ScriptingGlobals.add(function IsNoneSelected() {
// toggle: Set overlay status of selected document
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
- if (getSelected) return DocumentView.SelectedDocs();
- const selected = DocumentView.SelectedDocs().lastElement();
- selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
+ScriptingGlobals.add(function setView(view: string, shiftKey: boolean, checkResult?: boolean) {
+ if (checkResult) return DocumentView.SelectedDocs();
+ const selected = DocumentView.Selected().lastElement();
+ if (selected) {
+ if (shiftKey) {
+ const newCol = Doc.MakeEmbedding(selected.Document);
+ newCol._type_collection = view;
+ selected._props.addDocTab?.(newCol, OpenWhere.addRight);
+ } else {
+ selected.Document._type_collection = view;
+ }
+ } else {
+ console.log('[FontIconBox.tsx] changeView failed');
+ }
return undefined;
});
@@ -60,20 +70,21 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
const selectedViews = DocumentView.Selected();
+ const selectedDoc = selectedViews.lastElement()?.Document;
+ const defaultFill = selectedDoc?._layout_isSvg ? () => StrCast(selectedDoc[DocData].fillColor) : !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? () => StrCast(Doc.UserDoc().textBackgroundColor, 'transparent') : () => ActiveInkFillColor();
+ const setDefaultFill = !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? (c: string) => { Doc.UserDoc().textBackgroundColor = c; }: SetActiveInkFillColor; // prettier-ignore
if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) {
- if (checkResult) {
- return ActiveFillColor();
- }
- SetActiveFillColor(color ?? 'transparent');
+ if (checkResult) return defaultFill();
+ setDefaultFill(color ?? 'transparent');
} else if (selectedViews.length) {
if (checkResult) {
const selView = selectedViews.lastElement();
const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
- return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent';
+ return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill();
}
- selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent');
+ setDefaultFill(color ?? 'transparent');
selectedViews.forEach(dv => {
const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -92,10 +103,13 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
} else {
const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
if (checkResult) {
- return selected.lastElement()?._backgroundColor ?? 'transparent';
+ return selected.lastElement()?._backgroundColor ?? defaultFill();
}
- SetActiveFillColor(color ?? 'transparent');
- selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore
+ if (!selected.length) setDefaultFill(color ?? 'transparent');
+ else
+ selected.forEach(doc => {
+ doc[DocData][doc._layout_isSvg ? 'fillColor' : 'backgroundColor'] = color;
+ });
}
return '';
});
@@ -138,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' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag',
+ attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'toggle-chat' | 'toggle-tags' | 'tag',
checkResult?: boolean,
persist?: boolean
) {
@@ -149,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' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag',
+ const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down'| 'toggle-chat' | 'toggle-tags' | 'tag',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
@@ -184,43 +198,35 @@ ScriptingGlobals.add(function showFreeform(
checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false),
setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
}],
- ['flashcards', {
- checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
- setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
- }],
['time', {
- checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore
}],
['docType', {
- checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore
}],
['color', {
- checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore
}],
['tag', {
- checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
}],
['up', {
- checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
- setDoc: (doc: Doc, dv: DocumentView) => {
- doc.card_sort_isDesc = false;
- },
+ checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
}],
['down', {
- checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
- setDoc: (doc: Doc, dv: DocumentView) => {
- doc.card_sort_isDesc = true;
- },
+ checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+ setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
}],
['toggle-chat', {
- checkResult: (doc: Doc) => GPTPopup.Instance.visible,
+ checkResult: (doc: Doc) => GPTPopup.Instance.Visible,
setDoc: (doc: Doc, dv: DocumentView) => {
- if (GPTPopup.Instance.visible){
- doc.card_sort = ''
+ if (GPTPopup.Instance.Visible){
+ doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
GPTPopup.Instance.setVisible(false);
} else {
@@ -239,20 +245,6 @@ ScriptingGlobals.add(function showFreeform(
doc.showChildTags = !doc.showChildTags;
},
}],
- ['pile', {
- checkResult: (doc: Doc) => doc._type_collection == CollectionViewType.Freeform,
- setDoc: (doc: Doc, dv: DocumentView) => {
- const newCol = Docs.Create.CarouselDocument(DocListCast(doc[Doc.LayoutFieldKey(doc)]), {
- title: doc.title + "_carousel",
- _width: 250,
- _height: 200,
- _layout_fitWidth: false,
- _layout_autoHeight: true,
- childFilters: new List<string>(StrListCast(doc.childFilters))
- });
- dv._props.addDocTab?.(newCol, OpenWhere.addRight);
- },
- }],
]);
if (checkResult) {
@@ -295,10 +287,10 @@ ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, checkRes
}, '');
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: string | number, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
// prettier-ignore
- const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
+ const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
['font', {
checkResult: () => RichTextMenu.Instance?.fontFamily,
setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontFamily'),
@@ -311,10 +303,6 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
checkResult: () => RichTextMenu.Instance?.fontColor,
setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontColor'),
}],
- ['alignment', {
- checkResult: () => RichTextMenu.Instance?.textAlign,
- setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value.toString() as "center"|"left"|"right"):(Doc.UserDoc().textAlign = value); },
- }],
['fontSize', {
checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
setDoc: () => {
@@ -334,7 +322,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
return undefined;
});
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italic' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }];
// eslint-disable-next-line prefer-arrow-callback
@@ -343,14 +331,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
const editorView = textView?.EditorView;
// prettier-ignore
const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) =>
- [ where, { checkResult: () => editorView ? (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false:
- (RichTextMenu.Instance?.textAlign === where)):
- where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered):
- (Doc.UserDoc().textAlign === where),
- toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
- RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
- where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered:
- (Doc.UserDoc().textAlign = where); }
+ [ where, { checkResult: () => (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false:
+ (RichTextMenu.Instance?.textAlign === where)),
+ toggle: () => { (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
+ RichTextMenu.Instance?.align(editorView, editorView?.dispatch, where)); }
}]); // prettier-ignore
// prettier-ignore
const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
@@ -360,16 +344,18 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
const attrs:attrfuncs[] = [
['dictation', { checkResult: () => !!textView?._recordingDictation,
toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }],
+ ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false,
+ toggle: () => RichTextMenu.Instance?.toggleFitBox()}],
['elide', { checkResult: () => false,
toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false),
toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
['bold', { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')),
toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}],
- ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')),
- toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}],
- ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')),
- toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]]
+ ['italic', { checkResult: () => (editorView ? RichTextMenu.Instance?.italic ?? false : (Doc.UserDoc().fontStyle === 'italic')),
+ toggle: editorView ? RichTextMenu.Instance?.toggleItalic : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italic' ? undefined : 'italic'; }}],
+ ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().fontDecoration === 'underline')),
+ toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().fontDecoration = Doc.UserDoc().fontDecoration === 'underline' ? undefined : 'underline'; } }]]
const map = new Map(attrs.concat(alignments).concat(listings));
if (checkResult) {
@@ -379,43 +365,46 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
return undefined;
});
-function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
InkTranscription.Instance?.createInkGroup();
- const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
if (checkResult) {
- return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
- ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
+ return Doc.ActiveTool === tool || Doc.ActiveEraser === tool || Doc.ActiveInk === tool || SnappingManager.InkShape === tool
+ ? true //SnappingManager.KeepGestureMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
: false;
}
runInAction(() => {
+ const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
+ const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
if (GestureOverlay.Instance) {
- GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
+ SnappingManager.SetKeepGestureMode(keepPrim);
}
if (Object.values(Gestures).includes(tool as Gestures)) {
- if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
+ if (SnappingManager.InkShape === tool && !keepPrim) {
Doc.ActiveTool = InkTool.None;
- GestureOverlay.Instance.InkShape = undefined;
+ SnappingManager.SetInkShape(undefined);
} else {
- Doc.ActiveTool = InkTool.Pen;
- GestureOverlay.Instance.InkShape = tool as Gestures;
+ Doc.ActiveTool = InkTool.Ink;
+ SnappingManager.SetInkShape(tool as Gestures);
}
- } else if (tool) {
- if (Doc.UserDoc().ActiveTool === tool) {
+ } else if (eraserTool) {
+ if (Doc.ActiveTool === InkTool.Eraser && Doc.ActiveTool === tool) {
Doc.ActiveTool = InkTool.None;
} else {
- if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as InkTool)) {
- Doc.UserDoc().activeEraserTool = tool;
- }
- // pen or eraser
- if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
- Doc.ActiveTool = InkTool.None;
- } else {
- Doc.ActiveTool = tool as InkTool;
- GestureOverlay.Instance.InkShape = undefined;
- }
+ Doc.ActiveEraser = eraserTool;
+ Doc.ActiveTool = InkTool.Eraser;
+ SnappingManager.SetInkShape(undefined);
+ }
+ } else if (inkTool) {
+ if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveTool === tool) {
+ Doc.ActiveTool = InkTool.None;
+ } else {
+ Doc.ActiveInk = inkTool;
+ Doc.ActiveTool = InkTool.Ink;
+ SnappingManager.SetInkShape(undefined);
}
} else {
- Doc.ActiveTool = InkTool.None;
+ if ((Doc.ActiveTool === tool || !tool) && !keepPrim) Doc.ActiveTool = InkTool.None;
+ else Doc.ActiveTool = tool as InkTool;
}
});
return undefined;
@@ -425,44 +414,39 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function activeEraserTool() {
- return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
+ return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke);
}, 'returns the current eraser tool');
// toggle: Set overlay status of selected document
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: string | number, checkResult?: boolean) {
- const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc();
+ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string | number, checkResult?: boolean) {
+ const selected = DocumentView.SelectedDocs().lastElement();
// prettier-ignore
- const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
- ['inkMask', {
+ const map: Map<InkProperty, { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+ [InkProperty.Mask, {
checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; },
- setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
- }],
- ['labels', {
- checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
- setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; },
- setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
+ setMode: () => SetActiveIsInkMask(value ? true : false)
}],
- ['fillColor', {
- checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
- setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); },
- setMode: () => SetActiveFillColor(StrCast(value)),
+ [InkProperty.Labels, {
+ checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())),
+ setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; },
+ setMode: () => SetactiveHideTextLabels(value? false : true),
}],
- [ 'strokeWidth', {
- checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
+ [ InkProperty.StrokeWidth, {
+ checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width, 1) : ActiveInkWidth()),
setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
- setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+ setMode: () => SetActiveInkWidth(value.toString()),
}],
- ['strokeColor', {
+ [InkProperty.StrokeColor, {
checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()),
setInk: (doc: Doc) => { doc[DocData].color = String(value); },
- setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+ setMode: () => SetActiveInkColor(StrCast(value))
}],
- [ 'eraserWidth', {
+ [ InkProperty.EraserWidth, {
checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(),
setInk: (doc: Doc) => { },
- setMode: () => { SetEraserWidth(+value);},
+ setMode: () => SetEraserWidth(+value),
}]
]);
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index b24fca8e2..1c92c3b0d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
moveEv => {
const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed);
dragData.dropPropertiesToRemove = ['hidden'];
- DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true));
+ DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, () => (this._props.linkDoc._layout_isSvg = true));
return true;
},
emptyFunction,
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 3eb99f47a..8115bafbf 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
import { action } from 'mobx';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
@@ -35,10 +33,10 @@ export function ButtonMenu() {
<div
className="newLightboxView-penBtn"
title="toggle pen annotation"
- style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+ style={{ background: Doc.ActiveTool === InkTool.Ink ? 'white' : undefined }}
onClick={e => {
e.stopPropagation();
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
}}
/>
<div
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
index 882d28fba..64bafd9aa 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.tsx
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -1,4 +1,4 @@
-import { Button, IconButton, Size, Type } from 'browndash-components';
+import { Button, IconButton, Size, Type } from '@dash/components';
import * as React from 'react';
import { BsBookmark, BsBookmarkFill } from 'react-icons/bs';
import { MdTravelExplore } from 'react-icons/md';
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
index 27413bac3..7660da1f5 100644
--- a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable guard-for-in */
-import { IconButton, Size, Type } from 'browndash-components';
+import { IconButton, Size, Type } from '@dash/components';
import * as React from 'react';
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
import { GrClose } from 'react-icons/gr';
diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.tsx b/src/client/views/newlightbox/components/EditableText/EditableText.tsx
index 6273e1859..cff84e990 100644
--- a/src/client/views/newlightbox/components/EditableText/EditableText.tsx
+++ b/src/client/views/newlightbox/components/EditableText/EditableText.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import './EditableText.scss';
-import { Size } from 'browndash-components';
+import { Size } from '@dash/components';
export interface IEditableTextProps {
text: string;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d51b1cd3a..beea6ab3c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,7 +1,8 @@
+import { Colors } from '@dash/components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { OmitKeys } from '../../../ClientUtils';
+import { DashColor, OmitKeys } from '../../../ClientUtils';
import { numberRange } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { TransitionTimer } from '../../../fields/DocSymbols';
@@ -27,7 +28,7 @@ export enum GroupActive { // flags for whether a view is activate because of its
}
/// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need
/// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
-const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition'];
+const freeFormPropsKeys = ['x', 'y', 'z', 'width', 'height', 'zIndex', 'autoDim', 'rotation', 'color', 'backgroundColor', 'opacity', 'highlight', 'transition'];
interface freeFormProps {
x: number;
y: number;
@@ -197,7 +198,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
- const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true);
+ const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, true);
const timecode = Math.round(time);
docs.forEach(doc => {
this.animFields.forEach(val => {
@@ -296,12 +297,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transition: this.DataTransition(),
zIndex: this.ZIndex,
display: this.Width ? undefined : 'none',
+ mixBlendMode: !this.layoutDoc.disableMixBlend && DashColor(StrCast(this.layoutDoc[this.layoutDoc._layout_isSvg ? 'fillColor' : 'backgroundColor'], Colors.WHITE)).alpha() !== 1 ? 'multiply' : undefined,
}}>
{this.RenderCutoffProvider(this.Document) ? (
<div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
) : (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
Document={this._props.Document}
renderDepth={this._props.renderDepth}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f6c33d6ba..cb0831d3c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -10,7 +10,7 @@ import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { Animation, DocData } from '../../../fields/DocSymbols';
import { RichTextField } from '../../../fields/RichTextField';
-import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
import { nullAudio } from '../../../fields/URLField';
import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT';
import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
@@ -30,6 +30,7 @@ import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { TraceMobx } from '../../../fields/util';
const API_URL = 'https://api.unsplash.com/search/photos';
@@ -63,8 +64,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* @param useDoc doc to fill in instead of creating a Doc
* @returns the resulting flashcard Doc
*/
- public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) {
- const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
+ public static createFlashcard(tuple3: string, frontKey: string, backKey: string, useDoc?: Doc) {
+ const [qtoken, ktoken, atoken] = [ComparisonBox.qtoken, ComparisonBox.ktoken, ComparisonBox.atoken];
+ const [title, tuple] = tuple3.split(qtoken);
const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
const rest = tuple.replace(question, '');
// prettier-ignore
@@ -82,7 +84,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
useDoc[DocData][backKey] = back;
return useDoc;
}
- return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 });
+ return Docs.Create.FlashcardDocument(title, front, back, { _width: 300, _height: 300 });
};
return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
}
@@ -95,12 +97,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) {
return Promise.all(
text
- .split(ComparisonBox.qtoken)
+ .toLowerCase()
+ .split(ComparisonBox.ttoken)
.filter(t => t)
.map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
).then(docs => {
return Docs.Create.CarouselDocument(docs, {
- title: 'flashcard deck',
+ title: text,
_width: width,
_height: height,
_layout_fitWidth: false,
@@ -112,9 +115,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
- static qtoken = 'Question: ';
- static ktoken = 'Keyword: ';
- static atoken = 'Answer: ';
+ static qtoken = 'question: ';
+ static ktoken = 'keyword: ';
+ static atoken = 'answer: ';
+ static ttoken = 'title: ';
private _slideTiming = 200;
private _sideBtnWidth = 35;
private _closeRef = React.createRef<HTMLDivElement>();
@@ -140,19 +144,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this._reactDisposer.select = reaction(
() => this._props.isSelected(),
selected => {
- if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent();
- !selected && (this._childActive = false);
+ if (selected) {
+ switch (this.revealOp) {
+ default:
+ case flashcardRevealOp.FLIP: this.activateContent(); break;
+ case flashcardRevealOp.SLIDE: break;
+ } // prettier-ignore
+ } else {
+ this._childActive = false;
+ }
}, // what it should update to
{ fireImmediately: true }
);
- this._reactDisposer.hover = reaction(
- () => this._props.isContentActive(),
- hover => {
- if (!hover) {
- this.revealOp === flashcardRevealOp.FLIP && this.animateFlipping(this.frontKey);
- this.revealOp === flashcardRevealOp.SLIDE && this.animateSliding(this._props.PanelWidth() - 3);
+ this._reactDisposer.inactive = reaction(
+ () => !this._props.isContentActive(),
+ inactive => {
+ if (inactive) {
+ switch (this.revealOp) {
+ case flashcardRevealOp.FLIP: this.animateFlipping(this.frontKey); break;
+ case flashcardRevealOp.SLIDE: this.animateSliding(this._props.PanelWidth() - 3); break;
+ } // prettier-ignore
}
- }, // what it should update to
+ },
{ fireImmediately: true }
);
}
@@ -183,7 +196,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@computed get containerDoc() { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore
@computed get isQuizMode() { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
- @computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
+ @computed get isFlashcard() { return StrCast(this.Document.layout_flashcardType); } // prettier-ignore
@computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore
@computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore
@computed get frontText() { return RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; } // prettier-ignore
@@ -194,7 +207,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], this.isFlashcard ? 100: 50); } // prettier-ignore
@computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } // prettier-ignore
@computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this.containerDoc?.revealOp, this.isFlashcard ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE)) as flashcardRevealOp; } // prettier-ignore
+ set revealOp(op:flashcardRevealOp) { this.layoutDoc[this.revealOpKey] = op; } // prettier-ignore
@computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore
+ set revealOpHover(on:boolean) { this.layoutDoc[this.revealOpKey+"_hover"] = on; } // prettier-ignore
@computed get loading() { return this._loading; } // prettier-ignore
set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore
@@ -495,7 +510,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Calls GPT for each flashcard type.
*/
askGPT = async (callType: GPTCallType) => {
- const questionText = 'Question: ' + this.frontText;
+ const questionText = this.frontText;
const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
this.loading = true;
@@ -613,6 +628,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance?.subitems ?? [];
appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' });
+ appearanceItems.push({
+ description: 'Reveal by ' + (this.revealOp === flashcardRevealOp.FLIP ? 'Sliding' : 'Flipping'),
+ event: () => (this.revealOp = this.revealOp === flashcardRevealOp.FLIP ? flashcardRevealOp.SLIDE : flashcardRevealOp.FLIP),
+ icon: 'id-card',
+ });
+ appearanceItems.push({ description: (this.revealOpHover ? 'Click ' : 'Hover ') + ' to reveal', event: () => (this.revealOpHover = !this.revealOpHover), icon: 'id-card' });
!appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
};
@@ -656,6 +677,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
</div>
</Tooltip>
);
+ childFitWidth = () => Cast(this.Document.childLayoutFitWidth, 'boolean') ?? Cast(this.Document.childLayoutFitWidth, 'boolean');
+
displayDoc = (whichSlot: string) => {
const whichDoc = DocCast(this.dataDoc[whichSlot]);
const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
@@ -675,7 +698,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
isDocumentActive={returnFalse}
isContentActive={this.childActiveFunc}
showTags={undefined}
- fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
+ fitWidth={this.childFitWidth} // set to returnTrue to make images fill the comparisonBox-- should be a user option
ignoreUsePath={layoutString ? true : undefined}
moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
@@ -789,6 +812,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
render() {
+ TraceMobx();
const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([
[flashcardRevealOp.FLIP, this.renderAsFlip],
[flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 896048ab3..b874d077b 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox } from '@mui/material';
-import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
+import { Colors, Toggle, ToggleType, Type } from '@dash/components';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -73,7 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />;
}
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -323,7 +323,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
() => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar')
);
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
options.didMove = true;
this.toggleSidebar();
@@ -453,7 +453,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
onPointerDown = (e: React.PointerEvent): void => {
if ((this.Document._freeform_scale || 1) !== 1) return;
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
this._props.select(false);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
@@ -832,6 +832,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
annotationLayerScrollTop={NumCast(this.Document._layout_scrollTop)}
scaling={returnOne}
docView={this.DocumentView}
+ screenTransform={this.DocumentView().screenToViewTransform}
addDocument={this.sidebarAddDocument}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
index 94a37a19f..7fc906e59 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors } from 'browndash-components';
+import { Colors } from '@dash/components';
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { IDisposer } from 'mobx-utils';
@@ -416,7 +416,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
const height = bottom - top;
const width = right - left;
const doc = !field.title.includes('$$')
- ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` })
+ ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` })
: Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top });
return doc;
});
@@ -2266,11 +2266,11 @@ export class FieldUtils {
title: title,
x: coord.x,
y: coord.y,
- _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
+ text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
backgroundColor: opts.backgroundColor ?? '',
text_fontColor: opts.color,
contentBold: opts.fontBold,
- textTransform: opts.fontTransform,
+ text_transform: opts.fontTransform,
color: opts.color,
_layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
borderColor: opts.borderColor,
@@ -2312,7 +2312,7 @@ export class FieldUtils {
public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => {
const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
- const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` });
+ const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` });
return doc;
};
diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
index a6a6a6b46..8ae29a88c 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
@@ -1,4 +1,4 @@
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 14d7e9bf6..5a9442d2f 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components';
+import { ColorPicker, EditableText, IconButton, Size, Type } from '@dash/components';
import * as d3 from 'd3';
import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index c2f5388a2..b55d509ff 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -1,4 +1,4 @@
-import { Button, EditableText, Size } from 'browndash-components';
+import { Button, EditableText, Size } from '@dash/components';
import * as d3 from 'd3';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 19ea8e4fa..86e6ad8e4 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -1,5 +1,5 @@
import { Checkbox } from '@mui/material';
-import { ColorPicker, EditableText, Size, Type } from 'browndash-components';
+import { ColorPicker, EditableText, Size, Type } from '@dash/components';
import * as d3 from 'd3';
import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index fe596bc36..7ef4bca6b 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,4 +1,4 @@
-import { Button, Colors, Type } from 'browndash-components';
+import { Button, Colors, Type } from '@dash/components';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index d6c9bb013..ea3ab2887 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { RichTextField } from '../../../fields/RichTextField';
-import { Cast, DocCast, NumCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { Gestures } from '../../../pen-gestures/GestureTypes';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -44,7 +44,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable _errorMessage = '';
@computed get mermaidcode() {
- return Cast(this.Document[DocData].text, RichTextField, null)?.Text ?? '';
+ return StrCast(this.Document[DocData].text, RTFCast(this.Document[DocData].text)?.Text);
}
componentDidMount() {
@@ -129,7 +129,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
});
isValidCode = (html: string) => (html ? true : false);
- removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', '');
+ removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', '').replace(/^"/, '').replace(/"$/, '');
// method to convert the drawings on collection node side the mermaid code
convertDrawingToMermaidCode = async (docArray: Doc[]) => {
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7568e3b57..7e5507586 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -242,7 +242,7 @@
.contentFittingDocumentView * {
::-webkit-scrollbar-track {
- background: none;
+ background: none;
}
}
@@ -270,3 +270,29 @@
position: relative;
}
}
+
+.documentView-noAIWidgets {
+ transform-origin: top left;
+ position: relative;
+}
+
+.documentView-editorView-history {
+ position: absolute;
+ transform-origin: top right;
+ right: 0;
+ top: 0;
+ overflow-y: scroll;
+ scrollbar-width: thin;
+}
+
+.documentView-editorView {
+ width: 100%;
+ scrollbar-width: thin;
+ justify-items: center;
+ background-color: rgb(223, 223, 223);
+ transform-origin: top left;
+
+ .documentView-editorView-resizer {
+ height: 5px;
+ }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a343b9a39..0193fd328 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PresEffect, PresEffectDirection } from './trails/PresEnums';
import SpringAnimation from './trails/SlideEffect';
import { SpringType, springMappings } from './trails/SpringUtils';
+import { TagsView } from '../TagsView';
export interface DocumentViewProps extends FieldViewSharedProps {
hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -85,7 +86,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView)
}
@observer
-export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
+export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() {
// this makes mobx trace() statements more descriptive
public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
@@ -109,7 +110,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
private _dropDisposer?: DragManager.DragDropDisposer;
- constructor(props: FieldViewProps & DocumentViewProps) {
+ constructor(props: FieldViewProps & DocumentViewProps & { showAIEditor: boolean }) {
super(props);
makeObservable(this);
}
@@ -355,7 +356,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
onPointerDown = (e: React.PointerEvent): void => {
if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return;
this._longPressSelector = setTimeout(() => SnappingManager.LongPress && this._props.select(false), 1000);
- if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView;
this._downX = e.clientX;
this._downY = e.clientY;
@@ -380,7 +380,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
};
onPointerMove = (e: PointerEvent): void => {
- if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+ if (e.buttons !== 1 || Doc.ActiveTool === InkTool.Ink) return;
if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
this.cleanupPointerEvents();
@@ -459,10 +459,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
}
if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) {
const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document;
- const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
+ const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, { layout_isSvg: true }, undefined, [de.x, de.y - 50]);
if (linkDoc) {
de.complete.linkDocument = linkDoc;
- linkDoc.layout_isSvg = true;
DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
}
}
@@ -553,6 +552,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
}
appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
+ appearanceItems.push({ description: 'AI view', event: () => this._docView?.toggleAIEditor(), icon: 'map-pin' });
!Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
!appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -685,7 +685,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
rootSelected = () => this._rootSelected;
panelHeight = () => this._props.PanelHeight() - this.headerMargin;
- screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
+ screenToLocalContent = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .translate(0, -this.headerMargin)
+ .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1);
onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -711,35 +715,111 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
return this._props.styleProvider?.(doc, props, property);
};
+ @observable _aiWinHeight = 88;
+
+ private _tagsBtnHeight = 22;
+ @computed get currentScale() {
+ const viewXfScale = this._props.DocumentView!().screenToLocalScale();
+ const x = NumCast(this.Document.height) / viewXfScale / 80;
+ const xscale = x >= 1 ? 0 : 1 / (1 + x * (viewXfScale - 1));
+ const y = NumCast(this.Document.width) / viewXfScale / 200;
+ const yscale = y >= 1 ? 0 : 1 / (1 + y * viewXfScale - 1);
+ return Math.max(xscale, yscale, 1 / viewXfScale);
+ }
+ /**
+ * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+ */
+ @computed get viewScaling() { return 1 / this.currentScale; } // 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._tagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // 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.max(this.maxWidgetSize / this._tagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
+
+ aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+ aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling);
@computed get viewBoxContents() {
TraceMobx();
const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
return (
- <div
- className="documentView-contentsView"
- style={{
- pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
- height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
- }}>
- <DocumentContentsView
- {...this._props}
- layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
- pointerEvents={this.contentPointerEvents}
- setContentViewBox={this.setContentView}
- childFilters={this.childFilters}
- PanelHeight={this.panelHeight}
- setHeight={this.setHeight}
- isContentActive={this.isContentActive}
- ScreenToLocalTransform={this.screenToLocalContent}
- rootSelected={this.rootSelected}
- onClickScript={this.onClickFunc}
- setTitleFocus={this.setTitleFocus}
- hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
- />
- </div>
+ <>
+ <div
+ className="documentView-contentsView"
+ style={{
+ pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
+ width: this._props.showAIEditor ? this.aiContentsWidth() : undefined,
+ height: this._props.showAIEditor ? this.aiContentsHeight() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+ }}>
+ <DocumentContentsView
+ {...this._props}
+ layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
+ pointerEvents={this.contentPointerEvents}
+ setContentViewBox={this.setContentView}
+ childFilters={this.childFilters}
+ PanelWidth={this._props.showAIEditor ? this.aiContentsWidth : this._props.PanelWidth}
+ PanelHeight={this._props.showAIEditor ? this.aiContentsHeight : this.panelHeight}
+ setHeight={this.setHeight}
+ isContentActive={this.isContentActive}
+ ScreenToLocalTransform={this.screenToLocalContent}
+ rootSelected={this.rootSelected}
+ onClickScript={this.onClickFunc}
+ setTitleFocus={this.setTitleFocus}
+ hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
+ />
+ </div>
+ {!this._props.showAIEditor ? (
+ <div
+ className="documentView-noAiWidgets"
+ style={{
+ width: `${100 / this.uiBtnScaling}%`, //
+ transform: `scale(${this.uiBtnScaling})`,
+ bottom: this.maxWidgetSize,
+ }}>
+ {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
+ </div>
+ ) : (
+ <>
+ <div
+ className="documentView-editorView-history"
+ ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))}
+ style={{
+ transform: `scale(${this.uiBtnScaling})`,
+ height: this.aiContentsHeight() / this.uiBtnScaling,
+ width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling,
+ }}>
+ {this._componentView?.componentAIViewHistory?.() ?? null}
+ </div>
+ <div
+ className="documentView-editorView"
+ style={{
+ width: `${100 / this.uiBtnScaling}%`, //
+ transform: `scale(${this.uiBtnScaling})`,
+ }}
+ ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
+ <div className="documentView-editorView-resizer" />
+ {this._componentView?.componentAIView?.() ?? null}
+ {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
+ </div>
+ </>
+ )}
+ {this.widgetDecorations ?? null}
+ </>
);
}
+ _oldHistoryWheel: HTMLDivElement | null = null;
+ _oldAiWheel: HTMLDivElement | null = null;
+ onPassiveWheel = (e: WheelEvent) => {
+ e.stopPropagation();
+ };
+
+ protected historyRef = (lastEle: HTMLDivElement | null, ele: HTMLDivElement | null) => {
+ lastEle?.removeEventListener('wheel', this.onPassiveWheel);
+ ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ };
captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
fieldsDropdown = (placeholder: string) => (
@@ -880,7 +960,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
background: this.backgroundBoxColor,
opacity: this.opacity,
cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
- color: StrCast(this.layoutDoc.color, 'inherit'),
+ color: StrCast(this.Document._color, 'inherit'),
fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
fontSize: Cast(this.Document._text_fontSize, 'string', null),
transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
@@ -895,7 +975,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
{this.captionView}
</div>
)}
- {this.widgetDecorations ?? null}
</div>
));
};
@@ -964,13 +1043,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
>,
root: Doc
) {
- const dir = ((presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) || PresEffectDirection.Center) as PresEffectDirection;
+ const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection;
const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null));
const effectProps = {
- left: dir === PresEffectDirection.Left,
- right: dir === PresEffectDirection.Right,
- top: dir === PresEffectDirection.Top,
- bottom: dir === PresEffectDirection.Bottom,
+ left: effectDirection === PresEffectDirection.Left,
+ right: effectDirection === PresEffectDirection.Right,
+ top: effectDirection === PresEffectDirection.Top,
+ bottom: effectDirection === PresEffectDirection.Bottom,
opposite: true,
delay: 0,
duration,
@@ -981,12 +1060,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
type: SpringType.GENTLE,
...springMappings.gentle,
};
- switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
- case PresEffect.Expand: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Flip: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Rotate: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Rotate} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Bounce: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Bounce} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Roll} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+ const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
+ switch (presEffect) {
+ case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:
+ case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={effectDirection || PresEffectDirection.Left} presEffect={presEffect} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
// case PresEffect.Fade: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>
// keep as preset, doesn't really make sense with spring config
@@ -1073,15 +1150,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
* @param doc Doc to snapshot
* @returns promise of icon ImageField
*/
- public static GetDocImage(doc: Doc) {
+ public static GetDocImage(doc?: Doc) {
return DocumentView.getDocumentView(doc)
?.ComponentView?.updateIcon?.()
- .then(() => ImageCast(DocCast(doc).icon));
+ .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)])));
}
- /**
- * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to.
- */
- public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to.
public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore
private _htmlOverlayEffect: Opt<Doc>;
@@ -1133,10 +1206,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
@computed private get nativeScaling() {
if (this.shouldNotScale) return 1;
const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
- if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
- return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
+ const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0;
+ const effNW = Math.max(this.effectiveNativeWidth - ai, 1);
+ const effNH = Math.max(this.effectiveNativeHeight - ai, 1);
+ if (this.layout_fitWidth || (this._props.PanelHeight() - ai) / effNH > (this._props.PanelWidth() - ai) / effNW) {
+ return Math.max(minTextScale, (this._props.PanelWidth() - ai) / effNW); // width-limited or layout_fitWidth
}
- return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
+ return Math.max(minTextScale, (this._props.PanelHeight() - ai) / effNH); // height-limited or unscaled
}
@computed private get panelWidth() {
return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
@@ -1292,6 +1368,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}
};
+ @observable public _showAIEditor: boolean = false;
+
+ @action
+ public toggleAIEditor = () => {
+ this._showAIEditor = !this._showAIEditor;
+ };
+
public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
this._htmlOverlayText = text;
this._htmlOverlayEffect = effect;
@@ -1307,8 +1390,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
this.Document[Animation] = presEffect;
this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore
};
- public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
- this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans);
+ public setViewTransition = (transProp: string, timeInMs: number, dataTrans = false) => {
+ this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, dataTrans);
};
public setCustomView = undoable((custom: boolean, layout: string): void => {
@@ -1387,7 +1470,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth);
- screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
+ screenToLocalScale = () => this.screenToViewTransform().Scale;
isSelected = () => this.IsSelected;
select = (extendSelection: boolean, focusSelection?: boolean) => {
if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection);
@@ -1406,6 +1489,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
NativeHeight = () => this.effectiveNativeHeight;
PanelWidth = () => this.panelWidth;
PanelHeight = () => this.panelHeight;
+ ReducedPanelWidth = () => this.panelWidth / 2;
+ ReducedPanelHeight = () => this.panelWidth / 2;
NativeDimScaling = () => this.nativeScaling;
hideLinkCount = () => !!this.hideLinkButton;
isHovering = () => this._isHovering;
@@ -1474,6 +1559,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}}>
<DocumentViewInternal
{...this._props}
+ showAIEditor={this._showAIEditor}
reactParent={undefined}
isHovering={this.isHovering}
fieldKey={this.LayoutFieldKey}
@@ -1504,21 +1590,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
);
}
- public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) {
- docs.forEach(doc => {
- doc._viewTransition = `${transProp} ${timeInMs}ms`;
- dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
- });
+ public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, dataTrans = false) {
+ const setTrans = (transition?: string) =>
+ docs.forEach(doc => {
+ doc._viewTransition = transition;
+ dataTrans && (doc.dataTransition = transition);
+ });
+ setTrans(`${transProp} ${timeInMs}ms`);
timer && clearTimeout(timer);
- return setTimeout(
- () =>
- docs.forEach(doc => {
- doc._viewTransition = undefined;
- dataTrans && (doc.dataTransition = 'inherit');
- afterTrans?.();
- }),
- timeInMs + 10
- );
+ return setTimeout(setTrans, timeInMs + 10);
}
// shows a stacking view collection (by default, but the user can change) of all documents linked to the source
@@ -1562,55 +1642,45 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
} else func();
}
}
-
-export function ActiveFillColor(): string {
- const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined;
- return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, ""));
-} // prettier-ignore
-export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
-export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
-export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
-export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
-export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
-export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
-export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
-export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
-export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore
-
+export function ActiveHideTextLabels(): boolean { return BoolCast(Doc.UserDoc().activeHideTextLabels, false); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(Doc.UserDoc()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveEraserWidth(): number { return Number(Doc.UserDoc()?.activeEraserWidth ?? 25); } // prettier-ignore
+
+export function ActiveInkFillColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Fill`]); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Color`], 'black'); } // prettier-ignore
+export function ActiveInkArrowStart(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowStart`], ''); } // prettier-ignore
+export function ActiveInkArrowEnd(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowEnd`], ''); } // prettier-ignore
+export function ActiveInkArrowScale(): number { return NumCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowScale`], 1); } // prettier-ignore
+export function ActiveInkDash(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Dash`], '0'); } // prettier-ignore
+export function ActiveInkWidth(): number { return Number(Doc.UserDoc()?.[`active${Doc.ActiveInk}Width`]); } // prettier-ignore
+export function ActiveInkBezierApprox(): string { return StrCast(Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`]); } // prettier-ignore
+
+export function SetActiveIsInkMask(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeIsInkMask = value); } // prettier-ignore
+export function SetactiveHideTextLabels(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeHideTextLabels = value); } // prettier-ignore
+export function SetEraserWidth(width: number): void { Doc.UserDoc() && (Doc.UserDoc().activeEraserWidth = width); } // prettier-ignore
export function SetActiveInkWidth(width: string): void {
- !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+ !isNaN(parseInt(width)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Width`] = width);
}
-export function SetActiveBezierApprox(bezier: string): void {
- ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+export function SetActiveInkBezierApprox(bezier: string): void {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`] = isNaN(parseInt(bezier)) ? '' : bezier);
}
export function SetActiveInkColor(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
- ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
- ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Color`] = value);
}
-export function SetActiveArrowStart(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+export function SetActiveInkFillColor(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Fill`] = value);
}
-export function SetActiveArrowEnd(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+export function SetActiveInkArrowStart(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowStart`] = value);
}
-export function SetActiveArrowScale(value: number) {
- ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+export function SetActiveInkArrowEnd(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowEnd`] = value);
}
-export function SetActiveDash(dash: string): void {
- !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+export function SetActiveInkArrowScale(value: number) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowScale`] = value);
}
-export function SetEraserWidth(width: number): void {
- ActiveInkPen() && (ActiveInkPen().eraserWidth = width);
+export function SetActiveInkDash(dash: string): void {
+ !isNaN(parseInt(dash)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}`] = dash);
}
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 5009ec7a7..55e0f5184 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -2,8 +2,8 @@
.equationBox-cont {
transform-origin: center;
- background-color: #e7e7e7;
+ width: fit-content;
> span {
- width: 100%;
+ width: fit-content;
}
}
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index fefe25764..576b5bbe0 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,7 +1,6 @@
-import { action, makeObservable, reaction } from 'mobx';
+import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { DivHeight, DivWidth } from '../../../ClientUtils';
import { Doc } from '../../../fields/Doc';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
@@ -10,10 +9,12 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
import './EquationBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import EquationEditor from './formattedText/EquationEditor';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
@observer
export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -37,15 +38,6 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.SetSelectOnLoad(undefined);
}
reaction(
- () => StrCast(this.dataDoc.text),
- text => {
- if (text && text !== this._ref.current!.mathField.latex()) {
- this._ref.current!.mathField.latex(text);
- }
- }
- // { fireImmediately: true }
- );
- reaction(
() => this._props.isSelected(),
selected => {
if (this._ref.current) {
@@ -56,18 +48,23 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
{ fireImmediately: true }
);
}
+ @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
+ @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
@action
keyPressed = (e: KeyboardEvent) => {
- const _height = DivHeight(this._ref.current!.element?.current);
- const _width = DivWidth(this._ref.current!.element?.current);
if (e.key === 'Enter') {
- const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', {
+ const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : '', {
title: '# math',
- _width,
- _height: 25,
+ _width: NumCast(this.layoutDoc._width),
+ _height: NumCast(this.layoutDoc._height),
+ nativeHeight: NumCast(this.dataDoc.nativeHeight),
+ nativeWidth: NumCast(this.dataDoc.nativeWidth),
x: NumCast(this.layoutDoc.x),
- y: NumCast(this.layoutDoc.y) + _height + 10,
+ y: NumCast(this.layoutDoc.y) + NumCast(this.Document._height) + 10,
+ backgroundColor: StrCast(this.Document.backgroundColor),
+ color: StrCast(this.Document.color),
+ fontSize: this.fontSize,
});
Doc.SetSelectOnLoad(nextEq);
this._props.addDocument?.(nextEq);
@@ -81,7 +78,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
_height: 300,
backgroundColor: 'white',
});
- const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
+ const link = DocUtils.MakeLink(this.Document, graph, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
this._props.addDocument?.(graph);
link && this._props.addDocument?.(link);
e.stopPropagation();
@@ -93,39 +90,46 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.dataDoc.text = str;
};
- updateSize = () => {
- const style = this._ref.current?.element.current && getComputedStyle(this._ref.current.element.current);
- if (style?.width.endsWith('px') && style?.height.endsWith('px')) {
- if (this.layoutDoc._nativeWidth) {
- // if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio
- const prevNwidth = NumCast(this.layoutDoc._nativeWidth);
- const newNwidth = (this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', ''))));
- const newNheight = (this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', ''))));
- this.layoutDoc._width = (NumCast(this.layoutDoc._width) * NumCast(this.layoutDoc._nativeWidth)) / prevNwidth;
- this.layoutDoc._height = (NumCast(this.layoutDoc._width) * newNheight) / newNwidth;
- } else {
- this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', '')));
- this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', '')));
- }
- }
+ updateSize = (mathSpan: HTMLSpanElement) => {
+ const style = getComputedStyle(mathSpan);
+ const styleWidth = Number(style.width.replace('px', '') || 0);
+ const styleHeight = Number(style.height.replace('px', '') || 0);
+ const mathWidth = Math.max(35, NumCast(this.layoutDoc.xMargin) * 2 + styleWidth);
+ const mathHeight = Math.max(20, NumCast(this.layoutDoc.yMargin) * 2 + styleHeight);
+ const nScale = !this.dataDoc.nativeWidth ? 1
+ : (prevNwidth => { // if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio
+ [this.dataDoc.nativeWidth, this.dataDoc.nativeHeight] = [mathWidth, mathHeight];
+ return NumCast(this.layoutDoc._width) / prevNwidth;
+ })(NumCast(this.dataDoc.nativeWidth)); // prettier-ignore
+
+ this.layoutDoc._width = mathWidth * nScale;
+ this.layoutDoc._height = mathHeight * nScale;
};
render() {
TraceMobx();
- const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
+ const scale = this._props.NativeDimScaling?.() || 1;
return (
<div
- ref={() => this.updateSize()}
+ ref={r => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current)}
className="equationBox-cont"
+ onKeyDown={e => e.stopPropagation()}
onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
+ onBlur={() => {
+ FormattedTextBox.LiveTextUndo?.end();
+ }}
style={{
transform: `scale(${scale})`,
- width: 'fit-content', // `${100 / scale}%`,
+ minWidth: `${100 / scale}%`,
height: `${100 / scale}%`,
- pointerEvents: !this._props.isSelected() ? 'none' : undefined,
- fontSize: StrCast(this.layoutDoc._text_fontSize),
- }}
- onKeyDown={e => e.stopPropagation()}>
- <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
+ pointerEvents: !this._props.isContentActive() ? 'none' : undefined,
+ fontSize: this.fontSize,
+ color: this.fontColor,
+ paddingLeft: NumCast(this.layoutDoc.xMargin),
+ paddingRight: NumCast(this.layoutDoc.xMargin),
+ paddingTop: NumCast(this.layoutDoc.yMargin),
+ paddingBottom: NumCast(this.layoutDoc.yMargin),
+ }}>
+ <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, '')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
</div>
);
}
@@ -133,5 +137,17 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, {
layout: { view: EquationBox, dataField: 'text' },
- options: { acl: '', fontSize: '14px', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript
+ options: {
+ acl: '',
+ _xMargin: 10,
+ _yMargin: 10,
+ fontSize: '14px',
+ _nativeWidth: 40,
+ _nativeHeight: 40,
+ _layout_reflowHorizontal: false,
+ _layout_reflowVertical: false,
+ _layout_nativeDimEditable: false,
+ layout_hideDecorationTitle: true,
+ systemIcon: 'BsCalculatorFill',
+ }, // systemIcon: 'BsSuperscript' + BsSubscript
});
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 02d4d9adb..2e40f39ed 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -68,6 +68,7 @@ export interface FieldViewSharedProps {
isGroupActive?: () => string | undefined; // is this document part of a group that is active
// eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
+
PanelWidth: () => number;
PanelHeight: () => number;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts
index bb0d2b03c..1c462e98f 100644
--- a/src/client/views/nodes/FocusViewOptions.ts
+++ b/src/client/views/nodes/FocusViewOptions.ts
@@ -22,3 +22,14 @@ export interface FocusViewOptions {
pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
contextPath?: Doc[]; // path of inner documents that will also be focused
}
+
+/**
+ * if there's an options.effect, it will be handled from linkFollowHighlight. We delay the start of
+ * the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
+ * bcz: should this delay be an options parameter?
+ * @param options
+ * @returns
+ */
+export function FocusEffectDelay(options: FocusViewOptions) {
+ return (options.zoomTime ?? 0) * 0.5;
+}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 2db285910..2405889cf 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -10,6 +10,13 @@
height: 3px !important;
}
}
+.fonticonbox {
+ margin: auto;
+ width: 100%;
+ .formLabel {
+ height: 5px;
+ }
+}
.menuButton {
height: 100%;
display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index d4898eb3c..f58862028 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,10 +1,10 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from '@dash/components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { emptyFunction } from '../../../../Utils';
@@ -21,6 +21,8 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { OpenWhere } from '../OpenWhere';
import './FontIconBox.scss';
import TrailsIcon from './TrailsIcon';
+import { InkTool } from '../../../../fields/InkField';
+import { ScriptField } from '../../../../fields/ScriptField';
export enum ButtonType {
TextButton = 'textBtn',
@@ -126,7 +128,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
background={SnappingManager.userBackgroundColor}
numberDropdownType={type}
showPlusMinus={false}
- tooltip={this.label}
+ formLabel={(StrCast(this.Document.title).startsWith(' ') ? '\u00A0' : '') + StrCast(this.Document.title)}
+ tooltip={StrCast(this.Document.toolTip, this.label)}
type={Type.PRIM}
min={NumCast(this.dataDoc.numBtnMin, 0)}
max={NumCast(this.dataDoc.numBtnMax, 100)}
@@ -149,73 +152,91 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
};
/**
+ * Displays custom dropdown menu for fonts -- this is a HACK -- fix for generality, don't copy
+ */
+ handleFontDropdown = (script: () => string, buttonList: string[]) => {
+ // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ return {
+ buttonList,
+ jsx: undefined,
+ selectedVal: script(),
+ toolTip: 'Set text font',
+ getStyle: (val: string) => ({ fontFamily: val }),
+ };
+ };
+ /**
+ * Displays custom dropdown menu for view selection -- this is a HACK -- fix for generality, don't copy
+ */
+ handleViewDropdown = (script: ScriptField, buttonList: string[]) => {
+ const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
+ const noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+ return selected.length === 1 && selected[0].type === DocumentType.COL
+ ? {
+ buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)),
+ getStyle: undefined,
+ selectedVal: StrCast(selected[0]._type_collection),
+ toolTip: 'change view type (press Shift to add as a new view)',
+ }
+ : {
+ jsx: selected.length ? (
+ <Popup
+ icon={<FontAwesomeIcon size="1x" icon={selected.length > 1 ? 'caret-down' : (Doc.toIcon(selected.lastElement()) as IconProp)} />}
+ text={selected.length === 1 ? ClientUtils.cleanDocumentType(StrCast(selected[0].type) as DocumentType) : selected.length + ' selected'}
+ type={Type.TERT}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userVariantColor}
+ popup={<SelectedDocView selectedDocs={selected} />}
+ fillWidth
+ />
+ ) : (
+ <Button
+ text={`${Doc.ActiveTool === InkTool.None ? 'Text box' : Doc.ActiveInk} defaults`} //
+ type={Type.TERT}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userVariantColor}
+ fillWidth
+ inactive
+ />
+ ),
+ };
+ };
+
+ /**
* Dropdown list
*/
@computed get dropdownListButton() {
const script = ScriptCast(this.Document.script);
-
- let noviceList: string[] = [];
- let text: string | undefined;
- let getStyle: (val: string) => { [key: string]: string } = () => ({});
- let icon: IconProp = 'caret-down';
- const isViewDropdown = script?.script.originalScript.startsWith('{ return setView');
- if (isViewDropdown) {
- const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
- // const selected = DocumentView.SelectedDocs();
- if (selected.lastElement()) {
- if (StrCast(selected.lastElement().type) === DocumentType.COL) {
- text = StrCast(selected.lastElement()._type_collection);
- } else {
- if (selected.length > 1) {
- text = selected.length + ' selected';
- } else {
- text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType);
- icon = Doc.toIcon(selected.lastElement());
- }
- return (
- <Popup
- icon={<FontAwesomeIcon size="1x" icon={icon} />}
- text={text}
- type={Type.TERT}
- color={SnappingManager.userColor}
- background={SnappingManager.userVariantColor}
- popup={<SelectedDocView selectedDocs={selected} />}
- fillWidth
- />
- );
- }
- } else {
- return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />;
- }
- noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
- } else {
- text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
- // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
- }
+ const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
+ const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => {
+ switch (this.Document.title) {
+ case 'Font': return this.handleFontDropdown(selectedFunc, this.buttonList);
+ case 'Perspective': return this.handleViewDropdown(script, this.buttonList);
+ default: return { buttonList: this.buttonList, selectedVal: selectedFunc(), toolTip: undefined, jsx: undefined, getStyle: undefined };
+ } // prettier-ignore
+ })();
+ if (jsx) return jsx;
// Get items to place into the list
- const list: IListItemProps[] = this.buttonList
- .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
- .map(value => ({
- text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
- val: value,
- style: getStyle(value),
- // shortcut: '#',
- }));
+ const list: IListItemProps[] = buttonList.map(value => ({
+ text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
+ val: value,
+ style: getStyle?.(value),
+ // shortcut: '#',
+ }));
return (
<Dropdown
- selectedVal={text}
- setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)}
+ selectedVal={selectedVal}
+ setSelectedVal={undoable((value, e) => script.script.run({ this: this.Document, value, shiftKey: e.shiftKey }), `dropdown select ${this.label}`)}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
+ toolTip={toolTip}
type={Type.TERT}
closeOnSelect={false}
dropdownType={DropdownType.SELECT}
onItemDown={this.dropdownItemDown}
items={list}
- tooltip={this.label}
+ tooltip={StrCast(this.Document.toolTip, this.label)}
fillWidth
/>
);
@@ -235,49 +256,53 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const tooltip: string = StrCast(this.Document.toolTip);
return (
- <ColorPicker
- setSelectedColor={value => {
- if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
- this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
- }}
- setFinalColor={value => {
- this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
- this.colorBatch?.end();
- this.colorBatch = undefined;
- }}
- defaultPickerType="Classic"
- selectedColor={curColor}
- type={Type.PRIM}
- color={color}
- background={SnappingManager.userBackgroundColor}
- icon={this.Icon(color) ?? undefined}
- tooltip={tooltip}
- label={this.label}
- />
+ <div
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}>
+ <ColorPicker
+ setSelectedColor={value => {
+ if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+ this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ }}
+ setFinalColor={value => {
+ this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ this.colorBatch?.end();
+ this.colorBatch = undefined;
+ }}
+ defaultPickerType="Classic"
+ selectedColor={curColor}
+ type={Type.PRIM}
+ color={color}
+ background={SnappingManager.userBackgroundColor}
+ icon={this.Icon(color) ?? undefined}
+ tooltip={tooltip}
+ label={this.label}
+ />
+ </div>
);
}
@computed get multiToggleButton() {
- // Determine the type of toggle button
- const tooltip: string = StrCast(this.Document.toolTip);
+ const tooltip = StrCast(this.Document.toolTip);
const script = ScriptCast(this.Document.onClick)?.script;
const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
- // Colors
+
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
- const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string;
const items = DocListCast(this.dataDoc.data);
const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
+
return (
<MultiToggle
- tooltip={`Toggle ${tooltip}`}
+ tooltip={`Click to Toggle ${tooltip} or select new option`}
type={Type.PRIM}
color={color}
- background={background === SnappingManager.userBackgroundColor ? undefined : background}
+ background={undefined}
multiSelect={true}
onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
isToggle={false}
toggleStatus={toggleStatus}
- label={this.label}
+ label={selectedItems.length === 1 ? selectedItems[0] : this.label}
items={items.map(item => ({
icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
tooltip: StrCast(item.toolTip),
@@ -290,7 +315,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
// it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
// arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
- itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, itemDoc, _readOnly_: false }));
+ itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
}}
/>
);
@@ -308,17 +333,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const toggleStatus = (script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean) ?? false;
// Colors
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
- // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
+ // bcz: ink shapes are tri-state - off, one-shot, and on. Need to update Toggle buttons to allow this and update currentUserUtils to set the tri-state on the Doc
+ // in the meantime, if the button matches a tool type that is not locked, we want to set the background color to something distinct.
+ const inkShapeHack = ((this.Document.toolType && this.Document.toolType === SnappingManager.InkShape) || this.Document.toolType === Doc.ActiveTool) && !SnappingManager.KeepGestureMode;
return (
<Toggle
tooltip={`Toggle ${tooltip}`}
toggleType={ToggleType.BUTTON}
- type={Type.PRIM}
+ type={inkShapeHack ? Type.TERT : Type.PRIM}
toggleStatus={toggleStatus}
text={buttonText}
color={color}
- // background={SnappingManager.userBackgroundColor}
+ background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined}
icon={this.Icon(color)!}
label={this.label}
onPointerDown={e =>
@@ -392,7 +419,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
render() {
return (
- <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+ <div className="fonticonbox" onContextMenu={this.specificContextMenu}>
{this.renderButton()}
</div>
);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 6b439cd64..91c351895 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,5 +1,5 @@
import functionPlot, { Chart } from 'function-plot';
-import { computed, makeObservable, reaction } from 'mobx';
+import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
@@ -15,6 +15,8 @@ import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { FieldView, FieldViewProps } from './FieldView';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
@observer
export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -65,18 +67,24 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
);
return funcs;
}
+ computeYScale = (width: number, height: number, xScale: number[]) => {
+ const xDiff = xScale[1] - xScale[0];
+ const yDiff = (height * xDiff) / width;
+ return [-yDiff / 2, yDiff / 2];
+ };
createGraph = (ele?: HTMLDivElement) => {
this._plotEle = ele || this._plotEle;
const width = this._props.PanelWidth();
const height = this._props.PanelHeight();
+ const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]);
try {
this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
this._plot = functionPlot({
target: '#' + this._plotEle?.id,
width,
height,
- xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
- yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
+ xAxis: { domain: xrange },
+ yAxis: { domain: this.computeYScale(width, height, xrange) }, // Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
grid: true,
data: this.graphFuncs.map(fn => ({
fn,
@@ -94,7 +102,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
// const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
if (res) {
- const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+ const link = DocUtils.MakeLink(doc, this.Document, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
link && this._props.addDocument?.(link);
}
return res;
@@ -115,7 +123,32 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
// if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
@computed get theGraph() {
- return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
+ return (
+ <div
+ id={`${this._plotId}`}
+ ref={r => r && this.createGraph(r)}
+ style={{ position: 'absolute', width: '100%', height: '100%' }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ action(() => {
+ if (this._plot?.options.xAxis?.domain) {
+ this.Document.xRange = new List<number>(this._plot.options.xAxis.domain);
+ }
+ if (this._plot?.options.yAxis?.domain) {
+ this.Document.yRange = new List<number>(this._plot.options.yAxis.domain);
+ }
+ }),
+ emptyFunction,
+ false,
+ false
+ );
+ }}
+ />
+ );
}
render() {
TraceMobx();
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
index 90cc06092..c79d662f4 100644
--- a/src/client/views/nodes/IconTagBox.scss
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -10,8 +10,6 @@
gap: 5px;
padding-left: 5px;
padding-right: 5px;
- padding-top: 2px;
- padding-bottom: 2px;
button {
pointer-events: auto;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 3ffda5a35..fe4f0b1a2 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
max-height: 100%;
pointer-events: inherit;
background: transparent;
+ z-index: -10000;
img {
height: auto;
@@ -139,3 +140,88 @@
.imageBox-fadeBlocker-hover {
opacity: 0;
}
+
+.imageBox-aiView-history {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+
+ .imageBox-aiView-img {
+ width: 100%;
+ padding: 5px;
+
+ &:hover {
+ filter: brightness(0.8);
+ }
+ }
+
+ .imageBox-aiView-caption {
+ font-size: 7px;
+ }
+}
+
+.imageBox-aiView {
+ text-align: center;
+ font-weight: bold;
+ transform-origin: top left;
+ width: 100%;
+
+ .imageBox-aiView-subtitle {
+ position: relative;
+ align-content: center;
+ max-width: 10%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .imageBox-aiView-regenerate,
+ .imageBox-aiView-options {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 5px;
+ width: 100%;
+ .imageBox-aiView-regenerate-createBtn {
+ max-width: 10%;
+ .button-container {
+ width: 100% !important;
+ justify-content: left !important;
+ }
+ }
+ }
+
+ .imageBox-aiView-firefly {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 10%;
+ width: 100%;
+ }
+ .imageBox-aiView-regenerate-send {
+ max-width: 10%;
+ }
+
+ .imageBox-aiView-strength {
+ text-align: center;
+ align-items: center;
+ display: flex;
+ max-width: 25%;
+ width: 100%;
+ .imageBox-aiView-similarity {
+ max-width: 65;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ }
+ }
+ .imageBox-aiView-slider {
+ width: 90%;
+ margin-left: 5px;
+ }
+ .imageBox-aiView-input {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 65%;
+ width: 100%;
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0dfc0ec28..caefbf542 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,19 +1,19 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
+import { Slider, Tooltip } from '@mui/material';
import axios from 'axios';
-import { Colors } from 'browndash-components';
+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 ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -32,21 +32,28 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { OverlayView } from '../OverlayView';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { PinDocView, PinProps } from '../PinFuncs';
+import { StickerPalette } from '../smartdraw/StickerPalette';
import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
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
private static _instance: ImageEditorData;
private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore
@observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined });
- @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => {
+ private static set = action((open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => {
this._instance.imageData = { open, rootDoc, source, addDoc };
- };
+ });
constructor() {
makeObservable(this);
@@ -88,6 +95,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _error = '';
@observable private _isHovering = false; // flag to switch between primary and alternate images on hover
+ // variables for AI Image Editor
+ @observable private _regenInput = '';
+ @observable private _canInteract = true;
+ @observable private _regenerateLoading = false;
+ @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -266,7 +279,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
+ const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh;
cropping.title = 'crop: ' + this.Document.title;
cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
cropping.y = NumCast(this.Document.y);
@@ -284,9 +297,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
croppingProto.data_nativeWidth = anchw;
croppingProto.data_nativeHeight = anchh;
croppingProto.freeform_scale = viewScale;
- croppingProto.freeform_scale_min = viewScale;
croppingProto.freeform_panX = anchx / viewScale;
croppingProto.freeform_panY = anchy / viewScale;
+ croppingProto.freeform_scale_min = viewScale;
croppingProto.freeform_panX_min = anchx / viewScale;
croppingProto.freeform_panX_max = anchw / viewScale;
croppingProto.freeform_panY_min = anchy / viewScale;
@@ -309,6 +322,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
+ funcs.push({
+ description: 'GetImageText',
+ event: () => {
+ Networking.PostToServer('/queryFireflyImageText', {
+ file: (file => {
+ const ext = extname(file);
+ return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+ })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+ }).then(text => alert(text));
+ },
+ icon: 'expand-arrows-alt',
+ });
+ funcs.push({
+ description: 'Expand Image',
+ event: () => {
+ Networking.PostToServer('/expandImage', {
+ prompt: 'sunny skies',
+ file: (file => {
+ const ext = extname(file);
+ return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+ })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+ }).then((info: Upload.ImageInformation) => {
+ const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+ DocUtils.assignImageInfo(info, img);
+ this._props.addDocTab(img, OpenWhere.addRight);
+ });
+ },
+ icon: 'expand-arrows-alt',
+ });
funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
funcs.push({
description: 'Open Image Editor',
@@ -320,10 +362,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}),
icon: 'pencil-alt',
});
+ this.layoutDoc.ai &&
+ funcs.push({
+ description: 'Regenerate AI Image',
+ event: action(e => {
+ !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate();
+ }),
+ icon: 'pen-to-square',
+ });
+ funcs.push({
+ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
+ event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
+ icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
+ });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
+ // updateIcon = () => new Promise<void>(res => res());
+ updateIcon = (usePanelDimensions?: boolean) => {
+ const contentDiv = this._mainCont.current;
+ return !contentDiv
+ ? new Promise<void>(res => res())
+ : UpdateIcon(
+ this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
+ contentDiv,
+ usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+ usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+ this._props.PanelWidth(),
+ this._props.PanelHeight(),
+ 0,
+ 1,
+ false,
+ '',
+ (iconFile, nativeWidth, nativeHeight) => {
+ this.dataDoc.icon = new ImageField(iconFile);
+ this.dataDoc.icon_nativeWidth = nativeWidth;
+ this.dataDoc.icon_nativeHeight = nativeHeight;
+ }
+ );
+ };
+
choosePath = (url: URL) => {
if (!url?.href) return '';
const lower = url.href.toLowerCase();
@@ -338,7 +417,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get nativeSize() {
TraceMobx();
- if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0}
+ if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 };
const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
@@ -457,6 +536,160 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
}
+ protected _btnWidth = 50;
+ protected _inputWidth = 50;
+ protected _sideBtnMaxPanelPct = 0.12;
+ @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+ @observable private _fireflyRefStrength = 0;
+
+ componentAIViewHistory = () => (
+ <div className="imageBox-aiView-history">
+ <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
+ {this._prevImgs.map(img => (
+ <div key={img.pathname}>
+ <img
+ className="imageBox-aiView-img"
+ src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
+ onClick={() => {
+ this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+ this.dataDoc.ai_firefly_prompt = img.prompt;
+ this.dataDoc.ai_firefly_seed = img.seed;
+ }}
+ />
+ <span>{img.prompt}</span>
+ </div>
+ ))}
+ </div>
+ );
+
+ componentAIView = () => {
+ const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
+ return (
+ <div className="imageBox-aiView">
+ <div className="imageBox-aiView-regenerate">
+ <span className="imageBox-aiView-firefly">Firefly:</span>
+ <input
+ className="imageBox-aiView-input"
+ aria-label="Edit instructions input"
+ type="text"
+ value={this._regenInput}
+ onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+ placeholder={this._regenInput || StrCast(this.Document.title)}
+ />
+ <div className="imageBox-aiView-strength">
+ <span className="imageBox-aiView-similarity">Similarity</span>
+ <Slider
+ className="imageBox-aiView-slider"
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+ }}
+ min={0}
+ max={100}
+ step={1}
+ size="small"
+ value={this._fireflyRefStrength}
+ onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div className="imageBox-aiView-regenerate-createBtn">
+ <Button
+ text="Create"
+ type={Type.SEC}
+ // style={{ alignSelf: 'flex-end' }}
+ icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ onClick={action(async () => {
+ this._regenerateLoading = true;
+ if (this._fireflyRefStrength) {
+ DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(
+ action(() => {
+ this._regenerateLoading = false;
+ })
+ );
+ } else
+ SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
+ action(newImgs => {
+ if (newImgs[0]) {
+ const url = newImgs[0].pathname;
+ const imgField = new ImageField(url);
+ this._prevImgs.length === 0 &&
+ this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
+ this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url });
+ this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
+ this.dataDoc.ai_firefly_prompt = newImgs[0].prompt;
+ this.dataDoc[this.fieldKey] = imgField;
+ this._regenerateLoading = false;
+ this._regenInput = '';
+ }
+ })
+ );
+ })}
+ />
+ </div>
+ </div>
+ <div className="imageBox-aiView-options">
+ <span className="imageBox-aiView-subtitle"> More: </span>
+ <Button
+ type={Type.TERT}
+ text="Get Text"
+ icon={<FontAwesomeIcon icon="font" />}
+ color={SettingsManager.userBackgroundColor}
+ iconPlacement="right"
+ onClick={() => {
+ Networking.PostToServer('/queryFireflyImageText', {
+ file: (file => {
+ const ext = extname(file);
+ return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+ })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+ }).then(text => alert(text));
+ }}
+ />
+ <Button
+ type={Type.TERT}
+ text="Generative Fill"
+ icon={<FontAwesomeIcon icon="fill" />}
+ color={SettingsManager.userBackgroundColor}
+ iconPlacement="right"
+ onClick={action(() => {
+ ImageEditorData.Open = true;
+ ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+ ImageEditorData.AddDoc = this._props.addDocument;
+ ImageEditorData.RootDoc = this.Document;
+ })}
+ />
+ <Button
+ type={Type.TERT}
+ text="Expand"
+ icon={<FontAwesomeIcon icon="expand" />}
+ color={SettingsManager.userBackgroundColor}
+ iconPlacement="right"
+ onClick={() => {
+ Networking.PostToServer('/expandImage', {
+ prompt: 'sunny skies',
+ file: (file => {
+ const ext = extname(file);
+ return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+ })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+ }).then((info: Upload.ImageInformation) => {
+ const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+ DocUtils.assignImageInfo(info, img);
+ const genratedDocs = this.Document.generatedDocs
+ ? DocCast(this.Document.generatedDocs)
+ : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
+ Doc.AddDocToList(genratedDocs, undefined, img);
+ this.Document[DocData].generatedDocs = genratedDocs;
+ if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
+ });
+ }}
+ />
+ </div>
+ </div>
+ );
+ };
+
@computed get annotationLayer() {
TraceMobx();
return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
@@ -465,13 +698,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
marqueeDown = (e: React.PointerEvent) => {
if (!this.dataDoc[this.fieldKey]) {
this.chooseImage();
- } else if (
- !e.altKey &&
- e.button === 0 &&
- NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) &&
- this._props.isContentActive() &&
- ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)
- ) {
+ } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -502,8 +729,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
return { width, height };
};
-
savedAnnotations = () => this._savedAnnotations;
+
render() {
TraceMobx();
const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
@@ -568,6 +795,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
annotationLayerScrollTop={0}
scaling={returnOne}
annotationLayerScaling={this._props.NativeDimScaling}
+ screenTransform={this.DocumentView().screenToViewTransform}
docView={this.DocumentView}
addDocument={this.addDocument}
finishMarquee={this.finishMarquee}
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index ca4b3d467..889cdc0ca 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -13,7 +13,6 @@
height: 100%;
border-radius: inherit;
//letter-spacing: 2px; // bcz: doesn't work with LabelBigText
- text-transform: uppercase;
overflow: hidden;
display: inline-block;
margin: auto;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 94a9541f2..dcf9e1fed 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,19 +1,22 @@
import { Property } from 'csstype';
-import { action, computed, makeObservable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as textfit from 'textfit';
-import { Field, FieldType } from '../../../fields/Doc';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Doc, Field } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
+import { undoable } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { FieldView, FieldViewProps } from './FieldView';
import './LabelBox.scss';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextMenu } from './formattedText/RichTextMenu';
@observer
export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -22,7 +25,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: NodeJS.Timeout | undefined;
- _divRef: HTMLDivElement | null = null;
+ private _divRef: HTMLDivElement | null = null;
+ private _reaction: IReactionDisposer | undefined;
constructor(props: FieldViewProps) {
super(props);
@@ -36,21 +40,29 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @computed get Title() {
- return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title);
- }
-
- @computed get backgroundColor() {
- return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
- }
-
componentDidMount() {
this._props.setContentViewBox?.(this);
+ this._reaction = reaction(
+ () => this.Title,
+ () => document.activeElement !== this._divRef && this._forceRerender++
+ );
}
componentWillUnMount() {
this._timeout && clearTimeout(this._timeout);
+ this.setText(this._divRef?.innerText ?? '');
+ this._reaction?.();
}
+ @observable _forceRerender = 0;
+
+ @computed get Title() { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore
+ @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore
+ @computed get boxShadow() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore
+
+ setText = undoable((text: string) => {
+ this.dataDoc[this.fieldKey] = text;
+ }, 'set label text');
+
drop = (/* e: Event, de: DragManager.DropEvent */) => {
return false;
};
@@ -82,10 +94,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
const textfitParams = {
minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1),
maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100),
- multiLine: BoolCast(this.layoutDoc._singleLine, true) ? false : true,
- alignHoriz: true,
+ multiLine: r?.textContent?.includes('\n') ? true : false,
+ // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline
+ alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false,
alignVert: true,
- detectMultiLine: true,
+ detectMultiLine: false,
};
if (r) {
if (!r.offsetHeight || !r.offsetWidth) {
@@ -94,65 +107,140 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._timeout = setTimeout(() => this.fitTextToBox(r));
return textfitParams;
}
+ r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline
+ r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want
+ r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']));
textfit(r, textfitParams);
}
return textfitParams;
};
+ resetCursor = (cranchor?: number) => {
+ if (this._divRef && (cranchor || this._divRef === document.activeElement)) {
+ const range = document.createRange();
+ const anchor = cranchor ?? this._divRef.childNodes.length;
+ const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef);
+ range.setStart(container, anchor);
+ range.setEnd(container, anchor);
+ const sel = window.getSelection();
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
+ };
+
+ beforeInput = action((event: InputEvent) => {
+ const spanChild = this._divRef?.firstChild?.firstChild;
+ if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const selection = document.getSelection();
+ if (selection && document.activeElement === event.target) {
+ const text = spanChild.textContent ?? '';
+ const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset;
+ const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text
+ const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor);
+ spanChild.textContent = splitText;
+ this.resetCursor(cranchor + addReturnHack.length);
+ }
+ // const span = document.createElement('span');
+ // span.innerHTML = '&#8203;';
+ // this._divRef!.append(span);
+ }
+ });
+ // .labelBox-mainButton > div > span:nth-child(2) {
+
+ /**
+ * When an IconButton is clicked, it will receive focus. However, we don't want that since we want or need that since we really want
+ * to maintain focus in the label's editing div (and cursor position). so this relies on IconButton's having a tabindex set to -1 so that
+ * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox. If it is, we then
+ * restore focus
+ * @param e focusout event on the editing div
+ */
+ keepFocus = (e: FocusEvent) => {
+ if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
+ for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
+ if ((ele as HTMLElement)?.className === 'fonticonbox') {
+ setTimeout(() => this._divRef?.focus());
+ break;
+ }
+ }
+ }
+ };
+
render() {
TraceMobx();
const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
- const label = this.Title.startsWith('#') ? null : this.Title;
return (
- <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+ <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
<div
className="labelBox-mainButton"
style={{
backgroundColor: this.backgroundColor,
- // fontSize: StrCast(this.layoutDoc._text_fontSize),
- color: StrCast(this.layoutDoc._color),
- fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
+ color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
+ fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
- textTransform: StrCast(this.layoutDoc.textTransform) as Property.TextTransform,
+ textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform,
paddingLeft: NumCast(this.layoutDoc._xPadding),
paddingRight: NumCast(this.layoutDoc._xPadding),
paddingTop: NumCast(this.layoutDoc._yPadding),
paddingBottom: NumCast(this.layoutDoc._yPadding),
width: this._props.PanelWidth(),
height: this._props.PanelHeight(),
- whiteSpace: 'multiLine' in boxParams && boxParams.multiLine ? 'pre-wrap' : 'pre',
+ whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
}}>
<div
+ key={this._forceRerender}
style={{
width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding),
height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding),
outline: 'unset !important',
}}
- onKeyDown={action(e => {
+ onKeyDown={e => {
e.stopPropagation();
- })}
+ }}
onKeyUp={action(e => {
e.stopPropagation();
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
- setTimeout(() => this._props.select(false));
+ const text = this._divRef?.firstChild;
+ if (text && (text as HTMLElement)?.nodeType === 3) {
+ this._divRef?.removeChild(text);
+ this._divRef?.firstChild?.appendChild(text);
+ this.resetCursor();
+ }
+ this.fitTextToBox(this._divRef);
})}
+ onFocus={() => {
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this._divRef?.addEventListener('focusout', this.keepFocus);
+ }}
onBlur={() => {
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this.setText(this._divRef?.innerText ?? '');
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+ FormattedTextBox.LiveTextUndo?.end();
+ FormattedTextBox.LiveTextUndo = undefined;
+ }}
+ dangerouslySetInnerHTML={{
+ __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title.startsWith('#') ? null : (this.Title ?? '')}</span>`,
}}
contentEditable={this._props.onClickScript?.() ? undefined : true}
ref={r => {
+ this._divRef?.removeEventListener('beforeinput', this.beforeInput);
this._divRef = r;
- this.fitTextToBox(r);
- if (this._props.isSelected() && this._divRef) {
- const range = document.createRange();
- range.setStart(this._divRef, this._divRef.childNodes.length);
- range.setEnd(this._divRef, this._divRef.childNodes.length);
- const sel = window.getSelection();
- sel?.removeAllRanges();
- sel?.addRange(range);
+ if (this._divRef) {
+ this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+ if (Doc.SelectOnLoad === this.Document) {
+ Doc.SelectOnLoad = undefined;
+ this._divRef.focus();
+ }
+ this.fitTextToBox(this._divRef);
+ if (this.Title) {
+ this.resetCursor();
+ }
}
- }}>
- {label}
- </div>
+ }}
+ />
</div>
</div>
);
@@ -161,9 +249,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 4d9d2460e..d5dc256d9 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -20,6 +20,7 @@ import { StyleProp } from '../StyleProp';
import { ComparisonBox } from './ComparisonBox';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { RichTextMenu } from './formattedText/RichTextMenu';
import './LinkBox.scss';
@observer
@@ -29,6 +30,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
_hackToSeeIfDeleted: NodeJS.Timeout | undefined;
_disposers: { [name: string]: IReactionDisposer } = {};
+ _divRef: HTMLDivElement | null = null;
@observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
@observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor
@@ -78,6 +80,24 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
})) // prettier-ignore
);
}
+ /**
+ * When an IconButton is clicked, it will receive focus. However, we don't want that since we want or need that since we really want
+ * to maintain focus in the label's editing div (and cursor position). so this relies on IconButton's having a tabindex set to -1 so that
+ * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox. If it is, we then
+ * restore focus
+ * @param e focusout event on the editing div
+ */
+ keepFocus = (e: FocusEvent) => {
+ if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
+ for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
+ if (['listItem-container', 'fonticonbox'].includes((ele as HTMLElement)?.className ?? '')) {
+ console.log('RESTORE :', document.activeElement, this._divRef);
+ this._divRef?.focus();
+ break;
+ }
+ }
+ }
+ };
render() {
TraceMobx();
@@ -98,7 +118,6 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
a.Document[DocCss];
b.Document[DocCss];
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove)
const bxf = b.screenToViewTransform();
const scale = docView?.screenToViewTransform().Scale ?? 1;
@@ -157,10 +176,9 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string;
const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as number;
const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor));
- // eslint-disable-next-line camelcase
const { stroke_markerScale: strokeMarkerScale, stroke_width: strokeRawWidth, stroke_startMarker: strokeStartMarker, stroke_endMarker: strokeEndMarker, stroke_dash: strokeDash } = this.Document;
- const strokeWidth = NumCast(strokeRawWidth, 4);
+ const strokeWidth = NumCast(strokeRawWidth, 1);
const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
return (
@@ -197,8 +215,23 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
id={this.DocumentView?.().DocUniqueId}
className="linkBox-label"
+ tabIndex={-1}
+ ref={r => (this._divRef = r)}
+ onPointerDown={e => e.stopPropagation()}
+ onFocus={() => {
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this._divRef?.addEventListener('focusout', this.keepFocus);
+ }}
+ onBlur={() => {
+ if (document.activeElement !== this._divRef && document.activeElement?.parentElement !== this._divRef) {
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+ }
+ }}
style={{
borderRadius: '8px',
+ transform: `scale(${1 / scale})`,
pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
fontSize,
fontFamily /* , fontStyle: 'italic' */,
@@ -250,7 +283,6 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
return (
<div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string }}>
<ComparisonBox
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this.props} //
fieldKey="link_anchor"
setHeight={emptyFunction}
diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
index b8fd8ac6a..8784a709a 100644
--- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -1,6 +1,6 @@
import { IconLookup, faAdd, faCalendarDays, faRoute } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { IReactionDisposer, ObservableMap, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
index 103a35434..cef202256 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -2,7 +2,7 @@
import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material';
-import { IconButton } from 'browndash-components';
+import { IconButton } from '@dash/components';
import { Position } from 'geojson';
import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -109,7 +109,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._left > 0;
}
- constructor(props: any) {
+ constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
MapAnchorMenu.Instance = this;
@@ -117,10 +117,12 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
}
componentWillUnmount() {
- this.destinationFeatures = [];
- this.destinationSelected = false;
- this.selectedDestinationFeature = undefined;
- this.currentRouteInfoMap = undefined;
+ runInAction(() => {
+ this.destinationFeatures = [];
+ this.destinationSelected = false;
+ this.selectedDestinationFeature = undefined;
+ this.currentRouteInfoMap = undefined;
+ });
this._disposer?.();
}
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c66f7c726..c4bb7c47d 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -2,7 +2,7 @@ import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@f
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, FormControlLabel, TextField } from '@mui/material';
import * as turf from '@turf/turf';
-import { IconButton, Size, Type } from 'browndash-components';
+import { IconButton, Size, Type } from '@dash/components';
import * as d3 from 'd3';
import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson';
import mapboxgl, { LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl';
@@ -428,7 +428,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
this.toggleSidebar();
options.didMove = true;
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index c69cd8e89..eb0431b85 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -41,7 +41,6 @@
// };
// _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined;
-// childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
// addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean);
// removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean);
// render() {
@@ -69,7 +68,6 @@
// chromeHidden={true}
// childHideResizeHandles={true}
// childHideDecorationTitle={true}
-// childLayoutFitWidth={this.childLayoutFitWidth}
// // childDocumentsActive={returnFalse}
// removeDocument={this.removeDoc}
// addDocument={this.addDoc}
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index a4557196e..95f89a573 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, EditableText, IconButton, Type } from 'browndash-components';
+import { Button, EditableText, IconButton, Type } from '@dash/components';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -383,7 +383,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
}
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
this.toggleSidebar();
options.didMove = true;
@@ -732,7 +732,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
MapBoxContainer._rerenderDelay = 0;
}
this._rerenderTimeout = undefined;
- // eslint-disable-next-line operator-assignment
this.Document[DocCss] = this.Document[DocCss] + 1;
}), MapBoxContainer._rerenderDelay);
return null;
@@ -792,7 +791,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
.map(pushpin => (
<DocumentView
key={pushpin[Id]}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
renderDepth={this._props.renderDepth + 1}
Document={pushpin}
@@ -830,7 +828,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
<div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
fieldKey={this.fieldKey}
Document={this.Document}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 7bca1230f..f6908d5fd 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -250,6 +250,17 @@
cursor: ew-resize;
background: lightGray;
}
+.pdfBox-container {
+ position: absolute;
+ transform-origin: top left;
+ top: 0;
+}
+.pdfBox-sidebarContainer {
+ position: absolute;
+ height: 100%;
+ right: 0;
+ top: 0;
+}
.pdfBox-interactive {
width: 100%;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 816d4a3b0..06b75e243 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
@@ -40,8 +40,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(PDFBox, fieldKey);
}
+ static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
+ static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
+
private _searchString: string = '';
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
@@ -63,11 +66,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
!this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
if (this.pdfUrl) {
- if (PDFBox.pdfcache.get(this.pdfUrl.url.href))
- runInAction(() => {
- this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href);
- });
- else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href))
+ this._pdf = PDFBox.pdfcache.get(this.pdfUrl.url.href);
+ !this._pdf &&
PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(
action(pdf => {
this._pdf = pdf;
@@ -233,7 +233,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
options.didMove = true;
this.toggleSidebar(false);
@@ -265,12 +265,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
@action
- loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc[this._props.fieldKey + '_numPages'] = np;
- Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72));
- Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72);
+ loaded = (p: { width: number; height: number }, pages: number) => {
+ this.dataDoc[this._props.fieldKey + '_numPages'] = pages;
+ Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), p.width));
+ Doc.SetNativeHeight(this.dataDoc, p.height);
this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1);
- !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
+ !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (p.height / p.width));
};
override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
@@ -584,7 +584,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
- const scale = previewScale * (this._props.NativeDimScaling?.() || 1);
+ // PDFjs scales page renderings to be the render container size times the ratio of CSS/print pixels.
+ // So we have to scale the render container down by this ratio, so that the renderings will match the size of the container
+ const viewScale = (previewScale * (this._props.NativeDimScaling?.() || 1)) / Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS;
return !this._pdf ? null : (
<div
className="pdfBox"
@@ -594,13 +596,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}}>
<div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
<div
+ className="pdfBox-container"
style={{
- width: `calc(${100 / scale}% - ${(this.sidebarWidth() / scale) * (this._previewWidth ? scale : 1)}px)`,
- height: `${100 / scale}%`,
- transform: `scale(${scale})`,
- position: 'absolute',
- transformOrigin: 'top left',
- top: 0,
+ width: `calc(${100 / viewScale}% - ${(this.sidebarWidth() / viewScale) * (this._previewWidth ? viewScale : 1)}px)`,
+ height: `${100 / viewScale}%`,
+ transform: `scale(${viewScale})`,
}}>
<PDFViewer
{...this._props}
@@ -613,7 +613,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
focus={this.focus}
url={this.pdfUrl!.url.pathname}
anchorMenuClick={this.anchorMenuClick}
- loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
+ loaded={Doc.NativeAspect(this.dataDoc) ? emptyFunction : this.loaded}
setPdfViewer={this.setPdfViewer}
addDocument={this.addDocument}
moveDocument={this.moveDocument}
@@ -622,14 +622,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
crop={this.crop}
/>
</div>
- <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
+ <div className="pdfBox-sidebarContainer" style={{ width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
+ {this.sidebarCollection}
+ </div>
{this.settingsPanel()}
</div>
);
}
- static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
- static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
render() {
TraceMobx();
const pdfView = !this._pdf ? null : this.renderPdfView;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index de51f6447..9adee53e8 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -776,7 +776,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -1023,6 +1023,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
scaling={returnOne}
annotationLayerScaling={this._props.NativeDimScaling}
docView={this.DocumentView}
+ screenTransform={this.DocumentView().screenToViewTransform}
containerOffset={this.marqueeOffset}
addDocument={this.addDocWithTimecode}
finishMarquee={this.finishMarquee}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a5788d02a..6026d9ca7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview';
import { OpenWhere } from './OpenWhere';
import './WebBox.scss';
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { CreateImage } = require('./WebBoxRenderer');
@observer
@@ -201,8 +202,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
() => this.layoutDoc._layout_autoHeight,
layoutAutoHeight => {
if (layoutAutoHeight) {
- this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']);
- this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1));
+ const nh = NumCast(this.Document[this._props.fieldKey + '_nativeHeight'], NumCast(this.Document.nativeHeight));
+ this.layoutDoc._nativeHeight = nh;
+ this._props.setHeight?.(nh * (this._props.NativeDimScaling?.() || 1));
}
}
);
@@ -335,7 +337,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ele = document.createElement('div');
ele.append(contents);
}
- } catch (e) {
+ } catch {
/* empty */
}
const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
@@ -506,7 +508,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
let href: Opt<string>;
try {
href = iframe?.contentWindow?.location.href;
- } catch (e) {
+ } catch {
runInAction(() => this._warning++);
href = undefined;
}
@@ -713,7 +715,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._webUrl = this._url;
}
}
- } catch (e) {
+ } catch {
console.log('WebBox URL error:' + this._url);
}
return true;
@@ -805,7 +807,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
sel.empty(); // Chrome
else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
this.marqueeing = [e.clientX, e.clientY];
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -855,7 +857,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
})}
contentEditable
onPointerDown={this.webClipDown}
- // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: field.html }}
/>
);
@@ -1031,7 +1032,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{this.inlineTextAnnotations
.sort((a, b) => NumCast(a.y) - NumCast(b.y))
.map(anno => (
- // eslint-disable-next-line react/jsx-props-no-spreading
<Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
))}
</div>
@@ -1042,7 +1042,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
renderAnnotations = (childFilters: () => string[]) => (
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
setContentViewBox={this.setInnerContent}
NativeWidth={returnZero}
@@ -1197,6 +1196,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
scaling={this._props.NativeDimScaling}
addDocument={this.addDocumentWrapper}
docView={this.DocumentView}
+ screenTransform={this.DocumentView().screenToViewTransform}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotationsCreator}
selectionText={this.selectionText}
@@ -1217,7 +1217,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
fieldKey={this.fieldKey + '_' + this._urlHash}
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 8338879cf..689c152dd 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -1,28 +1,27 @@
import dotenv from 'dotenv';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
-import OpenAI from 'openai';
-import { ChatCompletionMessageParam } from 'openai/resources';
import { escape } from 'lodash'; // Imported escape from lodash
+import OpenAI from 'openai';
+import { DocumentOptions } from '../../../../documents/Documents';
import { AnswerParser } from '../response_parsers/AnswerParser';
import { StreamedAnswerParser } from '../response_parsers/StreamedAnswerParser';
+import { BaseTool } from '../tools/BaseTool';
import { CalculateTool } from '../tools/CalculateTool';
-import { CreateCSVTool } from '../tools/CreateCSVTool';
+import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool';
+import { CreateDocTool } from '../tools/CreateDocumentTool';
import { DataAnalysisTool } from '../tools/DataAnalysisTool';
+import { ImageCreationTool } from '../tools/ImageCreationTool';
import { NoTool } from '../tools/NoTool';
-import { RAGTool } from '../tools/RAGTool';
import { SearchTool } from '../tools/SearchTool';
-import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
+import { Parameter, ParametersType, TypeMap } from '../types/tool_types';
import { AgentMessage, ASSISTANT_ROLE, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo, TEXT_TYPE } from '../types/types';
import { Vectorstore } from '../vectorstore/Vectorstore';
import { getReactPrompt } from './prompts';
-import { BaseTool } from '../tools/BaseTool';
-import { Parameter, ParametersType, TypeMap } from '../types/tool_types';
-import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
-import { DocumentOptions } from '../../../../documents/Documents';
-import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool';
-import { ImageCreationTool } from '../tools/ImageCreationTool';
-import { DictionaryTool } from '../tools/DictionaryTool';
//import { DictionaryTool } from '../tools/DictionaryTool';
+import { ChatCompletionMessageParam } from 'openai/resources';
+import { Doc } from '../../../../../fields/Doc';
+import { parsedDoc } from '../chatboxcomponents/ChatBox';
+import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
dotenv.config();
@@ -61,9 +60,10 @@ export class Agent {
history: () => string,
csvData: () => { filename: string; id: string; text: string }[],
addLinkedUrlDoc: (url: string, id: string) => void,
- addLinkedDoc: (doc_type: string, data: string | undefined, options: DocumentOptions, id: string) => void,
- createCSVInDash: (url: string, title: string, id: string, data: string) => void,
- createImage: (result: any, options: DocumentOptions) => void
+ createImage: (result: any, 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
) {
// Initialize OpenAI client with API key from environment
this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true });
@@ -75,16 +75,17 @@ export class Agent {
// Define available tools for the assistant
this.tools = {
calculate: new CalculateTool(),
- rag: new RAGTool(this.vectorstore),
+ // rag: new RAGTool(this.vectorstore),
dataAnalysis: new DataAnalysisTool(csvData),
- websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc),
+ // websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc),
searchTool: new SearchTool(addLinkedUrlDoc),
- createCSV: new CreateCSVTool(createCSVInDash),
+ // createCSV: new CreateCSVTool(createCSVInDash),
noTool: new NoTool(),
imageCreationTool: new ImageCreationTool(createImage),
- //createTextDoc: new CreateTextDocTool(addLinkedDoc),
+ createTextDoc: new CreateTextDocTool(addLinkedDoc),
+ createDoc: new CreateDocTool(addLinkedDoc),
createAnyDocument: new CreateAnyDocumentTool(addLinkedDoc),
- dictionary: new DictionaryTool(),
+ // dictionary: new DictionaryTool(),
};
}
@@ -142,6 +143,7 @@ export class Agent {
console.log(this.interMessages);
console.log(`Turn ${i}/${maxTurns}`);
+ // eslint-disable-next-line no-await-in-loop
const result = await this.execute(onProcessingUpdate, onAnswerUpdate);
this.interMessages.push({ role: 'assistant', content: result });
@@ -197,11 +199,13 @@ export class Agent {
} else if (key === 'action_input') {
// Handle action input stage
const actionInput = stage[key];
+ console.log(`Action input full:`, actionInput);
console.log(`Action input:`, actionInput.inputs);
if (currentAction) {
try {
// Process the action with its input
+ // eslint-disable-next-line no-await-in-loop
const observation = (await this.processAction(currentAction, actionInput.inputs)) as Observation[];
const nextPrompt = [{ type: 'text', text: `<stage number="${i + 1}" role="user"> <observation>` }, ...observation, { type: 'text', text: '</observation></stage>' }] as Observation[];
console.log(observation);
@@ -306,7 +310,7 @@ export class Agent {
* @param response The parsed XML response from the assistant.
* @throws An error if the response does not meet the expected structure.
*/
- private validateAssistantResponse(response: any) {
+ private validateAssistantResponse(response: { stage: { [key: string]: object | string } }) {
if (!response.stage) {
throw new Error('Response does not contain a <stage> element');
}
@@ -349,7 +353,7 @@ export class Agent {
// If 'action_input' is present, validate its structure
if ('action_input' in stage) {
- const actionInput = stage.action_input;
+ const actionInput = stage.action_input as object;
if (!('action_input_description' in actionInput) || typeof actionInput.action_input_description !== 'string') {
throw new Error('action_input must contain an action_input_description string');
@@ -364,7 +368,7 @@ export class Agent {
// If 'answer' is present, validate its structure
if ('answer' in stage) {
- const answer = stage.answer;
+ const answer = stage.answer as object;
// Ensure answer contains at least one of the required elements
if (!('grounded_text' in answer || 'normal_text' in answer)) {
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 37059c635..f13116fdd 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -13,41 +13,40 @@ import { observer } from 'mobx-react';
import OpenAI, { ClientOptions } from 'openai';
import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { ClientUtils } from '../../../../../ClientUtils';
-import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
+import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
import { DocData, DocViews } from '../../../../../fields/DocSymbols';
-import { CsvCast, DocCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
-import { Networking } from '../../../../Network';
+import { RichTextField } from '../../../../../fields/RichTextField';
+import { ScriptField } from '../../../../../fields/ScriptField';
+import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
import { DocUtils } from '../../../../documents/DocUtils';
-import { DocumentType } from '../../../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes';
import { Docs, DocumentOptions } from '../../../../documents/Documents';
import { DocumentManager } from '../../../../util/DocumentManager';
+import { ImageUtils } from '../../../../util/Import & Export/ImageUtils';
import { LinkManager } from '../../../../util/LinkManager';
+import { CompileError, CompileScript } from '../../../../util/Scripting';
+import { DictationButton } from '../../../DictationButton';
import { ViewBoxAnnotatableComponent } from '../../../DocComponent';
-import { DocumentView } from '../../DocumentView';
+import { AudioBox } from '../../AudioBox';
+import { DocumentView, DocumentViewInternal } from '../../DocumentView';
import { FieldView, FieldViewProps } from '../../FieldView';
import { PDFBox } from '../../PDFBox';
+import { ScriptingBox } from '../../ScriptingBox';
+import { VideoBox } from '../../VideoBox';
import { Agent } from '../agentsystem/Agent';
+import { supportedDocumentTypes } from '../tools/CreateDocumentTool';
import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo, SimplifiedChunk, TEXT_TYPE } from '../types/types';
import { Vectorstore } from '../vectorstore/Vectorstore';
import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { ProgressBar } from './ProgressBar';
-import { RichTextField } from '../../../../../fields/RichTextField';
-import { VideoBox } from '../../VideoBox';
-import { AudioBox } from '../../AudioBox';
-import { DiagramBox } from '../../DiagramBox';
-import { ImageField } from '../../../../../fields/URLField';
-import { DashUploadUtils } from '../../../../../server/DashUploadUtils';
-import { DocCreatorMenu, Field, FieldUtils } from '../../DataVizBox/DocCreatorMenu';
-import { ImageUtils } from '../../../../util/Import & Export/ImageUtils';
-import { ScriptManager } from '../../../../util/ScriptManager';
-import { CompileError, CompileScript } from '../../../../util/Scripting';
-import { ScriptField } from '../../../../../fields/ScriptField';
-import { ScriptingBox } from '../../ScriptingBox';
+import { OpenWhere } from '../../OpenWhere';
dotenv.config();
+export type parsedDocData = { doc_type: string; data: unknown };
+export type parsedDoc = DocumentOptions & parsedDocData;
/**
* ChatBox is the main class responsible for managing the interaction between the user and the assistant,
* handling documents, and integrating with OpenAI for tasks such as document analysis, chat functionality,
@@ -56,17 +55,17 @@ dotenv.config();
@observer
export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// MobX observable properties to track UI state and data
- @observable history: AssistantMessage[] = [];
- @observable.deep current_message: AssistantMessage | undefined = undefined;
- @observable isLoading: boolean = false;
- @observable uploadProgress: number = 0;
- @observable currentStep: string = '';
- @observable expandedScratchpadIndex: number | null = null;
- @observable inputValue: string = '';
- @observable private linked_docs_to_add: ObservableSet = observable.set();
- @observable private linked_csv_files: { filename: string; id: string; text: string }[] = [];
- @observable private isUploadingDocs: boolean = false;
- @observable private citationPopup: { text: string; visible: boolean } = { text: '', visible: false };
+ @observable private _history: AssistantMessage[] = [];
+ @observable.deep private _current_message: AssistantMessage | undefined = undefined;
+ @observable private _isLoading: boolean = false;
+ @observable private _uploadProgress: number = 0;
+ @observable private _currentStep: string = '';
+ @observable private _expandedScratchpadIndex: number | null = null;
+ @observable private _inputValue: string = '';
+ @observable private _linked_docs_to_add: ObservableSet = observable.set();
+ @observable private _linked_csv_files: { filename: string; id: string; text: string }[] = [];
+ @observable private _isUploadingDocs: boolean = false;
+ @observable private _citationPopup: { text: string; visible: boolean } = { text: '', visible: false };
// Private properties for managing OpenAI API, vector store, agent, and UI elements
private openai: OpenAI;
@@ -74,6 +73,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private vectorstore: Vectorstore;
private agent: Agent;
private messagesRef: React.RefObject<HTMLDivElement>;
+ private _textInputRef: HTMLInputElement | undefined | null;
/**
* Static method that returns the layout string for the field.
@@ -83,6 +83,10 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return FieldView.LayoutString(ChatBox, fieldKey);
}
+ setChatInput = action((input: string) => {
+ this._inputValue = input;
+ });
+
/**
* Constructor initializes the component, sets up OpenAI, vector store, and agent instances,
* and observes changes in the chat history to save the state in dataDoc.
@@ -101,13 +105,13 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
}
this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds);
- this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createDocInDash, this.createCSVInDash, this.createImageInDash);
+ this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash);
this.messagesRef = React.createRef<HTMLDivElement>();
// Reaction to update dataDoc when chat history changes
reaction(
() =>
- this.history.map((msg: AssistantMessage) => ({
+ this._history.map((msg: AssistantMessage) => ({
role: msg.role,
content: msg.content,
follow_up_questions: msg.follow_up_questions,
@@ -126,20 +130,22 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
addDocToVectorstore = async (newLinkedDoc: Doc) => {
- this.uploadProgress = 0;
- this.currentStep = 'Initializing...';
- this.isUploadingDocs = true;
+ this._uploadProgress = 0;
+ this._currentStep = 'Initializing...';
+ this._isUploadingDocs = true;
try {
// Add the document to the vectorstore
await this.vectorstore.addAIDoc(newLinkedDoc, this.updateProgress);
} catch (error) {
console.error('Error uploading document:', error);
- this.currentStep = 'Error during upload';
+ this._currentStep = 'Error during upload';
} finally {
- this.isUploadingDocs = false;
- this.uploadProgress = 0;
- this.currentStep = '';
+ runInAction(() => {
+ this._isUploadingDocs = false;
+ this._uploadProgress = 0;
+ this._currentStep = '';
+ });
}
};
@@ -150,8 +156,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateProgress = (progress: number, step: string) => {
- this.uploadProgress = progress;
- this.currentStep = step;
+ this._uploadProgress = progress;
+ this._currentStep = step;
};
/**
@@ -188,7 +194,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const csvId = id ?? uuidv4();
// Add CSV details to linked files
- this.linked_csv_files.push({
+ this._linked_csv_files.push({
filename: CsvCast(newLinkedDoc.data).url.pathname,
id: csvId,
text: csvData,
@@ -210,7 +216,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
toggleToolLogs = (index: number) => {
- this.expandedScratchpadIndex = this.expandedScratchpadIndex === index ? null : index;
+ this._expandedScratchpadIndex = this._expandedScratchpadIndex === index ? null : index;
};
/**
@@ -269,7 +275,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
askGPT = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
- this.inputValue = '';
+ this._inputValue = '';
// Extract the user's message
const textInput = (event.currentTarget as HTMLFormElement).elements.namedItem('messageInput') as HTMLInputElement;
@@ -279,13 +285,13 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
textInput.value = '';
// Add the user's message to the history
- this.history.push({
+ this._history.push({
role: ASSISTANT_ROLE.USER,
content: [{ index: 0, type: TEXT_TYPE.NORMAL, text: trimmedText, citation_ids: null }],
processing_info: [],
});
- this.isLoading = true;
- this.current_message = {
+ this._isLoading = true;
+ this._current_message = {
role: ASSISTANT_ROLE.ASSISTANT,
content: [],
citations: [],
@@ -295,9 +301,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Define callbacks for real-time processing updates
const onProcessingUpdate = (processingUpdate: ProcessingInfo[]) => {
runInAction(() => {
- if (this.current_message) {
- this.current_message = {
- ...this.current_message,
+ if (this._current_message) {
+ this._current_message = {
+ ...this._current_message,
processing_info: processingUpdate,
};
}
@@ -307,9 +313,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const onAnswerUpdate = (answerUpdate: string) => {
runInAction(() => {
- if (this.current_message) {
- this.current_message = {
- ...this.current_message,
+ if (this._current_message) {
+ this._current_message = {
+ ...this._current_message,
content: [{ text: answerUpdate, type: TEXT_TYPE.NORMAL, index: 0, citation_ids: [] }],
};
}
@@ -321,22 +327,24 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Update the history with the final assistant message
runInAction(() => {
- if (this.current_message) {
- this.history.push({ ...finalMessage });
- this.current_message = undefined;
- this.dataDoc.data = JSON.stringify(this.history);
+ if (this._current_message) {
+ this._history.push({ ...finalMessage });
+ this._current_message = undefined;
+ this.dataDoc.data = JSON.stringify(this._history);
}
});
} catch (err) {
console.error('Error:', err);
// Handle error in processing
- this.history.push({
+ this._history.push({
role: ASSISTANT_ROLE.ASSISTANT,
content: [{ index: 0, type: TEXT_TYPE.ERROR, text: 'Sorry, I encountered an error while processing your request.', citation_ids: null }],
processing_info: [],
});
} finally {
- this.isLoading = false;
+ runInAction(() => {
+ this._isLoading = false;
+ });
this.scrollToBottom();
}
}
@@ -350,8 +358,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateMessageCitations = (index: number, citations: Citation[]) => {
- if (this.history[index]) {
- this.history[index].citations = citations;
+ if (this._history[index]) {
+ this._history[index].citations = citations;
}
};
@@ -392,17 +400,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param data The CSV data content.
*/
@action
- createCSVInDash = async (url: string, title: string, id: string, data: string) => {
- const doc = DocCast(await DocUtils.DocumentFromType('csv', url, { title: title, text: RTFCast(data) }));
-
- const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
- LinkManager.Instance.addLink(linkDoc);
-
- doc && this._props.addDocument?.(doc);
- await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
-
- this.addCSVForAnalysis(doc, id);
- };
+ createCSVInDash = (url: string, title: string, id: string, data: string) =>
+ DocUtils.DocumentFromType('csv', url, { title: title, text: RTFCast(data) }).then(doc => {
+ if (doc) {
+ LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
+ this._props.addDocument?.(doc);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}).then(() => this.addCSVForAnalysis(doc, id));
+ }
+ });
@action
createImageInDash = async (result: any, options: DocumentOptions) => {
@@ -414,7 +419,10 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.addDocument(ImageUtils.AssignImgInfo(doc, result));
const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
LinkManager.Instance.addLink(linkDoc);
- doc && this._props.addDocument?.(doc);
+ if (doc) {
+ if (this._props.addDocument) this._props.addDocument(doc);
+ else DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
+ }
await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
};
@@ -426,86 +434,173 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param id The unique ID for the document.
*/
@action
- createDocInDash = async (doc_type: string, data: string | undefined, options: DocumentOptions, id: string) => {
- let doc: Doc;
+ private createCollectionWithChildren = (data: parsedDoc[], insideCol: boolean): Opt<Doc>[] => data.map(doc => this.whichDoc(doc, insideCol));
- switch (doc_type.toLowerCase()) {
- case 'text':
- doc = Docs.Create.PdfDocument(data || '', { ...options, text: RTFCast(data) });
- break;
- case 'pdf':
- doc = Docs.Create.PdfDocument(data || '', options);
- break;
- case 'video':
- doc = Docs.Create.VideoDocument(data || '', options);
- break;
- case 'mermaid_diagram':
- doc = Docs.Create.DiagramDocument(data, options);
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
- const firstView = Array.from(doc[DocViews])[0] as DocumentView;
- (firstView.ComponentView as DiagramBox)?.renderMermaid?.(data!);
- });
- break;
- case 'audio':
- doc = Docs.Create.AudioDocument(data || '', options);
- break;
- case 'web':
- doc = Docs.Create.WebDocument(data || '', options);
- break;
- case 'equation':
- doc = Docs.Create.EquationDocument(data || '', options);
- break;
- case 'function_plot':
- doc = Docs.Create.FunctionPlotDocument([], options);
- break;
- case 'dataviz':
- const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
- filename: (options.title as string).replace(/\s+/g, '') + '.csv',
- data: data,
- });
- doc = Docs.Create.DataVizDocument(fileUrl, { ...options, text: RTFCast(data) });
- this.addCSVForAnalysis(doc, id);
- break;
- case 'chat':
- doc = Docs.Create.ChatDocument(options);
- break;
- case 'note_taking':
- doc = Docs.Create.NoteTakingDocument([Docs.Create.TextDocument(data!)], options);
- break;
- case 'script':
- const result = !data!.trim() ? ({ compiled: false, errors: [] } as CompileError) : CompileScript(data!, {});
- const script_field = result.compiled ? new ScriptField(result, undefined, data!) : undefined;
- doc = Docs.Create.ScriptingDocument(script_field, options);
- await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
- const firstView = Array.from(doc[DocViews])[0] as DocumentView;
- (firstView.ComponentView as ScriptingBox)?.onApply?.();
- (firstView.ComponentView as ScriptingBox)?.onRun?.();
- });
-
- break;
- // this.dataDoc.script = this.rawScript;
+ @action
+ whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
+ const options = OmitKeys(doc, ['doct_type', 'data']).omit as DocumentOptions;
+ const data = (doc as parsedDocData).data;
+ const ndoc = (() => {
+ switch (doc.doc_type) {
+ default:
+ case supportedDocumentTypes.text: return Docs.Create.TextDocument(data as string, options);
+ case supportedDocumentTypes.comparison: return this.createComparison(data as parsedDoc[], options);
+ case supportedDocumentTypes.flashcard: return this.createFlashcard(data as parsedDoc[], options);
+ case supportedDocumentTypes.deck: return this.createDeck(data as parsedDoc[], options);
+ case supportedDocumentTypes.image: return Docs.Create.ImageDocument(data as string, options);
+ case supportedDocumentTypes.equation: return Docs.Create.EquationDocument(data as string, options);
+ case supportedDocumentTypes.notetaking: return Docs.Create.NoteTakingDocument([], options);
+ case supportedDocumentTypes.web: return Docs.Create.WebDocument(data as string, { ...options, data_useCors: true });
+ case supportedDocumentTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options);
+ case supportedDocumentTypes.pdf: return Docs.Create.PdfDocument(data as string, options);
+ case supportedDocumentTypes.video: return Docs.Create.VideoDocument(data as string, options);
+ case supportedDocumentTypes.mermaid: return Docs.Create.DiagramDocument(undefined, { text: data as unknown as RichTextField, ...options}); // text: can take a string or RichTextField but it's typed for RichTextField.
+
+ // case supportedDocumentTypes.dataviz:
+ // {
+ // const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
+ // filename: (options.title as string).replace(/\s+/g, '') + '.csv',
+ // data: data,
+ // });
+ // const doc = Docs.Create.DataVizDocument(fileUrl, { ...options, text: RTFCast(data as string) });
+ // this.addCSVForAnalysis(doc, id);
+ // return doc;
+ // }
+ case supportedDocumentTypes.script: {
+ const result = !(data as string).trim() ? ({ compiled: false, errors: [] } as CompileError) : CompileScript(data as string, {});
+ const script_field = result.compiled ? new ScriptField(result, undefined, data as string) : undefined;
+ const sdoc = Docs.Create.ScriptingDocument(script_field, options);
+ DocumentManager.Instance.showDocument(sdoc, { willZoomCentered: true }, () => {
+ const firstView = Array.from(sdoc[DocViews])[0] as DocumentView;
+ (firstView.ComponentView as ScriptingBox)?.onApply?.();
+ (firstView.ComponentView as ScriptingBox)?.onRun?.();
+ });
+ return sdoc;
+ }
+ case supportedDocumentTypes.collection: {
+ const arr = this.createCollectionWithChildren(data as parsedDoc[], true).filter(d=>d).map(d => d!);
+ const collOpts = { ...options, _layout_fitWidth: true, _width:300, _height: 300, _freeform_backgroundGrid: true };
+ return (() => {
+ switch (options.type_collection) {
+ case CollectionViewType.Tree: return Docs.Create.TreeDocument(arr, collOpts);
+ case CollectionViewType.Masonry: return Docs.Create.MasonryDocument(arr, collOpts);
+ case CollectionViewType.Card: return Docs.Create.CardDeckDocument(arr, collOpts);
+ case CollectionViewType.Carousel: return Docs.Create.CarouselDocument(arr, collOpts);
+ case CollectionViewType.Carousel3D: return Docs.Create.Carousel3DDocument(arr, collOpts);
+ case CollectionViewType.Multicolumn: return Docs.Create.CarouselDocument(arr, collOpts);
+ default: return Docs.Create.FreeformDocument(arr, collOpts);
+ }
+ })();
+ }
+ // case supportedDocumentTypes.map: return Docs.Create.MapDocument([], options);
+ // case supportedDocumentTypes.button: return Docs.Create.ButtonDocument(options);
+ // case supportedDocumentTypes.trail: return Docs.Create.PresDocument(options);
+ } // prettier-ignore
+ })();
+
+ if (ndoc) {
+ ndoc.x = NumCast((options.x as number) ?? 0) + (insideCol ? 0 : NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc.width)) + 100;
+ ndoc.y = NumCast(options.y as number) + (insideCol ? 0 : NumCast(this.layoutDoc.y));
+ }
+ return ndoc;
+ };
- // ScriptManager.Instance.addScript(this.dataDoc);
+ /**
+ * Creates a document in the dashboard.
+ *
+ * @param {string} doc_type - The type of document to create.
+ * @param {string} data - The data used to generate the document.
+ * @param {DocumentOptions} options - Configuration options for the document.
+ * @returns {Promise<void>} A promise that resolves once the document is created and displayed.
+ */
+ @action
+ createDocInDash = (pdoc: parsedDoc) => {
+ const linkAndShowDoc = (doc: Opt<Doc>) => {
+ if (doc) {
+ LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
+ this._props.addDocument?.(doc);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ }
+ };
+ const doc = this.whichDoc(pdoc, false);
+ if (doc) linkAndShowDoc(doc);
+ return doc;
+ };
- // this._scriptKeys = ScriptingGlobals.getGlobals();
- // this._scriptingDescriptions = ScriptingGlobals.getDescriptions();
- // this._scriptingParams = ScriptingGlobals.getParameters();
- // Add more cases for other document types
- default:
- console.error('Unknown or unsupported document type:', doc_type);
- return;
+ /**
+ * Creates a deck of flashcards.
+ *
+ * @param {any} data - The data used to generate the flashcards. Can be a string or an object.
+ * @param {DocumentOptions} options - Configuration options for the flashcard deck.
+ * @returns {Doc} A carousel document containing the flashcard deck.
+ */
+ @action
+ createDeck = (data: parsedDoc[], options: DocumentOptions) => {
+ const flashcardDeck: Doc[] = [];
+ // Process each flashcard document in the `deckData` array
+ if (data.length == 2 && data[0].doc_type == 'text' && data[1].doc_type == 'text') {
+ this.createFlashcard(data, options);
+ } else {
+ data.forEach(doc => {
+ const flashcardDoc = this.createFlashcard((doc as parsedDocData).data as parsedDoc[] | string[], options);
+ if (flashcardDoc) flashcardDeck.push(flashcardDoc);
+ });
}
- const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
- LinkManager.Instance.addLink(linkDoc);
- doc && this._props.addDocument?.(doc);
- await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ // Create a carousel to contain the flashcard deck
+ return Docs.Create.CarouselDocument(flashcardDeck, {
+ title: options.title || 'Flashcard Deck',
+ _width: options._width || 300,
+ _height: options._height || 300,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ });
};
+ /**
+ * Creates a single flashcard document.
+ *
+ * @param {any} data - The data used to generate the flashcard. Can be a string or an object.
+ * @param {any} options - Configuration options for the flashcard.
+ * @returns {Doc | undefined} The created flashcard document, or undefined if the flashcard cannot be created.
+ */
+ @action
+ createFlashcard = (data: parsedDoc[] | string[], options: DocumentOptions) => {
+ const [front, back] = data;
+ const sideOptions = { _height: 300, ...options };
+
+ // Create front and back text documents
+ const side1 = typeof front === 'string' ? Docs.Create.CenteredTextCreator('question', front as string, sideOptions) : this.whichDoc(front, false);
+ const side2 = typeof back === 'string' ? Docs.Create.CenteredTextCreator('answer', back as string, sideOptions) : this.whichDoc(back, false);
+
+ // Create the flashcard document with both sides
+ return Docs.Create.FlashcardDocument('flashcard', side1, side2, sideOptions);
+ };
+
+ /**
+ * Creates a comparison document.
+ *
+ * @param {any} doc - The document data containing left and right components for comparison.
+ * @param {any} options - Configuration options for the comparison document.
+ * @returns {Doc} The created comparison document.
+ */
+ @action
+ createComparison = (doc: parsedDoc[], options: DocumentOptions) =>
+ Docs.Create.ComparisonDocument(options.title as string, {
+ data_back: this.whichDoc(doc[0], false),
+ data_front: this.whichDoc(doc[1], false),
+ _width: options._width,
+ _height: options._height || 300,
+ backgroundColor: options.backgroundColor,
+ });
+
+ /**
+ * Event handler to manage citations click in the message components.
+ * @param citation The citation object clicked by the user.
+ */
@action
handleCitationClick = async (citation: Citation) => {
const currentLinkedDocs: Doc[] = this.linkedDocs;
-
const chunkId = citation.chunk_id;
for (const doc of currentLinkedDocs) {
@@ -643,8 +738,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
break;
case CHUNK_TYPE.TEXT:
- this.citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
- setTimeout(() => (this.citationPopup.visible = false), 3000);
+ this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
+ setTimeout(() => (this._citationPopup.visible = false), 3000);
DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
const firstView = Array.from(doc[DocViews])[0] as DocumentView;
@@ -705,7 +800,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
const storedHistory = JSON.parse(StrCast(this.dataDoc.data));
runInAction(() => {
- this.history.push(
+ this._history.push(
...storedHistory.map((msg: AssistantMessage) => ({
role: msg.role,
content: msg.content,
@@ -720,7 +815,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
} else {
// Default welcome message
runInAction(() => {
- this.history.push({
+ this._history.push({
role: ASSISTANT_ROLE.ASSISTANT,
content: [
{
@@ -744,11 +839,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.filter(d => d);
return linkedDocs;
},
- linked => linked.forEach(doc => this.linked_docs_to_add.add(doc))
+ linked => linked.forEach(doc => this._linked_docs_to_add.add(doc))
);
// Observe changes to linked documents and handle document addition
- observe(this.linked_docs_to_add, change => {
+ observe(this._linked_docs_to_add, change => {
if (change.type === 'add') {
if (CsvCast(change.newValue.data)) {
this.addCSVForAnalysis(change.newValue);
@@ -824,18 +919,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
/**
* Getter that retrieves all linked CSV files for analysis.
*/
- @computed
- get linkedCSVs(): { filename: string; id: string; text: string }[] {
- return this.linked_csv_files;
+ @computed get linkedCSVs(): { filename: string; id: string; text: string }[] {
+ return this._linked_csv_files;
}
/**
* Getter that formats the entire chat history as a string for the agent's system message.
*/
- @computed
- get formattedHistory(): string {
+ @computed get formattedHistory(): string {
let history = '<chat_history>\n';
- for (const message of this.history) {
+ for (const message of this._history) {
history += `<${message.role}>${message.content.map(content => content.text).join(' ')}`;
if (message.loop_summary) {
history += `<loop_summary>${message.loop_summary}</loop_summary>`;
@@ -871,20 +964,21 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
handleFollowUpClick = (question: string) => {
- this.inputValue = question;
+ this._inputValue = question;
};
+ _dictation: DictationButton | null = null;
/**
* Renders the chat interface, including the message list, input field, and other UI elements.
*/
render() {
return (
<div className="chat-box">
- {this.isUploadingDocs && (
+ {this._isUploadingDocs && (
<div className="uploading-overlay">
<div className="progress-container">
<ProgressBar />
- <div className="step-name">{this.currentStep}</div>
+ <div className="step-name">{this._currentStep}</div>
</div>
</div>
)}
@@ -892,18 +986,29 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<h2>{this.userName()}&apos;s AI Assistant</h2>
</div>
<div className="chat-messages" ref={this.messagesRef}>
- {this.history.map((message, index) => (
+ {this._history.map((message, index) => (
<MessageComponentBox key={index} message={message} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
))}
- {this.current_message && (
- <MessageComponentBox key={this.history.length} message={this.current_message} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
+ {this._current_message && (
+ <MessageComponentBox key={this._history.length} message={this._current_message} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
)}
</div>
<form onSubmit={this.askGPT} className="chat-input">
- <input type="text" name="messageInput" autoComplete="off" placeholder="Type your message here..." value={this.inputValue} onChange={e => (this.inputValue = e.target.value)} disabled={this.isLoading} />
- <button className="submit-button" type="submit" disabled={this.isLoading || !this.inputValue.trim()}>
- {this.isLoading ? (
+ <input
+ ref={r => {
+ this._textInputRef = r;
+ }}
+ type="text"
+ name="messageInput"
+ autoComplete="off"
+ placeholder="Type your message here..."
+ value={this._inputValue}
+ onChange={action(e => (this._inputValue = e.target.value))}
+ disabled={this._isLoading}
+ />
+ <button className="submit-button" onClick={() => this._dictation?.stopDictation()} type="submit" disabled={this._isLoading || !this._inputValue.trim()}>
+ {this._isLoading ? (
<div className="spinner"></div>
) : (
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
@@ -912,12 +1017,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</svg>
)}
</button>
+ <DictationButton
+ ref={r => {
+ this._dictation = r;
+ }}
+ setInput={this.setChatInput}
+ inputRef={this._textInputRef}
+ />
</form>
{/* Popup for citation */}
- {this.citationPopup.visible && (
+ {this._citationPopup.visible && (
<div className="citation-popup">
<p>
- <strong>Text from your document: </strong> {this.citationPopup.text}
+ <strong>Text from your document: </strong> {this._citationPopup.text}
</p>
</div>
)}
diff --git a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts b/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts
index 5f3af8296..5cf858998 100644
--- a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts
@@ -1,57 +1,76 @@
-import { v4 as uuidv4 } from 'uuid';
-import { BaseTool } from './BaseTool';
+import { toLower } from 'lodash';
+import { Doc } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { DocumentOptions } from '../../../../documents/Documents';
+import { parsedDoc } from '../chatboxcomponents/ChatBox';
+import { ParametersType, ToolInfo } from '../types/tool_types';
import { Observation } from '../types/types';
-import { ParametersType, Parameter, ToolInfo } from '../types/tool_types';
-import { DocumentOptions, Docs } from '../../../../documents/Documents';
-
-/**
- * List of supported document types that can be created via text LLM.
- */
-type supportedDocumentTypesType = 'text' | 'html' | 'equation' | 'function_plot' | 'dataviz' | 'note_taking' | 'rtf' | 'message' | 'mermaid_diagram' | 'script';
-const supportedDocumentTypes: supportedDocumentTypesType[] = ['text', 'html', 'equation', 'function_plot', 'dataviz', 'note_taking', 'rtf', 'message', 'mermaid_diagram', 'script'];
+import { BaseTool } from './BaseTool';
+import { supportedDocumentTypes } from './CreateDocumentTool';
+const standardOptions = ['title', 'backgroundColor'];
/**
* Description of document options and data field for each type.
*/
-const documentTypesInfo = {
- text: {
- options: ['title', 'backgroundColor', 'fontColor', 'text_align', 'layout'],
- dataDescription: 'The text content of the text document. Should contain all the text content.',
+const documentTypesInfo: { [key in supportedDocumentTypes]: { options: string[]; dataDescription: string } } = {
+ [supportedDocumentTypes.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',
+ },
+ [supportedDocumentTypes.text]: {
+ options: [...standardOptions, 'fontColor', 'text_align'],
+ dataDescription: 'The text content of the document.',
},
- html: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.html]: {
+ options: [],
dataDescription: 'The HTML-formatted text content of the document.',
},
- equation: {
- options: ['title', 'backgroundColor', 'fontColor', 'layout'],
+ [supportedDocumentTypes.equation]: {
+ options: [...standardOptions, 'fontColor'],
dataDescription: 'The equation content as a string.',
},
- function_plot: {
- options: ['title', 'backgroundColor', 'layout', 'function_definition'],
+ [supportedDocumentTypes.functionplot]: {
+ options: [...standardOptions, 'function_definition'],
dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.',
},
- dataviz: {
- options: ['title', 'backgroundColor', 'layout', 'chartType'],
+ [supportedDocumentTypes.dataviz]: {
+ options: [...standardOptions, 'chartType'],
dataDescription: 'A string of comma-separated values representing the CSV data.',
},
- note_taking: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.notetaking]: {
+ options: standardOptions,
dataDescription: 'The initial content or structure for note-taking.',
},
- rtf: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.rtf]: {
+ options: standardOptions,
dataDescription: 'The rich text content in RTF format.',
},
- message: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.image]: {
+ options: standardOptions,
+ dataDescription: 'The image content as an image file URL.',
+ },
+ [supportedDocumentTypes.pdf]: {
+ options: standardOptions,
+ dataDescription: 'the pdf content as a PDF file url.',
+ },
+ [supportedDocumentTypes.audio]: {
+ options: standardOptions,
+ dataDescription: 'The audio content as a file url.',
+ },
+ [supportedDocumentTypes.video]: {
+ options: standardOptions,
+ dataDescription: 'The video content as a file url.',
+ },
+ [supportedDocumentTypes.message]: {
+ options: standardOptions,
dataDescription: 'The message content of the document.',
},
- mermaid_diagram: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.mermaid]: {
+ options: ['title', 'backgroundColor'],
dataDescription: 'The Mermaid diagram content.',
},
- script: {
- options: ['title', 'backgroundColor', 'layout'],
+ [supportedDocumentTypes.script]: {
+ options: ['title', 'backgroundColor'],
dataDescription: 'The compilable JavaScript code. Use this for creating scripts.',
},
};
@@ -60,7 +79,7 @@ const createAnyDocumentToolParams = [
{
name: 'document_type',
type: 'string',
- description: `The type of the document to create. Supported types are: ${supportedDocumentTypes.join(', ')}`,
+ description: `The type of the document to create. Supported types are: ${Object.values(supportedDocumentTypes).join(', ')}`,
required: true,
},
{
@@ -72,14 +91,11 @@ const createAnyDocumentToolParams = [
{
name: 'options',
type: 'string',
- description: `A JSON string representing the document options. Available options depend on the document type. For example:
-${supportedDocumentTypes
- .map(
- docType => `
-- For '${docType}' documents, options include: ${documentTypesInfo[docType].options.join(', ')}`
- )
- .join('\n')}`,
required: false,
+ description: `A JSON string representing the document options. Available options depend on the document type. For example:
+ ${Object.entries(documentTypesInfo).map( ([doc_type, info]) => `
+- For '${doc_type}' documents, options include: ${info.options.join(', ')}`)
+ .join('\n')}`, // prettier-ignore
},
] as const;
@@ -87,76 +103,56 @@ type CreateAnyDocumentToolParamsType = typeof createAnyDocumentToolParams;
const createAnyDocToolInfo: ToolInfo<CreateAnyDocumentToolParamsType> = {
name: 'createAnyDocument',
- description: `Creates any type of document (in Dash) with the provided options and data. Supported document types are: ${supportedDocumentTypes.join(', ')}. dataviz is a csv table tool, so for CSVs, use dataviz. Here are the options for each type:
- <supported_document_types>
- ${supportedDocumentTypes
+ description:
+ `Creates any type of document with the provided options and data.
+ Supported document types are: ${Object.values(supportedDocumentTypes).join(', ')}.
+ dataviz is a csv table tool, so for CSVs, use dataviz. Here are the options for each type:
+ <supported_document_types>` +
+ Object.entries(documentTypesInfo)
.map(
- docType => `
- <document_type name="${docType}">
- <data_description>${documentTypesInfo[docType].dataDescription}</data_description>
- <options>
- ${documentTypesInfo[docType].options.map(option => `<option>${option}</option>`).join('\n')}
- </options>
- </document_type>
- `
+ ([doc_type, info]) =>
+ `<document_type name="${doc_type}">
+ <data_description>${info.dataDescription}</data_description>
+ <options>` +
+ info.options.map(option => `<option>${option}</option>`).join('\n') +
+ `</options>
+ </document_type>`
)
- .join('\n')}
- </supported_document_types>`,
+ .join('\n') +
+ `</supported_document_types>`,
parameterRules: createAnyDocumentToolParams,
citationRules: 'No citation needed.',
};
export class CreateAnyDocumentTool extends BaseTool<CreateAnyDocumentToolParamsType> {
- private _addLinkedDoc: (doc_type: string, data: string | undefined, options: DocumentOptions, id: string) => void;
+ private _addLinkedDoc: (doc: parsedDoc) => Doc | undefined;
- constructor(addLinkedDoc: (doc_type: string, data: string | undefined, options: DocumentOptions, id: string) => void) {
+ constructor(addLinkedDoc: (doc: parsedDoc) => Doc | undefined) {
super(createAnyDocToolInfo);
this._addLinkedDoc = addLinkedDoc;
}
async execute(args: ParametersType<CreateAnyDocumentToolParamsType>): Promise<Observation[]> {
try {
- const documentType: supportedDocumentTypesType = args.document_type.toLowerCase() as supportedDocumentTypesType;
- let options: DocumentOptions = {};
+ const documentType = toLower(args.document_type) as unknown as supportedDocumentTypes;
+ const info = documentTypesInfo[documentType];
- if (!supportedDocumentTypes.includes(documentType)) {
- throw new Error(`Unsupported document type: ${documentType}. Supported types are: ${supportedDocumentTypes.join(', ')}.`);
+ if (info === undefined) {
+ throw new Error(`Unsupported document type: ${documentType}. Supported types are: ${Object.values(supportedDocumentTypes).join(', ')}.`);
}
if (!args.data) {
- throw new Error(`Data is required for ${documentType} documents. ${documentTypesInfo[documentType].dataDescription}`);
+ throw new Error(`Data is required for ${documentType} documents. ${info.dataDescription}`);
}
- if (args.options) {
- try {
- options = JSON.parse(args.options as string) as DocumentOptions;
- } catch (e) {
- throw new Error('Options must be a valid JSON string.');
- }
- }
-
- const data = args.data as string;
- const id = uuidv4();
-
- // Set default options if not provided
- options.title = options.title || `New ${documentType.charAt(0).toUpperCase() + documentType.slice(1)} Document`;
+ const options: DocumentOptions = !args.options ? {} : JSON.parse(args.options);
- // Call the function to add the linked document
- this._addLinkedDoc(documentType, data, options, id);
+ // Call the function to add the linked document (add default title that can be overriden if set in options)
+ const doc = this._addLinkedDoc({ doc_type: documentType, data: args.data, title: `New ${documentType.charAt(0).toUpperCase() + documentType.slice(1)} Document`, ...options });
- return [
- {
- type: 'text',
- text: `Created ${documentType} document with ID ${id}.`,
- },
- ];
+ return [{ type: 'text', text: `Created ${documentType} document with ID ${doc?.[Id]}.` }];
} catch (error) {
- return [
- {
- type: 'text',
- text: 'Error creating document: ' + (error as Error).message,
- },
- ];
+ return [{ type: 'text', text: 'Error creating document: ' + (error as Error).message }];
}
}
}
diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
new file mode 100644
index 000000000..7d6964f44
--- /dev/null
+++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
@@ -0,0 +1,415 @@
+import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType, ToolInfo } from '../types/tool_types';
+import { parsedDoc } from '../chatboxcomponents/ChatBox';
+
+/**
+ * List of supported document types that can be created via text LLM.
+ */
+export enum supportedDocumentTypes {
+ flashcard = 'flashcard',
+ text = 'text',
+ html = 'html',
+ equation = 'equation',
+ functionplot = 'functionplot',
+ dataviz = 'dataviz',
+ notetaking = 'notetaking',
+ audio = 'audio',
+ video = 'video',
+ pdf = 'pdf',
+ rtf = 'rtf',
+ message = 'message',
+ collection = 'collection',
+ image = 'image',
+ deck = 'deck',
+ web = 'web',
+ comparison = 'comparison',
+ mermaid = 'mermaid',
+ script = 'script',
+}
+/**
+ * Tthe CreateDocTool class is responsible for creating
+ * documents of various types (e.g., text, flashcards, collections) and organizing them in a
+ * structured manner. The tool supports creating dashboards with diverse document types and
+ * ensures proper placement of documents without overlap.
+ */
+
+// Example document structure for various document types
+const example = [
+ {
+ doc_type: supportedDocumentTypes.equation,
+ title: 'quadratic',
+ data: 'x^2 + y^2 = 3',
+ width: 300,
+ height: 300,
+ x: 0,
+ y: 0,
+ },
+ {
+ doc_type: supportedDocumentTypes.collection,
+ title: 'Advanced Biology',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'Cell Structure',
+ data: 'Cells are the basic building blocks of all living organisms.',
+ width: 300,
+ height: 300,
+ x: 500,
+ y: 0,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 600,
+ height: 600,
+ x: 600,
+ y: 0,
+ type_collection: 'tree',
+ },
+ {
+ doc_type: supportedDocumentTypes.image,
+ title: 'experiment',
+ data: 'https://plus.unsplash.com/premium_photo-1694819488591-a43907d1c5cc?q=80&w=2628&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
+ width: 300,
+ height: 300,
+ x: 600,
+ y: 300,
+ },
+ {
+ doc_type: supportedDocumentTypes.deck,
+ title: 'Chemistry',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.flashcard,
+ title: 'Photosynthesis',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'front_Photosynthesis',
+ data: 'What is photosynthesis?',
+ width: 300,
+ height: 300,
+ x: 100,
+ y: 600,
+ },
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'back_photosynthesis',
+ data: 'The process by which plants make food.',
+ width: 300,
+ height: 300,
+ x: 100,
+ y: 700,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 300,
+ height: 300,
+ x: 300,
+ y: 1000,
+ },
+ {
+ doc_type: supportedDocumentTypes.flashcard,
+ title: 'Photosynthesis',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'front_Photosynthesis',
+ data: 'What is photosynthesis?',
+ width: 300,
+ height: 300,
+ x: 200,
+ y: 800,
+ },
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'back_photosynthesis',
+ data: 'The process by which plants make food.',
+ width: 300,
+ height: 300,
+ x: 100,
+ y: -100,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 300,
+ height: 300,
+ x: 10,
+ y: 70,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 600,
+ height: 600,
+ x: 200,
+ y: 800,
+ },
+ {
+ doc_type: supportedDocumentTypes.web,
+ title: 'Brown University Wikipedia',
+ data: 'https://en.wikipedia.org/wiki/Brown_University',
+ width: 300,
+ height: 300,
+ x: 1000,
+ y: 2000,
+ },
+ {
+ doc_type: supportedDocumentTypes.comparison,
+ title: 'WWI vs. WWII',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'WWI',
+ data: 'From 1914 to 1918, fighting took place across several continents, at sea and, for the first time, in the air.',
+ width: 300,
+ height: 300,
+ x: 100,
+ y: 100,
+ },
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'WWII',
+ data: 'A devastating global conflict spanning from 1939 to 1945, saw the Allied powers fight against the Axis powers.',
+ width: 300,
+ height: 300,
+ x: 100,
+ y: 100,
+ },
+ ],
+ width: 300,
+ height: 300,
+ x: 100,
+ y: 100,
+ },
+ {
+ doc_type: supportedDocumentTypes.collection,
+ title: 'Science Collection',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.flashcard,
+ title: 'Photosynthesis',
+ data: [
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'front_Photosynthesis',
+ data: 'What is photosynthesis?',
+ width: 300,
+ height: 300,
+ },
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'back_photosynthesis',
+ data: 'The process by which plants make food.',
+ width: 300,
+ height: 300,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 300,
+ height: 300,
+ },
+ {
+ doc_type: supportedDocumentTypes.web,
+ title: 'Brown University Wikipedia',
+ data: 'https://en.wikipedia.org/wiki/Brown_University',
+ width: 300,
+ height: 300,
+ x: 1100,
+ y: 1100,
+ },
+ {
+ doc_type: supportedDocumentTypes.text,
+ title: 'Water Cycle',
+ data: 'The continuous movement of water on, above, and below the Earth’s surface.',
+ width: 300,
+ height: 300,
+ x: 1500,
+ y: 500,
+ },
+ {
+ doc_type: supportedDocumentTypes.collection,
+ title: 'Advanced Biology',
+ data: [
+ {
+ doc_type: 'text',
+ title: 'Cell Structure',
+ data: 'Cells are the basic building blocks of all living organisms.',
+ width: 300,
+ height: 300,
+ },
+ ],
+ backgroundColor: '#00ff00',
+ width: 600,
+ height: 600,
+ x: 1100,
+ y: 500,
+ type_collection: 'freeform',
+ },
+ ],
+ width: 600,
+ height: 600,
+ x: 500,
+ y: 500,
+ type_collection: 'freeform',
+ },
+];
+
+// Stringify the entire structure for transmission if needed
+const finalJsonString = JSON.stringify(example);
+
+// Instructions for creating various document types
+const docInstructions: [supportedDocumentTypes, string | { description: string; example: string }][] = [
+ [ supportedDocumentTypes.collection,
+ { description: `A recursive collection of documents as a stringified array. Each document can be a ${Object.keys(supportedDocumentTypes).map(key => '"' + key + '"').join(',')}.`,
+ example: finalJsonString },
+ ], // prettier-ignore
+ [supportedDocumentTypes.text, 'Provide text content as a plain string. Example: "This is a standalone text document."'],
+ [supportedDocumentTypes.flashcard, 'Two text documents with content for the front and back.'],
+ [supportedDocumentTypes.deck, 'A decks data is an array of flashcards.'],
+ [supportedDocumentTypes.comparison, 'two documents of any kind that can be compared.'],
+ [supportedDocumentTypes.image, `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`],
+ [supportedDocumentTypes.web, 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University'],
+ [supportedDocumentTypes.equation, 'Create an equation document, not a text document. Data is math equation.'],
+ [supportedDocumentTypes.notetaking, 'Create a noteboard document'],
+ [supportedDocumentTypes.audio, 'A url to an audio recording. Example: '],
+] as const;
+
+// Parameters for creating individual documents
+const createDocToolParams = [
+ {
+ name: 'data',
+ type: 'string', // Accepts either string or array, supporting individual and nested data
+ description:
+ typeof docInstructions === 'string'
+ ? docInstructions
+ : docInstructions.reduce(
+ (prev, [type, data]) => {
+ prev[type] = data;
+ return prev;
+ },
+ {} as { [key: string]: string | { description: string; example: string } }
+ ),
+ required: true,
+ },
+ {
+ name: 'doc_type',
+ type: 'string',
+ description: 'The type of the document. Options: "collection", "text", "flashcard", "web".',
+ required: true,
+ },
+ {
+ name: 'title',
+ type: 'string',
+ description: 'The title of the document.',
+ required: true,
+ },
+ {
+ name: 'x',
+ type: 'number',
+ description: 'The x location of the document; 0 <= x.',
+ required: true,
+ },
+ {
+ name: 'y',
+ type: 'number',
+ description: 'The y location of the document; 0 <= y.',
+ required: true,
+ },
+ {
+ name: 'backgroundColor',
+ type: 'string',
+ description: 'The background color of the document as a hex string.',
+ required: false,
+ },
+ {
+ name: 'fontColor',
+ type: 'string',
+ description: 'The font color of the document as a hex string.',
+ required: false,
+ },
+ {
+ name: 'width',
+ type: 'number',
+ description: 'The width of the document in pixels.',
+ required: true,
+ },
+ {
+ name: 'height',
+ type: 'number',
+ description: 'The height of the document in pixels.',
+ required: true,
+ },
+ {
+ name: 'type_collection',
+ type: 'string',
+ description: 'Either freeform, card, carousel, 3d-carousel, multicolumn, multirow, linear, map, notetaking, schema, stacking, grid, tree, or masonry.',
+ required: false,
+ },
+] as const;
+
+// Parameters for creating a list of documents
+const createListDocToolParams = [
+ {
+ name: 'docs',
+ type: 'string',
+ description:
+ 'Array of documents in stringified JSON format. Each item in the array should be an individual stringified JSON object. Each document can be of type "text", "flashcard", "comparison", "web", or "collection" (for nested documents). ' +
+ 'Use this structure for nesting collections within collections. Each document should follow the structure in ' +
+ createDocToolParams +
+ '. Example: ' +
+ finalJsonString,
+ required: true,
+ },
+] as const;
+
+type CreateListDocToolParamsType = typeof createListDocToolParams;
+
+type CreateDocumentToolParamsType = typeof createDocToolParams;
+
+const createDocToolInfo: ToolInfo<CreateDocumentToolParamsType> = {
+ name: 'createAnyDocument',
+ description: `Creates one or more documents that best fit the user’s request.
+ If the user requests a "dashboard," first call the search tool and then generate a variety of document types individually, with absolutely a minimum of 20 documents
+ with two stacks of flashcards that are small and it should have a couple nested freeform collections of things, each with different content and color schemes.
+ For example, create multiple individual documents like "text," "deck," "web", "equation," and "comparison."
+ Use decks instead of flashcards for dashboards. Decks should have at least three flashcards.
+ Really think about what documents are useful to the user. If they ask for a dashboard about the skeletal system, include flashcards, as they would be helpful.
+ Arrange the documents in a grid layout, ensuring that the x and y coordinates are calculated so no documents overlap but they should be directly next to each other with 20 padding in between.
+ Take into account the width and height of each document, spacing them appropriately to prevent collisions.
+ Use a systematic approach, such as placing each document in a grid cell based on its order, where cell dimensions match the document dimensions plus a fixed margin for spacing.
+ Do not nest all documents within a single collection unless explicitly requested by the user.
+ Instead, create a set of independent documents with diverse document types. Each type should appear separately unless specified otherwise.
+ Use the "data" parameter for document content and include title, color, and document dimensions.
+ Ensure web documents use URLs from the search tool if relevant. Each document in a dashboard should be unique and well-differentiated in type and content,
+ without repetition of similar types in any single collection.
+ When creating a dashboard, ensure that it consists of a broad range of document types.
+ Include a variety of documents, such as text, web, deck, comparison, image, simulation, and equation documents,
+ each with distinct titles and colors, following the user’s preferences.
+ Do not overuse collections or nest all document types within a single collection; instead, represent document types individually. Use this example for reference:
+ ${finalJsonString} .
+ Which documents are created should be random with different numbers of each document type and different for each dashboard.
+ Must use search tool before creating a dashboard.`,
+ parameterRules: createDocToolParams,
+ citationRules: 'No citation needed.',
+};
+
+// Tool class for creating documents
+export class CreateDocTool extends BaseTool<CreateListDocToolParamsType> {
+ private _addLinkedDoc: (doc: parsedDoc) => void;
+
+ constructor(addLinkedDoc: (doc: parsedDoc) => void) {
+ super(createDocToolInfo);
+ this._addLinkedDoc = addLinkedDoc;
+ }
+
+ // Executes the tool logic for creating documents
+ async execute(args: ParametersType<CreateListDocToolParamsType>): Promise<Observation[]> {
+ try {
+ const parsedDocs = JSON.parse(args.docs) as parsedDoc[];
+ parsedDocs.forEach(doc => this._addLinkedDoc({ ...doc, _layout_fitWidth: false, _layout_autoHeight: true }));
+ return [{ type: 'text', text: 'Created document.' }];
+ } catch (error) {
+ return [{ type: 'text', text: 'Error creating text document, ' + error }];
+ }
+ }
+}
diff --git a/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts
index 487fc951d..16dc938bb 100644
--- a/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts
@@ -1,11 +1,7 @@
-import { v4 as uuidv4 } from 'uuid';
-import { Networking } from '../../../../Network';
-import { BaseTool } from './BaseTool';
-import { Observation } from '../types/types';
+import { parsedDoc } from '../chatboxcomponents/ChatBox';
import { ParametersType, ToolInfo } from '../types/tool_types';
-import { DocumentOptions } from '../../../../documents/Documents';
-import { RTFCast, StrCast } from '../../../../../fields/Types';
-
+import { Observation } from '../types/types';
+import { BaseTool } from './BaseTool';
const createTextDocToolParams = [
{
name: 'text_content',
@@ -43,17 +39,16 @@ const createTextDocToolInfo: ToolInfo<CreateTextDocToolParamsType> = {
};
export class CreateTextDocTool extends BaseTool<CreateTextDocToolParamsType> {
- private _addLinkedDoc: (doc_type: string, data: string, options: DocumentOptions, id: string) => void;
+ private _addLinkedDoc: (doc: parsedDoc) => void;
- constructor(addLinkedDoc: (text_content: string, data: string, options: DocumentOptions, id: string) => void) {
+ constructor(addLinkedDoc: (doc: parsedDoc) => void) {
super(createTextDocToolInfo);
this._addLinkedDoc = addLinkedDoc;
}
async execute(args: ParametersType<CreateTextDocToolParamsType>): Promise<Observation[]> {
try {
- console.log(RTFCast(args.text_content));
- this._addLinkedDoc('text', args.text_content, { title: args.title }, uuidv4());
+ this._addLinkedDoc({ doc_type: 'text', data: args.text_content, title: args.title });
return [{ type: 'text', text: 'Created text document.' }];
} catch (error) {
return [{ type: 'text', text: 'Error creating text document, ' + error }];
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index ba1aa987a..177552c5c 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -1,12 +1,10 @@
import { v4 as uuidv4 } from 'uuid';
+import { RTFCast } from '../../../../../fields/Types';
+import { DocumentOptions } from '../../../../documents/Documents';
import { Networking } from '../../../../Network';
-import { BaseTool } from './BaseTool';
-import { Observation } from '../types/types';
import { ParametersType, ToolInfo } from '../types/tool_types';
-import { DocumentOptions } from '../../../../documents/Documents';
-import { ClientUtils } from '../../../../../ClientUtils';
-import { DashUploadUtils } from '../../../../../server/DashUploadUtils';
-import { RTFCast, StrCast } from '../../../../../fields/Types';
+import { Observation } from '../types/types';
+import { BaseTool } from './BaseTool';
const imageCreationToolParams = [
{
diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
index 5334f7df0..ef24e59bc 100644
--- a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
+++ b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
@@ -9,13 +9,12 @@ import { Index, IndexList, Pinecone, PineconeRecord, QueryResponse, RecordMetada
import { CohereClient } from 'cohere-ai';
import { EmbedResponse } from 'cohere-ai/api';
import dotenv from 'dotenv';
+import path from 'path';
+import { v4 as uuidv4 } from 'uuid';
import { Doc } from '../../../../../fields/Doc';
-import { AudioCast, Cast, CsvCast, DocCast, PDFCast, StrCast, VideoCast } from '../../../../../fields/Types';
+import { AudioCast, CsvCast, PDFCast, StrCast, VideoCast } from '../../../../../fields/Types';
import { Networking } from '../../../../Network';
import { AI_Document, CHUNK_TYPE, RAGChunk } from '../types/types';
-import path from 'path';
-import { v4 as uuidv4 } from 'uuid';
-import { indexes } from 'd3';
dotenv.config();
@@ -40,14 +39,14 @@ export class Vectorstore {
* @param doc_ids A function that returns a list of document IDs.
*/
constructor(id: string, doc_ids: () => string[]) {
- const pineconeApiKey = process.env.PINECONE_API_KEY;
+ const pineconeApiKey = '51738e9a-bea2-4c11-b6bf-48a825e774dc';
if (!pineconeApiKey) {
throw new Error('PINECONE_API_KEY is not defined.');
}
// Initialize Pinecone and Cohere clients with API keys from the environment.
this.pinecone = new Pinecone({ apiKey: pineconeApiKey });
- this.cohere = new CohereClient({ token: process.env.COHERE_API_KEY });
+ // this.cohere = new CohereClient({ token: process.env.COHERE_API_KEY });
this._id = id;
this._doc_ids = doc_ids;
this.initializeIndex();
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index f0313fba4..0684daeb6 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
import { NodeSelection } from 'prosemirror-state';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
-import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
+import { returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
import { Doc, DocListCast, Field } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
@@ -169,12 +169,17 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
selectedCells={this.selectedCells}
selectedCol={returnZero}
fieldKey={this._fieldKey}
+ highlightCells={emptyFunction} // fix
+ refSelectModeInfo={{ enabled: false, currEditing: undefined }} // fix
+ selectReference={emptyFunction} //
+ eqHighlightFunc={() => []} // fix
+ isolatedSelection={() => [true, true]} // fix
+ rowSelected={returnTrue} //fix
rowHeight={returnZero}
isRowActive={this.isRowActive}
padding={0}
getFinfo={emptyFunction}
setColumnValues={returnFalse}
- setSelectedColumnValues={returnFalse}
allowCRs
oneLine={!this._expanded && !this._props.nodeSelected()}
finishEdit={this.finishEdit}
diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx
index 8bb4a0a26..48efa6e63 100644
--- a/src/client/views/nodes/formattedText/EquationEditor.tsx
+++ b/src/client/views/nodes/formattedText/EquationEditor.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import React, { Component, createRef } from 'react';
// Import JQuery, required for the functioning of the equation editor
@@ -7,6 +6,7 @@ import './EquationEditor.scss';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).jQuery = $;
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require('mathquill/build/mathquill');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).MathQuill = (window as any).MathQuill.getInterface(1);
@@ -57,13 +57,8 @@ class EquationEditor extends Component<EquationEditorProps> {
const config = {
handlers: {
edit: () => {
- if (this.ignoreEditEvents > 0) {
- this.ignoreEditEvents -= 1;
- return;
- }
- if (this.mathField.latex() !== value) {
- onChange(this.mathField.latex());
- }
+ if (this.ignoreEditEvents <= 0) onChange(this.mathField.latex());
+ else this.ignoreEditEvents -= 1;
},
enter: onEnter,
},
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 29be8d285..55ad543ca 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -65,6 +65,8 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
import { Property } from 'csstype';
+import { LabelBox } from '../LabelBox';
+import { StickerPalette } from '../../smartdraw/StickerPalette';
// import * as applyDevTools from 'prosemirror-dev-tools';
export interface FormattedTextBoxProps extends FieldViewProps {
@@ -134,10 +136,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@observable _showSidebar = false;
- @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
- @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
- @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
- @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore
+ @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
+ @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
+ @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
+ @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore
+ @computed get fontStyle() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore
+ @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore
set _recordingDictation(value) {
!this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
@@ -158,6 +162,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore
@computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore
@computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore
+ @computed get isLabel() { return this.dataDoc[this.fieldKey+"_fitBox"]; } // prettier-ignore
constructor(props: FormattedTextBoxProps) {
super(props);
@@ -165,7 +170,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._recordingStart = Date.now();
}
- public get EditorView() { return this._editorView; } // prettier-ignore
+ public get EditorView() { return this.isLabel ? undefined : this._editorView; } // prettier-ignore
// public makeAIFlashcards: () => void = unimplementedFunction;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
@@ -175,10 +180,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
public RemoveLinkFromDoc(linkDoc?: Doc) {
this.unhighlightSearchTerms();
- const state = this._editorView?.state;
+ const state = this.EditorView?.state;
const a1 = DocCast(linkDoc?.link_anchor_1);
const a2 = DocCast(linkDoc?.link_anchor_2);
- if (state && a1 && a2 && this._editorView) {
+ if (state && a1 && a2 && this.EditorView) {
this.removeDocument(a1);
this.removeDocument(a2);
let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = [];
@@ -188,7 +193,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return true;
});
if (allFoundLinkAnchors.length) {
- this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
+ this.EditorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
this.setupEditor(this.config, this.fieldKey);
}
@@ -197,16 +202,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// removes all the specified link references from the selection.
// NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) {
- const state = this._editorView?.state;
- if (state && this._editorView) {
- this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
+ const state = this.EditorView?.state;
+ if (state && this.EditorView) {
+ this.EditorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
this.setupEditor(this.config, this.fieldKey);
}
}
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document);
- if (!pinProps && this._editorView?.state.selection.empty) return rootDoc;
+ if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc;
const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc });
this.addDocument(anchor);
this._finishingLink = true;
@@ -263,7 +268,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
};
- AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
+ AnchorMenu.Instance.Highlight = undoable((color: string) => this.EditorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
@@ -292,7 +297,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
- const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
+ const coordsB = this.EditorView!.coordsAtPos(this.EditorView!.state.selection.to);
this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
let ele: Opt<HTMLDivElement>;
try {
@@ -309,7 +314,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
leafText = (node: Node) => {
- if (node.type === this._editorView?.state.schema.nodes.dashField) {
+ if (node.type === this.EditorView?.state.schema.nodes.dashField) {
const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
const fieldKey = StrCast(node.attrs.fieldKey);
return (
@@ -320,16 +325,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return '';
};
dispatchTransaction = (tx: Transaction) => {
- if (this._editorView && !this._editorView.isDestroyed) {
- const state = this._editorView.state.apply(tx);
- this._editorView.updateState(state);
+ if (this.EditorView && !this.EditorView.isDestroyed) {
+ const state = this.EditorView.state.apply(tx);
+ this.EditorView.updateState(state);
this.tryUpdateDoc(false);
}
};
tryUpdateDoc = (force: boolean) => {
- if (this._editorView) {
- const { state } = this._editorView;
+ if (this.EditorView) {
+ const { state } = this.EditorView;
const { dataDoc } = this;
const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
const newJson = JSON.stringify(state.toJSON());
@@ -372,7 +377,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
// if we've deleted all the text in a note driven by a template, then restore the template data
dataDoc[this.fieldKey] = undefined;
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
+ this.EditorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
unchanged = false;
}
@@ -387,7 +392,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (jsonstring) {
const json = JSON.parse(jsonstring);
json.selection = state.toJSON().selection;
- this._editorView.updateState(EditorState.fromJSON(this.config, json));
+ this.EditorView.updateState(EditorState.fromJSON(this.config, json));
}
}
if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) {
@@ -407,8 +412,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
linkAnchor = anchor;
}
});
- if (this._editorView && linkTime) {
- const { state } = this._editorView;
+ if (this.EditorView && linkTime) {
+ const { state } = this.EditorView;
const node = state.selection.$from.node();
if (linkAnchor && node.type !== state.schema.nodes.code_block) {
const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000;
@@ -416,7 +421,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const { from } = state.selection;
const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] });
const replaced = state.tr.insert(from - 1, value);
- this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
+ this.EditorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
}
}
};
@@ -431,16 +436,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
(Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
link.link_relationship === LinkManager.AutoKeywords
); // prettier-ignore
- if (this._editorView?.state.doc.textContent) {
- let { tr } = this._editorView.state;
- const { from, to } = this._editorView.state.selection;
- const { autoLinkAnchor } = this._editorView.state.schema.marks;
+ if (this.EditorView?.state.doc.textContent) {
+ let { tr } = this.EditorView.state;
+ const { from, to } = this.EditorView.state.selection;
+ const { autoLinkAnchor } = this.EditorView.state.schema.marks;
tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor);
Doc.MyPublishedDocs.filter(term => term.title).forEach(term => {
tr = this.hyperlinkTerm(tr, term, newAutoLinks);
});
tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)));
- this._editorView?.dispatch(tr);
+ this.EditorView?.dispatch(tr);
}
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc));
};
@@ -450,11 +455,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (
!this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing
title.startsWith('-') &&
- this._editorView &&
+ this.EditorView &&
!this.dataDoc.title_custom &&
(Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text')
) {
- let node = this._editorView.state.doc;
+ let node = this.EditorView.state.doc;
while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild;
const str = node.textContent;
const prefix = '-';
@@ -477,7 +482,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
*/
hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => {
let tr = trIn;
- const editorView = this._editorView;
+ const editorView = this.EditorView;
if (editorView && !Doc.AreProtosEqual(target, this.Document)) {
const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, '');
let alink: Doc | undefined;
@@ -553,18 +558,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
unhighlightSearchTerms = () => {
- if (this._editorView) {
- const { state } = this._editorView;
+ if (this.EditorView) {
+ const { state } = this.EditorView;
if (state) {
const mark = state.schema.mark(state.schema.marks.search_highlight);
const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true });
const end = state.doc.nodeSize - 2;
- this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
+ this.EditorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
}
}
};
adoptAnnotation = (start: number, end: number, mark: Mark) => {
- const view = this._editorView!;
+ const view = this.EditorView!;
const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() });
view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
};
@@ -616,7 +621,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (added) {
draggedDoc._freeform_fitContentsToBox = true;
Doc.SetContainer(draggedDoc, this.Document);
- const view = this._editorView!;
+ const view = this.EditorView!;
try {
this._inDrop = true;
const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos;
@@ -810,7 +815,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement;
- const editor = this._editorView;
+ const editor = this.EditorView;
if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) {
const hrefs = (target.dataset?.targethrefs as string)
?.trim()
@@ -985,6 +990,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
},
icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
});
+ optionItems.push({
+ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
+ event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
+ icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
+ });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const help = cm.findByDescription('Help...');
const helpItems = help?.subitems ?? [];
@@ -996,8 +1006,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const c = this.ProseRef?.getElementsByTagName('img');
if (c) {
for (const i of c) {
- console.log(i);
-
// console.log(canvas.toDataURL());
// canvas.style.zIndex = '2000000';
// document.body.appendChild(canvas);
@@ -1022,8 +1030,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
animateRes = (resIndex: number, newText: string) => {
if (resIndex < newText.length) {
- const marks = this._editorView?.state.storedMarks ?? [];
- this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
+ const marks = this.EditorView?.state.storedMarks ?? [];
+ this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
}
};
@@ -1035,8 +1043,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
this.animateRes(0, 'Something went wrong.');
- } else if (this._editorView) {
- const { dispatch, state } = this._editorView;
+ } else if (this.EditorView) {
+ const { dispatch, state } = this.EditorView;
// for no animation, use: dispatch(state.tr.insertText(res));
// for animted response starting at end of text, use:
dispatch(state.tr.setSelection(Selection.atEnd(state.doc)));
@@ -1057,13 +1065,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
breakupDictation = () => {
- if (this._editorView && this._recordingDictation) {
+ if (this.EditorView && this._recordingDictation) {
this.stopDictation(/* true */);
this._break = true;
- const { state } = this._editorView;
+ const { state } = this.EditorView;
const { to } = state.selection;
const updated = TextSelection.create(state.doc, to, to);
- this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
+ this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
if (this._recordingDictation) {
this.recordDictation();
}
@@ -1082,7 +1090,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */);
setDictationContent = (value: string) => {
- if (this._editorView && this._recordingStart) {
+ if (this.EditorView && this._recordingStart) {
if (this._break) {
const textanchorFunc = () => {
const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
@@ -1095,22 +1103,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const textanchor = Cast(link.link_anchor_1, Doc, null);
if (audioanchor) {
audioanchor.backgroundColor = 'tan';
- const audiotag = this._editorView.state.schema.nodes.audiotag.create({
+ const audiotag = this.EditorView.state.schema.nodes.audiotag.create({
timeCode: NumCast(audioanchor._timecodeToShow),
audioId: audioanchor[Id],
textId: textanchor[Id],
});
textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode;
- const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
+ const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag);
const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
}
}
}
- const { from } = this._editorView.state.selection;
+ const { from } = this.EditorView.state.selection;
this._break = false;
- const tr = this._editorView.state.tr.insertText(value);
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
+ const tr = this.EditorView.state.tr.insertText(value);
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
}
};
@@ -1143,7 +1151,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
- this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
+ this.EditorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
anchor.text = selectedText;
anchor.text_html = this._selectionHTML ?? selectedText;
@@ -1156,7 +1164,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return anchorDoc ?? this.Document;
}
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
if (!this.SidebarShown) {
this.toggleSidebar(false);
@@ -1177,7 +1185,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
let hadStart = start !== 0;
frag.forEach((node, index) => {
const examinedNode = findAnchorNode(node, editor);
- if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
+ if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this.EditorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this.EditorView?.state.schema.nodes.audiotag)) {
nodes.push(examinedNode.node);
!hadStart && (start = index + examinedNode.start);
hadStart = true;
@@ -1186,13 +1194,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return { frag: Fragment.fromArray(nodes), start };
};
const findAnchorNode = (node: Node, editor: EditorView) => {
- if (node.type === this._editorView?.state.schema.nodes.audiotag) {
+ if (node.type === this.EditorView?.state.schema.nodes.audiotag) {
if (node.attrs.textId === textAnchorId) {
return { node, start: 0 };
}
return undefined;
}
- if (node.type === this._editorView?.state.schema.nodes.dashDoc) {
+ if (node.type === this.EditorView?.state.schema.nodes.dashDoc) {
if (node.attrs.docId === textAnchorId) {
return { node, start: 0 };
}
@@ -1208,9 +1216,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
- if (this._editorView && textAnchorId) {
- const { state } = this._editorView;
- const ret = findAnchorFrag(state.doc.content, this._editorView);
+ if (this.EditorView && textAnchorId) {
+ const { state } = this.EditorView;
+ const ret = findAnchorFrag(state.doc.content, this.EditorView);
const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined;
if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) {
@@ -1219,7 +1227,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (ret.frag.firstChild) {
selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
}
- this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ this.EditorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
setTimeout(() => {
@@ -1272,9 +1280,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
);
this._disposers.componentHeights = reaction(
// set the document height when one of the component heights changes and layout_autoHeight is on
- () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins, tagsHeight: this.tagsHeight }),
- ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight, tagsHeight }) => {
- const newHeight = this.contentScaling * (tagsHeight + marginsHeight + Math.max(sidebarHeight, textHeight));
+ () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
+ ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
+ const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
if (
(!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && //
layoutAutoHeight &&
@@ -1307,15 +1315,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
},
incomingValue => {
- if (this._editorView && this._applyingChange !== this.fieldKey) {
+ if (this.EditorView && this._applyingChange !== this.fieldKey) {
if (incomingValue?.data) {
const updatedState = JSON.parse(incomingValue.data.Data);
- if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) {
- this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+ if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
+ this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateScrollHeight();
}
- } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
- selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+ } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
+ selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
}
}
},
@@ -1333,9 +1341,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
action(selected => {
if (selected && this.dataDoc[this.fieldKey + '_placeholder']) {
setTimeout(() => {
- selectAll(this._editorView!.state, (tx: Transaction) => {
- this._editorView?.dispatch(tx);
- this._editorView!.focus();
+ selectAll(this.EditorView!.state, (tx: Transaction) => {
+ this.EditorView?.dispatch(tx);
+ this.EditorView!.focus();
});
});
}
@@ -1343,12 +1351,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (FormattedTextBox._globalHighlights.has('Bold Text')) {
this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
}
- if (RichTextMenu.Instance?.view === this._editorView && !selected) {
+ if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
}
- if (this._editorView && selected) {
- RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc);
- setTimeout(this.autoLink, 20);
+ if (selected) {
+ RichTextMenu.Instance?.updateMenu(this.EditorView, undefined, this._props, this.dataDoc);
+ this.EditorView && setTimeout(this.autoLink, 20);
}
}),
{ fireImmediately: true }
@@ -1389,7 +1397,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// DocCast(this.Document.image)._freeform_fitContentsToBox = true;
// Doc.SetContainer(DocCast(this.Document.image), this.Document);
- // const view = this._editorView!;
+ // const view = this.EditorView!;
// try {
// this._inDrop = true;
// const pos = view.posAtCoords({ left: 0, top: 0 })?.pos;
@@ -1436,7 +1444,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
addPdfReference = (pdfAnchorId: string) => {
- const view = this._editorView!;
+ const view = this.EditorView!;
if (pdfAnchorId) {
DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
if (pdfAnchor instanceof Doc) {
@@ -1487,7 +1495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]);
const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
if (this.ProseRef) {
- this._editorView?.destroy();
+ this.EditorView?.destroy();
this._editorView = new EditorView(this.ProseRef, {
state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
handleScrollToSelection: editorView => {
@@ -1519,14 +1527,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (!rtfField) {
const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
const startupText = Field.toString(dataDoc[fieldKey] as FieldType);
- const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign)) || 'left';
+ const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left';
if (textAlign !== 'left') {
selectAll(this._editorView.state, tr => {
- this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
+ this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
});
}
if (startupText) {
- this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText));
+ this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText));
}
this.tryUpdateDoc(true);
}
@@ -1539,56 +1547,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
Doc.SetSelectOnLoad(undefined);
FormattedTextBox.SelectOnLoadChar = '';
}
- if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+ if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
this._props.select(false);
if (selLoadChar) {
- const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
+ const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
+ const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
- const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1);
+ const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks);
+ const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
const tr = tr2.setStoredMarks(storedMarks);
- this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
+ this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
} else if (!FormattedTextBox.DontSelectInitialText) {
const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- selectAll(this._editorView.state, (tx: Transaction) => {
- this._editorView?.dispatch(tx.addStoredMark(mark));
+ selectAll(this.EditorView.state, (tx: Transaction) => {
+ this.EditorView?.dispatch(tx.addStoredMark(mark));
});
+ this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1))));
this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
} else {
- const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
+ const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
+ const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const { tr } = this._editorView.state;
- this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
+ const { tr } = this.EditorView.state;
+ this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
}
}
if (selectOnLoad) {
FormattedTextBox.DontSelectInitialText = false;
- this._editorView!.focus();
+ this.EditorView!.focus();
}
if (this._props.isContentActive()) this.prepareForTyping();
- if (this._editorView && FormattedTextBox.PasteOnLoad) {
+ if (this.EditorView && FormattedTextBox.PasteOnLoad) {
const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
FormattedTextBox.PasteOnLoad = undefined;
pdfAnchorId && this.addPdfReference(pdfAnchorId);
}
- if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
+ if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
}
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
prepareForTyping = () => {
- if (this._editorView) {
+ if (this.EditorView) {
const { text, paragraph } = schema.nodes;
- const selNode = this._editorView.state.selection.$anchor.node();
- if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
+ const selNode = this.EditorView.state.selection.$anchor.node();
+ if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
- this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
+ this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
}
}
};
@@ -1602,7 +1611,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
FormattedTextBox.LiveTextUndo?.end();
FormattedTextBox.LiveTextUndo = undefined;
this.unhighlightSearchTerms();
- this._editorView?.destroy();
+ this.EditorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none');
}
@@ -1681,26 +1690,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
setFocus = (ipos?: number) => {
- const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
- setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+ const pos = ipos ?? (this.EditorView?.state.selection.$from.pos || 1);
+ setTimeout(() => this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(TextSelection.near(this.EditorView.state.doc.resolve(pos)))), 100);
setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
};
@action
onFocused = (e: React.FocusEvent): void => {
- // applyDevTools.applyDevTools(this._editorView);
+ // applyDevTools.applyDevTools(this.EditorView);
e.stopPropagation();
};
onClick = (e: React.MouseEvent): void => {
if (!this._props.isContentActive()) return;
- const editorView = this._editorView;
+ const editorView = this.EditorView;
const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined;
if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) {
// this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
if (pcords && node?.type === editorView.state.schema.nodes.dashComment) {
- this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
+ this.EditorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
e.preventDefault();
}
if (!node && this.ProseRef) {
@@ -1726,33 +1735,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) {
this._forceUncollapse = false;
clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
- const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
+ const clickPos = this.EditorView!.posAtCoords({ left: x, top: y });
const clickPosVal = clickPos?.pos || 1;
let olistPos = clickPosVal;
if (clickPos && olistPos && this._props.rootSelected?.()) {
- const clickNode = this._editorView?.state.doc.resolve(olistPos).node();
- const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
- olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
- let $olistPos = this._editorView?.state.doc.resolve(olistPos);
- let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
- if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) {
+ const clickNode = this.EditorView?.state.doc.resolve(olistPos).node();
+ const nodeBef = this.EditorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
+ olistPos = nodeBef?.type === this.EditorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
+ let $olistPos = this.EditorView?.state.doc.resolve(olistPos);
+ let olistNode = (nodeBef !== null || clickNode?.type === this.EditorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
+ if (olistNode?.type === this.EditorView?.state.schema.nodes.list_item) {
if ($olistPos && $olistPos.depth) {
olistNode = $olistPos.parent;
- $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
+ $olistPos = this.EditorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
}
}
- const maxSize = this._editorView?.state.doc.content.size ?? 0;
- const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
+ const maxSize = this.EditorView?.state.doc.content.size ?? 0;
+ const listPos = this.EditorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
const listNode = listPos?.node();
- if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
+ if (olistNode && olistNode.type === this.EditorView?.state.schema.nodes.ordered_list && listNode) {
if (!highlightOnly) {
if (selectOrderedList) {
- this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
+ this.EditorView.dispatch(this.EditorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
} else {
const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1);
- if (this._editorView.state.doc.nodeAt(nodePos)) {
- const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
+ if (this.EditorView.state.doc.nodeAt(nodePos)) {
+ const tr = this.EditorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
}
}
}
@@ -1772,19 +1781,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
onBlur = (e: React.FocusEvent) => {
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
- const stordMarks = this._editorView?.state.storedMarks?.slice();
+ const stordMarks = this.EditorView?.state.storedMarks?.slice();
if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
this.autoLink();
- if (this._editorView?.state.tr) {
+ if (this.EditorView?.state.tr) {
const tr = stordMarks?.reduce((tr2, m) => {
tr2.addStoredMark(m);
return tr2;
- }, this._editorView.state.tr);
- tr && this._editorView.dispatch(tr);
+ }, this.EditorView.state.tr);
+ tr && this.EditorView.dispatch(tr);
}
}
}
- if (RichTextMenu.Instance?.view === this._editorView && !(this._props.isContentActive() || this._props.rootSelected?.())) {
+ if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
}
@@ -1831,7 +1840,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
switch (e.key) {
case 'Escape':
- this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ this.EditorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
(document.activeElement as HTMLElement).blur?.();
DocumentView.DeselectAll();
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
@@ -1857,7 +1866,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.startUndoTypingBatch();
};
ondrop = (e: React.DragEvent) => {
- this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
+ this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema));
e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
};
onScroll = (e: React.UIEvent) => {
@@ -1872,7 +1881,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
tryUpdateScrollHeight = () => {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
- if (children && !SnappingManager.IsDragging) {
+ if (this.EditorView && children && !SnappingManager.IsDragging) {
const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
const toNum = (val: string) => Number(val.replace('px', ''));
const toHgt = (node: Element): number => {
@@ -2093,7 +2102,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale);
const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale);
const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
- return styleFromLayout?.height === '0px' ? null : (
+ return this.isLabel ? (
+ <LabelBox {...this._props} />
+ ) : styleFromLayout?.height === '0px' ? null : (
<div
className="formattedTextBox"
ref={r => {
@@ -2116,6 +2127,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
fontSize: this.fontSize,
fontFamily: this.fontFamily,
fontWeight: this.fontWeight,
+ fontStyle: this.fontStyle,
+ textDecoration: this.fontDecoration,
...styleFromLayout,
}}>
<div
@@ -2156,8 +2169,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
color: StrCast(this.layoutDoc.text_fontColor),
- fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`,
- textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform,
+ fontWeight: this.layoutDoc.contentBold ? 'bold' : '',
+ textTransform: StrCast(this.dataDoc[this.fieldKey + '_transform']) as Property.TextTransform,
}}
/>
</div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 7a8b72be0..3c84e5a10 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -349,7 +349,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa
dispatch(tx4);
}
- if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
+ if (view.state.selection.$anchor.depth > 0 &&
+ view.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item &&
+ view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
// if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items.
enter(view.state, dispatch, view, false);
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 55e6a3a5b..c0acbe36f 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { lift, toggleMark, wrapIn } from 'prosemirror-commands';
import { Mark, MarkType } from 'prosemirror-model';
@@ -32,7 +32,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
private _linkToRef = React.createRef<HTMLInputElement>();
- layoutDoc: Doc | undefined;
+ dataDoc: Doc | undefined;
@observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined;
public editorProps: FieldViewProps | AntimodeMenuProps | undefined;
@@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private collapsed: boolean = false;
@observable private _noLinkActive: boolean = false;
@observable private _boldActive: boolean = false;
- @observable private _italicsActive: boolean = false;
+ @observable private _italicActive: boolean = false;
@observable private _underlineActive: boolean = false;
@observable private _strikethroughActive: boolean = false;
@observable private _subscriptActive: boolean = false;
@@ -49,6 +49,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _activeFontSize: string = '13px';
@observable private _activeFontFamily: string = '';
+ @observable private _activeFitBox: boolean = false;
@observable private _activeListType: string = '';
@observable private _activeAlignment: string = 'left';
@@ -64,13 +65,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private currentLink: string | undefined = '';
@observable private showLinkDropdown: boolean = false;
- _reaction: IReactionDisposer | undefined;
constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
runInAction(() => {
RichTextMenu._instance.menu = this;
- this.updateMenu(undefined, undefined, props, this.layoutDoc);
+ this.updateMenu(undefined, undefined, props, this.dataDoc);
this._canFade = false;
this.Pinned = true;
});
@@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get underline() {
return this._underlineActive;
}
- @computed get italics() {
- return this._italicsActive;
+ @computed get italic() {
+ return this._italicActive;
}
@computed get strikeThrough() {
return this._strikethroughActive;
@@ -101,6 +101,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get fontHighlight() {
return this._activeHighlightColor;
}
+ @computed get fitBox() {
+ return this._activeFitBox;
+ }
@computed get fontFamily() {
return this._activeFontFamily;
}
@@ -114,26 +117,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._activeAlignment;
}
@computed get textVcenter() {
- return BoolCast(this.layoutDoc?._layout_centered);
- }
- _disposer: IReactionDisposer | undefined;
- componentDidMount() {
- // this._disposer = reaction(
- // () => DocumentView.Selected().slice(),
- // () => this.updateMenu(undefined, undefined, undefined, undefined)
- // );
- }
- componentWillUnmount() {
- this._disposer?.();
+ return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered));
}
@action
- public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, layoutDoc: Doc | undefined) {
+ public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, dataDoc: Doc | undefined) {
if (this._linkToRef.current?.getBoundingClientRect().width) {
return;
}
this.view = view;
- this.layoutDoc = layoutDoc;
+ this.dataDoc = dataDoc;
props && (this.editorProps = props);
// Don't do anything if the document/selection didn't change
@@ -147,14 +140,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const { activeSizes } = active;
const { activeColors } = active;
const { activeHighlights } = active;
- const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc();
+ const refDoc = DocumentView.Selected().lastElement()?.dataDoc ?? Doc.UserDoc();
const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey);
const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt));
this._activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
- this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
- this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0];
+ this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox));
+ this._activeFontFamily = !activeFamilies.length
+ ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial')))
+ : activeFamilies.length === 1
+ ? String(activeFamilies[0])
+ : 'various';
+ this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0];
this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
@@ -181,7 +179,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
toggleMark(mark.type, mark.attrs)(state, dispatch);
}
}
- // this.updateMenu(this.view, undefined, undefined, this.layoutDoc);
}
};
@@ -195,8 +192,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return node.attrs.align || 'left';
}
}
+ } else if (this.dataDoc) {
+ return StrCast(this.dataDoc.text_align) || 'left';
}
- return 'left';
+ return StrCast(Doc.UserDoc().textAlign) || 'left';
};
// finds font sizes and families in selection
@@ -285,7 +284,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._noLinkActive = false;
this._boldActive = false;
- this._italicsActive = false;
+ this._italicActive = false;
this._underlineActive = false;
this._strikethroughActive = false;
this._subscriptActive = false;
@@ -295,7 +294,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
switch (mark.name) {
case 'noAutoLinkAnchor': this._noLinkActive = true; break;
case 'strong': this._boldActive = true; break;
- case 'em': this._italicsActive = true; break;
+ case 'em': this._italicActive = true; break;
case 'underline': this._underlineActive = true; break;
case 'strikethrough': this._strikethroughActive = true; break;
case 'subscript': this._subscriptActive = true; break;
@@ -330,6 +329,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view.focus();
}
};
+ toggleFitBox = () => {
+ if (this.dataDoc) {
+ const doc = this.dataDoc;
+ (document.activeElement as HTMLElement)?.blur();
+ doc.text_fitBox = !doc.text_fitBox;
+ } else {
+ Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox;
+ Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined;
+ }
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ };
toggleBold = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong);
@@ -346,7 +356,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- toggleItalics = () => {
+ toggleItalic = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
this.setMark(mark, this.view.state, this.view.dispatch, false);
@@ -354,8 +364,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
- if (this.TextView && this.view) {
+ setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
+ if (this.TextView && this.view && fontField !== 'fitBox') {
const { text, paragraph } = this.view.state.schema.nodes;
const selNode = this.view.state.selection.$anchor.node();
if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
@@ -367,9 +377,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
+ } else if (this.dataDoc) {
+ this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
} else {
Doc.UserDoc()[fontField] = value;
- // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
}
};
@@ -395,7 +408,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view!.dispatch(tx3);
});
this.view.focus();
- // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) {
@@ -410,10 +422,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
vcenterToggle = () => {
- this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
+ if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered;
+ else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered;
};
- align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => {
- if (this.RootSelected) {
+ align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => {
+ if (view && dispatch && this.RootSelected) {
let { tr } = view.state;
view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => {
if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
@@ -425,6 +438,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
view.focus();
dispatch?.(tr);
+ } else {
+ if (this.dataDoc) {
+ this.dataDoc.text_align = alignment;
+ } else Doc.UserDoc().textAlign = alignment;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
}
};
@@ -702,7 +720,7 @@ interface RichTextMenuPluginProps {
}
export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
- RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
+ RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.dataDoc);
}
render() {
return null;
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index f58434906..c332c592b 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -121,7 +121,7 @@ export class RichTextRules {
annotationOn: textDoc,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _text_fontSize: '9px',
+ text_fontSize: '9px',
title: 'inline comment',
});
textDocInline.title = inlineFieldKey; // give the annotation its own title
@@ -390,7 +390,7 @@ export class RichTextRules {
// %eq
new InputRule(/%eq/, (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
- this.TextBox.dataDoc[fieldKey] = 'y=';
+ this.TextBox.dataDoc[fieldKey] = '';
const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
}),
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
deleted file mode 100644
index 1e7801056..000000000
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export interface CursorData {
- x: number;
- y: number;
- width: number;
-}
-
-export interface Point {
- x: number;
- y: number;
-}
-
-export enum BrushMode {
- ADD,
- SUBTRACT,
-}
-
-export interface ImageDimensions {
- width: number;
- height: number;
-}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/imageEditor/GenerativeFillButtons.scss
index 0180ef904..0180ef904 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
+++ b/src/client/views/nodes/imageEditor/GenerativeFillButtons.scss
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/imageEditor/GenerativeFillButtons.tsx
index fe22b273d..fe9c39aad 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
+++ b/src/client/views/nodes/imageEditor/GenerativeFillButtons.tsx
@@ -1,9 +1,9 @@
import './GenerativeFillButtons.scss';
import * as React from 'react';
import ReactLoading from 'react-loading';
-import { Button, IconButton, Type } from 'browndash-components';
+import { Button, IconButton, Type } from '@dash/components';
import { AiOutlineInfo } from 'react-icons/ai';
-import { activeColor } from './generativeFillUtils/generativeFillConstants';
+import { activeColor } from './imageEditorUtils/imageEditorConstants';
interface ButtonContainerProps {
onClick: () => Promise<void>;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index c2669a950..c691e6a18 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -2,7 +2,7 @@ $navHeight: 5rem;
$canvasSize: 1024px;
$scale: 0.5;
-.generativeFillContainer {
+.imageEditorContainer {
position: absolute;
top: 0;
left: 0;
@@ -13,7 +13,7 @@ $scale: 0.5;
flex-direction: column;
overflow: hidden;
- .generativeFillControls {
+ .imageEditorTopBar {
flex-shrink: 0;
height: $navHeight;
color: #000000;
@@ -27,6 +27,12 @@ $scale: 0.5;
border-bottom: 1px solid #c7cdd0;
padding: 0 2rem;
+ .imageEditorControls {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ }
+
h1 {
font-size: 1.5rem;
}
@@ -69,13 +75,48 @@ $scale: 0.5;
}
}
- .iconContainer {
+ .sideControlsContainer {
+ width: 160px;
position: absolute;
- top: 2rem;
- left: 2rem;
- display: flex;
- flex-direction: column;
- gap: 2rem;
+ left: 0;
+ height: 100%;
+
+ .sideControls {
+ position: absolute;
+ width: 120px;
+ top: 3rem;
+ left: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ .imageToolsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .cutToolsContainer {
+ display: grid;
+ gap: 5px;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .undoRedoContainer {
+ justify-content: center;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .sliderContainer {
+ margin: 3rem 0;
+ height: 225px;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ cursor: pointer;
+ }
+ }
}
.editsBox {
@@ -86,7 +127,18 @@ $scale: 0.5;
flex-direction: column;
gap: 1rem;
+ .originalImageLabel {
+ position: absolute;
+ bottom: 10;
+ left: 10;
+ color: #ffffff;
+ font-size: 0.8rem;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ }
+
img {
+ cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
opacity: 0.8;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 261eb4bb4..6b1d05031 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -1,10 +1,6 @@
-/* eslint-disable jsx-a11y/label-has-associated-control */
-/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
-/* eslint-disable jsx-a11y/img-redundant-alt */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
-/* eslint-disable react/function-component-definition */
+/* eslint-disable no-use-before-define */
import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material';
-import { IconButton } from 'browndash-components';
+import { Button, IconButton, Type } from '@dash/components';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { CgClose } from 'react-icons/cg';
@@ -20,17 +16,16 @@ import { CollectionDockingView } from '../../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { ImageEditorData } from '../ImageBox';
import { OpenWhereMod } from '../OpenWhere';
-import './GenerativeFill.scss';
-import { EditButtons, CutButtons } from './GenerativeFillButtons';
-import { BrushHandler, BrushType } from './generativeFillUtils/BrushHandler';
-import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
-import { PointerHandler } from './generativeFillUtils/PointerHandler';
-import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
-import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
+import './ImageEditor.scss';
+import { ApplyFuncButtons, ImageToolButton } from './ImageEditorButtons';
+import { BrushHandler } from './imageEditorUtils/BrushHandler';
+import { APISuccess, ImageUtility } from './imageEditorUtils/ImageHandler';
+import { PointerHandler } from './imageEditorUtils/PointerHandler';
+import { activeColor, bgColor, brushWidthOffset, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants';
+import { CutMode, CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces';
import { DocumentView } from '../DocumentView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { ImageField } from '../../../../fields/URLField';
-import { resolve } from 'url';
+import { DocData } from '../../../../fields/DocSymbols';
+import { SettingsManager } from '../../../util/SettingsManager';
interface GenerativeFillProps {
imageEditorOpen: boolean;
@@ -41,7 +36,14 @@ interface GenerativeFillProps {
// Added field on image doc: gen_fill_children: List of children Docs
-const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+/**
+ * The image editor interface can be accessed by opening a document's context menu, then going to Options --> Open Image Editor.
+ * The image editor supports various operations on images. Currently, there is a Generative Fill feature that allows users to erase
+ * part of an image, add an optional prompt, and send this to GPT. GPT then returns a newly generated image that replaces the erased
+ * portion based on the optional prompt. There is also an image cutting tool that allows users to cut images in different ways to
+ * reshape the images, take out portions of images, and overall use them more creatively (see the header comment for cutImage() for more information).
+ */
+const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
const drawingAreaRef = useRef<HTMLDivElement>(null);
@@ -55,13 +57,14 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
// format: array of [image source, corresponding image Doc]
const [edits, setEdits] = useState<{ url: string; saveRes: Doc | undefined }[]>([]);
const [edited, setEdited] = useState(false);
- // const [brushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [isFirstDoc, setIsFirstDoc] = useState<boolean>(true);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [canvasDims, setCanvasDims] = useState<ImageDimensions>({
width: canvasSize,
height: canvasSize,
});
+ const [cutType, setCutType] = useState<CutMode>(CutMode.IN);
// whether to create a new collection or not
const [isNewCollection, setIsNewCollection] = useState(true);
// the current image in the main canvas
@@ -82,6 +85,24 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
// constants for image cutting
const cutPts = useRef<Point[]>([]);
+ /**
+ *
+ * @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;
+ }
+ };
// Undo and Redo
const handleUndo = () => {
const ctx = ImageUtility.getCanvasContext(canvasRef);
@@ -121,6 +142,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
ctx.clearRect(0, 0, canvasSize, canvasSize);
undoStack.current = [];
redoStack.current = [];
+ cutPts.current.length = 0;
ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
};
@@ -161,7 +183,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
x: currPoint.x - e.movementX / canvasScale,
y: currPoint.y - e.movementY / canvasScale,
};
- const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, BrushType.CUT);
+ const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor);
cutPts.current.push(...pts);
};
@@ -261,7 +283,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
}));
};
- // Get AI Edit
+ // Get AI Edit for Generative Fill
const getEdit = async () => {
const img = currImg.current;
if (!img) return;
@@ -282,32 +304,13 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
// create first image
if (!newCollectionRef.current) {
- if (!isNewCollection && imageRootDoc) {
- // if the parent hasn't been set yet
- if (!parentDoc.current) parentDoc.current = imageRootDoc;
- } else {
- if (!(originalImg.current && imageRootDoc)) return;
- // create new collection and add it to the view
- newCollectionRef.current = Docs.Create.FreeformDocument([], {
- x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
- y: NumCast(imageRootDoc.y),
- _width: newCollectionSize,
- _height: newCollectionSize,
- title: 'Image edit collection',
- });
- DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
-
- // opening new tab
- CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
-
- // add the doc to the main freeform
- // eslint-disable-next-line no-use-before-define
- await createNewImgDoc(originalImg.current, true);
- }
+ createNewCollection();
} else {
childrenDocs.current = [];
}
-
+ if (!(originalImg.current && imageRootDoc)) return;
+ // add the doc to the main freeform
+ await createNewImgDoc(originalImg.current, true);
originalImg.current = currImg.current;
originalDoc.current = parentDoc.current;
const { urls } = res as APISuccess;
@@ -315,14 +318,12 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height)));
const imgRes = await Promise.all(
imgUrls.map(async url => {
- // eslint-disable-next-line no-use-before-define
const saveRes = await onSave(url);
return { url, saveRes };
})
);
setEdits(imgRes);
const image = new Image();
- // eslint-disable-next-line prefer-destructuring
image.src = imgUrls[0];
ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height);
currImg.current = image;
@@ -334,66 +335,137 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
setLoading(false);
};
- const cutImage = async () => {
+ /**
+ * This function performs image cutting based on the inputted BrushMode. There are currently four ways to cut images:
+ * 1. By outlining the area that should be kept (BrushMode.IN)
+ * 2. By outlining the area that should be removed (BrushMode.OUT)
+ * 3. By drawing in the area that should be kept (where the image is brushed, the image will remain and everything else will be removed) (BrushMode.DRAW_IN)
+ * 4. By drawing the area that she be removed, so this operates as an eraser (BrushMode.ERASE)
+ * @param currCutType BrushMode enum that determines what kind of cutting operation to perform
+ * @param firstDoc boolean for whether it's the first edited image. This is for positioning of the edited images when they render on the canvas.
+ */
+ const cutImage = async (currCutType: CutMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], firstDoc: boolean) => {
const img = currImg.current;
const canvas = canvasRef.current;
if (!canvas || !img) return;
- canvas.width = img.naturalWidth;
- canvas.height = img.naturalHeight;
const ctx = ImageUtility.getCanvasContext(canvasRef);
if (!ctx) return;
- ctx.globalCompositeOperation = 'source-over';
- setLoading(true);
- setEdited(true);
// get the original image
const canvasOriginalImg = ImageUtility.getCanvasImg(img);
if (!canvasOriginalImg) return;
- // draw the image onto the canvas
- ctx.drawImage(img, 0, 0);
- // get the mask which i assume is the thing the user draws on
- // const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg);
- // if (!canvasMask) return;
- // canvasMask.width = canvas.width;
- // canvasMask.height = canvas.height;
- // now put the user's path around the mask
- if (cutPts.current.length) {
+ setLoading(true);
+ const currPts = [...cutPts.current];
+ if (currCutType !== CutMode.ERASE) handleReset(); // gets rid of the visible brush strokes (mostly needed for line_in) unless it's erasing (which depends on the brush strokes)
+ let minX = img.width;
+ let maxX = 0;
+ let minY = img.height;
+ let maxY = 0;
+ // currPts is populated by the brush strokes' points, so this code is drawing a path along the points
+ if (currPts.length) {
ctx.beginPath();
- ctx.moveTo(cutPts.current[0].x, cutPts.current[0].y); // later check edge case where cutPts is empty
- for (let i = 0; i < cutPts.current.length; i++) {
- ctx.lineTo(cutPts.current[i].x, cutPts.current[i].y);
+ ctx.moveTo(currPts[0].x, currPts[0].y);
+ for (let i = 0; i < currPts.length; i++) {
+ ctx.lineTo(currPts[i].x, currPts[i].y);
+ minX = Math.min(currPts[i].x, minX);
+ minY = Math.min(currPts[i].y, minY);
+ maxX = Math.max(currPts[i].x, maxX);
+ maxY = Math.max(currPts[i].y, maxY);
+ }
+ switch (
+ currCutType // use different canvas operations depending on the type of cutting we're applying
+ ) {
+ case CutMode.IN:
+ ctx.closePath();
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.fill();
+ break;
+ case CutMode.OUT:
+ ctx.closePath();
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.fill();
+ break;
+ case CutMode.DRAW_IN:
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.lineWidth = brushWidth + brushWidthOffset; // added offset because width gets cut off a little bit
+ ctx.stroke();
+ break;
}
- ctx.closePath();
- ctx.stroke();
- ctx.fill();
- // ctx.clip();
}
- const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl
+
+ const url = canvas.toDataURL();
if (!newCollectionRef.current) {
- if (!isNewCollection && imageRootDoc) {
- // if the parent hasn't been set yet
- if (!parentDoc.current) parentDoc.current = imageRootDoc;
- } else {
- if (!(originalImg.current && imageRootDoc)) return;
- // create new collection and add it to the view
- newCollectionRef.current = Docs.Create.FreeformDocument([], {
- x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
- y: NumCast(imageRootDoc.y),
- _width: newCollectionSize,
- _height: newCollectionSize,
- title: 'Image edit collection',
- });
- DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
- // opening new tab
- CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
- }
+ createNewCollection();
}
+
const image = new Image();
image.src = url;
- await createNewImgDoc(image, true);
- // add the doc to the main freeform
- // eslint-disable-next-line no-use-before-define
- setLoading(false);
- cutPts.current.length = 0;
+ image.onload = async () => {
+ let finalImg: HTMLImageElement | undefined = image;
+ let finalImgURL: string = url;
+ // crop the image for these brush modes to remove excess blank space around the image contents
+ if (currCutType == CutMode.IN || currCutType == CutMode.DRAW_IN) {
+ const croppedData = cropImage(image, Math.max(minX, 0), Math.min(maxX, image.width), Math.max(minY, 0), Math.min(maxY, image.height));
+ finalImg = croppedData;
+ finalImgURL = croppedData.src;
+ }
+ currImg.current = finalImg;
+ const newImgDoc = await createNewImgDoc(finalImg, firstDoc);
+ if (newImgDoc) {
+ // set the image to transparent to remove the background / brushstrokes
+ const docData = newImgDoc[DocData];
+ docData.backgroundColor = 'transparent';
+ docData.disableMixBlend = true;
+ if (firstDoc) setIsFirstDoc(false);
+ setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
+ }
+ setLoading(false);
+ cutPts.current.length = 0;
+ };
+ };
+
+ /**
+ * Creates a new collection to put the image edits on. Adds to a new tab on the right if "Create New Collection" is checked.
+ * @returns
+ */
+ const createNewCollection = () => {
+ if (!isNewCollection && imageRootDoc) {
+ // if the parent hasn't been set yet
+ if (!parentDoc.current) parentDoc.current = imageRootDoc;
+ } else {
+ if (!(originalImg.current && imageRootDoc)) return;
+ // create new collection and add it to the view
+ newCollectionRef.current = Docs.Create.FreeformDocument([], {
+ x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+ y: NumCast(imageRootDoc.y),
+ _width: newCollectionSize,
+ _height: newCollectionSize,
+ title: 'Image edit collection',
+ });
+ DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
+ // opening new tab
+ CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+ }
+ };
+
+ /**
+ * This function crops an image based on the inputted dimensions. This is used to automatically adjust the images that are
+ * edited to be smaller than the original (i.e. for cutting into a small part of the image)
+ */
+ const cropImage = (image: HTMLImageElement, minX: number, maxX: number, minY: number, maxY: number) => {
+ const croppedCanvas = document.createElement('canvas');
+ const croppedCtx = croppedCanvas.getContext('2d');
+ if (!croppedCtx) return image;
+ const cropWidth = Math.abs(maxX - minX);
+ const cropHeight = Math.abs(maxY - minY);
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ croppedCtx.globalCompositeOperation = 'source-over';
+ croppedCtx.clearRect(0, 0, cropWidth, cropHeight);
+ croppedCtx.drawImage(image, minX, minY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
+ const croppedURL = croppedCanvas.toDataURL();
+ const croppedImage = new Image();
+ croppedImage.src = croppedURL;
+ return croppedImage;
};
// adjusts all the img positions to be aligned
@@ -416,7 +488,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
};
// creates a new image document and returns its reference
- const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => {
+ const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean /*, parent?: Doc */): Promise<Doc | undefined> => {
if (!imageRootDoc) return undefined;
const { src } = img;
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
@@ -479,8 +551,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
img.src = src;
if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
try {
- const res = await createNewImgDoc(img, false);
- return res;
+ return await createNewImgDoc(img, false);
} catch (err) {
console.log(err);
}
@@ -495,176 +566,185 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
}
setEdits([]);
+ setIsFirstDoc(true);
};
- return (
- <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
- <div className="generativeFillControls">
+ // 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 imageEditTools: ImageEditTool[] = [genFillTool, cutTool];
+ const [currTool, setCurrTool] = useState<ImageEditTool>(genFillTool);
+
+ // the top controls for making a new collection, resetting, and applying edits,
+ function renderControls() {
+ return (
+ <div className="imageEditorTopBar">
<h1>Image Editor</h1>
{/* <IconButton text="Cut out" icon={<FontAwesomeIcon icon="scissors" />} /> */}
- <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
+ <div className="imageEditorControls">
<FormControlLabel
control={
<Checkbox
// disable once edited has been clicked (doesn't make sense to change after first edit)
disabled={edited}
checked={isNewCollection}
- onChange={() => {
- setIsNewCollection(prev => !prev);
- }}
+ onChange={() => setIsNewCollection(prev => !prev)}
/>
}
label="Create New Collection"
labelPlacement="end"
sx={{ whiteSpace: 'nowrap' }}
/>
- <EditButtons onClick={getEdit} loading={loading} onReset={handleReset} />
- <CutButtons onClick={cutImage} loading={loading} onReset={handleReset} />
+ <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>
- {/* Main canvas for editing */}
- <div
- className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
- ref={drawingAreaRef}
- onPointerOver={updateCursorData}
- onPointerMove={updateCursorData}
- onPointerDown={handlePointerDown}
- onPointerUp={handlePointerUp}>
- <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
- <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
- <div
- className="pointer"
- style={{
- left: cursorData.x,
- top: cursorData.y,
- width: cursorData.width,
- height: cursorData.width,
- }}>
- <div className="innerPointer" />
- </div>
- {/* Icons */}
- <div className="iconContainer">
+ );
+ }
+
+ // the side icons including tool type, the slider, and undo/redo
+ function renderSideIcons() {
+ 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="cutToolsContainer">
+ <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
+ <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
+ <Button style={{ width: '100%' }} text="Draw in" type={Type.TERT} color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.DRAW_IN)} />
+ <Button style={{ width: '100%' }} text="Erase" type={Type.TERT} color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.ERASE)} />
+ </div>
+ )}
+ <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
+ {currTool.type === ImageToolType.GenerativeFill && (
+ <Slider
+ sx={{
+ '& input[type="range"]': {
+ WebkitAppearance: 'slider-vertical',
+ },
+ }}
+ orientation="vertical"
+ min={genFillTool.sliderMin}
+ max={genFillTool.sliderMax}
+ defaultValue={genFillTool.sliderDefault}
+ size="small"
+ valueLabelDisplay="auto"
+ onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
+ />
+ )}
+ {currTool.type === ImageToolType.Cut && (
+ <Slider
+ sx={{
+ '& input[type="range"]': {
+ WebkitAppearance: 'slider-vertical',
+ },
+ }}
+ orientation="vertical"
+ min={cutTool.sliderMin}
+ max={cutTool.sliderMax}
+ defaultValue={cutTool.sliderDefault}
+ size="small"
+ valueLabelDisplay="auto"
+ onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
+ />
+ )}
+ </div>
{/* Undo and Redo */}
- <IconButton
- style={{ cursor: 'pointer' }}
- onPointerDown={e => {
- e.stopPropagation();
- handleUndo();
- }}
- onPointerUp={e => {
- e.stopPropagation();
- }}
- color={activeColor}
- tooltip="Undo"
- icon={<IoMdUndo />}
- />
- <IconButton
- style={{ cursor: 'pointer' }}
- onPointerDown={e => {
- e.stopPropagation();
- handleRedo();
- }}
- onPointerUp={e => {
- e.stopPropagation();
- }}
- color={activeColor}
- tooltip="Redo"
- icon={<IoMdRedo />}
- />
- <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}>
- <Slider
- sx={{
- '& input[type="range"]': {
- WebkitAppearance: 'slider-vertical',
- },
- }}
- orientation="vertical"
- min={25}
- max={500}
- defaultValue={150}
- size="small"
- valueLabelDisplay="auto"
- onChange={(e: any, val: any) => {
- setCursorData(prev => ({ ...prev, width: val as number }));
+ <div className="undoRedoContainer">
+ <IconButton
+ style={{ cursor: 'pointer' }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleUndo();
}}
+ onPointerUp={e => e.stopPropagation()}
+ color={activeColor}
+ tooltip="Undo"
+ icon={<IoMdUndo />}
/>
- </div>
- <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}>
- <Slider
- sx={{
- '& input[type="range"]': {
- WebkitAppearance: 'slider-vertical',
- },
- }}
- orientation="vertical"
- min={1}
- max={500}
- defaultValue={150}
- size="small"
- valueLabelDisplay="auto"
- onChange={(e: any, val: any) => {
- setCursorData(prev => ({ ...prev, width: val as number }));
+ <IconButton
+ style={{ cursor: 'pointer' }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleRedo();
}}
+ onPointerUp={e => e.stopPropagation()}
+ color={activeColor}
+ tooltip="Redo"
+ icon={<IoMdRedo />}
/>
</div>
</div>
- {/* Edits thumbnails */}
- <div className="editsBox">
- {edits.map((edit, i) => (
+ </div>
+ );
+ }
+
+ // circular pointer for drawing/erasing
+ function renderPointer() {
+ return (
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer" />
+ </div>
+ );
+ }
+
+ // the previews for each edit
+ function renderEditThumbnails() {
+ return (
+ <div className="editsBox">
+ {edits.map(edit => (
+ <img
+ key={edit.url}
+ alt="image edits"
+ width={75}
+ src={edit.url}
+ onClick={async () => {
+ const img = new Image();
+ img.src = edit.url;
+ ImageUtility.drawImgToCanvas(img, canvasRef, img.width, img.height);
+ currImg.current = img;
+ parentDoc.current = edit.saveRes ?? null;
+ }}
+ />
+ ))}
+ {/* Original img thumbnail */}
+ {edits.length > 0 && (
+ <div style={{ position: 'relative' }}>
+ <label className="originalImageLabel">Original</label>
<img
- // eslint-disable-next-line react/no-array-index-key
- key={i}
- alt="image edits"
+ alt="image stuff"
width={75}
- src={edit.url}
- style={{ cursor: 'pointer' }}
- onClick={async () => {
+ src={originalImg.current?.src}
+ onClick={() => {
+ if (!originalImg.current) return;
const img = new Image();
- img.src = edit.url;
+ img.src = originalImg.current.src;
ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
currImg.current = img;
- parentDoc.current = edit.saveRes ?? null;
+ if (!parentDoc.current) parentDoc.current = originalDoc.current;
}}
/>
- ))}
- {/* Original img thumbnail */}
- {edits.length > 0 && (
- <div style={{ position: 'relative' }}>
- <label
- style={{
- position: 'absolute',
- bottom: 10,
- left: 10,
- color: '#ffffff',
- fontSize: '0.8rem',
- letterSpacing: '1px',
- textTransform: 'uppercase',
- }}>
- Original
- </label>
- <img
- alt="image stuff"
- width={75}
- src={originalImg.current?.src}
- style={{ cursor: 'pointer' }}
- onClick={() => {
- if (!originalImg.current) return;
- const img = new Image();
- img.src = originalImg.current.src;
- ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
- currImg.current = img;
- parentDoc.current = originalDoc.current;
- }}
- />
- </div>
- )}
- </div>
+ </div>
+ )}
</div>
+ );
+ }
+
+ // the prompt box for generative fill
+ function renderPromptBox() {
+ return (
<div>
<TextField
value={input}
- onChange={(e: any) => setInput(e.target.value)}
+ onChange={e => setInput(e.target.value)}
disabled={isBrushing}
type="text"
label="Prompt"
@@ -680,8 +760,29 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
}}
/>
</div>
+ );
+ }
+
+ return (
+ <div className="imageEditorContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
+ {renderControls()}
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
+ {renderPointer()}
+ {renderSideIcons()}
+ {renderEditThumbnails()}
+ </div>
+ {currTool.type === ImageToolType.GenerativeFill && renderPromptBox()}
</div>
);
};
-export default GenerativeFill;
+export default ImageEditor;
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
new file mode 100644
index 000000000..985dc914f
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -0,0 +1,69 @@
+import './GenerativeFillButtons.scss';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { Button, IconButton, Type } from '@dash/components';
+import { AiOutlineInfo } from 'react-icons/ai';
+import { bgColor } from './imageEditorUtils/imageEditorConstants';
+import { ImageEditTool, ImageToolType } from './imageEditorUtils/imageEditorInterfaces';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SettingsManager } from '../../../util/SettingsManager';
+
+interface ButtonContainerProps {
+ onClick: () => Promise<void>;
+ loading: boolean;
+ onReset: () => void;
+ btnText: string;
+}
+
+export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }: ButtonContainerProps) {
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} />
+ {loading ? (
+ <Button
+ text={btnText}
+ type={Type.TERT}
+ color={SettingsManager.userVariantColor}
+ icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ if (!loading) getEdit();
+ }}
+ />
+ ) : (
+ <Button
+ text={btnText}
+ type={Type.TERT}
+ color={SettingsManager.userVariantColor}
+ onClick={() => {
+ if (!loading) getEdit();
+ }}
+ />
+ )}
+ <IconButton
+ type={Type.SEC}
+ color={SettingsManager.userVariantColor}
+ tooltip="Open Documentation"
+ icon={<AiOutlineInfo size="16px" />}
+ onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')}
+ />
+ </div>
+ );
+}
+
+export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) {
+ return (
+ <div key={tool.name} className="imageEditorButtonContainer">
+ <Button
+ style={{ width: '100%' }}
+ text={tool.name}
+ type={Type.TERT}
+ color={isActive ? SettingsManager.userVariantColor : bgColor}
+ icon={<FontAwesomeIcon icon={tool.icon} />}
+ onClick={() => {
+ selectTool(tool.type);
+ }}
+ />
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts
new file mode 100644
index 000000000..ed39375e0
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts
@@ -0,0 +1,29 @@
+import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers';
+import { eraserColor } from './imageEditorConstants';
+import { Point } from './imageEditorInterfaces';
+
+export class BrushHandler {
+ static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => {
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.fillStyle = fillColor;
+ ctx.shadowColor = eraserColor;
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint);
+ const pts: Point[] = [];
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ const x = startPoint.x * (1 - s) + endPoint.x * s;
+ const y = startPoint.y * (1 - s) + endPoint.y * s;
+ pts.push({ x: startPoint.x, y: startPoint.y });
+ BrushHandler.brushCircleOverlay(x, y, brushRadius, ctx, fillColor);
+ }
+ return pts;
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/GenerativeFillMathHelpers.ts
index 6da8c3da0..f820300f3 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/GenerativeFillMathHelpers.ts
@@ -1,4 +1,4 @@
-import { Point } from './generativeFillInterfaces';
+import { Point } from './imageEditorInterfaces';
export class GenerativeFillMathHelpers {
static distanceBetween = (p1: Point, p2: Point) => Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
new file mode 100644
index 000000000..ece0f4d7f
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
@@ -0,0 +1,312 @@
+import { RefObject } from 'react';
+import { bgColor, canvasSize } from './imageEditorConstants';
+
+export interface APISuccess {
+ status: 'success';
+ urls: string[];
+}
+
+export interface APIError {
+ status: 'error';
+ message: string;
+}
+
+export class ImageUtility {
+ /**
+ *
+ * @param canvas Canvas to convert
+ * @returns Blob of canvas
+ */
+ static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> =>
+ new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ }
+ }, 'image/png');
+ });
+
+ // given a square api image, get the cropped img
+ static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => {
+ // Create a new canvas element
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext('2d');
+ if (ctx) {
+ // Clear the canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ if (width < height) {
+ // horizontal padding, x offset
+ const xOffset = (canvasSize - width) / 2;
+ ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
+ } else {
+ // vertical padding, y offset
+ const yOffset = (canvasSize - height) / 2;
+ ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
+ }
+ return canvas;
+ }
+ return undefined;
+ };
+
+ // converts an image to a canvas data url
+ static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> =>
+ new Promise<string>((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ const canvas = this.getCroppedImg(img, width, height);
+ if (canvas) {
+ const dataUrl = canvas.toDataURL();
+ resolve(dataUrl);
+ }
+ };
+ img.onerror = error => {
+ reject(error);
+ };
+ img.src = imageSrc;
+ });
+
+ // calls the openai api to get image edits
+ static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
+ const apiUrl = 'https://api.openai.com/v1/images/edits';
+ const fd = new FormData();
+ fd.append('image', imgBlob, 'image.png');
+ fd.append('mask', maskBlob, 'mask.png');
+ fd.append('prompt', prompt);
+ fd.append('size', '1024x1024');
+ fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('response_format', 'b64_json');
+
+ try {
+ const res = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
+ },
+ body: fd,
+ });
+ const data = await res.json();
+ console.log(data.data);
+ return {
+ status: 'success',
+ urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`),
+ };
+ } catch (err) {
+ console.log(err);
+ return { status: 'error', message: 'API error.' };
+ }
+ };
+
+ // mock api call
+ static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => ({
+ status: 'success',
+ urls: [mockSrc, mockSrc, mockSrc],
+ });
+
+ // Gets the canvas rendering context of a canvas
+ static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
+ if (!canvasRef.current) return null;
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) return null;
+ return ctx;
+ };
+
+ // Helper for downloading the canvas (for debugging)
+ static downloadCanvas = (canvas: HTMLCanvasElement) => {
+ const url = canvas.toDataURL();
+ const downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.download = 'canvas';
+
+ downloadLink.click();
+ downloadLink.remove();
+ };
+
+ // Download the canvas (for debugging)
+ static downloadImageCanvas = (imgUrl: string) => {
+ const img = new Image();
+ img.src = imgUrl;
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ this.downloadCanvas(canvas);
+ };
+ };
+
+ // Clears the canvas
+ static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx || !canvasRef.current) return;
+ ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
+ };
+
+ // Draws the image to the current canvas
+ static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => {
+ const drawImg = (htmlImg: HTMLImageElement) => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.clearRect(0, 0, canvasRef.current?.width || width, canvasRef.current?.height || height);
+ ctx.drawImage(htmlImg, 0, 0, width, height);
+ };
+
+ if (img.complete) {
+ drawImg(img);
+ } else {
+ img.onload = () => {
+ drawImg(img);
+ };
+ }
+ };
+
+ // Gets the image mask for the openai endpoint
+ static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return undefined;
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(paddedCanvas, 0, 0);
+
+ // extract and set padding data
+ if (srcCanvas.height > srcCanvas.width) {
+ // horizontal padding, x offset
+ const xOffset = (canvasSize - srcCanvas.width) / 2;
+ ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height);
+ ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height);
+ } else {
+ // vertical padding, y offset
+ const yOffset = (canvasSize - srcCanvas.height) / 2;
+ ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height);
+ ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height);
+ }
+ return canvas;
+ };
+
+ // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
+ static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const { data } = imageData;
+ for (let i = 0; i < canvas.height; i++) {
+ for (let j = 0; j < xOffset; j++) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceI = i;
+ const sourceJ = xOffset + (xOffset - j);
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ for (let i = 0; i < canvas.height; i++) {
+ for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceI = i;
+ const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j));
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ };
+
+ // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
+ static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const { data } = imageData;
+ for (let j = 0; j < canvas.width; j++) {
+ for (let i = 0; i < yOffset; i++) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceJ = j;
+ const sourceI = yOffset + (yOffset - i);
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ for (let j = 0; j < canvas.width; j++) {
+ for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceJ = j;
+ const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i));
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ };
+
+ // Gets the unaltered (besides filling in padding) version of the image for the api call
+ static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return undefined;
+ // fix scaling
+ const scale = Math.min(canvasSize / img.width, canvasSize / img.height);
+ const width = Math.floor(img.width * scale);
+ const height = Math.floor(img.height * scale);
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.fillStyle = bgColor;
+ ctx.fillRect(0, 0, canvasSize, canvasSize);
+
+ // extract and set padding data
+ if (img.naturalHeight > img.naturalWidth) {
+ // horizontal padding, x offset
+ const xOffset = Math.floor((canvasSize - width) / 2);
+ ctx.drawImage(img, xOffset, 0, width, height);
+
+ // draw reflected image padding
+ this.drawHorizontalReflection(ctx, canvas, xOffset);
+ } else {
+ // vertical padding, y offset
+ const yOffset = Math.floor((canvasSize - height) / 2);
+ ctx.drawImage(img, 0, yOffset, width, height);
+
+ // draw reflected image padding
+ this.drawVerticalReflection(ctx, canvas, yOffset);
+ }
+ return canvas;
+ };
+
+ /**
+ * Converts a url to base64 (tainted canvas workaround)
+ */
+ static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => {
+ try {
+ const res = await fetch(imageUrl);
+ const blob = await res.blob();
+
+ return new Promise<string>((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const base64Data = reader.result?.toString().split(',')[1];
+ if (base64Data) {
+ resolve(base64Data);
+ } else {
+ reject(new Error('Failed to convert.'));
+ }
+ };
+ reader.onerror = () => {
+ reject(new Error('Error reading image data'));
+ };
+ reader.readAsDataURL(blob);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ return undefined;
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/PointerHandler.ts
index 260923a64..e86f46636 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/PointerHandler.ts
@@ -1,4 +1,4 @@
-import { Point } from './generativeFillInterfaces';
+import { Point } from './imageEditorInterfaces';
export class PointerHandler {
static getPointRelativeToElement = (element: HTMLElement, e: React.PointerEvent | PointerEvent, scale: number): Point => {
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts
index 4772304bc..594d6d9fc 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts
@@ -3,6 +3,7 @@ export const freeformRenderSize = 300;
export const offsetDistanceY = freeformRenderSize + 400;
export const offsetX = 200;
export const newCollectionSize = 500;
+export const brushWidthOffset = 10;
export const activeColor = '#1976d2';
export const eraserColor = '#e1e9ec';
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
new file mode 100644
index 000000000..a14b55439
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -0,0 +1,43 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Doc } from '../../../../../fields/Doc';
+
+export interface CursorData {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export enum ImageToolType {
+ GenerativeFill = 'genFill',
+ Cut = 'cut',
+}
+
+export enum CutMode {
+ IN,
+ OUT,
+ DRAW_IN,
+ ERASE,
+}
+
+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
+ applyFunc: (currCutType: CutMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], isFirstDoc: boolean) => Promise<void>;
+ // these optional parameters are here because different tools require different brush sizes and defaults
+ sliderMin?: number;
+ sliderMax?: number;
+ sliderDefault?: number;
+}
+
+export interface ImageDimensions {
+ width: number;
+ height: number;
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts
index 8a66d7347..7139bebc3 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts
@@ -1,6 +1,6 @@
-import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers';
-import { eraserColor } from './generativeFillConstants';
-import { Point } from './generativeFillInterfaces';
+import { GenerativeFillMathHelpers } from '../imageEditorUtils/GenerativeFillMathHelpers';
+import { eraserColor } from '../imageEditorUtils/imageEditorConstants';
+import { Point } from '../imageEditorUtils/imageEditorInterfaces';
import { points } from '@turf/turf';
export enum BrushType {
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts
index 24dba1778..b9723b5be 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts
@@ -1,5 +1,5 @@
import { RefObject } from 'react';
-import { bgColor, canvasSize } from './generativeFillConstants';
+import { bgColor, canvasSize } from '../imageEditorUtils/imageEditorConstants';
export interface APISuccess {
status: 'success';
diff --git a/src/client/views/nodes/trails/CubicBezierEditor.tsx b/src/client/views/nodes/trails/CubicBezierEditor.tsx
index e1ad1e6e5..627b77184 100644
--- a/src/client/views/nodes/trails/CubicBezierEditor.tsx
+++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx
@@ -118,84 +118,82 @@ function CubicBezierEditor({ setFunc, currPoints }: Props) {
}, [c2Down, currPoints]);
return (
- <div
- onPointerMove={e => {
- e.stopPropagation;
- }}>
- <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg">
- {/* Outlines */}
- <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" />
- {/* Box Outline */}
- <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" />
- {/* Editor */}
- <path
- d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${
- currPoints.p2[0] * EDITOR_WIDTH + OFFSET
- } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`}
- stroke="#ffffff"
- fill="transparent"
- />
- {/* Bottom left */}
- <line
- onPointerDown={() => {
- setC1Down(true);
- }}
- onPointerUp={() => {
- setC1Down(false);
- }}
- x1={`${0 + OFFSET}`}
- y1={`${EDITOR_WIDTH + OFFSET}`}
- x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
- y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
- stroke="#00000000"
- strokeWidth="5"
- />
- <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
- <circle
- cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
- cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
- r="5"
- fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`}
- onPointerDown={e => {
- e.stopPropagation();
- setC1Down(true);
- }}
- onPointerUp={() => {
- setC1Down(false);
- }}
- />
- {/* Top right */}
- <line
- onPointerDown={e => {
- e.stopPropagation();
- setC2Down(true);
- }}
- onPointerUp={() => {
- setC2Down(false);
- }}
- x1={`${EDITOR_WIDTH + OFFSET}`}
- y1={`${0 + OFFSET}`}
- x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
- y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
- stroke="#00000000"
- strokeWidth="5"
- />
- <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
- <circle
- cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
- cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
- r="5"
- fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`}
- onPointerDown={e => {
- e.stopPropagation();
- setC2Down(true);
- }}
- onPointerUp={() => {
- setC2Down(false);
- }}
- />
- </svg>
- </div>
+ <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg">
+ {/* Outlines */}
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" />
+ {/* Box Outline */}
+ <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" />
+ {/* Editor */}
+ <path
+ d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${
+ currPoints.p2[0] * EDITOR_WIDTH + OFFSET
+ } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`}
+ stroke="#ffffff"
+ fill="transparent"
+ />
+ {/* Bottom left */}
+ <line
+ onPointerDown={() => {
+ setC1Down(true);
+ }}
+ onPointerMove={e => {
+ e.stopPropagation;
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ x1={`${0 + OFFSET}`}
+ y1={`${EDITOR_WIDTH + OFFSET}`}
+ x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC1Down(true);
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ />
+ {/* Top right */}
+ <line
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ x1={`${EDITOR_WIDTH + OFFSET}`}
+ y1={`${0 + OFFSET}`}
+ x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ />
+ </svg>
);
}
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index 60d4e580d..e34e1b380 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -5,11 +5,25 @@
display: flex;
flex-direction: column;
gap: 1rem;
+ .presBox-gpt-chat-span {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ textarea {
+ width: 100%;
+ }
+}
+.presBox-subheading-slider {
+ max-width: calc(100% - 25px);
+ width: 100%;
+ padding: 15px;
+ padding-left: 0px;
}
.pres-chat {
display: flex;
- flex-direction: column;
+ flex-direction: row;
gap: 8px;
}
@@ -18,30 +32,38 @@
gap: 8px;
}
-.pres-chatbox-container {
- padding: 16px;
+.pres-chatbox-container,
+.pres-chatbox-container-ai {
+ width: 100%;
+ padding-left: 16px;
+ padding-right: 16px;
outline: 1px solid #999999;
- border-radius: 16px;
+ border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
+ overflow: auto;
+ max-height: 200px;
+ .pres-chatbox {
+ outline: none;
+ border: none;
+ resize: none;
+ font-family: Verdana, Geneva, sans-serif;
+ background-color: transparent;
+ overflow-y: hidden;
+ }
}
-.pres-chatbox {
- outline: none;
- border: none;
- resize: none;
- font-family: Verdana, Geneva, sans-serif;
- background-color: transparent;
- overflow-y: hidden;
+.pres-chatbox-container-ai {
+ padding-left: 8px;
+ padding-right: 8px;
+ margin-left: 8px;
}
-
// Effect Animations
.presBox-effects {
- display: grid;
- grid-template-columns: auto auto;
- gap: 8px;
+ display: flow;
+ margin: auto;
}
.presBox-effect-row {
@@ -55,7 +77,7 @@
overflow: hidden;
width: 80px;
height: 80px;
- display: flex;
+ display: inline-flex;
justify-content: center;
align-items: center;
border: 1px solid rgb(118, 118, 118);
@@ -74,12 +96,19 @@
.presBox-show-hide-dropdown {
cursor: pointer;
- padding: 8px 0;
display: flex;
align-items: center;
gap: 4px;
}
+.presBox-switches {
+ display: flex;
+ width: 100%;
+ > div {
+ width: 100%;
+ }
+}
+
.presBox-bezier-editor {
border: 1px solid rgb(221, 221, 221);
border-radius: 4px;
@@ -96,6 +125,18 @@
align-items: center;
}
+.presBox-previewContainer {
+ display: flex;
+ align-items: center;
+ width: fit-content;
+ margin: auto;
+ grid-template-columns: auto auto;
+ grid-gap: 10px;
+ .presBox-option-block {
+ padding: 0px;
+ }
+}
+
.presBox-cont {
cursor: auto;
position: absolute;
@@ -270,7 +311,7 @@
}
.presBox-toggles {
- display: flex;
+ display: flow;
overflow-x: auto;
}
@@ -280,6 +321,9 @@
font-family: Roboto;
z-index: 100;
transition: 0.7s;
+ .form-wrapper.left .formLabel {
+ width: 100px;
+ }
.ribbon-doubleButton {
display: flex;
@@ -352,6 +396,18 @@
font-size: 11;
font-weight: 400;
margin-top: 10px;
+ max-width: min(85px, 25%);
+ width: 100%;
+ }
+ .presBox-springSlider {
+ display: grid;
+ column-count: 2;
+ grid-template-columns: min(60px, 25%) calc(100% - min(60px, 25%) - min(5px, 10%));
+ grid-gap: min(5px, 10%);
+ > span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@@ -459,7 +515,7 @@
justify-content: space-between;
width: 100%;
height: max-content;
- grid-template-columns: auto auto auto;
+ grid-template-columns: auto auto;
grid-template-rows: max-content;
font-weight: 100;
margin-top: 3px;
@@ -520,20 +576,17 @@
}
.presBox-input {
- border: none;
background-color: transparent;
- width: 40;
- // padding: 8px;
- // border-radius: 4px;
- // width: 30;
- // height: 100%;
- // background: none;
- // border: none;
- // text-align: right;
+ text-align: center;
+ width: 100%;
+ height: 15px;
+ font-size: 10;
}
-
- .presBox-input:focus {
- outline: none;
+ .presBox-inputNumber {
+ border: none;
+ background-color: transparent;
+ width: 100%;
+ max-width: 25px;
}
.ribbon-frameSelector {
@@ -737,7 +790,7 @@
font-weight: 200;
height: 20;
background-color: $white;
- display: flex;
+ display: inline-flex;
color: $black;
border-radius: 5px;
width: max-content;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7448fa898..9ab5fb1bd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,12 +1,12 @@
+import { Button, Dropdown, DropdownType, IconButton, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import Slider from '@mui/material/Slider';
-import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from 'browndash-components';
+import _ from 'lodash';
import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
-import { BiMicrophone } from 'react-icons/bi';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa';
import ReactLoading from 'react-loading';
import ReactTextareaAutosize from 'react-textarea-autosize';
@@ -14,7 +14,7 @@ import { StopEvent, lightOrDark, returnFalse, returnOne, setupMoveUpEvents } fro
import { emptyFunction, stringHash } from '../../../../Utils';
import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
-import { Copy } from '../../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { ObjectField } from '../../../../fields/ObjectField';
@@ -25,12 +25,12 @@ import { DocServer } from '../../../DocServer';
import { getSlideTransitionSuggestions, gptSlideProperties, gptTrailSlideCustomization } from '../../../apis/gpt/PresCustomization';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
-import { DictationManager } from '../../../util/DictationManager';
import { dropActionType } from '../../../util/DropActionTypes';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SerializationHelper } from '../../../util/SerializationHelper';
import { SnappingManager } from '../../../util/SnappingManager';
import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager';
+import { DictationButton } from '../../DictationButton';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { pinDataTypes as dataTypes } from '../../PinFuncs';
import { CollectionView } from '../../collections/CollectionView';
@@ -40,7 +40,7 @@ import { CollectionFreeFormPannableContents } from '../../collections/collection
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
-import { FocusViewOptions } from '../FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../FocusViewOptions';
import { OpenWhere, OpenWhereMod } from '../OpenWhere';
import { ScriptingBox } from '../ScriptingBox';
import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor';
@@ -60,6 +60,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
static navigateToDocScript: ScriptField;
+ public static PanelName = 'PRESBOX'; // name of dockingview tab where presentations get added
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -71,10 +73,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
private _disposers: { [name: string]: IReactionDisposer } = {};
public selectedArray = new ObservableSet<Doc>();
+ public slideToModify: Doc | null = null;
_batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves
_keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
_unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
_presTimer: NodeJS.Timeout | undefined;
+ _animationDictation: DictationButton | null = null;
+ _slideDictation: DictationButton | null = null;
// eslint-disable-next-line no-use-before-define
@observable public static Instance: PresBox;
@@ -96,14 +101,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _presKeyEvents: boolean = false;
@observable _forceKeyEvents: boolean = false;
+ @observable _showAIGallery = false;
+ @observable _showPreview = true;
+ @observable _easeDropdownVal = 'ease';
+
// GPT
- private _inputref: HTMLTextAreaElement | null = null;
- private _inputref2: HTMLTextAreaElement | null = null;
- @observable chatActive: boolean = false;
- @observable chatInput: string = '';
- public slideToModify: Doc | null = null;
- @observable isRecording: boolean = false;
- @observable isLoading: boolean = false;
+ @observable _chatActive: boolean = false;
+ @observable _animationChat: string = '';
+ @observable _chatInput: string = '';
+ @observable _isLoading: boolean = false;
@observable generatedAnimations: AnimationSettings[] = [
// default presets
@@ -137,54 +143,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
},
];
- @action
- setGeneratedAnimations = (settings: AnimationSettings[]) => {
- this.generatedAnimations = settings;
- };
-
- @observable animationChat: string = '';
-
- @action
- setChatInput = (input: string) => {
- this.chatInput = input;
- };
-
- @action
- setAnimationChat = (input: string) => {
- this.animationChat = input;
- };
-
- @action
- setIsLoading = (isLoading: boolean) => {
- this.isLoading = isLoading;
- };
-
- @action
- public setIsRecording = (isRecording: boolean) => {
- this.isRecording = isRecording;
- };
-
- @observable showBezierEditor = false;
- @action setBezierEditorVisibility = (visible: boolean) => {
- this.showBezierEditor = visible;
- };
- @observable showSpringEditor = true;
- @action setSpringEditorVisibility = (visible: boolean) => {
- this.showSpringEditor = visible;
- };
-
- // Easing function variables
-
- @observable easeDropdownVal = 'ease';
-
- @action setBezierControlPoints = (newPoints: { p1: number[]; p2: number[] }) => {
+ setGeneratedAnimations = action((input: AnimationSettings[]) => { this.generatedAnimations = input; }) // prettier-ignore
+ setChatInput = action((input: string) => { this._chatInput = input; }); // prettier-ignore
+ setAnimationChat = action((input: string) => { this._animationChat = input; }); // prettier-ignore
+ setIsLoading = action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore
+ setShowAIGalleryVisibilty = action((visible: boolean) => { this._showAIGallery = visible; }); // prettier-ignore
+ setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => {
this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`);
- };
+ });
+
+ @computed get showEaseFunctions() {
+ return ![PresMovement.None, PresMovement.Jump, ''].includes(StrCast(this.activeItem?.presentation_movement) as PresMovement);
+ }
@computed
get currCPoints() {
- const strPoints = this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease';
- return EaseFuncToPoints(strPoints);
+ return EaseFuncToPoints(this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease');
}
@computed
@@ -220,25 +194,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true;
return false;
}
- @computed get panable() {
- if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
- return false;
- }
@computed get selectedDocumentView() {
if (DocumentView.Selected().length) return DocumentView.Selected()[0];
if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document);
return undefined;
}
- @computed get isPres() {
- return this.selectedDoc === this.Document;
- }
- @computed get selectedDoc() {
- return this.selectedDocumentView?.Document;
- }
- isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentation_targetDoc === layoutDoc;
- clearSelectedArray = () => this.selectedArray.clear();
- addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
- removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
componentWillUnmount() {
this._unmounting = true;
@@ -255,7 +215,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
() => this.pauseAutoPres()
);
this._disposers.keyboard = reaction(
- () => this.selectedDoc,
+ () => this.selectedDocumentView?.Document,
selected => {
document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
(this._presKeyEvents = selected === this.Document) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
@@ -292,13 +252,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
+ clearSelectedArray = () => this.selectedArray.clear();
+ addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
+ removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
+
@action
updateCurrentPresentation = (pres?: Doc) => {
Doc.ActivePresentation = pres ?? this.Document;
PresBox.Instance = this;
};
- _mediaTimer!: [NodeJS.Timeout, Doc];
// 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart);
@@ -316,84 +279,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- // Recording for GPT customization
-
- recordDictation = () => {
- this.setIsRecording(true);
- this.setChatInput('');
- DictationManager.Controls.listen({
- interimHandler: this.setDictationContent,
- continuous: { indefinite: false },
- }).then(results => {
- if (results && [DictationManager.Controls.Infringed].includes(results)) {
- DictationManager.Controls.stop();
- }
- });
- };
- stopDictation = () => {
- this.setIsRecording(false);
- DictationManager.Controls.stop();
- };
-
- setDictationContent = (value: string) => {
- console.log('Dictation value', value);
- this.setChatInput(value);
- };
+ setDictationContent = (value: string) => this.setChatInput(value);
- @action
- customizeAnimations = async () => {
+ customizeAnimations = action(() => {
this.setIsLoading(true);
- try {
- const res = await getSlideTransitionSuggestions(this.animationChat);
- if (typeof res === 'string') {
- const resObj = JSON.parse(res);
- console.log('Parsed GPT Result ', resObj);
- this.setGeneratedAnimations(resObj as AnimationSettings[]);
- }
- } catch (err) {
- console.error(err);
- }
- this.setIsLoading(false);
- };
+ getSlideTransitionSuggestions(this._animationChat)
+ .then(res => this.setGeneratedAnimations(JSON.parse(res) as AnimationSettings[]))
+ .catch(err => console.error(err))
+ .finally(this.setIsLoading);
+ });
- @action
- customizeWithGPT = async (input: string) => {
+ customizeWithGPT = action((input: string) => {
// const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect';
- this.setIsRecording(false);
this.setIsLoading(true);
-
- const currSlideProperties: { [key: string]: FieldResult } = {};
- gptSlideProperties.forEach(key => {
- if (this.activeItem[key]) {
- currSlideProperties[key] = this.activeItem[key];
- }
- // default values
- else if (key === 'presentation_transition') {
- currSlideProperties[key] = 500;
- } else if (key === 'config_zoom') {
- currSlideProperties[key] = 1.0;
- }
- });
- console.log('current slide props ', currSlideProperties);
-
- try {
- const res = await gptTrailSlideCustomization(input, currSlideProperties);
- if (typeof res === 'string') {
- const resObj = JSON.parse(res);
- console.log('Parsed GPT Result ', resObj);
- // eslint-disable-next-line no-restricted-syntax
- for (const key in resObj) {
- if (resObj[key]) {
- console.log('typeof property', typeof resObj[key]);
- this.activeItem[key] = resObj[key];
- }
- }
- }
- } catch (err) {
- console.error(err);
- }
- this.setIsLoading(false);
- };
+ const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 };
+ const currSlideProperties = gptSlideProperties.reduce(
+ (prev, key) => { prev[key] = Field.toString(this.activeItem[key]) ?? prev[key]; return prev; },
+ slideDefaults); // prettier-ignore
+
+ gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties))
+ .then(res =>
+ (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => {
+ this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key]);
+ })
+ )
+ .catch(e => console.error(e))
+ .finally(this.setIsLoading);
+ });
// TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
// TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
@@ -452,27 +364,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const progressiveReveal = (first: boolean) => {
const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null);
if (presIndexed !== undefined) {
- const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
- targetRenderedDoc._dataTransition = 'all 1s';
- targetRenderedDoc.opacity = 1;
- setTimeout(() => {
- targetRenderedDoc._dataTransition = 'inherit';
- }, 1000);
const listItems = this.progressivizedItems(this.activeItem);
- if (listItems && presIndexed < listItems.length) {
+ const listItemDoc = listItems?.[presIndexed];
+ if (listItems && listItemDoc) {
if (!first) {
- const listItemDoc = listItems[presIndexed];
- const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc);
+ const presBulletTiming = 500; // bcz: hardwired for now
Doc.linkFollowUnhighlight();
- Doc.HighlightDoc(listItemDoc);
+ Doc.linkFollowHighlight(listItemDoc);
listItemDoc.presentation_effect = this.activeItem.presBulletEffect;
- listItemDoc.presentation_transition = 500;
- targetView?.setAnimEffect(listItemDoc, 500);
- if (targetView && this.activeItem.presBulletExpand) {
- targetView.setAnimateScaling(1.2, 400);
- Doc.AddUnHighlightWatcher(() => targetView?.setAnimateScaling(0, undefined));
- }
+ listItemDoc.presentation_transition = presBulletTiming;
listItemDoc.opacity = undefined;
+
+ const targetView = DocumentView.getFirstDocumentView(listItemDoc);
+ if (targetView) {
+ targetView.setAnimEffect(listItemDoc, presBulletTiming);
+ if (this.activeItem.presBulletExpand) {
+ targetView.setAnimateScaling(1.2, presBulletTiming * 0.8);
+ Doc.AddUnHighlightWatcher(() => targetView.setAnimateScaling(0, undefined));
+ }
+ }
this.activeItem.presentation_indexed = presIndexed + 1;
}
return true;
@@ -547,8 +457,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (!group) this.clearSelectedArray();
this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array
this.turnOffEdit();
- this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list
- this.doHideBeforeAfter(); // Handles hide after/before
+ this.navigateToActiveItem((options: FocusViewOptions) => {
+ setTimeout(this.doHideBeforeAfter, FocusEffectDelay(options)); // Handles hide after/before
+ finished?.();
+ }); // Handles movement to element only when presentationTrail is list
}
});
static pinDataTypes(target?: Doc): dataTypes {
@@ -562,11 +474,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const datarange = [DocumentType.FUNCPLOT].includes(targetType);
const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
- const typeCollection = targetType === DocumentType.COL;
+ const collectionType = targetType === DocumentType.COL;
const filters = true;
const pivot = true;
const dataannos = false;
- return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+ return { scrollable, pannable, inkable, collectionType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
@@ -574,7 +486,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/* empty */
};
@action
- // eslint-disable-next-line default-param-last
static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) {
const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
if (!bestTarget) return undefined;
@@ -700,15 +611,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
}
- if ((pinDataTypes?.type_collection && activeItem.config_viewType !== undefined) || (!pinDataTypes && activeItem.config_viewType !== undefined)) {
- if (bestTarget._type_collection !== activeItem.config_viewType) {
- bestTarget._type_collection = activeItem.config_viewType;
+ if ((pinDataTypes?.collectionType && activeItem.config_card_curDoc !== undefined) || (!pinDataTypes && activeItem.config_card_curDoc !== undefined)) {
+ if (bestTarget._card_curDoc !== activeItem.config_card_curDoc) {
+ bestTarget._card_curDoc = activeItem.config_card_curDoc;
+ changed = true;
+ }
+ }
+ if ((pinDataTypes?.collectionType && activeItem.config_carousel_index !== undefined) || (!pinDataTypes && activeItem.config_carousel_index !== undefined)) {
+ if (bestTarget._carousel_index !== activeItem.config_carousel_index) {
+ bestTarget._carousel_index = activeItem.config_carousel_index;
+ changed = true;
+ }
+ }
+ if ((pinDataTypes?.collectionType && activeItem.config_type_collection !== undefined) || (!pinDataTypes && activeItem.config_type_collection !== undefined)) {
+ if (bestTarget._type_collection !== activeItem.config_type_collection) {
+ bestTarget._type_collection = activeItem.config_type_collection;
changed = true;
}
}
if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) {
- if (bestTarget.childFilters !== activeItem.config_docFilters) {
+ if (!_.isEqual(Array.from(StrListCast(bestTarget.childFilters)), Array.from(StrListCast(activeItem.config_docFilters)))) {
bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters as ObjectField) || new List<string>([]);
changed = true;
}
@@ -773,11 +696,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
setTimeout(
() =>
- Array.from(transitioned).forEach(
- action(doc => {
- doc._dataTransition = undefined;
- })
- ),
+ Array.from(transitioned).forEach(doc => {
+ doc._dataTransition = undefined;
+ }),
transTime + 10
);
}
@@ -815,16 +736,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* a new tab. If presCollection is undefined it will open the document
* on the right.
*/
- navigateToActiveItem = (afterNav?: () => void) => {
+ navigateToActiveItem = (afterNav?: (options: FocusViewOptions) => void) => {
const { activeItem, targetDoc } = this;
- const finished = () => {
- afterNav?.();
+ const finished = (options: FocusViewOptions) => {
+ afterNav?.(options);
targetDoc[Animation] = undefined;
};
const selViewCache = Array.from(this.selectedArray);
const dragViewCache = Array.from(this._dragArray);
const eleViewCache = Array.from(this._eleArray);
- const resetSelection = action(() => {
+ const resetSelection = action((options: FocusViewOptions) => {
if (!this._props.isSelected()) {
const presDocView = DocumentView.getDocumentView(this.Document);
if (presDocView) DocumentView.SelectView(presDocView, false);
@@ -833,14 +754,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
}
- finished();
+ finished(options);
});
PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
};
- public static PanelName = 'PRESBOX';
-
- static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
+ static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) {
if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
(DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
return;
@@ -875,9 +794,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
DocumentView.SetLightboxDoc(undefined);
}
- DocumentView.showDocument(targetDoc, options, finished);
+ DocumentView.showDocument(targetDoc, options, () => finished?.(options));
});
- } else finished?.();
+ } else finished?.(options);
}
/**
@@ -889,8 +808,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.childDocs.forEach((doc, index) => {
const curDoc = Cast(doc, Doc, null);
const tagDoc = PresBox.targetRenderedDoc(curDoc);
- const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc);
- let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
+ const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc);
+ let opacity = index === this.itemIndex ? 1 : undefined;
if (curDoc.presentation_hide) {
if (index !== this.itemIndex) {
opacity = 1;
@@ -902,9 +821,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
opacity = 0;
} else if (index === this.itemIndex || !curDoc.presentation_hideAfter) {
opacity = 1;
- setTimeout(() => {
- tagDoc._dataTransition = undefined;
- }, 1000);
}
}
const hidingIndAft =
@@ -1134,7 +1050,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return false;
}
} else if (doc.type !== DocumentType.PRES) {
- // eslint-disable-next-line operator-assignment
if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else.
doc.presentation_movement = PresMovement.Zoom;
@@ -1166,8 +1081,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null);
if (curDoc && curDoc === this.activeItem)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
<b>
{index + 1}. {StrCast(curDoc.title)})
</b>
@@ -1175,15 +1089,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
if (tagDoc)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
{index + 1}. {StrCast(curDoc.title)}
</div>
);
if (curDoc)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
{index + 1}. {StrCast(curDoc.title)}
</div>
);
@@ -1368,7 +1280,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const tagDoc = PresBox.targetRenderedDoc(doc);
const srcContext = Cast(tagDoc.embedContainer, Doc, null);
const labelCreator = (top: number, left: number, edge: number, fontSize: number) => (
- // eslint-disable-next-line react/no-array-index-key
<div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}>
<div className="pathOrder-frame">{index + 1}</div>
</div>
@@ -1619,7 +1530,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true);
this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true);
- // eslint-disable-next-line camelcase
const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem;
array.forEach(curDoc => {
curDoc.presentation_transition = pt;
@@ -1682,20 +1592,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
</div>
{[DocumentType.AUDIO, DocumentType.VID].includes(targetType as DocumentType) ? null : (
- <>
- <div className="ribbon-doubleButton">
- <div className="presBox-subheading">Slide Duration</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+ <div className="ribbon-doubleButton">
+ <Tooltip title={<div>How long to view the slide before transitioning to the next slide</div>}>
+ <div className="presBox-subheading">DURATION</div>
+ </Tooltip>
+ <div className="presBox-subheading-slider">
+ {PresBox.inputter('0.1', '0.1', '10', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
+ <div className="slider-headers">
+ <div className="slider-text">Short</div>
+ <div className="slider-text">Long</div>
</div>
</div>
- {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
- <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
- <div className="slider-text">Short</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Long</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}>
+ <input className="presBox-inputNumber" type="number" value={duration} onChange={action(e => this.updateDurationTime(e.target.value))} />
+ <span>s</span>
</div>
- </>
+ </div>
)}
</div>
</div>
@@ -1703,28 +1615,62 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
return undefined;
}
- @computed get progressivizeDropdown() {
+
+ @computed get mediaDropdown() {
const { activeItem } = this;
if (activeItem && this.targetDoc) {
- const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
- const bulletEffect = (presEffect: PresEffect) => (
- <div
- className={`presBox-dropdownOption ${activeItem.presentation_effect === presEffect || (presEffect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
- onPointerDown={StopEvent}
- onClick={() => this.updateEffect(presEffect, true)}>
- {presEffect}
+ return (
+ <div className="presBox-option-block">
+ <div className="presBox-ribbon presbox-toggles">
+ <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}>
+ <div
+ className={`ribbon-toggle ${BoolCast(activeItem.presentation_playAudio) ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: BoolCast(activeItem.presentation_playAudio) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => {
+ activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio);
+ }}>
+ Play Audio Annotation
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}>
+ <div
+ className={`ribbon-toggle ${BoolCast(activeItem.presentation_zoomText) ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: BoolCast(activeItem.presentation_zoomText) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => {
+ activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
+ }}>
+ Zoom Text Selections
+ </div>
+ </Tooltip>
+ </div>
</div>
);
+ }
+ return null;
+ }
+ @computed get progressivizeDropdown() {
+ const { activeItem } = this;
+ if (activeItem && this.targetDoc) {
return (
<div className="presBox-option-block">
- <div className="presBox-ribbon">
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Progressivize Collection</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
+ <div className="presBox-toggles presBox-ribbon">
+ <Tooltip title={<div className="dash-tooltip">whether progressivization is active for this slide</div>}>
+ <div
+ className={`ribbon-toggle ${Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => {
activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined;
activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined;
const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
@@ -1737,62 +1683,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`);
else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`);
+ }}>
+ Enable
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}>
+ <div
+ className={`ribbon-toggle ${!NumCast(activeItem.presentation_indexedStart) ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: !NumCast(activeItem.presentation_indexedStart) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
}}
- checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Progressivize First Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
+ onClick={() => {
activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1;
- }}
- checked={!NumCast(activeItem.presentation_indexedStart)}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Expand Current Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presBulletExpand = !activeItem.presBulletExpand;
- }}
- checked={BoolCast(activeItem.presBulletExpand)}
- />
- </div>
-
- <div className="ribbon-box">
- Bullet Effect
+ }}>
+ All Bullets
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Whether the active bullet expands when active.</div>}>
<div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
- })}
+ className={`ribbon-toggle ${BoolCast(activeItem.presBulletExpand) ? 'active' : ''}`}
style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
color: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
- border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
+ background: BoolCast(activeItem.presBulletExpand) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => {
+ activeItem.presBulletExpand = !activeItem.presBulletExpand;
}}>
- {effect?.toString()}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
- <div
- className="presBox-dropdownOptions"
- style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
- onPointerDown={e => e.stopPropagation()}>
- {Object.values(PresEffect)
- .filter(v => isNaN(Number(v)))
- .map(pEffect => bulletEffect(pEffect))}
- </div>
+ Expand Active
</div>
- </div>
+ </Tooltip>
</div>
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Effect"
+ toolTip="Animation effect to use when bullet activates"
+ formLabelPlacement="left"
+ closeOnSelect
+ items={Object.values(PresEffect).map(v => ({ text: v.toString(), val: v }))}
+ selectedVal={StrCast(activeItem.presBulletEffect, PresMovement.None)}
+ setSelectedVal={val => this.updateEffect(val as PresEffect, true)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
</div>
);
}
@@ -1803,23 +1738,95 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return <div />;
}
+ /**
+ * This chatbox is for getting slide effect transition suggestions from gpt and visualizing them
+ */
+ @computed get aiEffects() {
+ return (
+ <div className="presBox-gpt-chat" style={{ display: SnappingManager.PropertiesWidth < 1 || !this._showAIGallery ? 'none' : undefined }}>
+ {/* Custom */}
+ <div className="pres-chat">
+ <div className="pres-chatbox-container-ai">
+ <ReactTextareaAutosize
+ placeholder="Use AI to suggest effects. Leave blank for random results."
+ className="pres-chatbox"
+ ref={r => {
+ setTimeout(() => {
+ if (r && !r.textContent) {
+ r.style.height = '';
+ r.style.height = r.scrollHeight + 'px';
+ }
+ });
+ }}
+ value={this._animationChat}
+ onChange={e => {
+ e.currentTarget.style.height = '';
+ e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
+ this.setAnimationChat(e.target.value);
+ }}
+ onKeyDown={e => {
+ this._animationDictation?.stopDictation();
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ type={Type.TERT}
+ icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SnappingManager.userVariantColor}
+ onClick={this.customizeAnimations}
+ />
+ <DictationButton
+ ref={r => {
+ this._animationDictation = r;
+ }}
+ setInput={this.setAnimationChat}
+ />
+ </div>
+ <div style={{ alignItems: 'center' }}>
+ Click a box to use the effect.
+ {/* Preview Animations */}
+ <div className="presBox-effects">
+ {this.generatedAnimations.map((elem, i) => (
+ <div
+ key={i}
+ className="presBox-effect-container"
+ onClick={() => {
+ this.updateEffect(elem.effect, false);
+ this.updateEffectDirection(elem.direction);
+ this.updateEffectTiming(this.activeItem, {
+ type: SpringType.CUSTOM,
+ stiffness: elem.stiffness,
+ damping: elem.damping,
+ mass: elem.mass,
+ });
+ }}>
+ <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite>
+ <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} />
+ </SlideEffect>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
@computed get transitionDropdown() {
const { activeItem } = this;
// Retrieving spring timing properties
- const timing = StrCast(activeItem.presentation_effectTiming);
- let timingConfig: SpringSettings | undefined;
- if (timing) {
- timingConfig = JSON.parse(timing);
- }
-
- if (!timingConfig) {
- timingConfig = {
- type: SpringType.GENTLE,
- stiffness: 100,
- damping: 15,
- mass: 1,
- };
- }
+ const timing = StrCast(activeItem?.presentation_effectTiming);
+ const timingConfig: SpringSettings = timing
+ ? JSON.parse(timing)
+ : {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ };
if (activeItem && this.targetDoc) {
const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5;
@@ -1830,180 +1837,136 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return (
<>
{/* This chatbox is for customizing the properties of trails, like transition time, movement type (zoom, pan) using GPT */}
- <div className="presBox-gpt-chat">
- <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+ <div className="presBox-gpt-chat" style={{ display: SnappingManager.PropertiesWidth < 1 ? 'none' : undefined }}>
+ <span className="presBox-gpt-chat-span">
Customize Slide Properties{' '}
<div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/#slide-customization')}>
<IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SnappingManager.userColor} />
</div>
</span>
<div className="pres-chat">
- <div className="pres-chatbox-container">
+ <div className="pres-chatbox-container-ai">
<ReactTextareaAutosize
- placeholder="Describe how you would like to modify the slide properties."
+ placeholder="Describe how to modify the slide properties."
className="pres-chatbox"
- value={this.chatInput}
+ ref={r => {
+ setTimeout(() => {
+ if (r && !r.textContent) {
+ r.style.height = '';
+ r.style.height = r.scrollHeight + 'px';
+ }
+ });
+ }}
+ value={this._chatInput}
onChange={e => {
+ e.currentTarget.style.height = '';
+ e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
this.setChatInput(e.target.value);
}}
onKeyDown={e => {
- this.stopDictation();
+ this._slideDictation?.stopDictation();
e.stopPropagation();
}}
/>
- <IconButton
- type={Type.TERT}
- color={this.isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
- tooltip="Record"
- icon={<BiMicrophone size="16px" />}
- onClick={() => {
- if (!this.isRecording) {
- this.recordDictation();
- } else {
- this.stopDictation();
- }
+ <DictationButton
+ ref={r => {
+ this._slideDictation = r;
}}
+ setInput={this.setChatInput}
/>
</div>
<Button
style={{ alignSelf: 'flex-end' }}
text="Send"
type={Type.TERT}
- icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+ icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
iconPlacement="right"
color={SnappingManager.userVariantColor}
onClick={() => {
- this.stopDictation();
- this.customizeWithGPT(this.chatInput);
+ this._slideDictation?.stopDictation();
+ this.customizeWithGPT(this._chatInput);
}}
/>
</div>
</div>
+
{/* Movement */}
<div
className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
- onPointerDown={StopEvent}
- onPointerUp={StopEvent}
+ onPointerDown={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
onClick={action(e => {
e.stopPropagation();
this._openMovementDropdown = false;
this._openEffectDropdown = false;
this._openBulletEffectDropdown = false;
})}>
- <div
- className="presBox-option-block"
- // style={{ padding: '16px' }}
- >
- Movement
+ <div className="presBox-option-block">
+ <div className="ribbon-doubleButton">
+ <Tooltip title={<div>How long the transition (view navigation and slide animation effect) lasts</div>}>
+ <div className="presBox-subheading">SPEED</div>
+ </Tooltip>
+ <div className="presBox-subheading-slider">
+ {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)}
+ <div className="slider-headers">
+ <div className="slider-text">Fast</div>
+ <div className="slider-text">Slow</div>
+ </div>
+ </div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}>
+ <input className="presBox-inputNumber" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} />
+ <span>s</span>
+ </div>
+ </div>
<Dropdown
color={SnappingManager.userColor}
- formLabel="Movement"
+ formLabel="View"
+ formLabelPlacement="left"
closeOnSelect
items={movementItems}
selectedVal={this.movementName(activeItem)}
- setSelectedVal={val => {
- this.updateMovement(val as PresMovement);
- }}
+ setSelectedVal={val => this.updateMovement(val as PresMovement)}
dropdownType={DropdownType.SELECT}
type={Type.TERT}
/>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Zoom (% screen filled)</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" readOnly type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? undefined : 'none' }}>
+ <Tooltip title={<div>How much (%) of screen target should occupy</div>}>
+ <div className="presBox-subheading">ZOOM %</div>
+ </Tooltip>
+ <div className="presBox-subheading-slider">{PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}>
+ <input className="presBox-inputNumber" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />
+ <span>%</span>
</div>
</div>
- {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Transition Time</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
- </div>
- </div>
- {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)}
- <div className="slider-headers">
- <div className="slider-text">Fast</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Slow</div>
- </div>
{/* Easing function */}
- <Dropdown
- color={SnappingManager.userColor}
- formLabel="Easing Function"
- closeOnSelect
- items={easeItems}
- selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'}
- setSelectedVal={val => {
- if (typeof val === 'string') {
- if (val !== 'custom') {
- this.setEaseFunc(this.activeItem, val);
- } else {
- this.setBezierEditorVisibility(true);
- this.setEaseFunc(this.activeItem, TIMING_DEFAULT_MAPPINGS.ease);
- }
- }
- }}
- dropdownType={DropdownType.SELECT}
- type={Type.TERT}
- />
- {/* Custom */}
- <div
- className="presBox-show-hide-dropdown"
- style={{ alignSelf: 'flex-start' }}
- onClick={e => {
- e.stopPropagation();
- this.setBezierEditorVisibility(!this.showBezierEditor);
- }}>
- {`${this.showBezierEditor ? 'Hide' : 'Show'} Timing Editor`}
- <FontAwesomeIcon icon={this.showBezierEditor ? 'chevron-up' : 'chevron-down'} />
- </div>
+ {!this.showEaseFunctions ? null : (
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Timing"
+ formLabelPlacement="left"
+ closeOnSelect
+ items={easeItems}
+ selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'}
+ setSelectedVal={val => typeof val === 'string' && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+ )}
</div>
</div>
{/* Cubic bezier editor */}
- {this.showBezierEditor && (
- <div className="presBox-option-block" style={{ paddingTop: 0 }}>
- <p className="presBox-submenu-label" style={{ alignSelf: 'flex-start' }}>
- Custom Timing Function
- </p>
+ {!this.showEaseFunctions || !StrCast(activeItem.presentation_easeFunc).includes('cubic-bezier') ? null : (
+ <div className="presBox-option-block" style={{ paddingTop: 0, alignItems: 'center' }}>
<CubicBezierEditor setFunc={this.setBezierControlPoints} currPoints={this.currCPoints} />
</div>
)}
- {/* This chatbox is for getting slide effect transition suggestions from gpt and visualizing them */}
- <div className="presBox-gpt-chat">
- Effects
- <div className="pres-chat">
- <div className="pres-chatbox-container">
- <ReactTextareaAutosize
- placeholder="Customize prompt for effect suggestions. Leave blank for random results."
- className="pres-chatbox"
- value={this.animationChat}
- onChange={e => {
- this.setAnimationChat(e.target.value);
- }}
- onKeyDown={e => {
- this.stopDictation();
- e.stopPropagation();
- }}
- />
- </div>
- <Button
- style={{ alignSelf: 'flex-end' }}
- text="Send"
- type={Type.TERT}
- icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
- iconPlacement="right"
- color={SnappingManager.userVariantColor}
- onClick={this.customizeAnimations}
- />
- </div>
- </div>
-
<div
className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
- onPointerDown={StopEvent}
- onPointerUp={StopEvent}
+ onPointerDown={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
onClick={action(e => {
e.stopPropagation();
this._openMovementDropdown = false;
@@ -2011,214 +1974,169 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._openBulletEffectDropdown = false;
})}>
<div className="presBox-option-block">
- Click on a box to apply the effect.
- <div className="presBox-option-block presBox-option-center">
- {/* Preview Animations */}
- <div className="presBox-effects">
- {this.generatedAnimations.map((elem, i) => (
- <div
- // eslint-disable-next-line react/no-array-index-key
- key={i}
- className="presBox-effect-container"
- onClick={() => {
- this.updateEffect(elem.effect, false);
- this.updateEffectDirection(elem.direction);
- this.updateEffectTiming(this.activeItem, {
- type: SpringType.CUSTOM,
- stiffness: elem.stiffness,
- damping: elem.damping,
- mass: elem.mass,
- });
- }}>
- <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite>
- <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} />
- </SlideEffect>
- </div>
- ))}
+ {/* Effect dropdown */}
+ <div style={{ display: 'flex' }}>
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Effect"
+ toolTip="Animation effect to apply when transitiong to slide"
+ formLabelPlacement="left"
+ closeOnSelect
+ items={effectItems}
+ selectedVal={effect?.toString()}
+ setSelectedVal={val => {
+ this.updateEffect(val as PresEffect, false);
+ // set default spring options for that effect
+ this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]);
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+
+ <div
+ className={`ribbon-toggle ${this._showAIGallery ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: this._showAIGallery ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => this.setShowAIGalleryVisibilty(!this._showAIGallery)}>
+ MORE
</div>
</div>
- {/* Effect dropdown */}
- <Dropdown
- color={SnappingManager.userColor}
- formLabel="Slide Effect"
- closeOnSelect
- items={effectItems}
- selectedVal={effect?.toString()}
- setSelectedVal={val => {
- this.updateEffect(val as PresEffect, false);
- // set default spring options for that effect
- this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]);
- }}
- dropdownType={DropdownType.SELECT}
- type={Type.TERT}
- />
- {/* Effect direction */}
- {/* Only applies to certain effects */}
- {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && (
- <>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Effect direction</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- {StrCast(this.activeItem.presentation_effectDirection)}
- </div>
- </div>
- <div className="presBox-icon-list">
- <IconButton
- type={Type.TERT}
- color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
- tooltip="Left"
- icon={<FaArrowRight size="16px" />}
- onClick={() => this.updateEffectDirection(PresEffectDirection.Left)}
- />
- <IconButton
- type={Type.TERT}
- color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
- tooltip="Right"
- icon={<FaArrowLeft size="16px" />}
- onClick={() => this.updateEffectDirection(PresEffectDirection.Right)}
- />
- {effect !== PresEffect.Roll && (
- <>
+
+ {this.aiEffects}
+ <div className="presBox-gpt-chat">
+ {/* Effect direction */}
+ {/* Only applies to certain effects */}
+ {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && (
+ <div className="ribbon-doubleButton">
+ <div className="presBox-subheading">DIRECTION</div>
+ <div style={{ width: '100%' }}>
+ <div className="presBox-icon-list" style={{ width: 'fit-content', margin: 'auto' }}>
<IconButton
type={Type.TERT}
- color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
- tooltip="Top"
- icon={<FaArrowDown size="16px" />}
- onClick={() => this.updateEffectDirection(PresEffectDirection.Top)}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Left"
+ icon={<FaArrowRight size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Left)}
/>
<IconButton
type={Type.TERT}
- color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
- tooltip="Bottom"
- icon={<FaArrowUp size="16px" />}
- onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)}
- />
- </>
- )}
- </div>
- </>
- )}
- {/* Spring settings */}
- {/* No spring settings for jackinthebox (lightspeed) */}
- {effect !== PresEffect.Lightspeed && (
- <>
- <Dropdown
- color={SnappingManager.userColor}
- formLabel="Effect Timing"
- closeOnSelect
- items={effectTimings}
- selectedVal={timingConfig.type}
- setSelectedVal={val => {
- this.updateEffectTiming(activeItem, {
- type: val as SpringType,
- ...springMappings[val],
- });
- }}
- dropdownType={DropdownType.SELECT}
- type={Type.TERT}
- />
- <div
- className="presBox-show-hide-dropdown"
- onClick={e => {
- e.stopPropagation();
- this.setSpringEditorVisibility(!this.showSpringEditor);
- }}>
- {`${this.showSpringEditor ? 'Hide' : 'Show'} Spring Settings`}
- <FontAwesomeIcon icon={this.showSpringEditor ? 'chevron-up' : 'chevron-down'} />
- </div>
- {this.showSpringEditor && (
- <>
- <div>Tension</div>
- <div
- onPointerDown={e => {
- e.stopPropagation();
- }}>
- <Slider
- min={1}
- max={1000}
- step={5}
- size="small"
- value={timingConfig.stiffness}
- onChange={(e, val) => {
- if (!timingConfig) return;
- this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number });
- }}
- valueLabelDisplay="auto"
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Right"
+ icon={<FaArrowLeft size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Right)}
/>
+ {effect !== PresEffect.Roll && (
+ <>
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Top"
+ icon={<FaArrowDown size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Top)}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Bottom"
+ icon={<FaArrowUp size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)}
+ />
+ </>
+ )}
</div>
- <div>Damping</div>
- <div
- onPointerDown={e => {
- e.stopPropagation();
- }}>
- <Slider
- min={1}
- max={100}
- step={1}
- size="small"
- value={timingConfig.damping}
- onChange={(e, val) => {
- if (!timingConfig) return;
- this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number });
- }}
- valueLabelDisplay="auto"
- />
- </div>
- <div>Mass</div>
- <div
- onPointerDown={e => {
- e.stopPropagation();
- }}>
- <Slider
- min={1}
- max={10}
- step={1}
- size="small"
- value={timingConfig.mass}
- onChange={(e, val) => {
- if (!timingConfig) return;
- this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number });
- }}
- valueLabelDisplay="auto"
- />
- </div>
- Preview Effect
- <div className="presBox-option-block presBox-option-center">
- <div className="presBox-effect-container">
- <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig} infinite>
- <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} />
- </SlideEffect>
- </div>
- </div>
- </>
- )}
- </>
- )}
+ </div>
+ </div>
+ )}
+ {![PresEffect.Lightspeed, PresEffect.Fade, PresEffect.None, ''].includes(effect) && (
+ <>
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Springiness"
+ formLabelPlacement="left"
+ closeOnSelect
+ items={effectTimings}
+ selectedVal={timingConfig.type}
+ setSelectedVal={val => this.updateEffectTiming(activeItem, { type: val as SpringType, ...springMappings[val] })}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+
+ <div style={{ display: SnappingManager.PropertiesWidth < 1 ? 'none' : undefined }}>
+ {/* No spring settings for jackinthebox (lightspeed) */}
+ {StrCast(activeItem.presentation_effectTiming).includes('custom') && effect !== PresEffect.None && (
+ <>
+ <div className="presBox-springSlider">
+ <span>Tension</span>
+ <div onPointerDown={e => e.stopPropagation()}>
+ {/* prettier-ignore */}
+ <Slider min={1} max={1000} step={5} size="small"
+ value={timingConfig.stiffness}
+ onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number })}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ <div className="presBox-springSlider">
+ <span>Damping</span>
+ <div onPointerDown={e => e.stopPropagation()}>
+ {/* prettier-ignore */}
+ <Slider min={1} max={100} step={1} size="small"
+ value={timingConfig.damping}
+ onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number })}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ <div className="presBox-springSlider">
+ <span>Mass</span>
+ <div onPointerDown={e => e.stopPropagation()}>
+ {/* prettier-ignore */}
+ <Slider min={1} max={10} step={1} size="small"
+ value={timingConfig.mass}
+ onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number })}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ </>
+ )}
+ </div>
</div>
+ </div>
- {/* Toggles */}
- <div className="presBox-option-block">
- <Toggle
- formLabel="Play Audio Annotation"
- toggleType={ToggleType.SWITCH}
- toggleStatus={BoolCast(activeItem.presentation_playAudio)}
- onClick={() => {
- activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio);
- }}
- color={SnappingManager.userColor}
- />
- <Toggle
- formLabel="Zoom Text Selections"
- toggleType={ToggleType.SWITCH}
- toggleStatus={BoolCast(activeItem.presentation_zoomText)}
- onClick={() => {
- activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
- }}
+ {[PresEffect.None, PresEffect.Fade, ''].includes(effect) ? null : (
+ <div className="presBox-previewContainer">
+ <Button
+ type={Type.TERT}
+ tooltip="show preview of slide animation effect"
+ size={Size.SMALL}
color={SnappingManager.userColor}
+ background="transparent"
+ onClick={action(() => {
+ this._showPreview = false;
+ setTimeout(action(() => { this._showPreview = true; }) ); // prettier-ignore
+ })}
+ text="Preview Effect"
/>
- <Button text="Apply to all" type={Type.TERT} color={SnappingManager.userVariantColor} onClick={() => this.applyTo(this.childDocs)} />
+ <div className="presBox-option-block presBox-option-center">
+ <div className="presBox-effect-container">
+ {!this._showPreview ? null : (
+ <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig}>
+ <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} />
+ </SlideEffect>
+ )}
+ </div>
+ </div>
</div>
- </div>
+ )}
+
+ <Button text="Apply to all slides" type={Type.TERT} color={SnappingManager.userVariantColor} onClick={() => this.applyTo(this.childDocs)} />
</>
);
}
@@ -2244,7 +2162,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div id="startTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}>
<input
className="presBox-input"
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
readOnly
value={NumCast(activeItem.config_clipStart).toFixed(2)}
@@ -2271,7 +2188,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<input
className="presBox-input"
onKeyDown={e => e.stopPropagation()}
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
readOnly
value={configClipEnd.toFixed(2)}
@@ -2612,13 +2528,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
createTemplate = (layout: string, input?: string) => {
const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
- const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' });
- const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' });
- const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' });
- const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' });
- const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' });
- const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' });
- const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' });
+ const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, text_fontSize: '24pt' });
+ const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, text_fontSize: '16pt' });
+ const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, text_fontSize: '20pt' });
+ const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, text_fontSize: '24pt' });
+ const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, text_fontSize: '14pt' });
+ const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, text_fontSize: '14pt' });
+ const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, text_fontSize: '14pt' });
// prettier-ignore
switch (layout) {
case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y });
@@ -3045,7 +2961,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="Slide">
{mode !== CollectionViewType.Invalid ? (
<CollectionView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
PanelWidth={this._props.PanelWidth}
PanelHeight={this.panelHeight}
@@ -3054,7 +2969,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
ignoreUnrendered
childDragAction={dropActionType.move}
setContentViewBox={emptyFunction}
- // childLayoutFitWidth={returnTrue}
childOpacity={returnOne}
childClickScript={PresBox.navigateToDocScript}
childLayoutTemplate={this.childLayoutTemplate}
@@ -3080,7 +2994,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
} */}
</div>
{/* presbox chatbox */}
- {this.chatActive && <div className="presBox-chatbox" />}
+ {this._chatActive && <div className="presBox-chatbox" />}
</div>
);
}
diff --git a/src/client/views/nodes/trails/SpringUtils.ts b/src/client/views/nodes/trails/SpringUtils.ts
index 73e1e14f1..044afbeb1 100644
--- a/src/client/views/nodes/trails/SpringUtils.ts
+++ b/src/client/views/nodes/trails/SpringUtils.ts
@@ -22,7 +22,14 @@ export interface SpringSettings {
}
// Overall config
-
+// Keeps these settings in sync with the AnimationSettings interface (used by gpt);
+export enum AnimationSettingsProperties {
+ effect = 'effect',
+ direction = 'direction',
+ stiffness = 'stiffness',
+ damping = 'damping',
+ mass = 'mass',
+}
export interface AnimationSettings {
effect: PresEffect;
direction: PresEffectDirection;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 5ab9b556c..11f2f7988 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 'browndash-components';
+import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from '@dash/components';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -131,12 +131,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
/**
* Creates a GPT drawing based on selected text.
*/
- gptDraw = async (e: React.PointerEvent) => {
+ gptDraw = (e: React.PointerEvent) => {
try {
SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
runInAction(() => (this._isLoading = true));
- await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
- runInAction(() => (this._isLoading = false));
+ SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true)?.then(
+ action(() => {
+ this._isLoading = false;
+ })
+ );
} catch (err) {
console.error(err);
}
@@ -150,11 +153,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.AddDrawingAnnotation(drawing);
const docData = drawing[DocData];
docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
- docData.drawingInput = opts.text;
- docData.drawingComplexity = opts.complexity;
- docData.drawingColored = opts.autoColor;
- docData.drawingSize = opts.size;
- docData.drawingData = gptRes;
+ docData.ai_drawing_input = opts.text;
+ docData.ai_drawing_complexity = opts.complexity;
+ docData.ai_drawing_colored = opts.autoColor;
+ docData.ai_drawing_size = opts.size;
+ docData.ai_drawing_data = gptRes;
+ docData.ai = 'gpt';
});
pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index d5f5f620c..f5a9f9e6a 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Type } from 'browndash-components';
-import { action, makeObservable, observable } from 'mobx';
+import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
+import { action, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
@@ -37,10 +37,8 @@ export enum GPTQuizType {
MULTIPLE = 2,
}
-interface GPTPopupProps {}
-
@observer
-export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
+export class GPTPopup extends ObservableReactComponent<object> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
private messagesEndRef: React.RefObject<HTMLDivElement>;
@@ -48,115 +46,84 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
@observable private chatMode: boolean = false;
private correlatedColumns: string[] = [];
- @observable
- public visible: boolean = false;
- @action
- public setVisible = (vis: boolean) => {
- this.visible = vis;
+ @observable public Visible: boolean = false;
+ @action public setVisible = (vis: boolean) => {
+ this.Visible = vis;
};
- @observable
- public loading: boolean = false;
- @action
- public setLoading = (loading: boolean) => {
+ @observable public loading: boolean = false;
+ @action public setLoading = (loading: boolean) => {
this.loading = loading;
};
- @observable
- public text: string = '';
- @action
- public setText = (text: string) => {
+ @observable public text: string = '';
+ @action public setText = (text: string) => {
this.text = text;
};
- @observable
- public selectedText: string = '';
- @action
- public setSelectedText = (text: string) => {
+ @observable public selectedText: string = '';
+ @action public setSelectedText = (text: string) => {
this.selectedText = text;
};
- @observable
- public dataJson: string = '';
+ @observable public dataJson: string = '';
public dataChatPrompt: string | undefined = undefined;
- @action
- public setDataJson = (text: string) => {
+ @action public setDataJson = (text: string) => {
if (text === '') this.dataChatPrompt = '';
this.dataJson = text;
};
- @observable
- public imgDesc: string = '';
- @action
- public setImgDesc = (text: string) => {
+ @observable public imgDesc: string = '';
+ @action public setImgDesc = (text: string) => {
this.imgDesc = text;
};
- @observable
- public imgUrls: string[][] = [];
- @action
- public setImgUrls = (imgs: string[][]) => {
+ @observable public imgUrls: string[][] = [];
+ @action public setImgUrls = (imgs: string[][]) => {
this.imgUrls = imgs;
};
- @observable
- public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
- @action
- public setMode = (mode: GPTPopupMode) => {
+ @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action public setMode = (mode: GPTPopupMode) => {
this.mode = mode;
};
- @observable
- public highlightRange: number[] = [];
+ @observable public highlightRange: number[] = [];
@action callSummaryApi = () => {};
- @observable
- private done: boolean = false;
- @action
- public setDone = (done: boolean) => {
+ @observable private done: boolean = false;
+ @action public setDone = (done: boolean) => {
this.done = done;
this.chatMode = false;
};
- @observable
- private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl
-
- @action
- public setSortDone = (done: boolean) => {
- this.sortDone = done;
- };
-
// change what can be a ref into a ref
- @observable
- private sidebarId: string = '';
- @action
- public setSidebarId = (id: string) => {
+ @observable private sidebarId: string = '';
+ @action public setSidebarId = (id: string) => {
this.sidebarId = id;
};
- @observable
- private imgTargetDoc: Doc | undefined;
- @action
- public setImgTargetDoc = (anchor: Doc) => {
+ @observable private imgTargetDoc: Doc | undefined;
+ @action public setImgTargetDoc = (anchor: Doc) => {
this.imgTargetDoc = anchor;
};
- @observable
- private textAnchor: Doc | undefined;
- @action
- public setTextAnchor = (anchor: Doc) => {
+ @observable private textAnchor: Doc | undefined;
+ @action public setTextAnchor = (anchor: Doc) => {
this.textAnchor = anchor;
};
- @observable
- public sortDesc: string = '';
-
+ @observable public sortDesc: string = '';
@action public setSortDesc = (t: string) => {
this.sortDesc = t;
};
- @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
- @observable onQuizRandom?: () => void;
+ 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;
+ }
+
@action setCardsDoneLoading(done: boolean) {
- console.log(done + 'HI HIHI');
this.cardsDoneLoading = done;
}
@@ -186,39 +153,27 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
*/
generateQuiz = async () => {
this.setLoading(true);
- this.setSortDone(false);
- const quizType = this.quizMode;
+ await this.regenerateCallback?.();
const selected = DocumentView.SelectedDocs().lastElement();
-
- const questionText = 'Question: ' + StrCast(selected['gptInputText']);
-
- if (StrCast(selected['gptRubric']) === '') {
- const rubricText = 'Rubric: ' + (await this.generateRubric(StrCast(selected['gptInputText']), selected));
+ if (!StrCast(selected.gptRubric)) {
+ await this.generateRubric(StrCast(selected.gptInputText), selected);
}
- const rubricText = 'Rubric: ' + StrCast(selected['gptRubric']);
- const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText;
-
try {
- const res = await gptAPICall(queryText, GPTCallType.QUIZ);
- if (!res) {
- console.error('GPT call failed');
- return;
- }
- console.log(res);
- this.setQuizResp(res);
- this.conversationArray.push(res);
+ 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.setSortDone(true);
+ this.setLoading(false);
+ this.onQuizRandom?.();
+ } else {
+ console.error('GPT provided no response');
+ }
} catch (err) {
- console.error('GPT call failed');
- }
-
- if (this.onQuizRandom) {
- this.onQuizRandom();
+ console.error('GPT call failed', err);
}
};
@@ -231,10 +186,10 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
generateRubric = async (inputText: string, doc: Doc) => {
try {
const res = await gptAPICall(inputText, GPTCallType.RUBRIC);
- doc['gptRubric'] = res;
+ doc.gptRubric = res;
return res;
} catch (err) {
- console.error('GPT call failed');
+ console.error('GPT call failed', err);
}
};
@@ -244,7 +199,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
* Callback function that causes the card view to update the childpair string list
* @param callback
*/
- @action public setRegenerateCallback(callback: () => Promise<void>) {
+ @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
+ this.setCollectionDoc(collectionDoc);
this.regenerateCallback = callback;
}
@@ -262,37 +218,21 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
* Generates a response to the user's question depending on the type of their question
*/
generateCard = async () => {
- console.log(this.chatSortPrompt + 'USER PROMPT');
this.setLoading(true);
- this.setSortDone(false);
- if (this.regenerateCallback) {
- await this.regenerateCallback();
- }
+ await this.regenerateCallback?.();
try {
- // const res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
- const questionNumber = questionType.split(' ')[0];
- console.log(questionType);
- let res = '';
-
- switch (questionNumber) {
- case '1':
- case '2':
- case '4':
- res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
- break;
- case '6':
- res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
- break;
- default:
- const selected = DocumentView.SelectedDocs().lastElement();
- const questionText = StrCast(selected!['gptInputText']);
-
- res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt);
- break;
- }
+ 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) {
@@ -308,7 +248,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
// Set the extracted explanation to sortRespText
this.setSortRespText(explanation);
- this.conversationArray.push(this.sortRespText);
+ runInAction(() => this.conversationArray.push(this.sortRespText));
this.scrollToBottom();
console.log(res);
@@ -318,7 +258,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}
this.setLoading(false);
- this.setSortDone(true);
};
/**
@@ -448,7 +387,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
private getPreviewUrl = (source: string) => source.split('.').join('_m.');
- constructor(props: GPTPopupProps) {
+ constructor(props: object) {
super(props);
makeObservable(this);
GPTPopup.Instance = this;
@@ -498,9 +437,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
onClick={() => {
this.conversationArray = ['Define the selected card!'];
this.setMode(GPTPopupMode.QUIZ);
- if (this.onQuizRandom) {
- this.onQuizRandom();
- }
+ this.onQuizRandom?.();
}}
color={StrCast(Doc.UserDoc().userVariantColor)}
type={Type.TERT}
@@ -515,18 +452,25 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
</div>
);
- handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => {
+ @action
+ handleKeyPress = (e: React.KeyboardEvent, isSort: boolean) => {
if (e.key === 'Enter') {
e.stopPropagation();
if (isSort) {
this.conversationArray.push(this.chatSortPrompt);
- await this.generateCard();
- this.chatSortPrompt = '';
+ this.generateCard().then(
+ action(() => {
+ this.chatSortPrompt = '';
+ })
+ );
} else {
this.conversationArray.push(this.quizAnswer);
- await this.generateQuiz();
- this.quizAnswer = '';
+ this.generateQuiz().then(
+ action(() => {
+ this.quizAnswer = '';
+ })
+ );
}
this.scrollToBottom();
@@ -569,21 +513,22 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
sortBox = () => (
- <div style={{ height: '80%' }}>
+ <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>}
+ {
+ !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>
- </div>
- ) : this.mode === GPTPopupMode.CARD ? (
- this.cardMenu()
- ) : (
- this.cardActual(this.mode)
- ) // Call the functions to render JSX
+ ) : this.mode === GPTPopupMode.CARD ? (
+ this.cardMenu()
+ ) : (
+ this.cardActual(this.mode)
+ ) // Call the functions to render JSX
}
</>
</div>
@@ -741,6 +686,15 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
{(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"
@@ -777,7 +731,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}
return (
- <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
+ <div className="summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
{content}
<div className="resize-handle" />
</div>
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a225c4b59..030251762 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -7,6 +7,10 @@
left: 0;
}
+:root {
+ --devicePixelRatio: 1; // the actual value of this will be set in PDFViewer.tsx;
+}
+
.pdfViewerDash,
.pdfViewerDash-interactive {
position: absolute;
@@ -19,9 +23,16 @@
overflow-x: hidden;
transform-origin: top left;
+ .annotationLayer {
+ transform: scale(var(--devicePixelRatio));
+ }
.textLayer {
opacity: unset;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
+ transform: scale(var(--devicePixelRatio));
+ }
+ [data-main-rotation='90'] {
+ transform: scale(var(--devicePixelRatio)) rotate(90deg) translateY(-100%);
}
.textLayer ::selection {
background: #accef76a;
@@ -39,6 +50,7 @@
.page {
position: relative;
border: unset;
+ height: 100% !important;
}
.pdfViewerDash-text-selected {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 358557ad7..8728ce99c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -11,7 +11,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction } from '../../../Utils';
+import { emptyFunction, numberRange } from '../../../Utils';
import { DocUtils } from '../../documents/DocUtils';
import { SnappingManager } from '../../util/SnappingManager';
import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
@@ -30,6 +30,7 @@ import { GPTPopup } from './GPTPopup/GPTPopup';
import './PDFViewer.scss';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import ReactLoading from 'react-loading';
+import { Transform } from '../../util/Transform';
interface IViewerProps extends FieldViewProps {
pdfBox: PDFBox;
@@ -40,7 +41,7 @@ interface IViewerProps extends FieldViewProps {
pdf: Pdfjs.PDFDocumentProxy;
url: string;
sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
- loaded?: (nw: number, nh: number, np: number) => void;
+ loaded: (p: { width: number; height: number }, pages: number) => void;
// eslint-disable-next-line no-use-before-define
setPdfViewer: (view: PDFViewer) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
@@ -146,32 +147,30 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
}
};
- @observable _scrollHeight = 0;
+ @computed get _scrollHeight() {
+ return this._pageSizes.reduce((size, page) => size + page.height, 0);
+ }
- @action
- initialLoad = async () => {
+ initialLoad = () => {
+ const page0or180 = (page: { rotate: number }) => page.rotate === 0 || page.rotate === 180;
if (this._pageSizes.length === 0) {
- this._pageSizes = Array<{ width: number; height: number }>(this._props.pdf.numPages);
- await Promise.all(
- this._pageSizes.map((val, i) =>
- this._props.pdf.getPage(i + 1).then(
- action((page: Pdfjs.PDFPageProxy) => {
- const page0or180 = page.rotate === 0 || page.rotate === 180;
- this._pageSizes.splice(i, 1, {
- width: page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1],
- height: page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0],
- });
- if (i === this._props.pdf.numPages - 1) {
- this._props.loaded?.(page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1], page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], this._props.pdf.numPages);
- }
- })
- )
+ const devicePixelRatio = window.devicePixelRatio;
+ document.documentElement?.style.setProperty('--devicePixelRatio', window.devicePixelRatio.toString()); // set so that css can use this to adjust various PDFJs divs
+ Promise.all(
+ numberRange(this._props.pdf.numPages).map(i =>
+ this._props.pdf.getPage(i + 1).then(page => ({
+ width: (page.view[page0or180(page) ? 2 : 3] - page.view[page0or180(page) ? 0 : 1]) * devicePixelRatio,
+ height: (page.view[page0or180(page) ? 3 : 2] - page.view[page0or180(page) ? 1 : 0]) * devicePixelRatio,
+ }))
)
+ ).then(
+ action(pages => {
+ this._pageSizes = pages;
+ this._props.loaded(pages.lastElement(), this._props.pdf.numPages);
+ this.createPdfViewer();
+ })
);
}
- runInAction(() => {
- this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72;
- });
};
_scrollStopper: undefined | (() => void);
@@ -197,14 +196,12 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
crop = (region: Doc | undefined, addCrop?: boolean) => this._props.crop(region, addCrop);
@action
- setupPdfJsViewer = async () => {
+ setupPdfJsViewer = () => {
if (this._viewerIsSetup) return;
this._viewerIsSetup = true;
this._showWaiting = true;
this._props.setPdfViewer(this);
- await this.initialLoad();
-
- this.createPdfViewer();
+ this.initialLoad();
};
pagesinit = () => {
@@ -377,7 +374,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) {
this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document);
}
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
this._props.select(false);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this.isAnnotating = true;
@@ -533,7 +530,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
}
getScrollHeight = () => this._scrollHeight;
- scrollXf = () => this._props.ScreenToLocalTransform().translate(0, this._mainCont.current ? NumCast(this._props.layoutDoc._layout_scrollTop) : 0);
+ scrollXf = () => this._props.ScreenToLocalTransform().translate(0, this._mainCont.current ? NumCast(this._props.layoutDoc._layout_scrollTop) / 1.333 : 0);
overlayTransform = () => this.scrollXf().scale(1 / NumCast(this._props.layoutDoc._freeform_scale, 1));
panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1);
panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
@@ -554,7 +551,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
className="pdfViewerDash-overlay"
style={{
mixBlendMode,
- display: display,
+ display,
+ transform: `scale(${Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS})`,
pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
@@ -600,6 +598,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
}
savedAnnotations = () => this._savedAnnotations;
addDocumentWrapper = (doc: Doc | Doc[]) => this._props.addDocument!(doc);
+ screenToMarqueeXf = () => this.props.pdfBox.DocumentView?.()?.screenToContentsTransform().scale(Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS) ?? Transform.Identity();
render() {
TraceMobx();
return (
@@ -619,17 +618,18 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
{this.annotationLayer}
{this.overlayLayer}
{this._showWaiting ? <img alt="" className="pdfViewerDash-waiting" src="/assets/loading.gif" /> : null}
- {!this._mainCont.current || !this._annotationLayer.current ? null : (
+ {!this._mainCont.current || !this._annotationLayer.current || !this.props.pdfBox.DocumentView ? null : (
<MarqueeAnnotator
ref={this._marqueeref}
Document={this._props.Document}
getPageFromScroll={this.getPageFromScroll}
anchorMenuClick={this._props.anchorMenuClick}
scrollTop={0}
- isNativeScaled
+ annotationLayerScaling={() => Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS}
annotationLayerScrollTop={NumCast(this._props.Document._layout_scrollTop)}
addDocument={this.addDocumentWrapper}
- docView={this._props.pdfBox.DocumentView!}
+ docView={this.props.pdfBox.DocumentView}
+ screenTransform={this.screenToMarqueeXf}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
selectionText={this.selectionText}
diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx
index 78a1a92f7..49cdc6bf8 100644
--- a/src/client/views/selectedDoc/SelectedDocView.tsx
+++ b/src/client/views/selectedDoc/SelectedDocView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { ListBox } from 'browndash-components';
+import { ListBox } from '@dash/components';
import { computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
deleted file mode 100644
index f1e2e4f41..000000000
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ /dev/null
@@ -1,361 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Switch } from '@mui/material';
-import { Button } from 'browndash-components';
-import { action, makeObservable, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { AiOutlineSend } from 'react-icons/ai';
-import ReactLoading from 'react-loading';
-import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
-import { ImageCast, NumCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { Docs } from '../../documents/Documents';
-import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
-import { SettingsManager } from '../../util/SettingsManager';
-import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
-import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { FieldView } from '../nodes/FieldView';
-import './AnnotationPalette.scss';
-import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-
-interface AnnotationPaletteProps {
- Document: Doc;
-}
-
-/**
- * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette
- * is to offer an easy way for users to save then drag and drop repeated annotations onto a document.
- * These annotations can be of any annotation type and operate similarly to user templates.
- *
- * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can
- * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing
- * to choose from. These drawings can then be saved to the palette as annotations.
- */
-@observer
-export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
- @observable private _paletteMode: 'create' | 'view' = 'view';
- @observable private _userInput: string = '';
- @observable private _isLoading: boolean = false;
- @observable private _canInteract: boolean = true;
- @observable private _showRegenerate: boolean = false;
- @observable private _docView: DocumentView | null = null;
- @observable private _docCarouselView: DocumentView | null = null;
- @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
- private _gptRes: string[] = [];
-
- constructor(props: AnnotationPaletteProps) {
- super(props);
- makeObservable(this);
- }
-
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(AnnotationPalette, fieldKey);
- }
-
- Contains = (view: DocumentView) => {
- return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
- };
-
- return170 = () => 170;
-
- @action
- handleKeyPress = async (event: React.KeyboardEvent) => {
- if (event.key === 'Enter') {
- await this.generateDrawings();
- }
- };
-
- @action
- setPaletteMode = (mode: 'create' | 'view') => {
- this._paletteMode = mode;
- };
-
- @action
- setUserInput = (input: string) => {
- if (!this._isLoading) this._userInput = input;
- };
-
- @action
- setDetail = (detail: number) => {
- if (this._canInteract) this._opts.complexity = detail;
- };
-
- @action
- setColor = (autoColor: boolean) => {
- if (this._canInteract) this._opts.autoColor = autoColor;
- };
-
- @action
- setSize = (size: number) => {
- if (this._canInteract) this._opts.size = size;
- };
-
- @action
- resetPalette = (changePaletteMode: boolean) => {
- if (changePaletteMode) this.setPaletteMode('view');
- this.setUserInput('');
- this.setDetail(5);
- this.setColor(true);
- this.setSize(200);
- this._showRegenerate = false;
- this._canInteract = true;
- this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
- this._gptRes = [];
- this._props.Document[DocData].data = undefined;
- };
-
- /**
- * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this
- * preview is dragged onto a parent document, a copy of that document is added as an annotation.
- */
- public static addToPalette = async (doc: Doc) => {
- if (!doc.savedAsAnno) {
- const docView = DocumentView.getDocumentView(doc);
- await docView?.ComponentView?.updateIcon?.(true);
- const { clone } = await Doc.MakeClone(doc);
- clone.title = doc.title;
- const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
- Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image));
- doc.savedAsAnno = true;
- }
- };
-
- public static getIcon(group: Doc) {
- const docView = DocumentView.getDocumentView(group);
- if (docView) {
- docView.ComponentView?.updateIcon?.(true);
- return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
- }
- return undefined;
- }
-
- /**
- * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
- * the annotation palette.
- */
- @undoBatch
- generateDrawings = action(async () => {
- this._isLoading = true;
- this._props.Document[DocData].data = undefined;
- for (let i = 0; i < 3; i++) {
- try {
- SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
- this._canInteract = false;
- if (this._showRegenerate) {
- await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
- } else {
- await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
- }
- } catch (e) {
- console.log('Error generating drawing', e);
- }
- }
- this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
- this._userInput = '';
- this._isLoading = false;
- this._showRegenerate = true;
- });
-
- @action
- addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
- this._gptRes.push(gptRes);
- drawing[DocData].freeform_fitContentsToBox = true;
- Doc.AddDocToList(this._props.Document, 'data', drawing);
- };
-
- /**
- * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata.
- * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user
- * presses the "save drawing" button.
- */
- saveDrawing = async () => {
- const cIndex = NumCast(this._props.Document.carousel_index);
- const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
- const docData = focusedDrawing[DocData];
- docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
- docData.drawingInput = this._opts.text;
- docData.drawingComplexity = this._opts.complexity;
- docData.drawingColored = this._opts.autoColor;
- docData.drawingSize = this._opts.size;
- docData.drawingData = this._gptRes[cIndex];
- docData.width = this._opts.size;
- docData.x = this._opts.x;
- docData.y = this._opts.y;
- await AnnotationPalette.addToPalette(focusedDrawing);
- this.resetPalette(true);
- };
-
- render() {
- return (
- <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
- {this._paletteMode === 'view' && (
- <>
- <DocumentView
- ref={r => (this._docView = r)}
- Document={Doc.MyAnnos}
- addDocument={undefined}
- addDocTab={DocumentViewInternal.addDocTabFunc}
- pinToPres={DocumentView.PinDoc}
- containerViewPath={returnEmptyDocViewList}
- styleProvider={DefaultStyleProvider}
- removeDocument={returnFalse}
- ScreenToLocalTransform={Transform.Identity}
- PanelWidth={this.return170}
- PanelHeight={this.return170}
- renderDepth={0}
- isContentActive={returnTrue}
- focus={emptyFunction}
- whenChildContentsActiveChanged={emptyFunction}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- />
- <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
- </>
- )}
- {this._paletteMode === 'create' && (
- <>
- <div className="palette-create">
- <input
- className="palette-create-input"
- aria-label="label-input"
- id="new-label"
- type="text"
- value={this._userInput}
- onChange={e => {
- this.setUserInput(e.target.value);
- }}
- placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
- onKeyDown={this.handleKeyPress}
- />
- <Button
- style={{ alignSelf: 'flex-end' }}
- tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
- icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
- iconPlacement="right"
- color={SettingsManager.userColor}
- onClick={this.generateDrawings}
- />
- </div>
- <div className="palette-create-options">
- <div className="palette-color">
- Color
- <Switch
- sx={{
- '& .MuiSwitch-switchBase.Mui-checked': {
- color: SettingsManager.userColor,
- },
- '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
- backgroundColor: SettingsManager.userVariantColor,
- },
- }}
- defaultChecked={true}
- value={this._opts.autoColor}
- size="small"
- onChange={() => this.setColor(!this._opts.autoColor)}
- />
- </div>
- <div className="palette-detail">
- Detail
- <Slider
- sx={{
- '& .MuiSlider-thumb': {
- color: SettingsManager.userColor,
- '&.Mui-focusVisible, &:hover, &.Mui-active': {
- boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
- },
- },
- '& .MuiSlider-track': {
- color: SettingsManager.userVariantColor,
- },
- '& .MuiSlider-rail': {
- color: SettingsManager.userColor,
- },
- }}
- style={{ width: '80%' }}
- min={1}
- max={10}
- step={1}
- size="small"
- value={this._opts.complexity}
- onChange={(e, val) => {
- this.setDetail(val as number);
- }}
- valueLabelDisplay="auto"
- />
- </div>
- <div className="palette-size">
- Size
- <Slider
- sx={{
- '& .MuiSlider-thumb': {
- color: SettingsManager.userColor,
- '&.Mui-focusVisible, &:hover, &.Mui-active': {
- boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
- },
- },
- '& .MuiSlider-track': {
- color: SettingsManager.userVariantColor,
- },
- '& .MuiSlider-rail': {
- color: SettingsManager.userColor,
- },
- }}
- style={{ width: '80%' }}
- min={50}
- max={500}
- step={10}
- size="small"
- value={this._opts.size}
- onChange={(e, val) => {
- this.setSize(val as number);
- }}
- valueLabelDisplay="auto"
- />
- </div>
- </div>
- <DocumentView
- ref={r => (this._docCarouselView = r)}
- Document={this._props.Document}
- addDocument={undefined}
- addDocTab={DocumentViewInternal.addDocTabFunc}
- pinToPres={DocumentView.PinDoc}
- containerViewPath={returnEmptyDocViewList}
- styleProvider={DefaultStyleProvider}
- removeDocument={returnFalse}
- ScreenToLocalTransform={Transform.Identity}
- PanelWidth={this.return170}
- PanelHeight={this.return170}
- renderDepth={1}
- isContentActive={returnTrue}
- focus={emptyFunction}
- whenChildContentsActiveChanged={emptyFunction}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- />
- <div className="palette-buttons">
- <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
- <div className="palette-save-reset">
- <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
- <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
- </div>
- </div>
- </>
- )}
- </div>
- );
- }
-}
-
-Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
- layout: { view: AnnotationPalette, dataField: 'data' },
- options: { acl: '' },
-});
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
new file mode 100644
index 000000000..d0a840883
--- /dev/null
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -0,0 +1,79 @@
+import { imageUrlToBase64 } from '../../../ClientUtils';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { DocCast, ImageCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { Upload } from '../../../server/SharedMediaTypes';
+import { gptDescribeImage } from '../../apis/gpt/GPT';
+import { Docs } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/OpenWhere';
+import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
+
+const DashDropboxId = '2m86iveqdr9vzsa';
+export class DrawingFillHandler {
+ static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => {
+ const docData = drawing[DocData];
+ const tags = StrListCast(docData.tags).map(tag => tag.slice(1));
+ const styles = tags.filter(tag => FireflyStylePresets.has(tag));
+ const styleDocs = !Doc.Links(drawing).length
+ ? styleDoc && !tags.length
+ ? [styleDoc]
+ : []
+ : Doc.Links(drawing)
+ .map(link => Doc.getOppositeAnchor(link, drawing))
+ .map(anchor => anchor && DocCast(anchor.embedContainer));
+ const styleUrl = await DocumentView.GetDocImage(styleDocs.filter(doc => doc?.data instanceof ImageField).lastElement())?.then(styleImg => {
+ const hrefParts = ImageCast(styleImg).url.href.split('.');
+ return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
+ });
+ DocumentView.GetDocImage(drawing)?.then(imageField => {
+ if (imageField) {
+ const aspectRatio = (drawing.width as number) / (drawing.height as number);
+ let dims: { width: number; height: number };
+ if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) {
+ dims = FireflyDimensionsMap[FireflyImageDimensions.Widescreen];
+ } else if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) {
+ dims = FireflyDimensionsMap[FireflyImageDimensions.Landscape];
+ } else if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) {
+ dims = FireflyDimensionsMap[FireflyImageDimensions.Portrait];
+ } else {
+ dims = FireflyDimensionsMap[FireflyImageDimensions.Square];
+ }
+ const { href } = ImageCast(imageField).url;
+ const hrefParts = href.split('.');
+ const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
+ return imageUrlToBase64(structureUrl)
+ .then(gptDescribeImage)
+ .then((prompt, newPrompt = user_prompt || prompt) =>
+ Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
+ .then((infos: Upload.ImageInformation[]) => {
+ const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
+ drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
+ infos.map(info =>
+ Doc.AddDocToList(
+ genratedDocs,
+ undefined,
+ Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+ ai: 'firefly',
+ title: newPrompt,
+ ai_firefly_prompt: newPrompt,
+ _width: 500,
+ data_nativeWidth: info.nativeWidth,
+ data_nativeHeight: info.nativeHeight,
+ })
+ )
+ );
+ if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
+ })
+ .catch(e => {
+ if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().replace(/^[^"]*/, ''))) {
+ window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus();
+ } else alert(e.toString());
+ })
+ ); // prettier-ignore:q
+ }
+ });
+ };
+}
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
new file mode 100644
index 000000000..1f1781617
--- /dev/null
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -0,0 +1,49 @@
+export interface FireflyImageData {
+ prompt: string;
+ seed: number | undefined;
+ pathname: string;
+ href?: string;
+}
+
+export enum FireflyImageDimensions {
+ Square = 'square',
+ Landscape = 'landscape',
+ Portrait = 'portrait',
+ Widescreen = 'widescreen',
+}
+
+export const FireflyDimensionsMap = {
+ square: { width: 2048, height: 2048 },
+ landscape: { width: 2304, height: 1792 },
+ portrait: { width: 1792, height: 2304 },
+ widescreen: { width: 2688, height: 1536 },
+};
+
+export const AspectRatioLimits = {
+ square: 1,
+ landscape: 1.167,
+ portrait: 0.875,
+ widescreen: 1.472,
+};
+
+// prettier-ignore
+export const FireflyStylePresets =
+ new Set<string>(['graphic', 'wireframe',
+ 'vector_look','bw','cool_colors','golden','monochromatic','muted_color','toned_image','vibrant_colors','warm_tone','closeup',
+ 'knolling','landscape_photography','macrophotography','photographed_through_window','shallow_depth_of_field','shot_from_above',
+ 'shot_from_below','surface_detail','wide_angle','beautiful','bohemian','chaotic','dais','divine','eclectic','futuristic','kitschy',
+ 'nostalgic','simple','antique_photo','bioluminescent','bokeh','color_explosion','dark','faded_image','fisheye','gomori_photography',
+ 'grainy_film','iridescent','isometric','misty','neon','otherworldly_depiction','ultraviolet','underwater', 'backlighting',
+ 'dramatic_light', 'golden_hour', 'harsh_light','long','low_lighting','multiexposure','studio_light','surreal_lighting',
+ '3d_patterns','charcoal','claymation','fabric','fur','guilloche_patterns','layered_paper','marble_sculpture','made_of_metal',
+ 'origami','paper_mache','polka','strange_patterns','wood_carving','yarn','art_deco','art_nouveau','baroque','bauhaus',
+ 'constructivism','cubism','cyberpunk','fantasy','fauvism', 'film_noir','glitch_art','impressionism','industrialism','maximalism',
+ 'minimalism','modern_art','modernism','neo','pointillism','psychedelic','science_fiction','steampunk','surrealism','synthetism',
+ 'synthwave','vaporwave','acrylic_paint','bold_lines','chiaroscuro','color_shift_art','daguerreotype','digital_fractal',
+ 'doodle_drawing','double_exposure_portrait','fresco','geometric_pen','halftone','ink','light_painting','line_drawing','linocut',
+ 'oil_paint','paint_spattering','painting','palette_knife','photo_manipulation','scribble_texture','sketch','splattering',
+ 'stippling_drawing','watercolor','3d','anime','cartoon','cinematic','comic_book','concept_art','cyber_matrix','digital_art',
+ 'flat_design','geometric','glassmorphism','glitch_graphic','graffiti','hyper_realistic','interior_design','line_gradient',
+ 'low_poly','newspaper_collage','optical_illusion','pattern_pixel','pixel_art','pop_art','product_photo','psychedelic_background',
+ 'psychedelic_wonderland','scandinavian','splash_images','stamp','trompe_loeil'
+ ]);
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
index 0e8bd3349..cca7d77c7 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.scss
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -1,44 +1,77 @@
.smart-draw-handler {
position: absolute;
-}
-
-.smartdraw-input {
- color: black;
-}
-.smartdraw-options {
- display: flex;
- flex-direction: row;
- justify-content: space-around;
-
- .auto-color {
+ .smart-draw-main {
display: flex;
- flex-direction: column;
- justify-content: center;
- width: 30%;
+ flex-direction: row;
+
+ .smartdraw-input {
+ color: black;
+ margin: auto;
+ }
}
- .complexity {
+ .smartdraw-output-options {
display: flex;
- flex-direction: column;
+ flex-direction: row;
justify-content: center;
- width: 31%;
+ margin-top: 3px;
}
- .size {
+ .smartdraw-options-container {
+ width: 265px;
+ padding: 5px;
+ font-weight: bolder;
+ text-align: center;
display: flex;
flex-direction: column;
- justify-content: center;
- width: 39%;
- .size-slider {
- width: 80%;
+ .smartdraw-options {
+ font-weight: normal;
+ margin-top: 5px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+
+ .smartdraw-auto-color {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 30%;
+ margin-top: 3px;
+ }
+
+ .smartdraw-complexity {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 31%;
+ margin-top: 3px;
+ }
+
+ .smartdraw-size {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 39%;
+ margin-top: 3px;
+ }
+ }
+
+ .smartdraw-dimensions {
+ font-weight: normal;
+ margin-top: 7px;
+ padding-left: 25px;
}
}
-}
-.regenerate-box,
-.edit-box {
- display: flex;
- flex-direction: row;
+ .smartdraw-slider {
+ width: 65px;
+ }
+
+ .regenerate-box,
+ .edit-box {
+ display: flex;
+ flex-direction: row;
+ }
}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index b4635673c..7db9ef133 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Switch } from '@mui/material';
-import { Button, IconButton } from 'browndash-components';
+import { Checkbox, FormControlLabel, Radio, RadioGroup, Slider, Switch } from '@mui/material';
+import { Button, IconButton } from '@dash/components';
import { action, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
@@ -21,8 +21,12 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
import { InkingStroke } from '../InkingStroke';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
import './SmartDrawHandler.scss';
+import { Networking } from '../../Network';
+import { OpenWhere } from '../nodes/OpenWhere';
+import { FireflyDimensionsMap, FireflyImageDimensions, FireflyImageData } from './FireflyConstants';
+import { DocumentType } from '../../documents/DocumentTypes';
export interface DrawingOptions {
text: string;
@@ -55,22 +59,28 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
private _lastResponse: string = '';
- private _selectedDoc: Doc | undefined = undefined;
+ private _selectedDocs: Doc[] = [];
private _errorOccurredOnce = false;
@observable private _display: boolean = false;
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
+ @observable private _scale: number = 0;
@observable private _yRelativeToTop: boolean = true;
@observable private _isLoading: boolean = false;
+
@observable private _userInput: string = '';
+ @observable private _regenInput: string = '';
@observable private _showOptions: boolean = false;
@observable private _showEditBox: boolean = false;
@observable private _complexity: number = 5;
@observable private _size: number = 200;
@observable private _autoColor: boolean = true;
- @observable private _regenInput: string = '';
+ @observable private _imgDims: FireflyImageDimensions = FireflyImageDimensions.Square;
+
@observable private _canInteract: boolean = true;
+ @observable private _generateDrawing: boolean = true;
+ @observable private _generateImage: boolean = true;
@observable public ShowRegenerate: boolean = false;
@@ -82,8 +92,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
/**
* AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e.
- CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added
- or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.)
+ CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added
+ or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.)
*/
public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction;
@@ -105,14 +115,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
y: bounds.top - inkWidth / 2,
_width: bounds.width + inkWidth,
_height: bounds.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
drawing.push(inkDoc);
@@ -122,9 +132,10 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
};
@action
- displaySmartDrawHandler = (x: number, y: number) => {
+ displaySmartDrawHandler = (x: number, y: number, scale: number) => {
[this._pageX, this._pageY] = [x, y];
this._display = true;
+ this._scale = scale;
};
/**
@@ -134,14 +145,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
*/
@action
displayRegenerate = (x: number, y: number) => {
- this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
+ this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()];
[this._pageX, this._pageY] = [x, y];
this._display = false;
this.ShowRegenerate = true;
this._showEditBox = false;
- const docData = this._selectedDoc[DocData];
+ const docData = this._selectedDocs[0][DocData];
this._lastResponse = StrCast(docData.drawingData);
- this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+ this._lastInput = { text: StrCast(docData.ai_drawing_input), complexity: NumCast(docData.ai_drawing_complexity), size: NumCast(docData.ai_drawing_size), autoColor: BoolCast(docData.ai_drawing_colored), x: this._pageX, y: this._pageY };
};
/**
@@ -191,11 +202,13 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
*/
@action
handleSendClick = async () => {
+ if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return;
this._isLoading = true;
this._canInteract = false;
if (this.ShowRegenerate) {
- await this.regenerate();
+ await this.regenerate(this._selectedDocs);
runInAction(() => {
+ this._selectedDocs = [];
this._regenInput = '';
this._showEditBox = false;
});
@@ -204,7 +217,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
this._showOptions = false;
});
try {
- await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ if (this._generateImage) {
+ await this.createImageWithFirefly(this._userInput);
+ }
+ if (this._generateDrawing) {
+ await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ }
this.hideSmartDrawHandler();
runInAction(() => {
@@ -216,7 +234,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
this._errorOccurredOnce = false;
} else {
this._errorOccurredOnce = true;
- await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ await this.handleSendClick();
}
}
}
@@ -229,59 +247,101 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
/**
* Calls GPT API to create a drawing based on user input.
*/
- @action
drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
- if (input === '') return;
- this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
- const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
- if (!res) {
- console.error('GPT call failed');
- return;
+ if (input) {
+ this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+ const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
+ if (res) {
+ const strokeData = await this.parseSvg(res, startPt, false, autoColor);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+ drawingDoc && this._selectedDocs.push(drawingDoc);
+ this._errorOccurredOnce = false;
+ return strokeData;
+ } else {
+ console.error('GPT call failed');
+ }
}
- const strokeData = await this.parseSvg(res, startPt, false, autoColor);
- const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
- drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+ return undefined;
+ };
- this._errorOccurredOnce = false;
- return strokeData;
+ /**
+ * Calls Firefly API to create an image based on user input
+ */
+ createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
+ this._lastInput.text = input;
+ const dims = FireflyDimensionsMap[this._imgDims];
+ return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
+ .then(img => {
+ const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+ if (!changeInPlace) {
+ const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+ title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+ nativeWidth: dims.width,
+ nativeHeight: dims.height,
+ ai: 'firefly',
+ ai_firefly_seed: newseed,
+ ai_firefly_prompt: input,
+ });
+ DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+ this._selectedDocs.push(imgDoc);
+ }
+ return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
+ })
+ .catch(e => alert('create image failed: ' + e.toString()));
};
/**
* Regenerates drawings with the option to add a specific regenerate prompt/request.
+ * @param doc the drawing Docs to regenerate
*/
@action
- regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+ regenerate = (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => {
if (lastInput) this._lastInput = lastInput;
if (lastResponse) this._lastResponse = lastResponse;
if (regenInput) this._regenInput = regenInput;
-
- try {
- let res;
- if (this._regenInput !== '') {
- const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
- res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
- this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
- } else {
- res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
- }
- if (!res) {
- console.error('GPT call failed');
- return;
- }
- const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
- this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc);
- const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
- drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
- return strokeData;
- } catch (err) {
- console.error('Error regenerating drawing', err);
- }
+ return Promise.all(
+ drawingDocs.map(async doc => {
+ switch (doc.type) {
+ case DocumentType.IMG:
+ if (this._regenInput) {
+ // if (this._selectedDoc) {
+ const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
+ return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace);
+ // }
+ }
+ return this.createImageWithFirefly(this._lastInput.text || StrCast(doc.ai_firefly_prompt), undefined, changeInPlace);
+ case DocumentType.COL: {
+ try {
+ let res;
+ if (this._regenInput) {
+ const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+ res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+ this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+ } else {
+ res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+ }
+ if (res) {
+ const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+ } else {
+ console.error('GPT call failed');
+ }
+ } catch (err) {
+ console.error('Error regenerating drawing', err);
+ }
+ break;
+ }
+ }
+ })
+ );
};
/**
* Parses the svg code that GPT returns into Bezier curves, with coordinates and colors.
*/
- @action
parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
if (svg) {
@@ -292,7 +352,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
svgStrokes.forEach(child => {
const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
strokeData.push([
- convertedBezier.map(point => ({ X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 })),
+ convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })),
(regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
(regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
]);
@@ -342,10 +402,110 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
});
}, 'color strokes');
- renderDisplay() {
+ renderGenerateOutputOptions = () => (
+ <div className="smartdraw-output-options">
+ <div className="drawing-checkbox">
+ Generate Ink
+ <Checkbox
+ sx={{
+ color: 'white',
+ '&.Mui-checked': {
+ color: SettingsManager.userVariantColor,
+ },
+ }}
+ checked={this._generateDrawing}
+ onChange={() => this._canInteract && (this._generateDrawing = !this._generateDrawing)}
+ />
+ </div>
+ <div className="image-checkbox">
+ Generate Image
+ <Checkbox
+ sx={{
+ color: 'white',
+ '&.Mui-checked': {
+ color: SettingsManager.userVariantColor,
+ },
+ }}
+ checked={this._generateImage}
+ onChange={() => this._canInteract && (this._generateImage = !this._generateImage)}
+ />
+ </div>
+ </div>
+ );
+
+ renderGenerateDrawing = () => (
+ <div className="smartdraw-options-container">
+ Drawing Options
+ <div className="smartdraw-options">
+ <div className="smartdraw-auto-color">
+ Auto color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor },
+ }}
+ defaultChecked={true}
+ value={this._autoColor}
+ size="small"
+ onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
+ />
+ </div>
+ <div className="smartdraw-complexity">
+ Complexity
+ <Slider
+ className="smartdraw-slider"
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+ }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._complexity}
+ onChange={action((e, val) => this._canInteract && (this._complexity = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div className="smartdraw-size">
+ Size (in pixels)
+ <Slider
+ className="smartdraw-slider"
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } },
+ }}
+ min={50}
+ max={700}
+ step={10}
+ size="small"
+ value={this._size}
+ onChange={action((e, val) => this._canInteract && (this._size = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ renderGenerateImage = () => (
+ <div className="smartdraw-options-container">
+ Image Options
+ <div className="smartdraw-dimensions">
+ <RadioGroup row defaultValue="square" sx={{ alignItems: 'center' }}>
+ {Object.values(FireflyImageDimensions).map(dim => (
+ <FormControlLabel sx={{ width: '40%' }} key={dim} value={dim} control={<Radio />} onChange={() => this._canInteract && (this._imgDims = dim)} label={dim} />
+ ))}
+ </RadioGroup>
+ </div>
+ </div>
+ );
+
+ renderDisplay = () => {
return (
<div
- id="label-handler"
className="smart-draw-handler"
style={{
display: this._display ? '' : 'none',
@@ -354,7 +514,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
background: SettingsManager.userBackgroundColor,
color: SettingsManager.userColor,
}}>
- <div>
+ <div className="smart-draw-main">
<IconButton
tooltip="Cancel"
onClick={() => {
@@ -385,107 +545,65 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
/>
</div>
{this._showOptions && (
- <div className="smartdraw-options">
- <div className="auto-color">
- Auto color
- <Switch
- sx={{
- '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor },
- '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor },
- }}
- defaultChecked={true}
- value={this._autoColor}
- size="small"
- onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
- />
- </div>
- <div className="complexity">
- Complexity
- <Slider
- sx={{
- '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
- '& .MuiSlider-rail': { color: SettingsManager.userColor },
- '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
- }}
- style={{ width: '80%' }}
- min={1}
- max={10}
- step={1}
- size="small"
- value={this._complexity}
- onChange={action((e, val) => this._canInteract && (this._complexity = val as number))}
- valueLabelDisplay="auto"
- />
- </div>
- <div className="size">
- Size (in pixels)
- <Slider
- className="size-slider"
- sx={{
- '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
- '& .MuiSlider-rail': { color: SettingsManager.userColor },
- '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } },
- }}
- min={50}
- max={700}
- step={10}
- size="small"
- value={this._size}
- onChange={action((e, val) => this._canInteract && (this._size = val as number))}
- valueLabelDisplay="auto"
- />
- </div>
+ <div>
+ {this.renderGenerateOutputOptions()}
+ {this._generateDrawing ? this.renderGenerateDrawing() : null}
+ {this._generateImage ? this.renderGenerateImage() : null}
</div>
)}
</div>
);
- }
+ };
- renderRegenerate() {
- return (
- <div
- className="smart-draw-handler"
- style={{
- left: this._pageX,
- ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
- background: SettingsManager.userBackgroundColor,
- color: SettingsManager.userColor,
- }}>
- <div className="regenerate-box">
- <IconButton
- tooltip="Regenerate"
- icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
- color={SettingsManager.userColor}
- onClick={this.handleSendClick}
- />
- <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
- {this._showEditBox && (
- <div className="edit-box">
- <input
- aria-label="Edit instructions input"
- className="smartdraw-input"
- type="text"
- value={this._regenInput}
- onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
- onKeyDown={this.handleKeyPress}
- placeholder="Edit instructions"
- />
- <Button
- style={{ alignSelf: 'flex-end' }}
- text="Send"
- icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
- iconPlacement="right"
- color={SettingsManager.userColor}
- onClick={this.handleSendClick}
- />
- </div>
- )}
- </div>
+ renderRegenerateEditBox = () => (
+ <div className="edit-box">
+ <input
+ aria-label="Edit instructions input"
+ className="smartdraw-input"
+ type="text"
+ value={this._regenInput}
+ onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+ onKeyDown={this.handleKeyPress}
+ placeholder="Edit instructions"
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ );
+
+ renderRegenerate = () => (
+ <div
+ className="smart-draw-handler"
+ style={{
+ left: this._pageX,
+ ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div className="regenerate-box">
+ <IconButton
+ tooltip="Regenerate"
+ icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
+ {this._showEditBox ? this.renderRegenerateEditBox() : null}
</div>
- );
- }
+ </div>
+ );
render() {
- return this._display ? this.renderDisplay() : this.ShowRegenerate ? this.renderRegenerate() : null;
+ return this._display
+ ? this.renderDisplay() //
+ : this.ShowRegenerate
+ ? this.renderRegenerate()
+ : null;
}
}
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/StickerPalette.scss
index 4f11e8afc..ca99410cf 100644
--- a/src/client/views/smartdraw/AnnotationPalette.scss
+++ b/src/client/views/smartdraw/StickerPalette.scss
@@ -1,4 +1,4 @@
-.annotation-palette {
+.sticker-palette {
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
new file mode 100644
index 000000000..d5307974f
--- /dev/null
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -0,0 +1,352 @@
+import { Button } from '@dash/components';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import ReactLoading from 'react-loading';
+import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction, numberRange } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
+import { SettingsManager } from '../../util/SettingsManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import './StickerPalette.scss';
+
+interface StickerPaletteProps {
+ Document: Doc;
+}
+
+enum StickerPaletteMode {
+ create,
+ view,
+}
+
+/**
+ * The StickerPalette can be toggled in the lightbox view of a document. The goal of the palette
+ * is to offer an easy way for users to create stickers and drag and drop them onto a document.
+ * These stickers can technically be of any document type and operate similarly to user templates.
+ * However, the palette is designed to be geared toward ink stickers and image stickers.
+ *
+ * On the "add" side of the palette, there is a way to create a drawing sticker with GPT. Users can
+ * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing
+ * to choose from. These drawings can then be saved to the palette as stickers.
+ */
+@observer
+export class StickerPalette extends ObservableReactComponent<StickerPaletteProps> {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(StickerPalette, fieldKey);
+ }
+ /**
+ * Adds a doc to the sticker palette. Gets a snapshot of the document to use as a preview in the palette. When this
+ * preview is dragged onto a parent document, a copy of that document is added as a sticker.
+ */
+ public static addToPalette = async (doc: Doc) => {
+ if (!doc.savedAsSticker) {
+ const docView = DocumentView.getDocumentView(doc);
+ await docView?.ComponentView?.updateIcon?.(true);
+ const { clone } = await Doc.MakeClone(doc);
+ clone.title = doc.title;
+ const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
+ Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image));
+ doc.savedAsSticker = true;
+ }
+ };
+
+ public static getIcon(group: Doc) {
+ const docView = DocumentView.getDocumentView(group);
+ docView?.ComponentView?.updateIcon?.(true);
+ return !docView ? undefined : new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+
+ private _gptRes: string[] = [];
+
+ @observable private _paletteMode = StickerPaletteMode.view;
+ @observable private _userInput: string = '';
+ @observable private _isLoading: boolean = false;
+ @observable private _canInteract: boolean = true;
+ @observable private _showRegenerate: boolean = false;
+ @observable private _docView: DocumentView | null = null;
+ @observable private _docCarouselView: DocumentView | null = null;
+ @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+
+ constructor(props: StickerPaletteProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentWillUnmount() {
+ this.resetPalette(true);
+ }
+
+ Contains = (view: DocumentView) =>
+ (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || //
+ (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
+
+ return170 = () => 170;
+
+ handleKeyPress = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ this.generateDrawings();
+ }
+ };
+
+ setPaletteMode = action((mode: StickerPaletteMode) => {
+ this._paletteMode = mode;
+ });
+
+ setUserInput = action((input: string) => {
+ if (!this._isLoading) this._userInput = input;
+ });
+
+ setDetail = action((detail: number) => {
+ if (this._canInteract) this._opts.complexity = detail;
+ });
+
+ setColor = action((autoColor: boolean) => {
+ if (this._canInteract) this._opts.autoColor = autoColor;
+ });
+
+ setSize = action((size: number) => {
+ if (this._canInteract) this._opts.size = size;
+ });
+
+ resetPalette = action((changePaletteMode: boolean) => {
+ if (changePaletteMode) this.setPaletteMode(StickerPaletteMode.view);
+ this.setUserInput('');
+ this.setDetail(5);
+ this.setColor(true);
+ this.setSize(200);
+ this._showRegenerate = false;
+ this._canInteract = true;
+ this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+ this._gptRes = [];
+ this._props.Document[DocData].data = undefined;
+ });
+
+ /**
+ * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from
+ * the sticker palette.
+ */
+ @undoBatch
+ generateDrawings = action(() => {
+ this._isLoading = true;
+ const prevDrawings = DocListCast(this._props.Document[DocData].data);
+ this._props.Document[DocData].data = undefined;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ this._canInteract = false;
+ Promise.all(
+ numberRange(3).map(i => {
+ return this._showRegenerate
+ ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput)
+ : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+ })
+ ).then(() => {
+ this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
+ this._userInput = '';
+ this._isLoading = false;
+ this._showRegenerate = true;
+ });
+ });
+
+ @action
+ addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+ this._gptRes.push(gptRes);
+ drawing[DocData].freeform_fitContentsToBox = true;
+ Doc.AddDocToList(this._props.Document, 'data', drawing);
+ };
+
+ /**
+ * Saves the currently showing, newly generated drawing to the sticker palette and sets the metadata.
+ * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user
+ * presses the "save drawing" button.
+ */
+ saveDrawing = () => {
+ const cIndex = NumCast(this._props.Document.carousel_index);
+ const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
+ const docData = focusedDrawing[DocData];
+ docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+ docData.ai_drawing_input = this._opts.text;
+ docData.ai_drawing_complexity = this._opts.complexity;
+ docData.ai_drawing_colored = this._opts.autoColor;
+ docData.ai_drawing_size = this._opts.size;
+ docData.ai_drawing_data = this._gptRes[cIndex];
+ docData.ai = 'gpt';
+ focusedDrawing.width = this._opts.size;
+ docData.x = this._opts.x;
+ docData.y = this._opts.y;
+ StickerPalette.addToPalette(focusedDrawing).then(() => this.resetPalette(true));
+ };
+
+ renderCreateInput = () => (
+ <div className="palette-create">
+ <input
+ className="palette-create-input"
+ aria-label="label-input"
+ id="new-label"
+ type="text"
+ value={this._userInput}
+ onChange={e => this.setUserInput(e.target.value)}
+ placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
+ onKeyDown={this.handleKeyPress}
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.generateDrawings}
+ />
+ </div>
+ );
+ renderCreateOptions = () => (
+ <div className="palette-create-options">
+ <div className="palette-color">
+ Color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': {
+ color: SettingsManager.userColor,
+ },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+ backgroundColor: SettingsManager.userVariantColor,
+ },
+ }}
+ defaultChecked={true}
+ value={this._opts.autoColor}
+ size="small"
+ onChange={() => this.setColor(!this._opts.autoColor)}
+ />
+ </div>
+ <div className="palette-detail">
+ Detail
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._opts.complexity}
+ onChange={(e, val) => typeof val === 'number' && this.setDetail(val)}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div className="palette-size">
+ Size
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={50}
+ max={500}
+ step={10}
+ size="small"
+ value={this._opts.size}
+ onChange={(e, val) => typeof val === 'number' && this.setSize(val)}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ );
+ renderDoc = (doc: Doc, refFunc: (r: DocumentView) => void) => {
+ return (
+ <DocumentView
+ ref={refFunc}
+ Document={doc}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDocViewList}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={1}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ );
+ };
+ renderPaletteCreate = () => (
+ <>
+ {this.renderCreateInput()}
+ {this.renderCreateOptions()}
+ {this.renderDoc(this._props.Document, (r: DocumentView) => {
+ this._docCarouselView = r;
+ })}
+ <div className="palette-buttons">
+ <Button text="Back" tooltip="Back to All Stickers" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
+ <div className="palette-save-reset">
+ <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
+ <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
+ </div>
+ </div>
+ </>
+ );
+ renderPaletteView = () => (
+ <>
+ {this.renderDoc(Doc.MyStickers, (r: DocumentView) => {
+ this._docView = r;
+ })}
+ <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode(StickerPaletteMode.create)} />
+ </>
+ );
+
+ render() {
+ return (
+ <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
+ {this._paletteMode === StickerPaletteMode.view ? this.renderPaletteView() : null}
+ {this._paletteMode === StickerPaletteMode.create ? this.renderPaletteCreate() : null}
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+ layout: { view: StickerPalette, dataField: 'data' },
+ options: { acl: '' },
+});
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index a85606bc4..00114a3f9 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, isDark, Size, Type } from 'browndash-components';
+import { Button, IconButton, isDark, Size, Type } from '@dash/components';
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/extensions/ExtensionsTypings.ts b/src/extensions/ExtensionsTypings.ts
index d6ffd3be3..fa8851bb3 100644
--- a/src/extensions/ExtensionsTypings.ts
+++ b/src/extensions/ExtensionsTypings.ts
@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Array<T> {
+ /**
+ * returns the last element of the array or undefined
+ */
lastElement(): T;
+ /**
+ * if val is in the list, it returns its index, otherwise undefined;
+ * @param val
+ */
+ getIndex(val: T): number | undefined;
}
interface String {
diff --git a/src/extensions/Extensions_Array.ts b/src/extensions/Extensions_Array.ts
index a50fb330f..d61585e28 100644
--- a/src/extensions/Extensions_Array.ts
+++ b/src/extensions/Extensions_Array.ts
@@ -1,14 +1,13 @@
export default class ArrayExtension {
private readonly property: string;
- private readonly body: <T>(this: Array<T>) => any;
+ private readonly body: <T>(this: Array<T>, args: unknown) => unknown;
- constructor(property: string, body: <T>(this: Array<T>) => any) {
+ constructor(property: string, body: <T>(this: Array<T>, args: unknown) => unknown) {
this.property = property;
this.body = body;
}
assign() {
- // eslint-disable-next-line no-extend-native
Object.defineProperty(Array.prototype, this.property, {
value: this.body,
enumerable: false,
@@ -28,6 +27,11 @@ const extensions = [
}
return this[this.length - 1];
}),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ new ArrayExtension('getIndex', function (val: any) {
+ const index = this.indexOf(val);
+ return index === -1 ? undefined : index;
+ }),
];
function Assign() {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6ec195910..e62ca4bb8 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -14,7 +14,7 @@ import {
Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
} from './DocSymbols'; // prettier-ignore
import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { InkTool } from './InkField';
+import { InkEraserTool, InkInkTool, InkTool } from './InkField';
import { List } from './List';
import { ObjectField, serverOpType } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
@@ -236,7 +236,7 @@ export class Doc extends RefField {
public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore
public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore
public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore
- public static get MyAnnos() { return DocCast(Doc.UserDoc().myAnnos); } // prettier-ignore
+ public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore
public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore
public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore
public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore
@@ -253,6 +253,10 @@ export class Doc extends RefField {
public static set ActivePage(val) { Doc.UserDoc().activePage = val; } // prettier-ignore
public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } // prettier-ignore
public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore
+ public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool; } // prettier-ignore
+ public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore
+ public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool; } // prettier-ignore
+ public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore
public static get ActivePresentation() { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt<Doc>; } // prettier-ignore
public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore
public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 17b99b033..d1dda106a 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -8,19 +8,32 @@ import { ObjectField } from './ObjectField';
// Helps keep track of the current ink tool in use.
export enum InkTool {
- None = 'none',
- Pen = 'pen',
- Highlighter = 'highlighter',
- StrokeEraser = 'strokeeraser',
- SegmentEraser = 'segmenteraser',
- RadiusEraser = 'radiuseraser',
- Eraser = 'eraser', // not a real tool, but a class of tools
- Stamp = 'stamp',
- Write = 'write',
- PresentationPin = 'presentationpin',
+ None = 'None',
+ Ink = 'Ink',
+ Eraser = 'Eraser', // not a real tool, but a class of tools
SmartDraw = 'smartdraw',
}
+export enum InkInkTool {
+ Pen = 'Pen',
+ Highlight = 'Highlight',
+ Write = 'Write',
+}
+
+export enum InkEraserTool {
+ Stroke = 'Stroke',
+ Segment = 'Segment',
+ Radius = 'Radius',
+}
+
+export enum InkProperty {
+ Mask = 'inkMask',
+ Labels = 'labels',
+ StrokeWidth = 'strokeWidth',
+ StrokeColor = 'strokeColor',
+ EraserWidth = ' eraserWidth',
+}
+
export type Segment = Array<Bezier>;
// Defines an ink as an array of points.
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 582c09f29..b294ee8c6 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -85,6 +85,7 @@ async function deserializeScript(scriptIn: ScriptField) {
}
@scriptingGlobal
+// eslint-disable-next-line no-use-before-define
@Deserializable('script', (obj: unknown) => deserializeScript(obj as ScriptField))
export class ScriptField extends ObjectField {
@serializable
@@ -137,7 +138,6 @@ export class ScriptField extends ObjectField {
[ToString]() {
return this.script.originalScript;
}
- // eslint-disable-next-line default-param-last
public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
return CompileScript(script, {
params: {
@@ -156,13 +156,11 @@ export class ScriptField extends ObjectField {
});
}
- // eslint-disable-next-line default-param-last
public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
}
- // eslint-disable-next-line default-param-last
public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
@@ -186,6 +184,7 @@ export class ScriptField extends ObjectField {
}
@scriptingGlobal
+// eslint-disable-next-line no-use-before-define
@Deserializable('computed', (obj: unknown) => deserializeScript(obj as ComputedField))
export class ComputedField extends ScriptField {
static undefined = '__undefined';
@@ -229,7 +228,6 @@ export class ComputedField extends ScriptField {
[ToValue](doc: Doc) { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore
[Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
- // eslint-disable-next-line default-param-last
public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
@@ -265,14 +263,9 @@ export class ComputedField extends ScriptField {
doc[`${fieldKey}_indexed`] = flist;
}
const getField = ScriptField.CompileScript(`getIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey})`, {}, true, {});
- const setField = ScriptField.CompileScript(
- `{setIndexVal (this['${fieldKey}_indexed'], this.${interpolatorKey}, value); console.log(this["${fieldKey}_indexed"][this.${interpolatorKey}],this.data,this["${fieldKey}_indexed"]))}`,
- { value: 'any' },
- false,
- {}
- );
+ const setField = ScriptField.CompileScript(`{setIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey}, value);}`, { value: 'any' }, false, {});
doc[fieldKey] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
- return doc[fieldKey];
+ return Field.Copy(doc[fieldKey]);
}
}
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 335683270..b27816f55 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -48,8 +48,8 @@ export const documentSchema = createSchema({
_columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
// _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
// _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _text_fontSize: 'string',
- _text_fontFamily: 'string',
+ text_fontSize: 'string',
+ text_fontFamily: 'string',
_layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
// appearance properties on the data document
@@ -70,7 +70,7 @@ export const documentSchema = createSchema({
stroke_startMarker: 'string',
stroke_endMarker: 'string',
stroke_dash: 'string',
- textTransform: 'string',
+ text_transform: 'string',
treeView_Open: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden
treeView_ExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
treeView_ExpandedViewLock: 'boolean', // whether the expanded view can be changed
@@ -112,5 +112,4 @@ export const collectionSchema = createSchema({
});
export type Document = makeInterface<[typeof documentSchema]>;
-// eslint-disable-next-line no-redeclare
export const Document = makeInterface(documentSchema);
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index 5a8e9bd97..10f9ba6d0 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -8,7 +8,6 @@ export enum Gestures {
Arrow = 'arrow',
RightAngle = 'rightangle',
}
-
// Defines a point in an ink as a pair of x- and y-coordinates.
export interface PointData {
X: number;
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 04262b61f..f6e1c87fa 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -1,4 +1,5 @@
/* eslint-disable no-use-before-define */
+import { numberRange } from '../Utils';
import { Gestures } from './GestureTypes';
/**
@@ -193,77 +194,38 @@ export class NDollarRecognizer {
constructor(
useBoundedRotationInvariance: boolean // constructor
) {
+ const rectMaker = (width: number, height1: number, height2: number) => [
+ new Point(0, 0), //
+ new Point(0, height1),
+ new Point(width, height2),
+ new Point(width, 0),
+ new Point(0, 0),
+ ];
+
+ const arect = rectMaker(100, 100, 50);
+ const aorect = rectMaker(300, 100, 50);
+ const brect = rectMaker(100, 100, 200);
+ const borect = rectMaker(300, 100, 200);
+ const rect = rectMaker(100, 100, 100);
+ const orect = rectMaker(300, 100, 100);
+ const equilateral = [new Point(50, 100), new Point(100, 0), new Point(0, 0), new Point(50, 100)];
+ const aequilateral = [new Point(20, 100), new Point(200, 0), new Point(0, 0), new Point(20, 100)];
+ const bequilateral = [new Point(180, 100), new Point(200, 0), new Point(0, 0), new Point(180, 100)];
+ const circle = numberRange(11).map(i => new Point(100 + 100 * Math.cos((i / 10) * Math.PI * 2), 100 + 100 * Math.sin((i / 10) * Math.PI * 2)));
+ const rightAngle = [new Point(0, 0), new Point(0, 100), new Point(200, 100)];
//
- // one predefined multistroke for each multistroke type
+ // one predefined multistroke (plus its counterclockwise reversal for closed shapes) for each multistroke type
//
this.Multistrokes.push(
- new Multistroke(
- Gestures.Rectangle,
- useBoundedRotationInvariance,
- new Array([
- new Point(30, 146), // new Point(29, 160), new Point(30, 180), new Point(31, 200),
- new Point(30, 222), // new Point(50, 219), new Point(70, 225), new Point(90, 230),
- new Point(106, 225), // new Point(100, 200), new Point(106, 180), new Point(110, 160),
- new Point(106, 146), // new Point(80, 150), new Point(50, 146),
- new Point(30, 143),
- ])
- )
- );
- this.Multistrokes.push(new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, new Array([new Point(30, 143), new Point(106, 146), new Point(106, 225), new Point(30, 222), new Point(30, 146)])));
- this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]]));
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Triangle, // equilateral
- useBoundedRotationInvariance,
- new Array([new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Triangle, // equilateral
- useBoundedRotationInvariance,
- new Array([new Point(42, 100), new Point(140, 102), new Point(100, 200), new Point(40, 100)])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Circle,
- useBoundedRotationInvariance,
- new Array([
- new Point(200, 250),
- new Point(240, 230),
- new Point(248, 210),
- new Point(248, 190),
- new Point(240, 170),
- new Point(200, 150),
- new Point(160, 170),
- new Point(151, 190),
- new Point(151, 210),
- new Point(160, 230),
- new Point(201, 250),
- ])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Circle,
- useBoundedRotationInvariance,
- new Array([
- new Point(201, 250),
- new Point(160, 230),
- new Point(151, 210),
- new Point(151, 190),
- new Point(160, 170),
- new Point(200, 150),
- new Point(240, 170),
- new Point(248, 190),
- new Point(248, 210),
- new Point(240, 230),
- new Point(200, 250),
- ])
- )
+ ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s])),
+ ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s.reverse()])),
+ ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s])),
+ ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s.reverse()])),
+ new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle]),
+ new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle.reverse()]),
+ new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, [rightAngle]),
+ new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]])
);
- this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)])));
NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
//
// PREDEFINED STROKES
diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts
index c5ba4b830..c41f697db 100644
--- a/src/server/ApiManagers/AssistantManager.ts
+++ b/src/server/ApiManagers/AssistantManager.ts
@@ -9,28 +9,22 @@
*/
import { Readability } from '@mozilla/readability';
-import axios, { AxiosResponse } from 'axios';
+import axios from 'axios';
import { spawn } from 'child_process';
import * as fs from 'fs';
import { writeFile } from 'fs';
import { google } from 'googleapis';
import { JSDOM } from 'jsdom';
+import OpenAI from 'openai';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { promisify } from 'util';
import * as uuid from 'uuid';
import { AI_Document } from '../../client/views/nodes/chatbot/types/types';
+import { DashUploadUtils } from '../DashUploadUtils';
import { Method } from '../RouteManager';
import { filesDirectory, publicDirectory } from '../SocketData';
import ApiManager, { Registration } from './ApiManager';
-import { getServerPath } from '../../client/util/reportManager/reportManagerUtils';
-import { file } from 'jszip';
-import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
-import ffmpeg from 'fluent-ffmpeg';
-import OpenAI from 'openai';
-import * as xmlbuilder from 'xmlbuilder';
-import { last } from 'lodash';
-import { DashUploadUtils } from '../DashUploadUtils';
// Enumeration of directories where different file types are stored
export enum Directory {
@@ -133,8 +127,6 @@ export default class AssistantManager extends ApiManager {
const { query, max_results } = req.body;
const MIN_VALID_RESULTS_RATIO = 0.75; // 3/4 threshold
let startIndex = 1; // Start at the first result initially
- let validResults: any[] = [];
-
const fetchSearchResults = async (start: number) => {
return customsearch.cse.list({
q: query,
@@ -146,20 +138,27 @@ export default class AssistantManager extends ApiManager {
});
};
- const filterResultsByXFrameOptions = async (results: any[]) => {
+ const filterResultsByXFrameOptions = async (
+ results: {
+ url: string | null | undefined;
+ snippet: string | null | undefined;
+ }[]
+ ) => {
const filteredResults = await Promise.all(
- results.map(async result => {
- try {
- const urlResponse: AxiosResponse = await axios.head(result.url, { timeout: 5000 });
- const xFrameOptions = urlResponse.headers['x-frame-options'];
- if (xFrameOptions && xFrameOptions.toUpperCase() === 'SAMEORIGIN') {
- return result;
+ results
+ .filter(result => result.url)
+ .map(async result => {
+ try {
+ const urlResponse = await axios.head(result.url!, { timeout: 5000 });
+ const xFrameOptions = urlResponse.headers['x-frame-options'];
+ if (xFrameOptions && xFrameOptions.toUpperCase() === 'SAMEORIGIN') {
+ return result;
+ }
+ } catch (error) {
+ console.error(`Error checking x-frame-options for URL: ${result.url}`, error);
}
- } catch (error) {
- console.error(`Error checking x-frame-options for URL: ${result.url}`, error);
- }
- return null; // Exclude the result if it doesn't match
- })
+ return null; // Exclude the result if it doesn't match
+ })
);
return filteredResults.filter(result => result !== null); // Remove null results
};
@@ -167,14 +166,14 @@ export default class AssistantManager extends ApiManager {
try {
// Fetch initial search results
let response = await fetchSearchResults(startIndex);
- let initialResults =
+ const initialResults =
response.data.items?.map(item => ({
url: item.link,
snippet: item.snippet,
})) || [];
// Filter the initial results
- validResults = await filterResultsByXFrameOptions(initialResults);
+ let validResults = await filterResultsByXFrameOptions(initialResults);
// If valid results are less than 3/4 of max_results, fetch more results
while (validResults.length < max_results * MIN_VALID_RESULTS_RATIO) {
@@ -288,7 +287,7 @@ export default class AssistantManager extends ApiManager {
// Step 2: Transcribe audio using OpenAI Whisper
console.log('Transcribing audio...');
const transcription = await openai.audio.transcriptions.create({
- file: fs.createReadStream(audioPath) as any,
+ file: fs.createReadStream(audioPath),
model: 'whisper-1',
response_format: 'verbose_json',
timestamp_granularities: ['segment'],
@@ -298,7 +297,7 @@ export default class AssistantManager extends ApiManager {
// Step 3: Extract concise JSON
console.log('Extracting concise JSON...');
- const originalSegments = transcription.segments?.map((segment: any, index: number) => ({
+ const originalSegments = transcription.segments?.map((segment, index) => ({
index: index.toString(),
text: segment.text,
start: segment.start,
@@ -767,7 +766,7 @@ function spawnPythonProcess(jobId: string, file_name: string, file_path: string)
try {
const errorOutput = JSON.parse(lastLine);
jobResults[jobId] = errorOutput;
- } catch (err) {
+ } catch {
jobResults[jobId] = { error: 'Python process failed' };
}
} else {
@@ -829,6 +828,3 @@ function spawnPythonProcess(jobId: string, file_name: string, file_path: string)
runPythonScript();
}
}
-function customFfmpeg(filePath: string) {
- throw new Error('Function not implemented.');
-}
diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts
index 88f22992d..d2028f23b 100644
--- a/src/server/ApiManagers/DataVizManager.ts
+++ b/src/server/ApiManagers/DataVizManager.ts
@@ -9,7 +9,7 @@ export default class DataVizManager extends ApiManager {
register({
method: Method.GET,
subscription: '/csvData',
- secureHandler: async ({ req, res }) => {
+ secureHandler: ({ req, res }) => {
const uri = req.query.uri as string;
return new Promise<void>(resolve => {
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
new file mode 100644
index 000000000..160a94d40
--- /dev/null
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -0,0 +1,407 @@
+import axios from 'axios';
+import { Dropbox } from 'dropbox';
+import * as fs from 'fs';
+import * as multipart from 'parse-multipart-data';
+import * as path from 'path';
+import { DashUserModel } from '../authentication/DashUserModel';
+import { DashUploadUtils } from '../DashUploadUtils';
+import { _error, _invalid, _success, Method } from '../RouteManager';
+import { Directory, filesDirectory } from '../SocketData';
+import ApiManager, { Registration } from './ApiManager';
+
+export default class FireflyManager extends ApiManager {
+ getBearerToken = () =>
+ fetch('https://ims-na1.adobelogin.com/ims/token/v3', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: `grant_type=client_credentials&client_id=${process.env._CLIENT_FIREFLY_CLIENT_ID}&client_secret=${process.env._CLIENT_FIREFLY_SECRET}&scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis`,
+ }).catch(error => {
+ console.error('Error:', error);
+ return undefined;
+ });
+
+ generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string | undefined) =>
+ this.getBearerToken().then(response =>
+ response?.json().then((data: { access_token: string }) =>
+ //prettier-ignore
+ fetch('https://firefly-api.adobe.io/v3/images/generate', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${data.access_token}`],
+ ],
+ body: JSON.stringify({
+ prompt,
+ numVariations: 4,
+ detailLevel: 'preview',
+ modelVersion: 'image3_fast',
+ size: { width, height },
+ structure: !structureUrl
+ ? undefined
+ : {
+ strength,
+ imageReference: {
+ source: { url: structureUrl },
+ },
+ },
+ // prettier-ignore
+ style: {
+ presets: styles,
+ imageReference : !styleUrl
+ ? undefined
+ : {
+ source: { url: styleUrl },
+ }
+ }
+ }),
+ })
+ .then(response2 => response2.json().then(json =>
+ {
+ if (json.outputs?.length)
+ return (json.outputs as {image: {url:string }}[]).map(output => output.image);
+ throw new Error(JSON.stringify(json));
+ })
+ )
+ )
+ );
+
+ uploadImageToDropbox = (fileUrl: string, user: DashUserModel | undefined, dbx = new Dropbox({ accessToken: user?.dropboxToken || '' })) =>
+ new Promise<string | Error>((res, rej) =>
+ fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
+ if (err) {
+ console.log('Error: ', err);
+ rej();
+ } else {
+ dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents })
+ .then(response => {
+ dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' })
+ .then(link => res(link.result.link))
+ .catch(e => res(new Error(e.toString())));
+ })
+ .catch(e => {
+ if (user?.dropboxRefresh) {
+ console.log('*********** try refresh dropbox for: ' + user.email + ' ***********');
+ this.refreshDropboxToken(user).then(token => {
+ if (!token) {
+ console.log('Dropbox error: cannot refresh token');
+ res(new Error(e.toString()));
+ } else {
+ const dbxNew = new Dropbox({ accessToken: user.dropboxToken || '' });
+ dbxNew
+ .filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents })
+ .then(response => {
+ dbxNew
+ .filesGetTemporaryLink({ path: response.result.path_display ?? '' })
+ .then(link => res(link.result.link))
+ .catch(linkErr => res(new Error(linkErr.toString())));
+ })
+ .catch(uploadErr => {
+ console.log('Dropbox error:', uploadErr);
+ res(new Error(uploadErr.toString()));
+ });
+ }
+ });
+ } else {
+ console.log('Dropbox error:', e);
+ res(new Error(e.toString()));
+ }
+ });
+ }
+ })
+ );
+
+ generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => {
+ let body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`;
+ if (seed) {
+ console.log('RECEIVED SEED', seed);
+ body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}}, "seeds": [${seed}]}`;
+ }
+ const fetched = this.getBearerToken().then(response =>
+ response?.json().then((data: { access_token: string }) =>
+ fetch('https://firefly-api.adobe.io/v3/images/generate', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${data.access_token}`],
+ ],
+ body: body,
+ })
+ .then(response2 => response2.json().then(json => ({ seed: json.outputs?.[0]?.seed, url: json.outputs?.[0]?.image?.url })))
+ .catch(error => {
+ console.error('Error:', error);
+ return undefined;
+ })
+ )
+ );
+ return fetched;
+ };
+ expandImage = (imgUrl: string, prompt?: string) => {
+ const dropboxImgUrl = imgUrl;
+ const fetched = this.getBearerToken().then(response =>
+ response
+ ?.json()
+ .then((data: { access_token: string }) => {
+ return fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${data.access_token}`],
+ ],
+ body: JSON.stringify({
+ image: {
+ source: {
+ url: dropboxImgUrl,
+ },
+ },
+ numVariations: 1,
+ seeds: [0],
+ size: {
+ width: 3048,
+ height: 2048,
+ },
+ prompt: prompt ?? 'cloudy skies',
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ }),
+ });
+ })
+ .then(resp => resp.json())
+ );
+ return fetched;
+ };
+ getImageText = (imageBlob: Blob) => {
+ const inputFileVarName = 'infile';
+ const outputVarName = 'result';
+ const fetched = this.getBearerToken().then(response =>
+ response?.json().then((data: { access_token: string }) => {
+ return fetch('https://sensei.adobe.io/services/v2/predict', {
+ method: 'POST',
+ headers: [
+ ['Prefer', 'respond-async, wait=59'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ // ['content-type', 'multipart/form-data'], // bcz: Don't set this!! content-type will get set automatically including the Boundary string
+ ['Authorization', `Bearer ${data.access_token}`],
+ ],
+ body: ((form: FormData) => {
+ form.set(inputFileVarName, imageBlob);
+ form.set(
+ 'contentAnalyzerRequests',
+ JSON.stringify({
+ 'sensei:name': 'Feature:cintel-object-detection:Service-b9ace8b348b6433e9e7d82371aa16690',
+ 'sensei:invocation_mode': 'asynchronous',
+ 'sensei:invocation_batch': false,
+ 'sensei:engines': [
+ {
+ 'sensei:execution_info': {
+ 'sensei:engine': 'Feature:cintel-object-detection:Service-b9ace8b348b6433e9e7d82371aa16690',
+ },
+ 'sensei:inputs': {
+ documents: [
+ {
+ 'sensei:multipart_field_name': inputFileVarName,
+ 'dc:format': 'image/png',
+ },
+ ],
+ },
+ 'sensei:params': {
+ correct_with_dictionary: true,
+ },
+ 'sensei:outputs': {
+ result: {
+ 'sensei:multipart_field_name': outputVarName,
+ 'dc:format': 'application/json',
+ },
+ },
+ },
+ ],
+ })
+ );
+ return form;
+ })(new FormData()),
+ }).then(response2 => {
+ const contentType = response2.headers.get('content-type') ?? '';
+ if (contentType.includes('application/json')) {
+ return response2.json().then((json: object) => JSON.stringify(json));
+ }
+ if (contentType.includes('multipart')) {
+ return response2
+ .arrayBuffer()
+ .then(arrayBuffer =>
+ multipart
+ .parse(Buffer.from(arrayBuffer), 'Boundary' + (response2.headers.get('content-type')?.match(/=Boundary(.*);/)?.[1] ?? ''))
+ .filter(part => part.name === outputVarName)
+ .map(part => JSON.parse(part.data.toString())[0])
+ .reduce((text, json) => text + (json?.is_text_present ? json.tags.map((tag: { text: string }) => tag.text).join(' ') : ''), '')
+ )
+ .catch(error => {
+ console.error('Error:', error);
+ return '';
+ });
+ }
+ return response2.text();
+ });
+ })
+ );
+ return fetched;
+ };
+
+ refreshDropboxToken = (user: DashUserModel) =>
+ axios
+ .post(
+ 'https://api.dropbox.com/oauth2/token',
+ new URLSearchParams({
+ refresh_token: user.dropboxRefresh || '',
+ grant_type: 'refresh_token',
+ client_id: process.env._CLIENT_DROPBOX_CLIENT_ID || '',
+ client_secret: process.env._CLIENT_DROPBOX_SECRET || '',
+ }).toString()
+ )
+ .then(refresh => {
+ console.log('***** dropbox token refreshed for ' + user?.email + ' ******* ');
+ user.dropboxToken = refresh.data.access_token;
+ user.save();
+ return user.dropboxToken;
+ })
+ .catch(e => {
+ console.log(e);
+ return undefined;
+ });
+
+ protected initialize(register: Registration): void {
+ register({
+ method: Method.POST,
+ subscription: '/queryFireflyImageFromStructure',
+ secureHandler: ({ req, res }) =>
+ new Promise<void>(resolver => {
+ (req.body.styleUrl ? this.uploadImageToDropbox(req.body.styleUrl, req.user as DashUserModel) : Promise.resolve(undefined))
+ .then(styleUrl => {
+ if (styleUrl instanceof Error) {
+ _invalid(res, styleUrl.message);
+ throw new Error('Error uploading images to dropbox');
+ }
+ this.uploadImageToDropbox(req.body.structure, req.user as DashUserModel)
+ .then(structureUrl => {
+ if (structureUrl instanceof Error) {
+ _invalid(res, structureUrl.message);
+ throw new Error('Error uploading images to dropbox');
+ }
+ return { styleUrl, structureUrl };
+ })
+ .then(uploads =>
+ this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
+ .then(images => {
+ Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url))))
+ .then(dashImages => {
+ if (dashImages.every(img => img instanceof Error)) _invalid(res, dashImages[0]!.message);
+ else _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error))));
+ })
+ .then(resolver);
+ })
+ .catch(e => {
+ _invalid(res, e.message);
+ resolver();
+ })
+ );
+ })
+ .catch(() => {
+ /* do nothing */
+ resolver();
+ });
+ }),
+ });
+ register({
+ method: Method.POST,
+ subscription: '/queryFireflyImage',
+ secureHandler: ({ req, res }) =>
+ this.generateImage(req.body.prompt, req.body.width, req.body.height, req.body.seed).then(img =>
+ DashUploadUtils.UploadImage(img?.url ?? '', undefined, img?.seed).then(info => {
+ if (info instanceof Error) _invalid(res, info.message);
+ else _success(res, info);
+ })
+ ),
+ });
+
+ register({
+ method: Method.POST,
+ subscription: '/queryFireflyImageText',
+ secureHandler: ({ req, res }) =>
+ fetch(req.body.file).then(json =>
+ json.blob().then(file =>
+ this.getImageText(file).then(text => {
+ _success(res, text);
+ })
+ )
+ ),
+ });
+ register({
+ method: Method.POST,
+ subscription: '/expandImage',
+ secureHandler: ({ req, res }) =>
+ this.uploadImageToDropbox(req.body.file, req.user as DashUserModel).then(uploadUrl =>
+ uploadUrl instanceof Error
+ ? _invalid(res, uploadUrl.message)
+ : this.expandImage(uploadUrl, req.body.prompt).then(text => {
+ if (text.error_code) _error(res, text.message);
+ else
+ DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
+ if (info instanceof Error) _invalid(res, info.message);
+ else _success(res, info);
+ });
+ })
+ ),
+ });
+
+ // construct this url and send user to it. It will allow them to authorize their dropbox account and will send the resulting token to our endpoint /refreshDropbox
+ // https://www.dropbox.com/oauth2/authorize?client_id=DROPBOX_CLIENT_ID&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox
+ // see: https://dropbox.tech/developers/using-oauth-2-0-with-offline-access
+ //
+ register({
+ method: Method.GET,
+ subscription: '/refreshDropbox',
+ secureHandler: ({ req, res }) => {
+ const user = req.user as DashUserModel;
+ console.log(`******************* dropbox authorized for ${user?.email} ******************`);
+ _success(res, 'dropbox authorized for ' + user?.email);
+
+ const data = new URLSearchParams({
+ code: req.query.code as string,
+ grant_type: 'authorization_code',
+ client_id: process.env._CLIENT_DROPBOX_CLIENT_ID ?? '',
+ client_secret: process.env._CLIENT_DROPBOX_SECRET ?? '',
+ redirect_uri: 'http://localhost:1050/refreshDropbox',
+ });
+ axios
+ .post('https://api.dropbox.com/oauth2/token', data.toString())
+ .then(response => {
+ console.log('***** dropbox token (and refresh) received for ' + user?.email + ' ******* ');
+ user.dropboxToken = response.data.access_token;
+ user.dropboxRefresh = response.data.refresh_token;
+ user.save();
+
+ setTimeout(() => this.refreshDropboxToken(user), response.data.expires_in - 600);
+ })
+ .catch(e => {
+ console.log(e);
+ });
+ },
+ });
+ }
+}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 868373474..c9d5df547 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -70,10 +70,16 @@ export default class UploadManager extends ApiManager {
]);
} else {
fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `resampling images`));
+ // original filenames with '.'s, such as a Macbook screenshot, can be a problem - their extension is not kept in formidable's newFilename.
+ // This makes sure that the extension is preserved in the newFilename.
+ const fixNewFilename = (f: formidable.File) => {
+ if (path.extname(f.originalFilename ?? '') !== path.extname(f.newFilename)) f.newFilename = f.newFilename + path.extname(f.originalFilename ?? '');
+ return f;
+ };
const results = (
await Promise.all(
Array.from(Object.keys(files)).map(
- async key => (!files[key] ? undefined : DashUploadUtils.upload(files[key]![0] /* , key */)) // key is the guid used by the client to track upload progress.
+ async key => (!files[key] ? undefined : DashUploadUtils.upload(fixNewFilename(files[key][0]) /* , key */)) // key is the guid used by the client to track upload progress.
)
)
).filter(result => result && !(result.result instanceof Error));
@@ -147,13 +153,10 @@ export default class UploadManager extends ApiManager {
if (doc.id) {
doc.id = getId(doc.id);
}
- // eslint-disable-next-line no-restricted-syntax
for (const key in doc.fields) {
- // eslint-disable-next-line no-continue
if (!Object.prototype.hasOwnProperty.call(doc.fields, key)) continue;
const field = doc.fields[key];
- // eslint-disable-next-line no-continue
if (field === undefined || field === null) continue;
if (field.__type === 'Doc') {
@@ -182,11 +185,9 @@ export default class UploadManager extends ApiManager {
let docids: string[] = [];
let linkids: string[] = [];
try {
- // eslint-disable-next-line no-restricted-syntax
for (const name in files) {
if (Object.prototype.hasOwnProperty.call(files, name)) {
const f = files[name];
- // eslint-disable-next-line no-continue
if (!f) continue;
const path2 = f[0]; // what about the rest of the array? are we guaranteed only one value is set?
const zip = new AdmZip(path2.filepath);
@@ -273,14 +274,20 @@ export default class UploadManager extends ApiManager {
.filter(f => regex.test(f))
.map(f => fs.unlinkSync(serverPath + f));
}
- imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
- const ext = path.extname(savedName).toLowerCase();
- const outputPath = serverPathToFile(Directory.images, filename + ext);
- if (AcceptableMedia.imageFormats.includes(ext)) {
- workerResample(savedName, outputPath, origSuffix, false);
- }
- res.send(clientPathToFile(Directory.images, filename + ext));
- });
+ imageDataUri
+ .outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix)))
+ .then((savedName: string) => {
+ const ext = path.extname(savedName).toLowerCase();
+ const outputPath = serverPathToFile(Directory.images, filename + ext);
+ if (AcceptableMedia.imageFormats.includes(ext)) {
+ workerResample(savedName, outputPath, origSuffix, false);
+ }
+ res.send(clientPathToFile(Directory.images, filename + ext));
+ })
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ .catch((e: any) => {
+ res.status(404).json({ error: e.toString() });
+ });
},
});
}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 1e55a885a..2177c5d97 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
import axios from 'axios';
import { exec, spawn } from 'child_process';
import { green, red } from 'colors';
@@ -21,7 +22,6 @@ import { AzureManager } from './ApiManagers/AzureManager';
import { AcceptableMedia, Upload } from './SharedMediaTypes';
import { Directory, clientPathToFile, filesDirectory, pathToDirectory, publicDirectory, serverPathToFile } from './SocketData';
import { resolvedServerUrl } from './server_Initialization';
-
import { Worker, isMainThread, parentPort } from 'worker_threads';
// Create an array to store worker threads
@@ -47,20 +47,22 @@ if (isMainThread) {
async function workerResampleImage(message: { imgSourcePath: string; outputPath: string; origSuffix: string; unlinkSource: boolean }) {
const { imgSourcePath, outputPath, origSuffix, unlinkSource } = message;
- const sizes = !origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : DashUploadUtils.imageResampleSizes(path.extname(imgSourcePath));
+ const extension = path.extname(imgSourcePath);
+ const sizes = !origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : DashUploadUtils.imageResampleSizes(extension === '.xml' ? '.png' : extension);
// prettier-ignore
Jimp.read(imgSourcePath)
.then(img =>
sizes.forEach(({ width, suffix }) =>
img.resize({ w: width || img.bitmap.width })
- .write(InjectSize(outputPath, suffix) as `${string}.${string}`)
+ .write(InjectSize(outputPath, suffix) as `${string}.${string}`)
+ .catch(e => console.log("Jimp error:", e))
))
.catch(e => console.log('Error Jimp:', e))
.finally(() => unlinkSource && unlinkSync(imgSourcePath));
}
}
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const requestImageSize = require('../client/util/request-image-size');
export enum SizeSuffix {
@@ -221,7 +223,6 @@ export namespace DashUploadUtils {
const parseExifData = async (source: string) => {
const image = await request.get(source, { encoding: null });
const { /* data, */ error } = await new Promise<{ data: ExifData; error: string | undefined }>(resolve => {
- // eslint-disable-next-line no-new
new ExifImage({ image }, (exifError, data) => {
resolve({ data, error: exifError?.message });
});
@@ -300,7 +301,6 @@ export namespace DashUploadUtils {
// Bundle up the information into an object
return {
source,
- // eslint-disable-next-line radix
contentSize: parseInt(headers[size]),
contentType: headers[type],
nativeWidth,
@@ -343,15 +343,24 @@ export namespace DashUploadUtils {
const outputPath = path.resolve(pathToDirectory(Directory.images), outputFileName);
const sizes = imageResampleSizes(path.extname(outputFileName));
- const imgReadStream = new Duplex();
- imgReadStream.push(fs.readFileSync(imgSourcePath));
- imgReadStream.push(null);
- await Promise.all(
- sizes.map(({ suffix }) =>
- new Promise<unknown>(res =>
- imgReadStream.pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res)
- )
- )); // prettier-ignore
+ if (unlinkSource) {
+ const imgReadStream = new Duplex();
+ imgReadStream.push(fs.readFileSync(imgSourcePath));
+ imgReadStream.push(null);
+ await Promise.all(
+ sizes.map(({ suffix }) =>
+ new Promise<unknown>(res =>
+ imgReadStream.pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res)
+ )
+ )); // prettier-ignore
+ } else {
+ await Promise.all(
+ sizes.map(({ suffix }) =>
+ new Promise<unknown>(res =>
+ request.get(imgSourcePath).pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res)
+ )
+ )); // prettier-ignore
+ }
workerResample(imgSourcePath, outputPath, SizeSuffix.Original, unlinkSource);
return writtenFiles;
@@ -368,8 +377,9 @@ export namespace DashUploadUtils {
* @returns the accessPaths for the resized files.
*/
export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
- const { requestable, source, ...remaining } = metadata;
- const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
+ const { requestable, ...remaining } = metadata;
+ const dfltSuffix = remaining.contentType.split('/')[1].toLowerCase();
+ const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${dfltSuffix === 'xml' ? 'jpg' : dfltSuffix}`;
const { images } = Directory;
const information: Upload.ImageInformation = {
accessPaths: {
@@ -400,10 +410,10 @@ export namespace DashUploadUtils {
writtenFiles = {};
}
} else {
- const unlinkSrcWhenFinished = isLocal().test(source) && cleanUp;
+ const unlinkSrcWhenFinished = cleanUp; // isLocal().test(source) && cleanUp;
try {
writtenFiles = await outputResizedImages(metadata.source, resolved, unlinkSrcWhenFinished);
- } catch (e) {
+ } catch {
// input is a blob or other, try reading it to create a metadata source file.
const reqSource = request(metadata.source);
const readStream: Stream = reqSource instanceof Promise ? await reqSource : reqSource;
@@ -415,7 +425,7 @@ export namespace DashUploadUtils {
.on('error', () => rej());
});
writtenFiles = await outputResizedImages(readSource, resolved, unlinkSrcWhenFinished);
- fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource, err));
+ //fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource, err));
}
}
Array.from(Object.keys(writtenFiles)).forEach(suffix => {
@@ -448,8 +458,7 @@ export namespace DashUploadUtils {
return { name: result.name, message: result.message };
}
const outputFile = filename || result.filename || '';
-
- return UploadInspectedImage(result, outputFile, prefix);
+ return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false);
};
type md5 = 'md5';
@@ -567,7 +576,9 @@ export namespace DashUploadUtils {
switch (category) {
case 'image':
if (imageFormats.includes(format)) {
- const result = await UploadImage(filepath, basename(filepath));
+ const outputName = basename(filepath);
+ const extname = path.extname(originalFilename ?? '');
+ const result = await UploadImage(filepath, outputName.endsWith(extname) ? outputName : outputName + extname, undefined);
return { source: file, result };
}
fs.unlink(filepath, () => {});
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index d8e0455f6..2f6cf80b5 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -39,8 +39,7 @@ export function _success(res: Response, body: any) {
}
export function _invalid(res: Response, message: string) {
- res.statusMessage = message;
- res.status(STATUS.BAD_REQUEST).send();
+ res.status(STATUS.BAD_REQUEST).send(message);
}
export function _permissionDenied(res: Response, message?: string) {
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index bfa6d7bdb..debeef60c 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -9,6 +9,9 @@ export type DashUserModel = mongoose.Document & {
passwordResetToken?: string;
passwordResetExpires?: Date;
+ dropboxRefresh?: string;
+ dropboxToken?: string;
+
userDocumentId: string;
sharingDocumentId: string;
linkDatabaseId: string;
@@ -37,6 +40,8 @@ const userSchema = new mongoose.Schema(
passwordResetToken: String,
passwordResetExpires: Date,
+ dropboxRefresh: String,
+ dropboxToken: String,
userDocumentId: String, // id that identifies a document which hosts all of a user's account data
sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users
linkDatabaseId: String,
diff --git a/src/server/flashcard/labels.py b/src/server/flashcard/labels.py
new file mode 100644
index 000000000..546fc4bd3
--- /dev/null
+++ b/src/server/flashcard/labels.py
@@ -0,0 +1,285 @@
+import base64
+import numpy as np
+import base64
+import easyocr
+import sys
+from PIL import Image
+from io import BytesIO
+import requests
+import json
+import numpy as np
+
+class BoundingBoxUtils:
+ """Utility class for bounding box operations and OCR result corrections."""
+
+ @staticmethod
+ def is_close(box1, box2, x_threshold=20, y_threshold=20):
+ """
+ Determines if two bounding boxes are horizontally and vertically close.
+
+ Parameters:
+ box1, box2 (list): The bounding boxes to compare.
+ x_threshold (int): The threshold for horizontal proximity.
+ y_threshold (int): The threshold for vertical proximity.
+
+ Returns:
+ bool: True if boxes are close, False otherwise.
+ """
+ horizontally_close = (abs(box1[2] - box2[0]) < x_threshold or # Right edge of box1 and left edge of box2
+ abs(box2[2] - box1[0]) < x_threshold or # Right edge of box2 and left edge of box1
+ abs(box1[2] - box2[2]) < x_threshold or
+ abs(box2[0] - box1[0]) < x_threshold)
+
+ vertically_close = (abs(box1[3] - box2[1]) < y_threshold or # Bottom edge of box1 and top edge of box2
+ abs(box2[3] - box1[1]) < y_threshold or
+ box1[1] == box2[1] or box1[3] == box2[3])
+
+ return horizontally_close and vertically_close
+
+ @staticmethod
+ def adjust_bounding_box(bbox, original_text, corrected_text):
+ """
+ Adjusts a bounding box based on differences in text length.
+
+ Parameters:
+ bbox (list): The original bounding box coordinates.
+ original_text (str): The original text detected by OCR.
+ corrected_text (str): The corrected text after cleaning.
+
+ Returns:
+ list: The adjusted bounding box.
+ """
+ if not bbox or len(bbox) != 4:
+ return bbox
+
+ # Adjust the x-coordinates slightly to account for text correction
+ x_adjustment = 5
+ adjusted_bbox = [
+ [bbox[0][0] + x_adjustment, bbox[0][1]],
+ [bbox[1][0], bbox[1][1]],
+ [bbox[2][0] + x_adjustment, bbox[2][1]],
+ [bbox[3][0], bbox[3][1]]
+ ]
+ return adjusted_bbox
+
+ @staticmethod
+ def correct_ocr_results(results):
+ """
+ Corrects common OCR misinterpretations in the detected text and adjusts bounding boxes accordingly.
+
+ Parameters:
+ results (list): A list of OCR results, each containing bounding box, text, and confidence score.
+
+ Returns:
+ list: Corrected OCR results with adjusted bounding boxes.
+ """
+ corrections = {
+ "~": "", # Replace '~' with empty string
+ "-": "" # Replace '-' with empty string
+ }
+
+ corrected_results = []
+ for (bbox, text, prob) in results:
+ corrected_text = ''.join(corrections.get(char, char) for char in text)
+ adjusted_bbox = BoundingBoxUtils.adjust_bounding_box(bbox, text, corrected_text)
+ corrected_results.append((adjusted_bbox, corrected_text, prob))
+
+ return corrected_results
+
+ @staticmethod
+ def convert_to_json_serializable(data):
+ """
+ Converts a list containing various types, including numpy types, to a JSON-serializable format.
+
+ Parameters:
+ data (list): A list containing numpy or other non-serializable types.
+
+ Returns:
+ list: A JSON-serializable version of the input list.
+ """
+ def convert_element(element):
+ if isinstance(element, list):
+ return [convert_element(e) for e in element]
+ elif isinstance(element, tuple):
+ return tuple(convert_element(e) for e in element)
+ elif isinstance(element, np.integer):
+ return int(element)
+ elif isinstance(element, np.floating):
+ return float(element)
+ elif isinstance(element, np.ndarray):
+ return element.tolist()
+ else:
+ return element
+
+ return convert_element(data)
+
+class ImageLabelProcessor:
+ """Class to process images and perform OCR with EasyOCR."""
+
+ VERTICAL_THRESHOLD = 20
+ HORIZONTAL_THRESHOLD = 8
+
+ def __init__(self, img_source, source_type, smart_mode):
+ self.img_source = img_source
+ self.source_type = source_type
+ self.smart_mode = smart_mode
+ self.img_val = self.load_image()
+
+ def load_image(self):
+ """Load image from either a base64 string or URL."""
+ if self.source_type == 'drag':
+ return self._load_base64_image()
+ else:
+ return self._load_url_image()
+
+ def _load_base64_image(self):
+ """Decode and save the base64 image."""
+ base64_string = self.img_source
+ if base64_string.startswith("data:image"):
+ base64_string = base64_string.split(",")[1]
+
+
+ # Decode the base64 string
+ image_data = base64.b64decode(base64_string)
+ image = Image.open(BytesIO(image_data)).convert('RGB')
+ image.save("temp_image.jpg")
+ return "temp_image.jpg"
+
+ def _load_url_image(self):
+ """Download image from URL and return it in byte format."""
+ url = self.img_source
+ response = requests.get(url)
+ image = Image.open(BytesIO(response.content)).convert('RGB')
+
+ image_bytes = BytesIO()
+ image.save(image_bytes, format='PNG')
+ return image_bytes.getvalue()
+
+ def process_image(self):
+ """Process the image and return the OCR results."""
+ if self.smart_mode:
+ return self._process_smart_mode()
+ else:
+ return self._process_standard_mode()
+
+ def _process_smart_mode(self):
+ """Process the image in smart mode using EasyOCR."""
+ reader = easyocr.Reader(['en'])
+ result = reader.readtext(self.img_val, detail=1, paragraph=True)
+
+ all_boxes = [bbox for bbox, text in result]
+ all_texts = [text for bbox, text in result]
+
+ response_data = {
+ 'status': 'success',
+ 'message': 'Data received',
+ 'boxes': BoundingBoxUtils.convert_to_json_serializable(all_boxes),
+ 'text': BoundingBoxUtils.convert_to_json_serializable(all_texts),
+ }
+
+ return response_data
+
+ def _process_standard_mode(self):
+ """Process the image in standard mode using EasyOCR."""
+ reader = easyocr.Reader(['en'])
+ results = reader.readtext(self.img_val)
+
+ filtered_results = BoundingBoxUtils.correct_ocr_results([
+ (bbox, text, prob) for bbox, text, prob in results if prob >= 0.7
+ ])
+
+ return self._merge_and_prepare_response(filtered_results)
+
+ def are_vertically_close(self, box1, box2):
+ """Check if two bounding boxes are vertically close."""
+ box1_bottom = max(box1[2][1], box1[3][1])
+ box2_top = min(box2[0][1], box2[1][1])
+ vertical_distance = box2_top - box1_bottom
+
+ box1_left = box1[0][0]
+ box2_left = box2[0][0]
+ box1_right = box1[1][0]
+ box2_right = box2[1][0]
+ hori_close = abs(box2_left - box1_left) <= self.HORIZONTAL_THRESHOLD or abs(box2_right - box1_right) <= self.HORIZONTAL_THRESHOLD
+
+ return vertical_distance <= self.VERTICAL_THRESHOLD and hori_close
+
+ def merge_boxes(self, boxes, texts):
+ """Merge multiple bounding boxes and their associated text."""
+ x_coords = []
+ y_coords = []
+
+ # Collect all x and y coordinates
+ for box in boxes:
+ for point in box:
+ x_coords.append(point[0])
+ y_coords.append(point[1])
+
+ # Create the merged bounding box
+ merged_box = [
+ [min(x_coords), min(y_coords)],
+ [max(x_coords), min(y_coords)],
+ [max(x_coords), max(y_coords)],
+ [min(x_coords), max(y_coords)]
+ ]
+
+ # Combine the texts
+ merged_text = ' '.join(texts)
+
+ return merged_box, merged_text
+
+ def _merge_and_prepare_response(self, filtered_results):
+ """Merge vertically close boxes and prepare the final response."""
+ current_boxes, current_texts = [], []
+ all_boxes, all_texts = [], []
+
+ for ind in range(len(filtered_results) - 1):
+ if not current_boxes:
+ current_boxes.append(filtered_results[ind][0])
+ current_texts.append(filtered_results[ind][1])
+
+ if self.are_vertically_close(filtered_results[ind][0], filtered_results[ind + 1][0]):
+ current_boxes.append(filtered_results[ind + 1][0])
+ current_texts.append(filtered_results[ind + 1][1])
+ else:
+ merged = self.merge_boxes(current_boxes, current_texts)
+ all_boxes.append(merged[0])
+ all_texts.append(merged[1])
+ current_boxes, current_texts = [], []
+
+ if current_boxes:
+ merged = self.merge_boxes(current_boxes, current_texts)
+ all_boxes.append(merged[0])
+ all_texts.append(merged[1])
+
+ if not current_boxes and filtered_results:
+ merged = self.merge_boxes([filtered_results[-1][0]], [filtered_results[-1][1]])
+ all_boxes.append(merged[0])
+ all_texts.append(merged[1])
+
+ response = {
+ 'status': 'success',
+ 'message': 'Data received',
+ 'boxes': BoundingBoxUtils.convert_to_json_serializable(all_boxes),
+ 'text': BoundingBoxUtils.convert_to_json_serializable(all_texts),
+ }
+
+ return response
+
+# Main execution function
+def labels():
+ """Main function to handle image OCR processing based on input arguments."""
+ source_type = sys.argv[2]
+ smart_mode = (sys.argv[3] == 'smart')
+ with open(sys.argv[1], 'r') as f:
+ img_source = f.read()
+ # Create ImageLabelProcessor instance
+ processor = ImageLabelProcessor(img_source, source_type, smart_mode)
+ response = processor.process_image()
+
+ # Print and return the response
+ print(response)
+ return response
+
+
+labels()
diff --git a/src/server/flashcard/requirements.txt b/src/server/flashcard/requirements.txt
new file mode 100644
index 000000000..eb92a819b
--- /dev/null
+++ b/src/server/flashcard/requirements.txt
@@ -0,0 +1,12 @@
+easyocr==1.7.1
+requests==2.32.3
+pillow==10.4.0
+numpy==1.26.4
+tqdm==4.66.4
+Werkzeug==3.0.3
+python-dateutil==2.9.0.post0
+six==1.16.0
+certifi==2024.6.2
+charset-normalizer==3.3.2
+idna==3.7
+urllib3==1.26.19 \ No newline at end of file
diff --git a/src/server/flashcard/venv/pyvenv.cfg b/src/server/flashcard/venv/pyvenv.cfg
new file mode 100644
index 000000000..740014e00
--- /dev/null
+++ b/src/server/flashcard/venv/pyvenv.cfg
@@ -0,0 +1,3 @@
+home = /Library/Frameworks/Python.framework/Versions/3.10/bin
+include-system-site-packages = false
+version = 3.10.11
diff --git a/src/server/index.ts b/src/server/index.ts
index 88dbd232d..1f9af9ee0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -7,6 +7,7 @@ import AssistantManager from './ApiManagers/AssistantManager';
import DataVizManager from './ApiManagers/DataVizManager';
import DeleteManager from './ApiManagers/DeleteManager';
import DownloadManager from './ApiManagers/DownloadManager';
+import FireflyManager from './ApiManagers/FireflyManager';
import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager';
import SessionManager from './ApiManagers/SessionManager';
import UploadManager from './ApiManagers/UploadManager';
@@ -71,6 +72,7 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage
new GeneralGoogleManager(),
/* new GooglePhotosManager(), */ new DataVizManager(),
new AssistantManager(),
+ new FireflyManager(),
];
// initialize API Managers
@@ -112,7 +114,6 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage
});
const serve: PublicHandler = ({ req, res }) => {
- // eslint-disable-next-line new-cap
const detector = new mobileDetect(req.headers['user-agent'] || '');
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 4dcb32f8b..a56ab5d18 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -108,14 +108,14 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
// detect search query and use default search engine
res.redirect(req.headers.referer + 'corsProxy/' + encodeURIComponent('http://www.google.com' + relativeUrl));
} else {
- res.end();
+ res.status(404).json({ error: 'no such file or endpoint: try /home /logout /login' });
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function proxyServe(req: any, requrl: string, response: any) {
- // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
const htmlBodyMemoryStream = new (require('memorystream'))();
let wasinBrFormat = false;
const sendModifiedBody = () => {
@@ -189,7 +189,6 @@ function proxyServe(req: any, requrl: string, response: any) {
res.headers['x-permitted-cross-domain-policies'] = 'all';
res.headers['x-frame-options'] = '';
res.headers['content-security-policy'] = '';
- // eslint-disable-next-line no-multi-assign
response.headers = response._headers = res.headers;
})
.on('end', sendModifiedBody)
@@ -247,6 +246,10 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
const app = buildWithMiddleware(express());
const compiler = webpack(config as webpack.Configuration);
+ // Default route
+ app.get('/', (req, res) => {
+ res.redirect(req.user ? '/home' : '/login'); //res.send('This is the default route.');
+ });
// route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
app.use(whm(compiler));
@@ -259,7 +262,6 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
isRelease && !SSL.Loaded && SSL.exit();
routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content
-
isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));
const server = isRelease ? createServer(SSL.Credentials, app) : app;
await new Promise<void>(resolve => {