diff options
author | bobzel <zzzman@gmail.com> | 2024-10-15 13:25:47 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-10-15 13:25:47 -0400 |
commit | 87d2d1d0305683a07166e44cfbcabfc94ff2069b (patch) | |
tree | 12d529d92d378870f8e1a66c534680fa0e28f3aa /src | |
parent | 63d1731bb675b71c20c0e460cf50ef9d674a6c08 (diff) | |
parent | 29b83f023442c313ca5cf95f70f6430f101060e6 (diff) |
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 8 | ||||
-rw-r--r-- | src/client/documents/DocUtils.ts | 2 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 44 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 6 | ||||
-rw-r--r-- | src/client/util/Scripting.ts | 12 | ||||
-rw-r--r-- | src/client/util/node_modules/type_decls.d | 3 | ||||
-rw-r--r-- | src/client/views/TagsView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarousel3DView.tsx | 33 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 58 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.tsx | 36 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 620 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 34 | ||||
-rw-r--r-- | src/fields/Doc.ts | 2 | ||||
-rw-r--r-- | src/fields/util.ts | 1 |
19 files changed, 452 insertions, 425 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 66c49abc7..8a2c91269 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -5,8 +5,8 @@ enum GPTCallType { SUMMARY = 'summary', COMPLETION = 'completion', EDIT = 'edit', - CHATCARD = 'chatcard', - FLASHCARD = 'flashcard', + CHATCARD = 'chatcard', // a single flashcard style response to a question + FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic QUIZ = 'quiz', SORT = 'sort', DESCRIBE = 'describe', @@ -38,7 +38,6 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { // newest model: gpt-4 summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, - 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: ' }, stack: { model: 'gpt-4o', maxTokens: 2048, @@ -66,6 +65,7 @@ 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: ' }, 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', @@ -127,7 +127,7 @@ let lastResp = ''; */ 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 opts: GPTCallOpts = callTypeMap[callType]; + const opts = callTypeMap[callType]; if (lastCall === inputText && dontCache !== true) return lastResp; try { lastCall = inputText; diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 5f54f9d0a..19f3c89ef 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -103,7 +103,6 @@ export namespace DocUtils { return false; } const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]); - // eslint-disable-next-line no-restricted-syntax for (const facetKey of facetKeys) { const facet = filterFacets[facetKey]; @@ -288,7 +287,6 @@ export namespace DocUtils { return doc; } export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) { - // eslint-disable-next-line no-return-assign return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 0d7e0b20e..99af1f1a9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -18,6 +18,7 @@ import { PointData } from '../../pen-gestures/GestureTypes'; import { DocServer } from '../DocServer'; import { dropActionType } from '../util/DropActionTypes'; import { CollectionViewType, DocumentType } from './DocumentTypes'; +import { Id } from '../../fields/FieldSymbols'; class EmptyBox { public static LayoutString() { @@ -305,6 +306,7 @@ export class DocumentOptions { _text_fontFamily?: string; _text_fontWeight?: string; text_align?: STRt = new StrInfo('horizontal text alignment default'); + 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 @@ -359,6 +361,7 @@ export class DocumentOptions { isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); + cloneOnCopy?: BOOLt = new BoolInfo('if this Doc is a field of another Doc, then it should be copied when the other Doc is copied'); mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false); config_latitude?: NUMt = new NumInfo('latitude of a map', false); @@ -419,6 +422,12 @@ export class DocumentOptions { flexGap?: NUMt = new NumInfo('Linear view flex gap'); flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse'; + // Comparison + data_revealOp?: STRt = new StrInfo("visual reveal type for front and back of comparison - 'slide' or 'flip' "); + data_revealOp_hover?: BOOLt = new BoolInfo('reveal back of comparison manually or by hovering'); + data_front?: DOCt = new DocInfo('contents of front of flashcard/comparison'); + data_back?: DOCt = new DocInfo('contents of back of flashcard/comparison'); + link?: string; link_description?: string; // added for links link_relationship?: string; // type of relatinoship a link represents @@ -783,8 +792,39 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options); } - export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options); + export function ComparisonDocument(title: string, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options); + } + /** + * Creates a text box where the supplied text (and optional iimage) will be vertically + * and horizontally centered. If text_placeholder is set to true, then the text will be + * treated as placeholder text and automatically selected when the text box is selected. + * @param title name of text box + * @param text text to display in text box + * @param opts metadata fields to set on text box + * @param img optional image to add to text box + * @returns + */ + export function CenteredTextCreator(title: string, text: string, opts: DocumentOptions, img?: Doc) { + return TextDocument(RichTextField.textToRtf(text, img?.[Id]), { + title, // + _layout_autoHeight: true, + _layout_centered: true, + text_align: 'center', + _layout_fitWidth: true, + ...opts, + }); + } + + export function FlashcardDocument(title: string, front?: Doc, back?: Doc, options: DocumentOptions = { title: 'Flashcard' }) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', { + 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, + title, + ...options, + }); } export function DiagramDocument(options: DocumentOptions = { title: '' }) { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 274bc79be..555ccdd88 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -369,14 +369,14 @@ pie title Minerals in my tap water creator:(opts:DocumentOptions)=> Doc // how to create the empty thing if it doesn't exist }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, - {key: "Flashcard", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _layout_isFlashcard: true, _layout_fitWidth: true, _width: 300, _height: 300}}, + {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: "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: "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: "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, }}, @@ -782,7 +782,7 @@ pie title Minerals in my tap water return [ {title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} }, {title: "1 Line", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, - {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: '{ datavizFromSchema()'} }, ]; + {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: 'datavizFromSchema()'} }, ]; } static webTools() { diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index b1db0bf39..5d78c2fab 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -48,7 +48,7 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is // eslint-disable-next-line no-use-before-define function Run(script: string | undefined, customParams: string[], diagnostics: ts.Diagnostic[], originalScript: string, options: ScriptOptions): CompileResult { - const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); + const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error).filter(diag => diag.code !== 2304 && diag.code !== 2339); if ((options.typecheck !== false && errors.length) || !script) { return { compiled: false, errors }; } @@ -188,6 +188,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp }, ''); const found = ScriptField.GetScriptFieldCache(script + ':' + signature); // if already compiled, found is the result; cache set below if (found) return found as CompiledScript; + options.typecheck = true; const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; if (options.params && !options.params.this) options.params.this = Doc.name; if (options.globals) { @@ -221,13 +222,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if ('this' in params || 'this' in capturedVariables) { paramNames.push('this'); } - for (const key in params) { - if (key !== 'this') { - paramNames.push(key); - } - } + paramNames.push(...Object.keys(params).filter(p => p!== 'this' && !Object.keys(capturedVariables).includes(p))); + const paramList = paramNames.map(key => { - const val = params[key]; + const val = typeof params[key] === "string" && params[key].length && !"\"'`".includes(params[key][0]) ? `"${params[key]}"` : params[key]; return `${key}: ${val}`; }); for (const key in capturedVariables) { diff --git a/src/client/util/node_modules/type_decls.d b/src/client/util/node_modules/type_decls.d index 1a93bbe59..9058b4394 100644 --- a/src/client/util/node_modules/type_decls.d +++ b/src/client/util/node_modules/type_decls.d @@ -198,6 +198,9 @@ declare class InkField extends ObjectField { constructor(data:Array<{X:number, Y:number}>); [Copy](): ObjectField; } +declare class DocumentDragData { + constructor(dragDoc: Doc[], dropAction?: string); +} // @ts-ignore declare const console: any; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 072cae3af..2615bc5fb 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -361,7 +361,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined, transformOrigin: 'top left', maxWidth: `${100 * this.currentScale}%`, - width: 'max-content', + width: `${100 * this.currentScale}%`, transform: `scale(${1 / this.currentScale})`, backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 05be376ca..e9ace733e 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -15,6 +15,7 @@ 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'); @@ -53,18 +54,22 @@ export class CollectionCarousel3DView extends CollectionSubView() { centerScale = Number(CAROUSEL3D_CENTER_SCALE); sideScale = Number(CAROUSEL3D_SIDE_SCALE); - panelWidth = () => this._props.PanelWidth() / 3; - panelHeight = () => this._props.PanelHeight() * this.sideScale; + panelWidth = () => this._props.PanelWidth() / 3 / this.nativeScaling(); + panelHeight = () => (this._props.PanelHeight() * this.sideScale) / this.nativeScaling(); onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); - isChildContentActive = () => - this._props.isContentActive?.() === false - ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) - ? true - : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + isChildContentActive = computedFn( + (doc: Doc) => () => + this._props.isContentActive?.() === false ? false - : undefined; + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.isContentActive?.() && this.curDoc() === doc + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined + ); contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); childScreenLeftToLocal = () => this.contentScreenToLocalXf() @@ -110,7 +115,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { LayoutTemplateString={this._props.childLayoutString} focus={this.focus} ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} + isContentActive={this.isChildContentActive(child)} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -125,7 +130,6 @@ export class CollectionCarousel3DView extends CollectionSubView() { } changeSlide = (direction: number) => { - DocumentView.DeselectAll(); this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1); }; @@ -205,9 +209,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { docViewProps = () => ({ ...this._props, // isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, - isContentActive: this.isChildContentActive, + isContentActive: this._props.isContentActive, ScreenToLocalTransform: this.contentScreenToLocalXf, }); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; render() { return ( <div @@ -216,6 +221,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + transformOrigin: 'top left', + transform: `scale(${this.nativeScaling()})`, + width: `${100 / this.nativeScaling()}%`, + height: `${100 / this.nativeScaling()}%`, }}> <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}> {this.content} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index ef66a2c83..ff587b199 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -58,18 +58,12 @@ export class CollectionCarouselView extends CollectionSubView() { /** * Goes to the next Doc in the stack subject to the currently selected filter option. */ - advance = (e?: React.MouseEvent) => { - e?.stopPropagation(); - this.move(1); - }; + advance = () => this.move(1); /** * Goes to the previous Doc in the stack subject to the currently selected filter option. */ - goback = (e: React.MouseEvent) => { - e.stopPropagation(); - this.move(-1); - }; + goback = () => this.move(-1); curDoc = () => this.carouselItems[this.carouselIndex]?.layout; @@ -78,24 +72,23 @@ export class CollectionCarouselView extends CollectionSubView() { const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property); }; - contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); - contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin); + contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling(); + contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling(); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; contentScreenToLocalXf = () => this._props - .ScreenToLocalTransform() - .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)) - .scale(this._props.NativeDimScaling?.() || 1); + .ScreenToLocalTransform() // + .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling()); isChildContentActive = () => this._props.isContentActive?.() === false ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + : this._props.isContentActive() ? true : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false - : undefined; + : undefined; // prettier-ignore onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( @@ -202,6 +195,8 @@ export class CollectionCarouselView extends CollectionSubView() { ); } + nativeScaling = () => this._props.NativeDimScaling?.() || 1; + docViewProps = () => ({ ...this._props, // isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, @@ -212,20 +207,25 @@ export class CollectionCarouselView extends CollectionSubView() { render() { return ( - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, - width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, - height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, - left: NumCast(this.layoutDoc._xMargin), - top: NumCast(this.layoutDoc._yMargin), - }}> - {this.content} - {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} - {this.navButtons} + <div> + <div + className="collectionCarouselView-outer" + ref={this.createDashEventsTarget} + style={{ + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + left: NumCast(this.layoutDoc._xMargin), + top: NumCast(this.layoutDoc._yMargin), + transformOrigin: 'top left', + transform: `scale(${this.nativeScaling()})`, + width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`, + height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`, + position: 'relative', + }}> + {this.content} + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} + {this.navButtons} + </div> </div> ); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f85b0b433..ab5b70a85 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -523,7 +523,7 @@ export function CollectionSubView<X>() { /** * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings */ - @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore /** * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection * This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7418d4360..6f0833a22 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -83,7 +83,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr return viewField; } - screenToLocalTransform = () => (this._props.renderDepth ? this.ScreenToLocalBoxXf() : this.ScreenToLocalBoxXf().scale(this._props.PanelWidth() / this.bodyPanelWidth())); + screenToLocalTransform = this.ScreenToLocalBoxXf; // prettier-ignore private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => { TraceMobx(); @@ -202,8 +202,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr } }; - bodyPanelWidth = () => this._props.PanelWidth(); - childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null); isContentActive = () => this._isContentActive; @@ -221,7 +219,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr removeDocument: this.removeDocument, isContentActive: this.isContentActive, isAnyChildContentActive: this.isAnyChildContentActive, - PanelWidth: this.bodyPanelWidth, + PanelWidth: this._props.PanelWidth, PanelHeight: this._props.PanelHeight, ScreenToLocalTransform: this.screenToLocalTransform, childLayoutTemplate: this.childLayoutTemplate, diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index 45e040653..79eb7f107 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 { IconButton, MultiToggle, Type } from 'browndash-components'; +import { MultiToggle, Type } from 'browndash-components'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -25,8 +25,8 @@ enum practiceVal { } export enum flashcardRevealOp { - HOVER = 'hover', FLIP = 'flip', + SLIDE = 'slide', } interface PracticeUIProps { @@ -153,20 +153,28 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp selectedItems={this.practiceMode} onSelectionChange={(val: (string | number) | (string | number)[]) => togglePracticeMode(val as practiceMode)} /> - <IconButton - tooltip="hover over card to reveal answer" - type={Type.TERT} - text={StrCast(this._props.layoutDoc.revealOp)} + <MultiToggle + tooltip="How to reveal flashcard answer" + type={Type.PRIM} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} - icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? 'hand-point-up' : 'question'} size="sm" />} - label={StrCast(this._props.layoutDoc.revealOp)} - onPointerDown={e => - setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => { - this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? flashcardRevealOp.FLIP : flashcardRevealOp.HOVER; - this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined; - }) - } + multiSelect={false} + isToggle={false} + toggleStatus={!!this.practiceMode} + label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)} + items={[ + ['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)], + ['trigger', this._props.layoutDoc.revealOp_hover ? 'hand-point-up' : 'hand', this._props.layoutDoc.revealOp_hover ? 'show on hover' : 'show on click'], + ].map(([item, icon, tooltip]) => ({ + icon: <FontAwesomeIcon className={`FlashcardPracticeUI-${item}`} color={setColor(item as practiceMode)} icon={icon as IconProp} size="sm" />, + tooltip: tooltip, + val: item, + }))} + selectedItems={this._props.layoutDoc.revealOp_hover ? ['reveal', 'trigger'] : 'reveal'} + onSelectionChange={(val: (string | number) | (string | number)[]) => { + if (val === 'reveal') this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.SLIDE ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE; + if (val === 'trigger') this._props.layoutDoc.revealOp_hover = !this._props.layoutDoc.revealOp_hover; + }} /> </div> ); diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index c328ef4bf..d1cc48051 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -246,8 +246,7 @@ pointer-events: none; } -.comparisonBox-interactive { - pointer-events: unset; +.comparisonBox-slide { cursor: ew-resize; .slide-bar { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 0582bc996..38ce5f2f7 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -9,7 +9,6 @@ import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setu import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; -import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; import { nullAudio } from '../../../fields/URLField'; @@ -26,28 +25,31 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import '../pdf/GPTPopup/GPTPopup.scss'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; const API_URL = 'https://api.unsplash.com/search/photos'; /** - * This view serves two distinct functions depending on the metadata field layout_isFlashcard - * 1) it provides a before/after animated sliding transition between two Docs - * 2) it provides a question/answer switch between two Docs (flashcard) + * This view serves two distinct functions depending on the revealOp field ('slide' or 'flip) + * 1) ('slide') - provides a before/after animated sliding transition between two Docs + * 2) ('flip') - provides a question/answer flip between two Docs + * And a third function that overrides the first two if the doc's container has its 'practiceMode' set to 'quiz' + * 3) ('quiz') - it provides a quiz view that displays a question and a user answer that can be "scored" by GPT + * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz. + * + * In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields * - * In either case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields + * For 'flip' and 'slide', the trigger can either be clicking, or hovering as determined by the revealOp_hover field. + * For 'quiz' the data of both Docs are shown in a single-view quiz display. * - * In the case of the flashcard, there is an icon that allows the user to choose between a - * hover and a flip action to switch between cards. The transition is stored in the 'revealOp' field. - * In addition, if a flashcard is created without data in the front/back fields, this will - * create Text documents with placeholder text indicating to the user how to fill in the cards. - * One option is to allow the user to enter a topic and, by clicking on the flashcard stack button, - * convert the comparision box into a stack of comparison boxes filled in by GPT about the topic. + * Users can create a stack of flashcards all at once (only) from an empty flashcard by entering a topic into the front card + * and clicking on the flashcard stack button. This will convert the comparision box into a stack of comparison boxes + * filled in by GPT about the topic. * */ @@ -56,6 +58,59 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + /** + * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer + * @param tuple string containing Question:, Answer: and optionally a Keyword: + * @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]; + const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0]; + const rest = tuple.replace(question, ''); + // prettier-ignore + const answer = rest.startsWith(ktoken) ? // if keyword comes first, + tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer + rest.includes(ktoken) ? // otherwise if keyword is present it must come after answer, + rest.split(ktoken)[0].split(atoken)[1] : // split at keyword and take what comes first and split that at answer and take what's left + rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left + const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim(); + const fillInFlashcard = (img?: Doc) => { + const front = Docs.Create.CenteredTextCreator('question', question, {}, img); + const back = Docs.Create.CenteredTextCreator('answer', answer, {}); + if (useDoc) { + useDoc[DocData][frontKey] = front; + useDoc[DocData][backKey] = back; + return useDoc; + } + return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 }); + }; + return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); + } + + /** + * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by: + * Question: ... Answer: ... Keyword: ... + * Note that Keyword or Answer may not be present, or their orders may be reversed. + */ + public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) { + return Promise.all( + text + .split(ComparisonBox.qtoken) + .filter(t => t) + .map(tuple => ComparisonBox.createFlashcard(tuple, front, back)) + ).then(docs => { + return Docs.Create.CarouselDocument(docs, { + title: 'flashcard deck', + _width: width, + _height: height, + _layout_fitWidth: false, + _layout_autoHeight: true, + _xMargin: 5, + _yMargin: 5, + }); + }); + } private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; static qtoken = 'Question: '; @@ -65,12 +120,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() private _sideBtnWidth = 35; private _closeRef = React.createRef<HTMLDivElement>(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; - private _reactDisposer: IReactionDisposer | undefined; + private _reactDisposer: { [key: string]: IReactionDisposer } = {}; @observable private _inputValue = ''; @observable private _outputValue = ''; @observable private _loading = false; - @observable private _isEmpty = false; @observable private _childActive = false; @observable private _animating = ''; @observable private _listening = false; @@ -84,18 +138,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() componentDidMount() { this._props.setContentViewBox?.(this); - this._reactDisposer = reaction( - () => this._props.isSelected(), // when this reaction should update + this._reactDisposer.select = reaction( + () => this._props.isSelected(), selected => { - if (selected && this.isFlashcard) this.activateContent(); + if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent(); !selected && (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); + } + }, // what it should update to + { fireImmediately: true } + ); } componentWillUnmount() { - this._reactDisposer?.(); + Object.values(this._reactDisposer).forEach(disposer => disposer?.()); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { @@ -118,16 +182,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return undefined; }, 'internal drop'); + @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 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 + @computed get backText() { return RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; } // prettier-ignore @computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore @computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore - @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore + @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._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore - set revealOp(value:string) { this.layoutDoc[this.revealOpKey] = value; } // 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 + @computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore @computed get loading() { return this._loading; } // prettier-ignore set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore @@ -139,13 +207,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) { - this.flipFlashcard(); + this.animateFlipping(); } }) } style={{ - background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black', - color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white', + background: this.revealOpHover ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black', + color: this.revealOpHover ? 'black' : this._renderSide === this.backKey ? 'black' : 'white', display: 'inline-block', }}> <FontAwesomeIcon icon="turn-up" size="xl" /> @@ -169,7 +237,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @computed get flashcardMenu() { return SnappingManager.HideDecorations ? null : ( <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}> - {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon} + {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon} {!this._props.isSelected() || this._renderSide === this.frontKey ? null : ( <Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}> <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}> @@ -177,9 +245,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> </Tooltip> )} - {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : ( + {!this._props.isSelected() || this._renderSide === this.backKey || !CollectionFreeFormView.from(this.DocumentView?.()) || (this.dataDoc[this.backKey] && !DocCast(this.dataDoc[this.backKey])?.text_placeholder) ? null : ( <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> - <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> + <div + className="comparisonBox-button" + onClick={() => + this.askGPT(GPTCallType.STACK).then(async text => { + const newCol = await ComparisonBox.createFlashcardDeck(text, NumCast(this.layoutDoc._width, 250) + 50, NumCast(this.layoutDoc._height, 200), this.frontKey, this.backKey); + newCol.x = NumCast(this.layoutDoc.x); + newCol.y = NumCast(this.layoutDoc.y); + this._props.DocumentView?.()._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + }) + }> <FontAwesomeIcon icon="layer-group" size="xl" /> </div> </Tooltip> @@ -232,13 +310,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); addDoc = (doc: Doc, which: string) => { - if (this.dataDoc[which] && !this._isEmpty) return false; this.dataDoc[which] = doc; return true; }; remDoc = (doc: Doc, which: string) => { if (this.dataDoc[which] === doc) { - this._isEmpty = true; this.dataDoc[which] = undefined; return true; } @@ -270,6 +346,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true); remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true); remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true); + animateSliding = action((targetWidth: number) => { + this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + setTimeout(action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore + }); + + _flipAnim: NodeJS.Timeout | undefined; + animateFlipping = action((side?: string) => { + if (side !== this._renderSide) { + this._renderSide = side ?? (this._renderSide === this.frontKey ? this.backKey : this.frontKey); // switches to new front + this._animating = '0'; // reveals old front on the bottom layer by making top layer transparent + setTimeout( + action(() => { + this._animating = `all ${this._slideTiming * 5}ms`; // makes new front fade in + clearTimeout(this._flipAnim); + this._flipAnim = setTimeout( action(() => { this._animating = ''; }), this._slideTiming * 5 ); // prettier-ignore + }) + ); + } + }); registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { @@ -287,13 +383,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }), false, undefined, - action(() => { - if (!this._childActive) { - this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); - setTimeout( action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore - } - }) + action(() => !this._childActive && this.animateSliding(targetWidth)) ); } }; @@ -397,124 +487,51 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() axios .post( 'http://localhost:105/youtube/', // - { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) }, + { file: this.getYouTubeVideoId(this.frontText) }, { headers: { 'Content-Type': 'application/json' } } ) .then(response => response.data.transcription); /** - * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer - * @param tuple string containing Question:, Answer: and optionally a Keyword: - * @param useDoc doc to fill in instead of creating a Doc - * @returns the resulting flashcard Doc - */ - createFlashcard = (tuple: string, useDoc?: Doc) => { - const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken]; - const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); - const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0]; - const rest = tuple.replace(question, ''); - // prettier-ignore - const answer = rest.startsWith(ktoken) ? // if keyword comes first, - tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer - rest.includes(ktoken) ? // otherwise if keyword is present it must come after answer, - rest.split(ktoken)[0].split(atoken)[1] : // split at keyword and take what comes first and split that at answer and take what's left - rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left - const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim(); - const fillInFlashcard = (img?: Doc) => { - newDoc[DocData][this.frontKey] = this.textCreator('question', question, img); - newDoc[DocData][this.backKey] = this.textCreator('answer', answer); - return newDoc; - }; - return keyword && keyword.toLowerCase() !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); - }; - - /** - * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by: - * Question: ... Answer: ... Keyword: ... - * Note that Keyword or Answer may not be present, or their orders may be reversed. - */ - createFlashcardDeck = (text: string) => { - Promise.all( - text - .split(ComparisonBox.qtoken) - .filter(t => t) - .map(tuple => this.createFlashcard(tuple)) - ).then(docs => { - const newCol = Docs.Create.CarouselDocument(docs, { - _width: NumCast(this.layoutDoc._width, 250) + 50, - _height: NumCast(this.layoutDoc._height, 200) + 50, - _layout_fitWidth: false, - _layout_autoHeight: true, - _xMargin: 5, - _yMargin: 5, - x: NumCast(this.layoutDoc.x), - y: NumCast(this.layoutDoc.y), - }); - - this._props.DocumentView?.()._props.addDocument?.(newCol); - this._props.removeDocument?.(this.Document); - }); - }; - - /** * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { - const frontText = RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; - const backText = RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; - const questionText = 'Question: ' + frontText; - const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + backText : ''); + const questionText = 'Question: ' + this.frontText; + const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); + this.loading = true; - let res = ''; - - if (callType !== GPTCallType.CHATCARD || frontText) { - try { - res = await gptAPICall(queryText, callType); - if (!res) { - console.error('GPT call failed'); - } else - switch (callType) { - case GPTCallType.CHATCARD: - DocCast(this.dataDoc[this.backKey])[DocData].text = res; - break; - case GPTCallType.QUIZ: - runInAction(() => { - this._renderSide = this.backKey; - this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); - }); - break; - case GPTCallType.FLASHCARD: - default: - } - } catch (err) { - console.error('GPT call failed', err); - } - } + const res = !this.frontText + ? '' + : await gptAPICall(queryText, callType).then( + action(resp => { + switch (resp && callType) { + case GPTCallType.CHATCARD: + DocCast(this.dataDoc[this.backKey])[DocData].text = resp; + break; + case GPTCallType.QUIZ: + this._renderSide = this.backKey; + this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); + break; + case GPTCallType.FLASHCARD: + default: + } + return resp; + }) + ); this.loading = false; + if (!res) console.error('GPT call failed'); return res; }; layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); - findImageTags = async () => { - const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img'); - if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD); - if (c) { - this.loading = true; - for (const i of c) { - if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); - } - this.loading = false; - } - }; - /** * Ask GPT for advice on how to improve speech by comparing the phonetic transcription of * a users audio recording with the phonetic transcription of their intended sentence. * @param phonemes */ askGPTPhonemes = async (phonemes: string) => { - const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); + const sentence = this.frontText; const phon6 = 'huː ɑɹ juː tədeɪ'; const phon4 = 'kamo estas hɔi'; const promptEng = @@ -567,24 +584,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * @param selection * @returns Image Document */ - fetchImages = async (selection: string) => { + public static async fetchImages(selection: string) { try { const { data } = await axios.get(`${API_URL}?query=${selection}&page=1&per_page=${1}&client_id=Q4zruu6k6lum2kExiGhLNBJIgXDxD6NNj0SRHH_XXU0`); const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, { - _nativeWidth: Doc.NativeWidth(this.layoutDoc), - _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: NumCast(this.layoutDoc.x), - y: NumCast(this.layoutDoc.y), onClick: FollowLinkScript(), _width: 150, _height: 150, - title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', + title: selection, }); return imageSnapshot; } catch (error) { console.log(error); } - }; + } getImageDesc = async (u: string) => { try { @@ -597,22 +610,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }; - @action - flipFlashcard = () => { - this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey; - }; - - @action - hoverFlip = (side: string) => { - if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side; - }; - flashcardContextMenu = () => { const appearance = ContextMenu.Instance.findByDescription('Appearance...'); const appearanceItems = appearance?.subitems ?? []; - if (this.Document._layout_isFlashcard) { - appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); - } + appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); }; @@ -641,179 +642,166 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } return layoutTemplateString; }; - textCreator = (title: string, text: string, img?: Doc) => { - const newDoc = Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), { - title, // - _layout_autoHeight: true, - _layout_centered: true, - text_align: 'center', - _layout_fitWidth: true, - }); - return newDoc; - }; childActiveFunc = () => this._childActive; contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); - render() { - const clearButton = (which: string) => ( - <Tooltip title={<div className="dash-tooltip">remove</div>}> - <div - ref={this._closeRef} - className={`clear-button ${which}`} - onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding - > - <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> - </div> - </Tooltip> - ); - const displayDoc = (whichSlot: string) => { - const whichDoc = DocCast(this.dataDoc[whichSlot]); - const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); - - return targetDoc || layoutString ? ( - <> - <DocumentView - {...this._props} - showTags={undefined} - fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option - ignoreUsePath={layoutString ? true : undefined} - renderDepth={this.props.renderDepth + 1} - LayoutTemplateString={layoutString} - Document={layoutString ? this.Document : targetDoc} - containerViewPath={this._props.docViewPath} - moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} - removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} - NativeWidth={returnZero} - NativeHeight={returnZero} - ScreenToLocalTransform={this.contentScreenToLocalXf} - isContentActive={this.childActiveFunc} - isDocumentActive={returnFalse} - dontSelect={returnTrue} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider} - hideLinkButton - pointerEvents={this._childActive ? undefined : returnNone} - /> - {!this.isFlashcard ? clearButton(whichSlot) : null} - </> - ) : ( - <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> - </div> - ); - }; - const displayBox = (which: string, cover: number) => ( + + clearButton = (which: string) => ( + <Tooltip title={<div className="dash-tooltip">remove</div>}> <div - className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} - key={which} - style={{ width: this._props.PanelWidth() }} - onPointerDown={e => { - this.registerSliding(e, cover); - this.isFlashcard && this.activateContent(); - }} - ref={ele => this.createDropTarget(ele, which)}> - {!this._isEmpty ? displayDoc(which) : null} + ref={this._closeRef} + className={`clear-button ${which}`} + onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding + > + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> + </div> + </Tooltip> + ); + displayDoc = (whichSlot: string) => { + const whichDoc = DocCast(this.dataDoc[whichSlot]); + const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); + const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + + return targetDoc || layoutString ? ( + <> + <DocumentView + {...this._props} + showTags={undefined} + fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option + ignoreUsePath={layoutString ? true : undefined} + renderDepth={this.props.renderDepth + 1} + LayoutTemplateString={layoutString} + Document={layoutString ? this.Document : targetDoc} + containerViewPath={this._props.docViewPath} + moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} + removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} + NativeWidth={returnZero} + NativeHeight={returnZero} + ScreenToLocalTransform={this.contentScreenToLocalXf} + isContentActive={this.childActiveFunc} + isDocumentActive={returnFalse} + dontSelect={returnTrue} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider} + hideLinkButton + pointerEvents={this._childActive ? undefined : returnNone} + /> + {!this.isFlashcard ? this.clearButton(whichSlot) : null} + </> + ) : ( + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> </div> ); + }; - if (this.isFlashcard) { - if (this.dataDoc.data) { - if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) this.createFlashcard(StrCast(this.dataDoc.data), this.Document); - } else { - // add text box to each side when comparison box is first created - if (!this.dataDoc[this.backKey] && !this._isEmpty) { - const answer = this.textCreator('answer', 'answer here'); - this.dataDoc[this.backKey] = answer; - answer[DocData].text_placeholder = true; - } - - if (!this.dataDoc[this.frontKey] && !this._isEmpty) { - const question = this.textCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards'); - this.dataDoc[this.frontKey] = question; - question[DocData].text_placeholder = true; - } - } - - if (DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ) { - const text = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); - return ( - <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}> - <p style={{ color: 'white', padding: 10 }}>{text}</p> - <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p> - <div className="input-box"> - <textarea - value={this._renderSide === this.backKey ? this._outputValue : this._inputValue} - onChange={action(e => { - this._inputValue = e.target.value; - })} - placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} - readOnly={this._renderSide === this.backKey} - /> - {!this.loading ? null : ( - <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color='blue' /> - </div> - )} - </div> - <div> - <div className="submit-button"> - <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> - <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}> - {<FontAwesomeIcon icon="microphone" size="lg" />} - </button> - <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> - <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}> - Evaluate Pronunciation - </button> - <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}> - {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'} - </button> - </div> - </div> + displayBox = (which: string, cover: number) => ( + <div className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which)}> + {this.displayDoc(which)} + </div> + ); + + /* renders front(qustion) and back(answer) at the same time, then on user input replaces the answer with a GPT analysis of the answer */ + renderAsQuiz = (text: string) => ( + <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}> + <p style={{ color: 'white', padding: 10 }}>{text}</p> + <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p> + <div className="input-box"> + <textarea + value={this._renderSide === this.backKey ? this._outputValue : this._inputValue} + onChange={action(e => { + this._inputValue = e.target.value; + })} + placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} + readOnly={this._renderSide === this.backKey} + /> + {!this.loading ? null : ( + <div className="loading-spinner"> + <ReactLoading type="spin" height={30} width={30} color="blue" /> </div> - ); - } - - // render a normal flashcard when not a QuizCard - return ( - <div - className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ - onContextMenu={this.flashcardContextMenu} - onMouseEnter={() => this.hoverFlip(this.backKey)} - onMouseLeave={() => this.hoverFlip(this.frontKey)}> - {displayBox(this._renderSide, this._props.PanelWidth() - 3)} - {this.loading ? ( - <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color="blue" /> - </div> - ) : null} - {this.flashcardMenu} - </div> - ); - } - // render a comparison box that compares items side by side - return ( - <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox(this.backKey, this._props.PanelWidth() - 3)} - <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> - {displayBox(this.frontKey, 0)} + )} + </div> + <div> + <div className="submit-button"> + <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}> + <FontAwesomeIcon color="white" icon="caret-down" /> + </div> + <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}> + {<FontAwesomeIcon icon="microphone" size="lg" />} + </button> + <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}> + <FontAwesomeIcon color="white" icon="caret-down" /> + </div> + <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}> + Evaluate Pronunciation + </button> + <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? () => this.animateFlipping(this.frontKey) : this.handleRenderGPTClick}> + {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'} + </button> </div> + </div> + </div> + ); + + // if flashcard is rendered that has no data, then add some placeholders for question and answer + // addPlaceholdersForEmptyFlashcard = () => { + // if (this.dataDoc.data) { + // if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document); + // } + // }; + + // render a button that flips between front and back + renderAsFlip = () => ( + <div + style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} // + onMouseEnter={() => this.revealOpHover && this.animateFlipping(this.backKey)} + onMouseLeave={() => this.revealOpHover && this.animateFlipping(this.frontKey)}> + <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 1 : 0 }}> + {this.displayBox(this._renderSide === this.backKey ? this.frontKey : this.backKey, 0)} + </div> + <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 0 : 1 }}>{this.displayBox(this._renderSide, 0)}</div> + {this.flashcardMenu} + </div> + ); + + // render a slider that reveals front and back as slider is dragged horizonally + renderAsBeforeAfter = () => ( + <div + className="comparisonBox-slide" + style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} + onMouseEnter={() => this.revealOpHover && this.animateSliding(0)} + onMouseLeave={() => this.revealOpHover && this.animateSliding(this._props.PanelWidth() - 3)}> + {this.displayBox(this.backKey, this._props.PanelWidth() - 3)} + <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> + {this.displayBox(this.frontKey, 0)} + </div> - <div - className="slide-bar" - style={{ - left: `calc(${this.clipWidth + '%'} - 0.5px)`, - cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, - }} - onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ - > - <div className="slide-handle" /> - </div> + <div + className="slide-bar" + style={{ + left: `calc(${this.clipWidth + '%'} - 0.5px)`, + cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, + }} + onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + > + <div className="slide-handle" /> + </div> + </div> + ); + + render() { + const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([ + [flashcardRevealOp.FLIP, this.renderAsFlip], + [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore + if (this.isQuizMode) this.renderAsQuiz(this.frontText); + return ( + <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}> + {renderMode.get(this.revealOp)?.() ?? null} + {this.loading ? ( + <div className="loading-spinner"> + <ReactLoading type="spin" height={30} width={30} color="blue" /> + </div> + ) : null} </div> ); } diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx index 6c649bde3..16c016d6c 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -231,7 +231,8 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { this._disposers.lightbox = reaction( () => LightboxView.LightboxDoc(), doc => { - doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); + // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox + // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); } ); //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}})) diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 3daacc9bb..40c687b7e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -114,7 +114,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { if (key) target[key] = script.originalScript; return false; } - field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (res.result as FieldType)); + field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (typeof res.result === 'function' ? res.result.name : res.result as FieldType)); } } if (!key) return false; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 7243473e0..5ab9b556c 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -10,7 +10,6 @@ import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; -import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; import { undoBatch } from '../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; @@ -19,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; import './AnchorMenu.scss'; import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { ComparisonBox } from '../nodes/ComparisonBox'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -117,29 +117,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { */ transferToFlashcard = (text: string, x: number, y: number) => { - // put each question generated by GPT on the front of the flashcard - const senArr = text.trim().split('Question:'); - const collectionArr: Doc[] = []; - for (let i = 1; i < senArr.length; i++) { - const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); - newDoc.text = senArr[i]; - - collectionArr.push(newDoc); - } - // create a new carousel collection of these flashcards - const newCol = Docs.Create.CarouselDocument(collectionArr, { - _width: 250, - _height: 200, - _layout_fitWidth: false, - _layout_autoHeight: true, - }); - - newCol.x = x; - newCol.y = y; - newCol.zIndex = 1000; - - this.addToCollection?.(newCol); - this._loading = false; + ComparisonBox.createFlashcardDeck(text, 250, 200, 'data_front', 'data_back').then( + action(newCol => { + newCol.x = x; + newCol.y = y; + newCol.zIndex = 1000; + this.addToCollection?.(newCol); + this._loading = false; + }) + ); }; /** diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 45dfe233f..6ec195910 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1003,7 +1003,7 @@ export namespace Doc { } else if (field instanceof ObjectField) { const docAtKey = doc[key]; copy[key] = - docAtKey instanceof Doc && key.includes('layout[') + docAtKey instanceof Doc && (key.includes('layout[') || docAtKey.cloneOnCopy) ? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { diff --git a/src/fields/util.ts b/src/fields/util.ts index 60eadcdfd..33764aca5 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -227,7 +227,6 @@ function getEffectiveAcl(target: Doc | ListImpl<FieldType>, user?: string): symb * @param allowUpgrade whether permissions can be made less restrictive * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well) */ -// eslint-disable-next-line default-param-last export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) { const selfKey = `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}`; if (!target || visited.includes(target) || key === selfKey) return; |