diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/.LinkAnchorBox.scss.icloud | bin | 0 -> 168 bytes | |||
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 19 | ||||
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 163 | ||||
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 837 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 36 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.scss | 41 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 360 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkAnchorBox.tsx | 115 | ||||
| -rw-r--r-- | src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav | bin | 0 -> 765006 bytes | |||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 52 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 147 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.scss | 8 |
13 files changed, 1580 insertions, 199 deletions
diff --git a/src/client/views/nodes/.LinkAnchorBox.scss.icloud b/src/client/views/nodes/.LinkAnchorBox.scss.icloud Binary files differnew file mode 100644 index 000000000..193619551 --- /dev/null +++ b/src/client/views/nodes/.LinkAnchorBox.scss.icloud diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 9deed4de4..5d0ed7eab 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -10,7 +10,7 @@ import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, DateCast, NumCast } from '../../../fields/Types'; +import { Cast, DateCast, DocCast, NumCast } from '../../../fields/Types'; import { AudioField, nullAudio } from '../../../fields/URLField'; import { formatTime } from '../../../Utils'; import { Docs } from '../../documents/Documents'; @@ -29,6 +29,7 @@ import './AudioBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { OpenWhere } from './OpenWhere'; +import axios from 'axios'; /** * AudioBox @@ -257,6 +258,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); + this.Document.url = result.accessPaths.agnostic.client; + await this.pushInfo(); } }; this._recordStart = new Date().getTime(); @@ -284,9 +287,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; + pushInfo = async () => { + const audio = { + file: this.path, + }; + const response = await axios.post('http://localhost:105/recognize/', audio, { + headers: { + 'Content-Type': 'application/json', + }, + }); + this.Document[DocData].phoneticTranscription = response.data['transcription']; + console.log('RESPONSE: ' + response.data['transcription']); + }; + // context menu specificContextMenu = (): void => { const funcs: ContextMenuProps[] = []; + // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' }); funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors', event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 08d9e6010..da1d352f2 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -5,42 +5,136 @@ width: 100%; height: 100%; position: relative; + background: gray; z-index: 0; pointer-events: none; display: flex; p { + // bcz: what is this styling for? if text in the comparison box is colored, then this causes it to render with a black outline color: rgb(0, 0, 0); -webkit-text-stroke-color: black; -webkit-text-stroke-width: 0.2px; } - .input-box { - position: relative; + position: absolute; + top: 50; padding: 10px; width: 100%; - height: 100%; + height: 70%; display: flex; } .submit-button { - position: relative; + position: absolute; padding-bottom: 10px; + padding-top: 5px; padding-left: 5px; padding-right: 5px; - width: 100%; - height: 15%; - display: flex; + // width: 80%; + border-radius: 2px; + height: 17%; + display: inline-block; + bottom: 0; + // right: 0; + + &.schema-header-button { + color: gray; + margin: 3px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 15px; + } + } + + &.pronunciation { + width: 40%; + align-items: center; + justify-content: center; + } + &.submit { + width: 40%; + // float: right; + + // position: absolute; + // position: 10px; + // padding-left: 35%; + // padding-right: 80%; + // // width: 80px; + // // right: 0; + // right: 0; + // bottom: 0; + } + &.record { + width: 20%; + float: left; + border-radius: 2px; + // right: 0; + // height: 30%; + } button { - flex: 1; - position: relative; + // flex: 1; + // position: relative; } } + + .dropbtn { + background-color: #3498db; + color: white; + padding: 16px; + font-size: 16px; + border: none; + } + + .dropup { + position: absolute; + display: inline-block; + margin-top: 150px; + bottom: 0; + } + + .dropup-content { + display: none; + position: absolute; + background-color: #f1f1f1; + min-width: 160px; + bottom: 40px; + z-index: 1000; + } + + .dropup-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + } + + .dropup-content a:hover { + background-color: #ccc; + } + + .dropup:hover .dropup-content { + display: block; + } + + .dropup:hover .dropbtn { + background-color: #2980b9; + } + textarea { flex: 1; padding: 10px; - position: relative; + // position: relative; resize: none; + position: 'absolute'; + width: '91%'; + height: '80%'; + z-index: '-1'; + overscroll-behavior: contain; } .clip-div { @@ -117,6 +211,33 @@ opacity: 0.5; } } + + .loading-spinner { + display: flex; + justify-content: center; + align-items: center; + height: 90%; + width: 93%; + font-size: 20px; + font-weight: bold; + color: #0b0a0a; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } +} + +.comparisonBox-explain { + position: absolute; + top: 10px; + left: 10px; + z-index: 200; + // padding: 5px; + background: #dfdfdf; + pointer-events: none; } .comparisonBox-interactive { @@ -128,8 +249,9 @@ display: flex; } } + // .input-box { - // position: relative; + // position: absolute; // padding: 10px; // } // input[type='text'] { @@ -216,24 +338,5 @@ } } } - - .loading-circle { - position: relative; - width: 50px; - height: 50px; - border-radius: 50%; - border: 3px solid #ccc; - border-top-color: #333; - animation: spin 1s infinite linear; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } } } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index efaf6807a..139978a13 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,15 +1,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MathJax, MathJaxContext } from 'better-react-mathjax'; import { Tooltip } from '@mui/material'; -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils'; +import { returnFalse, returnNone, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; -import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; +import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT'; +import '../pdf/GPTPopup/GPTPopup.scss'; import { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -23,93 +25,323 @@ import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import ReactLoading from 'react-loading'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { tickStep } from 'd3'; +import { CollectionCarouselView } from '../collections/CollectionCarouselView'; +import { FollowLinkScript } from '../../documents/DocUtils'; +import { schema } from '../nodes/formattedText/schema_rts'; +import { Id } from '../../../fields/FieldSymbols'; +import axios from 'axios'; +import ReactMarkdown from 'react-markdown'; +import { WebField, nullAudio } from '../../../fields/URLField'; + +const API_URL = 'https://api.unsplash.com/search/photos'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; - private _closeRef = React.createRef<HTMLDivElement>(); - @observable _inputValue = ''; - @observable _outputValue = ''; - @observable _loading = false; - @observable _errorMessage = ''; - @observable _outputMessage = ''; - @observable _animating = ''; - constructor(props: FieldViewProps) { super(props); makeObservable(this); + this.setListening(); + } + + @observable private _inputValue = ''; + @observable private _outputValue = ''; + @observable private _loading = false; + @observable private _isEmpty = false; + @observable private _audio: Doc = Docs.Create.TextDocument(''); + @observable childActive = false; + @observable _yRelativeToTop: boolean = true; + @observable _animating = ''; + @observable mathJaxConfig = { + loader: { load: ['input/asciimath'] }, + }; + private _ref = React.createRef<HTMLDivElement>(); + + get revealOp() { + return this.layoutDoc[`_${this._props.fieldKey}_revealOp`]; + } + get clipHeightKey() { + return '_' + this._props.fieldKey + '_clipHeight'; + } + + get clipWidthKey() { + return '_' + this._props.fieldKey + '_clipWidth'; + } + + @computed get clipWidth() { + return NumCast(this.layoutDoc[this.clipWidthKey], 50); + } + + @computed get clipHeight() { + return NumCast(this.layoutDoc[this.clipHeightKey], 200); + } + + @computed get overlayAlternateIcon() { + const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + return ( + <Tooltip title={<div className="dash-tooltip">flip</div>}> + <div + className="formattedTextBox-alternateButton" + onPointerDown={e => + setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { + if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') { + this.flipFlashcard(); + + // console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); + // console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? '')); + } + }) + } + style={{ + background: this.revealOp === 'hover' ? 'gray' : usepath === 'alternate' ? 'white' : 'black', + color: this.revealOp === 'hover' ? 'black' : usepath === 'alternate' ? 'black' : 'white', + display: 'inline-block', + }}> + <div key="alternate" className="formattedTextBox-flip"> + <FontAwesomeIcon icon="turn-up" size="1x" /> + </div> + </div> + </Tooltip> + ); + } + + @computed get flashcardMenu() { + return ( + <div> + <Tooltip + title={ + this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? ( + <div className="dash-tooltip">Flip to front side to use GPT</div> + ) : ( + <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div> + ) + }> + <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.findImageTags() : null)}> + <FontAwesomeIcon icon="lightbulb" size="xl" /> + </div> + </Tooltip> + {DocCast(this.Document.embedContainer).type_collection === 'carousel' ? null : ( + <div> + <Tooltip title={<div>Create a flashcard pile</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}> + <FontAwesomeIcon icon="folder-plus" size="xl" /> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={e => this.gptFlashcardPile()}> + <FontAwesomeIcon icon="layer-group" size="xl" /> + </div> + </Tooltip> + </div> + )} + <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}> + <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> + </div> + </Tooltip> + {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}> + <div + style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }} + onPointerDown={e => this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}> + <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="trash-can" size="xl" /> + </div> + </Tooltip> */} + {/* {this.overlayAlternateIcon} */} + </div> + ); } componentDidMount() { this._props.setContentViewBox?.(this); + reaction( + () => this._props.isSelected(), // when this reaction should update + selected => !selected && (this.childActive = false) // what it should update to + ); } - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); + // this.childActive = true; if (ele) { this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; - @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as ('flip'|'hover'|undefined); } // prettier-ignore - @computed get clipWidth() { return NumCast(this.layoutDoc[`_${this.fieldKey}_clipWidth`], 50); } // prettier-ignore - set clipWidth(width: number) { this.layoutDoc[`_${this.fieldKey}_clipWidth`] = width; } // prettier-ignore - @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === 'alternate'; } // prettier-ignore - set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? 'alternate' : undefined; } // prettier-ignore - - animateClipWidth = action((clipWidth: number, duration = 200 /* ms */) => { - this._animating = `all ${duration}ms`; // turn on clip animation transition, then turn it off at end of animation - setTimeout(action(() => { this._animating = ''; }), duration); // prettier-ignore - this.clipWidth = clipWidth; - }); - - internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { const { droppedDocuments } = dropEvent.complete.docDragData; const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + // this.childActive = false; return added; } return undefined; }, 'internal drop'); - registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { + private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, - action((clickEv, doubleTap) => { + action((moveEv, doubleTap) => { if (doubleTap) { - this._isAnyChildContentActive = true; + this.childActive = true; if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); - // DocumentView.addViewRenderedCb(DocCast(this.dataDoc[this.fieldKey + '_1']), dv => { - // dv?.select(false); - // }); } }), - true, + false, undefined, - () => !this._isAnyChildContentActive && this.animateClipWidth((targetWidth * 100) / this._props.PanelWidth()) + action(() => { + if (this.childActive) return; + this._animating = 'all 200ms'; + // on click, animate slider movement to the targetWidth + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + // this.layoutDoc[this.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight(); + + setTimeout( + action(() => { + this._animating = ''; + }), + 200 + ); + }) ); } }; - onPointerMove = ({ movementX }: PointerEvent) => { + // private onClick(e: React.PointerEvent<HTMLDivElement>) { + // setupMoveUpEvents( + // this, e, this.onPointerMOve, emptyFunction(), () => {this._isAnyChildContentActive = true;}, emptyFunction(), emptyFunction() + // ) + // } + + @action + private onPointerMove = ({ movementX }: PointerEvent) => { const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth(); - if (width > 5 && width < this._props.PanelWidth()) { - this.clipWidth = (width * 100) / this._props.PanelWidth(); + if (width && width > 5 && width < this._props.PanelWidth()) { + this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth(); } return false; }; + @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + this._inputValue = e.target.value; + console.log(this._inputValue); + }; + + // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')} + @action activateContent = () => { + this.childActive = true; + }; + + @action handleRenderGPTClick = async () => { + // Call the GPT model and get the output + // await this.pushInfo(); + // console.log('PHONETIC TRANSCRIPTION: ' + DocCast(this._audio)[DocData]); + // this.Document.audio = this._audio; + console.log('Phonetic transcription: ' + DocCast(this.Document.audio).phoneticTranscription); + const phonTrans = DocCast(this.Document.audio).phoneticTranscription; + // const phonTrans = 's'; + if (phonTrans) { + // console.log(phonTrans.toString()); + this._inputValue = StrCast(phonTrans); + console.log('INPUT:' + this._inputValue); + this.askGPTPhonemes(this._inputValue); + } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ); + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; + this._outputValue = ''; + }; + + askGPTPhonemes = async (phonemes: string) => { + const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); + const phon = 'w ʌ ɪ z j ɔː ɹ n e ɪ m '; + const phon2 = 'h ʌ ɛ r j ʌ t ʌ d eɪ'; + const phon3 = 'ʃ eɪ oʊ s i ʃ oʊ z b aɪ ð ə s iː ʃ oʊ'; + const phon4 = 'kamo estas hɔi'; + const phon5 = 'la s e n a l'; + console.log('REG' + this.recognition.lang); + const promptEng = + 'Consider all possible phonetic transcriptions of the intended sentence "' + + sentence + + '" that is standard in American speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + + phonemes + + '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in American speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart. The goal is to be understood, not sound like a native speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "ceeffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; + const promptSpa = + 'Consider all possible phonetic transcriptions of the intended sentence "' + + 'como estás hoy' + + '" that is standard in Spanish speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + + phon4 + + '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in Spanish speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart; say good job if it would be understood by a native Spanish speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Identify "ɔi" sounds like "oy". Ignore accents and do not say anything to the user about this.'; + const promptAll = + 'Consider all possible phonetic transcriptions of the intended sentence "' + + sentence + + '" that is standard in ' + + this.convertAbr() + + ' speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + + phonemes + + '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in ' + + this.convertAbr() + + ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; + + switch (this.recognition.lang) { + case 'en-US': + console.log('English'); + this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); + break; + case 'es-ES': + console.log('Spanish'); + this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); + break; + default: + console.log('All'); + this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); + break; + } + }; + + pushInfo = async () => { + const formData = new FormData(); + + console.log(DocCast(this._audio).dataDoc); + const audio = { + file: this._audio.url, + }; + const response = await axios.post('http://localhost:105/recognize/', audio, { + headers: { + 'Content-Type': 'application/json', + }, + }); + this.Document.phoneticTranscription = response.data['transcription']; + console.log('RESPONSE: ' + response.data['transcription']); + }; + + @action handleHover = () => { + if (this.revealOp === 'hover') { + this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; + this.Document.forceActive = false; + } else { + this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; + this.Document.forceActive = true; + } + //this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'); + }; + + @action handleRenderClick = () => { + // Call the GPT model and get the output + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined; + }; + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ title: 'CompareAnchor:' + this.Document.title, @@ -127,23 +359,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; clearDoc = undoable((fieldKey: string) => { - delete this.dataDoc[fieldKey]; - this.dataDoc[fieldKey] = 'empty'; + // delete this.dataDoc[fieldKey]; + this.dataDoc[fieldKey] = undefined; + this._isEmpty = true; + // this.dataDoc[fieldKey] = 'empty'; + console.log('HERE' + fieldKey + ';'); }, 'clear doc'); // clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey]; 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.dataDoc[which] !== 'empty') return false; + 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] = 'empty'; + console.log('HEREEEE'); this.dataDoc[which] = undefined; return true; } + console.log('FALSE'); return false; }; @@ -163,10 +401,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; docStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { - switch (property) { - case StyleProp.PointerEvents: return 'none'; - default: return this._props.styleProvider?.(doc, props, property); - } // prettier-ignore + if (property === StyleProp.PointerEvents) return 'none'; + return this._props.styleProvider?.(doc, props, property); }; moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); @@ -205,91 +441,353 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return layoutTemplateString; }; + _closeRef = React.createRef<HTMLDivElement>(); + + createFlashcardPile(collectionArr: Doc[], gpt: boolean) { + const newCol = Docs.Create.CarouselDocument(collectionArr, { + _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50, + _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50, + _layout_fitWidth: false, + _layout_autoHeight: true, + }); + newCol['x'] = this.layoutDoc['x']; + newCol['y'] = NumCast(this.layoutDoc['y']) + 50; + newCol.type_collection = 'carousel'; + // console.log(newCol.data); + + if (gpt) { + this._props.DocumentView?.()._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + } else { + this._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + this.Document.embedContainer = newCol; + } + } + + gptFlashcardPile = async () => { + var text = await this.askGPT(GPTCallType.STACK); + console.log(text); + + var senArr = text?.split('Question: '); + var collectionArr: Doc[] = []; + for (let i = 1; i < senArr?.length!; i++) { + const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); + + if (StrCast(senArr![i]).includes('Keyword: ')) { + const question = StrCast(senArr![i]).split('Keyword: '); + const img = await this.fetchImages(question[1]); + // newDoc['image'] = img; + // const newDoc = Docs.Create.TextDocument(dataSplit[1]); + const textSide1 = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0]; + const textDoc1 = Docs.Create.TextDocument(question[0]); + const rtfiel = new RichTextField( + JSON.stringify({ + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + { type: 'text', text: question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0] }, + { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img![Id] } }, + ], + }, + ], + }, + selection: { type: 'text', anchor: 2, head: 2 }, + }), + textSide1 + ); + + textDoc1[DocData].text = rtfiel; + DocCast(newDoc)[DocData][this.fieldKey + '_1'] = textDoc1; + + DocCast(newDoc)[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1]); + // Doc.AddToMyOverlay(img!); + } + + collectionArr.push(newDoc); + } + this.createFlashcardPile(collectionArr, true); + }; + /** * Flips a flashcard to the alternate side for the user to view. */ flipFlashcard = () => { - this.useAlternate = !this.useAlternate; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined; }; /** * Changes the view option to hover for a flashcard. */ - hoverFlip = (alternate: boolean) => { - if (this.revealOp === 'hover') this.useAlternate = alternate; + hoverFlip = (side: string | undefined) => { + if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side; }; - /** - * Creates the button used to flip the flashcards. - */ - @computed get overlayAlternateIcon() { - return ( - <Tooltip title={<div className="dash-tooltip">flip</div>}> - <div - className="formattedTextBox-alternateButton" - onPointerDown={e => - setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - if (!this.revealOp || this.revealOp === 'flip') { - this.flipFlashcard(); - console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); - console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? '')); - } - }) - } - style={{ - background: this.useAlternate ? 'white' : 'black', - color: this.useAlternate ? 'black' : 'white', - }}> - <FontAwesomeIcon icon="turn-up" size="sm" /> - </div> - </Tooltip> - ); - } - - @action handleRenderGPTClick = () => { - // Call the GPT model and get the output - this.useAlternate = true; - this._outputValue = ''; - if (this._inputValue) this.askGPT(); - }; + animateRes = (resIndex: number, newText: string, callType: GPTCallType) => { + if (resIndex < newText.length) { + // const marks = this._editorView?.state.storedMarks ?? []; + switch (callType) { + case GPTCallType.CHATCARD: + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text += newText[resIndex]; + break; + case GPTCallType.QUIZ: + this._outputValue += newText[resIndex]; + break; + default: + return; + } - @action handleRenderClick = () => { - // Call the GPT model and get the output - this.useAlternate = false; + // this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(this._outputValue)); + setTimeout(() => this.animateRes(resIndex + 1, newText, callType), 20); + } }; /** * Calls the GPT model to create QuizCards. Evaluates how similar the user's response is to the alternate * side of the flashcard. */ - askGPT = async (): Promise<string | undefined> => { + askGPT = async (callType: GPTCallType): Promise<string | undefined> => { const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; - + this._loading = true; + const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']); + if (callType == GPTCallType.CHATCARD) { + if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') { + this._loading = false; + return; + } + this.flipFlashcard(); + } try { - const res = await gptAPICall(queryText, GPTCallType.QUIZ); + const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); if (!res) { console.error('GPT call failed'); return; } - this._outputValue = res; + // this.animateRes(0, res, callType); + if (callType == GPTCallType.CHATCARD) { + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; + // this.flipFlashcard(); + } else if (callType == GPTCallType.QUIZ) { + console.log(this._inputValue); + this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); + } + // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; + // this._outputValue = res; + else if (callType === GPTCallType.FLASHCARD) { + // console.log(res); + this._loading = false; + return res; + } else if (callType === GPTCallType.STACK) { + } + this._loading = false; + return res; + // console.log(res); } catch (err) { console.error('GPT call failed'); } + this._loading = false; }; layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); + // specificMenu = (): void => { + // const cm = ContextMenu.Instance; + // cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' }); + // }; + + findImageTags = async () => { + // const d = DocCast(this.dataDoc[this.props.fieldKey + '_0']); + // const copy = Doc.MakeCopy(this.Document, true); + const c = this.DocumentView?.().ContentDiv!.getElementsByTagName('img'); + // this.ProseRef?.getElementsByTagName('img'); + if (c?.length === 0) await this.askGPT(GPTCallType.CHATCARD); + if (c) { + this._loading = true; + for (let i of c) { + console.log(i); + if (i.className !== 'ProseMirror-separator') await this.getImageDesc(i.src); + } + this._loading = false; + // this.flipFlashcard(); + } + // console.log('HI' + this.ProseRef?.getElementsByTagName('img')); + }; + + static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; + } + }; + + getImageDesc = async (u: string) => { + try { + const hrefBase64 = await ComparisonBox.imageUrlToBase64(u); + const response = await gptImageLabel(hrefBase64, 'Answer the following question as a short flashcard response. Do not include a label.' + (this.dataDoc.text as RichTextField)?.Text); + + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = response; + } catch (error) { + console.log('Error'); + } + }; + + fetchImages = async (selection: string) => { + try { + const { data } = await axios.get(`${API_URL}?query=${selection}&page=1&per_page=${1}&client_id=Q4zruu6k6lum2kExiGhLNBJIgXDxD6NNj0SRHH_XXU0`); + console.log(data.results); + 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-', + }); + imageSnapshot['x'] = this.layoutDoc['x']; + imageSnapshot['y'] = this.layoutDoc['y']; + return imageSnapshot; + } catch (error) { + console.log(error); + } + }; + + // handleSelection = async (selection: string, newDoc: Doc) => { + // const images = await this.fetchImages(selection); + // return images; + // // Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true); + // images!.embedContainer = newDoc; + // Doc.AddEmbedding(newDoc, images!); + // const c = this.DocumentView?.().ContentDiv!.getElementsByClassName('afterBox-cont'); + // for (let i in c) { + // console.log('HERE' + i); + // } + // this.addDoc(images!, this.fieldKey + '_0'); + // Doc.AddEmbedding(newDoc, images!); + // this._props. + // Doc.AddToMyOverlay(images!); + // const node = schema.nodes.dashDoc.create({ + // width: NumCast(images?._width), + // height: NumCast(images?._height), + // title: 'dashDoc', + // docId: images![Id], + // float: 'unset', + // }); + // }; + + @observable private _listening = false; + @observable transcriptElement = ''; + SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + @observable recognition = new this.SpeechRecognition(); + + handleResult = (e: SpeechRecognitionEvent) => { + let interimTranscript = ''; + let finalTranscript = ''; + for (let i = e.resultIndex; i < e.results.length; i++) { + const transcript = e.results[i][0].transcript; + if (e.results[i].isFinal) { + finalTranscript += transcript; + } else { + interimTranscript += transcript; + } + } + console.log(interimTranscript); + this._inputValue += finalTranscript; + }; + + setListening = () => { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (SpeechRecognition) { + this.recognition.continuous = true; + this.recognition.interimResults = true; + this.recognition.lang = 'en-US'; + this.recognition.onresult = this.handleResult.bind(this); + } + ContextMenu.Instance.setLangIndex(0); + }; + + setLanguage = (e: React.MouseEvent, language: string, ind: number) => { + this.recognition.lang = language; + ContextMenu.Instance.setLangIndex(ind); + }; + + startListening = () => { + this.recognition.start(); + this._listening = true; + }; + + stopListening = () => { + this.recognition.stop(); + this._listening = false; + }; + + convertAbr = () => { + switch (this.recognition.lang) { + case 'en-US': + return 'English'; + case 'es-ES': + return 'Spanish'; + case 'fr-FR': + return 'French'; + case 'it-IT': + return 'Italian'; + case 'zh-CH': + return 'Mandarin Chinese'; + case 'ja': + return 'Japanese'; + default: + return 'Korean'; + } + }; + + openContextMenu = (x: number, y: number, evalu: boolean) => { + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: 'English', event: e => this.setLanguage(e, 'en-US', 0) }); //prettier-ignore + ContextMenu.Instance.addItem({ description: 'Spanish', event: e => this.setLanguage(e, 'es-ES', 1 )}); //prettier-ignore + ContextMenu.Instance.addItem({ description: 'French', event: e => this.setLanguage(e, 'fr-FR', 2) }); //prettier-ignore + ContextMenu.Instance.addItem({ description: 'Italian', event: e => this.setLanguage(e, 'it-IT', 3) }); //prettier-ignore + if (!evalu) ContextMenu.Instance.addItem({ description: 'Mandarin Chinese', event: e => this.setLanguage(e, 'zh-CH', 4) }); //prettier-ignore + ContextMenu.Instance.addItem({ description: 'Japanese', event: e => this.setLanguage(e, 'ja', 5) }); //prettier-ignore + ContextMenu.Instance.addItem({ description: 'Korean', event: e => this.setLanguage(e, 'ko', 6) }); //prettier-ignore + ContextMenu.Instance.displayMenu(x, y); + }; + + evaluatePronunciation = () => { + const newAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100 }); + this.Document.audio = newAudio[DocData]; + // DocCast(this.Document.embedContainer)()._props.addDocument?.(newAudio); + this._props.DocumentView?.()._props.addDocument?.(newAudio); + // Doc.AddToMyOverlay(newAudio); + }; + render() { const clearButton = (which: string) => ( <Tooltip title={<div className="dash-tooltip">remove</div>}> <div + // style={{ position: 'relative', top: '0px', left: '10px' }} 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="sm" /> + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> </div> </Tooltip> ); @@ -297,8 +795,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor']; return targetDoc || layoutString ? ( + // <MathJaxContext config={this.mathJaxConfig}> + // <MathJax> <> <DocumentView // eslint-disable-next-line react/jsx-props-no-spreading @@ -312,15 +813,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={this.layoutWidth} NativeHeight={this.layoutHeight} - isContentActive={emptyFunction} + isContentActive={() => this.childActive} isDocumentActive={returnFalse} + dontSelect={returnTrue} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider} + styleProvider={this.childActive ? this._props.styleProvider : this.docStyleProvider} hideLinkButton - pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} + pointerEvents={this.childActive ? undefined : returnNone} /> - {layoutString ? null : clearButton(whichSlot)} - </> // placeholder image if doc is missing + {/* </MathJax> */} + {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */} + {/* </MathJaxContext> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` */} + </> ) : ( <div className="placeholder"> <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> @@ -328,69 +832,147 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; const displayBox = (which: string, index: number, cover: number) => ( - <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> - {displayDoc(which)} + <div + className={`${index === 0 ? 'before' : 'after'}Box-cont`} + key={which} + style={{ width: this._props.PanelWidth() }} + onPointerDown={e => { + this.registerSliding(e, cover); + this.activateContent(); + }} + ref={ele => this.createDropTarget(ele, which, index)}> + {!this._isEmpty ? displayDoc(which) : null} + {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */} </div> ); if (this.Document._layout_isFlashcard) { - const side = this.useAlternate ? 1 : 0; + const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0; // add text box to each side when comparison box is first created - if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] === 'empty')) { - const dataSplit = StrCast(this.dataDoc.data).split('Answer'); + // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty') + if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) { + const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: '); const newDoc = Docs.Create.TextDocument(dataSplit[1]); - // if there is text from the pdf ai cards, put the question on the front side. - // eslint-disable-next-line prefer-destructuring - newDoc[DocData].text = dataSplit[1]; this.addDoc(newDoc, this.fieldKey + '_0'); } - if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) { - const dataSplit = StrCast(this.dataDoc.data).split('Answer'); + + if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) { + const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: '); const newDoc = Docs.Create.TextDocument(dataSplit[0]); - // if there is text from the pdf ai cards, put the answer on the alternate side. - // eslint-disable-next-line prefer-destructuring - newDoc[DocData].text = dataSplit[0]; this.addDoc(newDoc, this.fieldKey + '_1'); + // if (this.Document.image) { + // console.log('ID: ' + DocCast(this.Document.image)[Id]); + // const rtfiel = new RichTextField( + // JSON.stringify({ + // doc: { + // type: 'doc', + // content: [ + // { + // type: 'paragraph', + // attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + // content: [ + // { type: 'text', text: dataSplit[0] }, + // { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: DocCast(this.Document.image)[Id] } }, + // ], + // }, + // ], + // }, + // selection: { type: 'text', anchor: 2, head: 2 }, + // }) + // ); + + // newDoc[DocData].text = rtfiel; + // } } // render the QuizCards - if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') { + // console.log('GERE' + DocCast(this.Document.embedContainer).filterOp); + if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).practiceMode === 'quiz') { + const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}> - <p style={{ color: 'white', padding: 10 }}>{StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)}</p> - {/* {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)} */} + <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.useAlternate ? this._outputValue : this._inputValue} - onChange={action(e => { - this._inputValue = e.target.value; - })} - readOnly={this.useAlternate} - /> - </div> - <div className="submit-button" style={{ display: this.useAlternate ? 'none' : 'flex' }}> - <button type="button" onClick={this.handleRenderGPTClick}> - Submit - </button> + value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue} + onChange={this.handleInputChange} + onScroll={e => { + e.stopPropagation(); + e.preventDefault(); + }} + placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} + readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea> + + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + </div> + ) : null} </div> - <div className="submit-button" style={{ display: this.useAlternate ? 'flex' : 'none' }}> - <button type="button" onClick={this.handleRenderClick}> - Edit Your Response - </button> + <div> + <div className="submit-button" style={{ overflow: 'hidden', display: 'flex', width: '100%' }}> + <div + className="submit-buttonschema-header-button" + onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)} + style={{ position: 'absolute', top: '5px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}> + <FontAwesomeIcon color={'white'} icon="caret-down" /> + </div> + <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '', borderRadius: '2px' }}> + {<FontAwesomeIcon icon="microphone" size="lg" />} + </button> + <div + className="submit-buttonschema-header-button" + onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} + style={{ position: 'absolute', top: '5px', left: '50px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}> + <FontAwesomeIcon color={'white'} icon="caret-down" /> + </div> + <button + className="submit-buttonpronunciation" + onClick={this.evaluatePronunciation} + style={{ display: 'inline-flex', alignItems: 'center', background: this._listening ? 'lightgray' : '', borderRadius: '2px', width: '100%' }}> + Evaluate Pronunciation + </button> + + {this.layoutDoc[`_${this._props.fieldKey}_usePath`] !== 'alternate' ? ( + <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}> + Submit + </button> + ) : ( + <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ display: 'inline-flex', alignItems: 'center', borderRadius: '2px', marginBottom: '3px', width: '100%' }}> + Redo the Question + </button> + )} + </div> </div> </div> ); } + //console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)); // 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 */ - style={{ display: 'flex', flexDirection: 'column' }} - onMouseEnter={() => this.hoverFlip(true)} - onMouseLeave={() => this.hoverFlip(false)}> + // onContextMenu={this.specificMenu} + style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + onMouseEnter={() => { + this.hoverFlip('alternate'); + }} + onMouseLeave={() => { + this.hoverFlip(undefined); + }} + // onPointerUp={() => (this._isAnyChildContentActive = true)} + > + {/* {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null} */} {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + </div> + ) : null} + {this._props.isContentActive() ? this.flashcardMenu : null} {this.overlayAlternateIcon} </div> ); @@ -407,10 +989,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className="slide-bar" style={{ left: `calc(${this.clipWidth + '%'} - 0.5px)`, - transition: this._animating, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => !this.childActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ce7cfa5f4..6647a1354 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -112,6 +112,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document private _downTime: number = 0; private _lastTap: number = 0; private _doubleTap = false; + private _loading = false; private _mainCont = React.createRef<HTMLDivElement>(); private _titleRef = React.createRef<EditableView>(); private _dropDisposer?: DragManager.DragDropDisposer; @@ -495,22 +496,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document input.click(); }; - askGPT = async (): Promise<string | undefined> => { - const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text; - try { - const res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD); - if (!res) { - console.error('GPT call failed'); - return; - } - DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; - console.log(res); - } catch (err) { - console.error('GPT call failed'); - } - }; - onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { + if (this._props.dontSelect?.()) return; if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); @@ -568,21 +555,18 @@ 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: 'eye' }); - if (this.Document._layout_isFlashcard) { - appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' }); - } !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' }); // creates menu for the user to select how to reveal the flashcards - if (this.Document._layout_isFlashcard) { - const revealOptions = cm.findByDescription('Reveal Options'); - const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; - revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore - revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore - !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); - } + // if (this.Document._layout_isFlashcard) { + // const revealOptions = cm.findByDescription('Reveal Options'); + // const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; + // revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore + // revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore + // !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); + // } if (this._props.bringToFront) { const zorders = cm.findByDescription('ZOrder...'); @@ -1378,7 +1362,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale; isSelected = () => this.IsSelected; select = (extendSelection: boolean, focusSelection?: boolean) => { - DocumentView.SelectView(this, extendSelection); + if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection); if (focusSelection) { DocumentView.showDocument(this.Document, { willZoomCentered: true, diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 818d26956..b9c3528d3 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -50,6 +50,7 @@ export interface FieldViewSharedProps { PanelHeight: () => number; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events + dontSelect?: () => boolean | undefined; childFilters: () => string[]; childFiltersByRanges: () => string[]; styleProvider: Opt<StyleProviderFuncType>; diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3ffda5a35..4d199b360 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -139,3 +139,44 @@ .imageBox-fadeBlocker-hover { opacity: 0; } + +.loading-spinner { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + // left: 50%; + // top: 50%; + z-index: 200; + font-size: 20px; + font-weight: bold; + color: #17175e; +} + +.check-icon { + position: absolute; + right: 40; + bottom: 10; + color: green; + display: inline-block; + font-size: 20px; + overflow: hidden; +} + +.redo-icon { + position: absolute; + right: 10; + bottom: 10; + color: black; + display: inline-block; + font-size: 20px; + overflow: hidden; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 68c313480..86da64e5e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -11,7 +11,6 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -33,8 +32,27 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; +import { DocCast, NumCast, RTFCast, StrCast, ImageCast, Cast, toList } from '../../../fields/Types'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { URLField } from '../../../fields/URLField'; +import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT'; +import ReactLoading from 'react-loading'; +import { FollowLinkScript } from '../../documents/DocUtils'; +import { basename } from 'path'; +import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler'; +import { dropActionType } from '../../util/DropActionTypes'; +import { canvasSize } from './generativeFill/generativeFillUtils/generativeFillConstants'; +import Tesseract from 'tesseract.js'; +import axios from 'axios'; +import { TupleType } from 'typescript'; +// import stringSimilarity from 'string-similarity'; + +enum quizMode { + SMART = 'smart', + NORMAL = 'normal', + NONE = 'none', +} export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -59,6 +77,8 @@ export class ImageEditorData { public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore } + +const API_URL = 'https://api.unsplash.com/search/photos'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -73,9 +93,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { private _marqueeref = React.createRef<MarqueeAnnotator>(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _imageRef: HTMLImageElement | null = null; // <video> ref + @observable private _quizBoxes: Doc[] = []; + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable private searchInput = ''; + @observable private _quizMode = quizMode.NONE; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _curSuffix = ''; @observable _error = ''; + @observable private _loading = false; @observable _isHovering = false; // flag to switch between primary and alternate images on hover _ffref = React.createRef<CollectionFreeFormView>(); @@ -148,6 +175,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Object.values(this._disposers).forEach(disposer => disposer?.()); } + fetchImages = async () => { + try { + const { data } = await axios.get(`${API_URL}?query=${this.searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`); + console.log('data', data); + console.log(data.results); + 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-', + }); + this._props.addDocument?.(imageSnapshot); + } catch (error) { + console.log(error); + } + }; + + handleSelection = async (selection: string) => { + this.searchInput = selection; + const images = await this.fetchImages(); + }; + @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { @@ -197,6 +250,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.dataDoc._freeform_panX_min = this.dataDoc._freeform_panX_min ? nw * NumCast(this.dataDoc._freeform_panX_min) : undefined; this.dataDoc._freeform_panY_max = this.dataDoc._freeform_panY_max ? nw * NumCast(this.dataDoc._freeform_panY_max) : undefined; this.dataDoc._freeform_panY_min = this.dataDoc._freeform_panY_min ? nw * NumCast(this.dataDoc._freeform_panY_min) : undefined; + return nw; }); @undoBatch rotate = action(() => { @@ -259,10 +313,295 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return cropping; }; + createCanvas = async (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { + const canvas = document.createElement('canvas'); + const scaling = 1 / (this._props.NativeDimScaling?.() || 1); + const w = AnchorMenu.Instance.marqueeWidth * scaling; + const h = AnchorMenu.Instance.marqueeHeight * scaling; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions + if (ctx) { + this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left) * scaling, NumCast(this._marqueeref.current?.top) * scaling, w, h, 0, 0, w, h); + } + // canvas.style.zIndex = '2000000'; + // document.body.appendChild(canvas); + const blob = await ImageUtility.canvasToBlob(canvas); + return ImageBox.selectUrlToBase64(blob); + }; + + createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => { + const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath; + const width = NumCast(this.layoutDoc._width) || 1; + const height = NumCast(this.layoutDoc._height); + const imageSnapshot = Docs.Create.ImageDocument(url, { + _nativeWidth: Doc.NativeWidth(this.layoutDoc), + _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x) + width, + y: NumCast(this.layoutDoc.y), + onClick: FollowLinkScript(), + _width: 150, + _height: (height / width) * 150, + title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', + }); + Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc)); + Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); + this._props.addDocument?.(imageSnapshot); + DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); + // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 + setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); + }; + + static selectUrlToBase64 = async (blob: Blob): Promise<string> => { + try { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; + } + }; + + pushInfo = async (quiz: quizMode, i?: string) => { + this._quizMode = quiz; + this._loading = true; + console.log('JHSDKFJHKSDJFHKSJDHFKJSDHFKJHSDKF'); + + const img = { + file: i ? i : this.paths[0], + drag: i ? 'drag' : 'full', + smart: quiz, + }; + const response = await axios.post('http://localhost:105/labels/', img, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + console.log('RESPONSE:'); + console.log(response.data['boxes']); + console.log(response.data['text']); + this.createBoxes(response.data['boxes'], response.data['text']); + }; + + createBoxes = (boxes: [[[number, number]]], texts: [string]) => { + const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1); + const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + for (var i = 0; i < boxes.length; i++) { + const coords = boxes[i] ? boxes[i] : []; + const width = coords[1][0] - coords[0][0]; + const height = coords[2][1] - coords[0][1]; + const text = texts[i]; + + const newCol = Docs.Create.TextDocument('', { + _width: width, + //width * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), + _height: height, + //height * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']), + _layout_fitWidth: true, + // _layout_autoHeight: true, + }); + const scaling = 1 / (this._props.NativeDimScaling?.() || 1); + newCol.x = coords[0][0] + NumCast(this._marqueeref.current?.left) * scaling; + newCol.y = coords[0][1] + NumCast(this._marqueeref.current?.top) * scaling; + // newCol[DocData].text_fontSize = height + 'px'; + + newCol.zIndex = 1000; + newCol.forceActive = true; + newCol.quiz = text; + newCol.showQuiz = false; + this._quizBoxes.push(newCol); + this.addDocument(newCol); + this._loading = false; + } + }; + + getImageDesc = async () => { + this._loading = true; + try { + const hrefBase64 = await this.createCanvas(); + const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: '); + console.log(response); + AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y'])); + } catch (error) { + console.log('Error'); + } + this._loading = false; + }; + + makeLabels = async () => { + this._loading = true; + try { + const hrefBase64 = await this.createCanvas(); + this.pushInfo(quizMode.NORMAL, hrefBase64); + } catch (error) { + console.log('Error'); + } + }; + + levenshteinDistance = (a: string, b: string) => { + const an = a.length; + const bn = b.length; + const matrix = []; + + // Ensure non-zero length strings + if (an === 0) return bn; + if (bn === 0) return an; + + // Initialize the matrix + for (let i = 0; i <= an; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= bn; j++) { + matrix[0][j] = j; + } + + // Populate the matrix + for (let i = 1; i <= an; i++) { + for (let j = 1; j <= bn; j++) { + if (a[i - 1] === b[j - 1]) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + Math.min( + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ) + ); + } + } + } + + return matrix[an][bn]; + }; + + @computed get checkIcon() { + return ( + <Tooltip title={<div className="dash-tooltip">Check</div>}> + <div className="check-icon" onPointerDown={this.check}> + <FontAwesomeIcon icon="circle-check" size="lg" /> + </div> + </Tooltip> + ); + } + + @computed get redoIcon() { + return ( + <Tooltip title={<div className="dash-tooltip">Redo</div>}> + <div className="redo-icon" onPointerDown={this.redo}> + <FontAwesomeIcon icon="redo-alt" size="lg" /> + </div> + </Tooltip> + ); + } + + compareWords = (input: string, target: string) => { + const distance = this.levenshteinDistance(input.toLowerCase(), target.toLowerCase()); + const threshold = Math.max(input.length, target.length) * 0.2; // Allow up to 20% of the length as difference + return distance <= threshold; + }; + + extractHexAndSentences = (inputString: string) => { + // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences + const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s; + const match = inputString.match(regex); + + if (match) { + const hexNumber = match[1]; + const sentences = match[2].trim(); + return { hexNumber, sentences }; + } else { + return { error: 'The input string does not match the expected format.' }; + } + }; + + check = () => { + this._loading = true; + this._quizBoxes.forEach(async doc => { + const input = StrCast(RTFCast(DocCast(doc).text)?.Text); + console.log('INP: ' + StrCast(input) + '; DOC: ' + StrCast(doc.quiz)); + if (this._quizMode == quizMode.SMART && input) { + const questionText = 'Question: What was labeled in this image?'; + const rubricText = ' Rubric: ' + StrCast(doc.quiz); + // const queryText = 'RealAnswer: ' + StrCast(doc.quiz) + '. UserAnswer: ' + input + '.'; + const queryText = + questionText + + ' UserAnswer: ' + + input + + '. ' + + rubricText + + '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."'; + const response = await gptAPICall(queryText, GPTCallType.QUIZ); + const hexSent = this.extractHexAndSentences(response); + console.log(hexSent.hexNumber); + doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); + doc.backgroundColor = '#' + hexSent.hexNumber; + } else { + const match = this.compareWords(input, StrCast(doc.quiz)); + doc.backgroundColor = match ? '#11c249' : '#eb2d2d'; + } + doc.showQuiz = true; + // console.log(this.compareWords(input, StrCast(doc.quiz)) ? 'Match' : 'No Match'); + }); + this._loading = false; + }; + + redo = () => { + this._quizBoxes.forEach(doc => { + DocCast(doc)[DocData].text = ''; + doc.backgroundColor = '#e4e4e4'; + doc.showQuiz = false; + }); + }; + + exitQuizMode = () => { + this._quizMode = quizMode.NONE; + this._quizBoxes.forEach(doc => { + // this._props.removeDocument?.(DocCast(doc)); + // this._props.DocumentView?.()._props.removeDocument?.(doc); + }); + this._quizBoxes = []; + }; + + @action + setRef = (iref: HTMLImageElement | null) => { + this._imageRef = iref; + }; + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; + const quizes: ContextMenuProps[] = []; + // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' }); + quizes.push({ + description: 'Smart Check', + event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.SMART) : this.exitQuizMode, + icon: 'pen-to-square', + }); + quizes.push({ + description: 'Normal', + event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.NORMAL) : this.exitQuizMode, + icon: 'pencil', + }); + // funcs.push({ description: 'Quiz Mode', subitems: optionItems, icon: 'eye' }); + // funcs.push({ + // description: 'Quiz Mode', + // event: !this._quizMode + // ? () => this.pushInfo(false) + // : () => { + // this._quizMode = false; + // }, + // icon: 'redo-alt', + // }); + // funcs.push({ description: 'Get Text', event: this.check, icon: 'redo-alt' }); + // funcs.push({ description: 'Get Labels2', event: this.getImageLabels2, icon: 'redo-alt' }); + // funcs.push({ description: 'Get Labels', event: this.getImageLabels, icon: 'redo-alt' }); 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' }); @@ -277,6 +616,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }), icon: 'pencil-alt', }); + ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' }); ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -391,6 +731,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="imageBox-fader" style={{ opacity: backAlpha }}> <img alt="" + ref={this.setRef} key="paths" src={srcpath} style={{ transform, transformOrigin }} @@ -432,6 +773,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + this._width = moveEv.clientX; + this._height = moveEv.clientY; return true; }), returnFalse, @@ -443,6 +786,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; + AnchorMenu.Instance.gptFlashcards = this.getImageDesc; + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; + AnchorMenu.Instance.makeLabels = this.makeLabels; + AnchorMenu.Instance.marqueeWidth = this._marqueeref.current?.Width ?? 0; + AnchorMenu.Instance.marqueeHeight = this._marqueeref.current?.Height ?? 0; this._marqueeref.current?.onTerminateSelection(); this._props.select(false); }; @@ -477,7 +825,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { height: this._props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : undefined, + overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden', }}> <CollectionFreeFormView ref={this._ffref} @@ -505,6 +853,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { addDocument={this.addDocument}> {this.content} </CollectionFreeFormView> + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + </div> + ) : null} {this.annotationLayer} {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator @@ -523,8 +876,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { marqueeContainer={this._mainCont.current} highlightDragSrcColor="" anchorMenuCrop={this.crop} + // anchorMenuFlashcard={() => this.getImageDesc()} /> )} + {this._quizMode != quizMode.NONE ? this.checkIcon : null} + {this._quizMode != quizMode.NONE ? this.redoIcon : null} </div> ); } diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx new file mode 100644 index 000000000..0a4325d8c --- /dev/null +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -0,0 +1,115 @@ +import { action, computed, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { Doc } from '../../../fields/Doc'; +import { NumCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { DragManager, dropActionType } from '../../util/DragManager'; +import { LinkFollower } from '../../util/LinkFollower'; +import { SelectionManager } from '../../util/SelectionManager'; +import { ViewBoxBaseComponent } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; +import { FieldView, FieldViewProps } from './FieldView'; +import './LinkAnchorBox.scss'; +import { LinkInfo } from './LinkDocPreview'; +const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore +@observer +export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(LinkAnchorBox, fieldKey); + } + _doubleTap = false; + _lastTap: number = 0; + _ref = React.createRef<HTMLDivElement>(); + _isOpen = false; + _timeout: NodeJS.Timeout | undefined; + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + componentDidMount() { + this._props.setContentViewBox?.(this); + } + + @computed get linkSource() { + return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource); + } + + onPointerDown = (e: React.PointerEvent) => { + const linkSource = this.linkSource; + linkSource && + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => { + if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false); + else this._props.select(false); + }); + }; + onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { + const cdiv = this._ref?.current?.parentElement; + if (!this._isOpen && cdiv) { + const bounds = cdiv.getBoundingClientRect(); + const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); + const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); + if (separation > 100) { + const dragData = new DragManager.DocumentDragData([this.Document]); + dragData.dropAction = dropActionType.embed; + dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; + DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); + return true; + } else { + this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; + this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; + this.layoutDoc.link_autoMoveAnchors = false; + } + } + return false; + }); + + specificContextMenu = (e: React.MouseEvent): void => {}; + + render() { + TraceMobx(); + const small = this._props.PanelWidth() <= 1; // this happens when rendered in a treeView + const x = NumCast(this.layoutDoc[this.fieldKey + '_x'], 100); + const y = NumCast(this.layoutDoc[this.fieldKey + '_y'], 100); + const background = this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.BackgroundColor + ':anchor'); + const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1'; + const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; + const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); + const selView = SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_1') + ? 'link_anchor_1' + : SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_2') + ? 'link_anchor_2' + : ''; + return ( + <div + ref={this._ref} + title={targetTitle} + className={`linkAnchorBox-cont${small ? '-small' : ''}`} + onPointerEnter={e => + LinkInfo.SetLinkInfo({ + DocumentView: this.DocumentView, + styleProvider: this._props.styleProvider, + linkSrc: this.linkSource, + linkDoc: this.Document, + showHeader: true, + location: [e.clientX, e.clientY + 20], + noPreview: false, + }) + } + onPointerDown={this.onPointerDown} + onContextMenu={this.specificContextMenu} + style={{ + border: selView && this.dataDoc[selView] === this.dataDoc[this.fieldKey] ? `solid ${MEDIUM_GRAY} 2px` : undefined, + background, + left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, + top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, + transform: `scale(${anchorScale})`, + cursor: 'grab', + }} + /> + ); + } +} diff --git a/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav Binary files differnew file mode 100644 index 000000000..dc71e7886 --- /dev/null +++ b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 99b4a84fc..71706084f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -79,7 +79,47 @@ audiotag:hover { right: 8px; position: absolute; } + .answer-icon { + position: absolute; + right: 10; + bottom: 10; + color: black; + display: inline-block; + font-size: 20px; + cursor: pointer; + border-radius: 50%; + } + + .q-icon { + position: absolute; + right: 6; + bottom: 10; + color: white; + display: inline-block; + font-size: 20px; + cursor: pointer; + border-radius: 50%; + } + + .edit-icon { + position: absolute; + right: 35; + bottom: 10; + display: inline-block; + font-size: 20px; + cursor: pointer; + border-radius: 50%; + } +} + +.answer-tooltip { + font-size: 15px; + padding: 2px; + max-width: 150; + line-height: 150%; + position: relative; } + .formattedTextBox-alternateButton { align-items: center; flex-direction: column; @@ -88,10 +128,16 @@ audiotag:hover { background: black; right: 0; bottom: 0; - width: 11; - height: 11; + width: 15; + height: 22; cursor: default; } +.formattedTextBox-flip { + align-items: center; + position: absolute; + right: 2px; + bottom: 4px; +} .formattedTextBox-outer { position: relative; @@ -171,6 +217,8 @@ audiotag:hover { border-style: inset; border-width: 1px; } + // margin-left: 5px; + // margin-right: 5px; } .gpt-typing-wrapper { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 5b435e44a..e1ea93c3f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -65,6 +65,8 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; +import { URLField } from '../../../../fields/URLField'; +import { gptImageLabel } from '../../../apis/gpt/GPT'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -112,6 +114,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _rules: RichTextRules | undefined; private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle private _break = true; + @observable private _editLabel = false; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; @@ -181,7 +184,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable private gptRes: string = ''; - public makeAIFlashcards: () => void = unimplementedFunction; + // public makeAIFlashcards: () => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; public static PasteOnLoad: ClipboardEvent | undefined; @@ -359,10 +362,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData; if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) { this._applyingChange = this.fieldKey; - textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) { + textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; @@ -371,6 +374,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB unchanged = false; } } else if (rtField) { + 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))); @@ -816,6 +820,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle); specificContextMenu = (e: React.MouseEvent): void => { + if (this._props.dontSelect?.()) return; const cm = ContextMenu.Instance; let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> @@ -906,7 +911,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); const appearance = cm.findByDescription('Appearance...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; - + // appearanceItems.push({ + // description: 'Find image tags', + // event: this.findImageTags, + // icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', + // }); appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => { @@ -969,7 +978,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB icon: 'star', }); optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); - optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); + // optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' }); this._props.renderDepth && optionItems.push({ @@ -994,6 +1003,61 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; + findImageTags = async () => { + const c = this.ProseRef?.getElementsByTagName('img'); + if (c) { + for (let i of c) { + console.log(i); + + // console.log(canvas.toDataURL()); + // canvas.style.zIndex = '2000000'; + // document.body.appendChild(canvas); + if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); + } + } + // console.log('HI' + this.ProseRef?.getElementsByTagName('img')); + }; + + static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; + } + }; + + getImageDesc = async (u: string) => { + // if (StrCast(this.dataDoc.description)) return StrCast(this.dataDoc.description); // Return existing description + // const { href } = (u as URLField).url; + const hrefParts = u.split('.'); + const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; + try { + const hrefBase64 = await FormattedTextBox.imageUrlToBase64(u); + const response = await gptImageLabel( + hrefBase64, + 'Make flashcards out of this text and image with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' + (this.dataDoc.text as RichTextField)?.Text + ); + //const response = await gptImageLabel(u, '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: '); + // console.log(response); + AnchorMenu.Instance.transferToFlashcard(response || 'Something went wrong', NumCast(this.dataDoc['x']), NumCast(this.dataDoc['y'])); + // this._props.addto_; + // this.Document[DocData].description = response.trim(); + // return response; // Return the response + } catch (error) { + console.log('Error'); + } + // return ''; + }; + animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this._editorView?.state.storedMarks ?? []; @@ -1341,7 +1405,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB { fireImmediately: true } ); this.tryUpdateScrollHeight(); + + if (this.Document.image) { + // const node = schema.nodes.dashDoc.create({ + // width: 200, + // height: 200, + // title: 'dashDoc', + // docId: DocCast(this.Document.image)[Id], + // float: 'unset', + // }); + + // DocCast(this.Document.image)._freeform_fitContentsToBox = true; + // Doc.SetContainer(DocCast(this.Document.image), this.Document); + // const view = this._editorView!; + // try { + // this._inDrop = true; + // const pos = view.posAtCoords({ left: 0, top: 0 })?.pos; + // pos && view.dispatch(view.state.tr.insert(pos, node)); + // } catch (err) { + // console.log('Drop failed', err); + // } + // console.log('LKSDFLJ'); + this.addDocument?.(DocCast(this.Document.image)); + } + + //if (this.Document.image) this.addDocument?.(DocCast(this.Document.image)); setTimeout(this.tryUpdateScrollHeight, 250); + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; } clipboardTextSerializer = (slice: Slice): string => { @@ -2001,6 +2091,49 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB </Tooltip> ); } + + @computed get answerIcon() { + return ( + <Tooltip + title={ + <div className="answer-tooltip" style={{ minWidth: '150px' }}> + {StrCast(this.Document.quiz)} + </div> + }> + <div className="answer-tool-tip"> + <FontAwesomeIcon className="q-icon" icon="circle" color="white" /> + <FontAwesomeIcon className="answer-icon" icon="question" /> + </div> + </Tooltip> + ); + } + + @computed get editAnswer() { + return ( + <Tooltip + title={ + <div className="answer-tooltip" style={{ minWidth: '150px' }}> + {this._editLabel ? 'save' : 'edit correct answer'} + </div> + }> + <div className="answer-tool-tip" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.editLabelAnswer())}> + <FontAwesomeIcon className="edit-icon" color={this._editLabel ? 'white' : 'black'} icon="pencil" size="sm" /> + </div> + </Tooltip> + ); + } + + editLabelAnswer = () => { + // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing. + if (!this._editLabel) { + this.dataDoc.text = StrCast(this.Document.quiz); + } else { + this.Document.quiz = RTFCast(this.dataDoc.text).Text; + this.dataDoc.text = ''; + } + this._editLabel = !this._editLabel; + }; + get fieldKey() { return this._fieldKey; } @@ -2116,9 +2249,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB /> </div> {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} + {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} + {this.Document.showQuiz ? this.answerIcon : null} + {this.Document.showQuiz ? this.editAnswer : null} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 55b8446e9..bc0810f22 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -13,7 +13,7 @@ box-shadow: 3px 3px 1.5px grey; max-width: 400; max-height: 235; - height:max-content; + height: max-content; .formattedTextBox-tooltipText { height: max-content; text-overflow: ellipsis; @@ -21,7 +21,7 @@ } .formattedTextBox-tooltip:before { - content: ""; + content: ''; height: 0; width: 0; position: absolute; @@ -34,7 +34,7 @@ } .formattedTextBox-tooltip:after { - content: ""; + content: ''; height: 0; width: 0; position: absolute; @@ -44,4 +44,4 @@ border: 5px solid transparent; border-bottom-width: 0; border-top-color: white; -}
\ No newline at end of file +} |
