diff options
-rw-r--r-- | src/client/views/StyleProvider.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 217 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 21 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 18 |
4 files changed, 115 insertions, 144 deletions
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 02e0a34d8..8859f6464 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -54,7 +54,6 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu } export function border(doc: Doc, pw: number, ph: number, rad: number = 0, inset: number = 0) { - if (!rad) rad = 0; const width = pw * inset; const height = ph * inset; @@ -218,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & const radiusRatio = borderRadius / docWidth; const radius = radiusRatio * ((2 * borderWidth) + docWidth); - const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2 ?? 0); + const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2); return !borderPath ? null : { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 672008968..0582bc996 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -15,7 +15,7 @@ import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../f import { nullAudio } from '../../../fields/URLField'; import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; -import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; @@ -31,6 +31,7 @@ import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; const API_URL = 'https://api.unsplash.com/search/photos'; @@ -55,17 +56,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + static qtoken = 'Question: '; static ktoken = 'Keyword: '; static atoken = 'Answer: '; - private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + private _slideTiming = 200; + private _sideBtnWidth = 35; private _closeRef = React.createRef<HTMLDivElement>(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; private _reactDisposer: IReactionDisposer | undefined; - constructor(props: FieldViewProps) { - super(props); - makeObservable(this); - } @observable private _inputValue = ''; @observable private _outputValue = ''; @@ -77,6 +77,47 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @observable private _renderSide = this.frontKey; @observable private _recognition = new this.SpeechRecognition(); + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + componentDidMount() { + this._props.setContentViewBox?.(this); + this._reactDisposer = reaction( + () => this._props.isSelected(), // when this reaction should update + selected => { + if (selected && this.isFlashcard) this.activateContent(); + !selected && (this._childActive = false); + }, // what it should update to + { fireImmediately: true } + ); + } + + componentWillUnmount() { + this._reactDisposer?.(); + } + + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { + this._disposers[fieldKey]?.(); + if (ele) { + this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); + } + }; + + 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'); + @computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore @computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore @computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore @@ -112,8 +153,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </Tooltip> ); } - - _sideBtnWidth = 35; /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ @@ -131,35 +170,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return SnappingManager.HideDecorations ? null : ( <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}> {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon} - {!this._props.isSelected() ? null : ( - <> - {this._renderSide === this.frontKey ? null : ( - <Tooltip - title={ - <div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div> // prettier-ignore - }> - <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}> - <FontAwesomeIcon icon="lightbulb" size="xl" /> - </div> - </Tooltip> - )} - {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : ( - <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> - <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> - <FontAwesomeIcon icon="layer-group" size="xl" /> - </div> - </Tooltip> - )} - </> + {!this._props.isSelected() || this._renderSide === this.frontKey ? null : ( + <Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}> + <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}> + <FontAwesomeIcon icon="lightbulb" size="xl" /> + </div> + </Tooltip> + )} + {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : ( + <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> + <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> + <FontAwesomeIcon icon="layer-group" size="xl" /> + </div> + </Tooltip> )} </div> ); } - @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - this._inputValue = e.target.value; - }; - @action activateContent = () => { this._childActive = true; }; @@ -182,42 +210,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return false; }; - componentDidMount() { - this._props.setContentViewBox?.(this); - this._reactDisposer = reaction( - () => this._props.isSelected(), // when this reaction should update - selected => { - if (selected && this.isFlashcard) this.activateContent(); - !selected && (this._childActive = false); - }, // what it should update to - { fireImmediately: true } - ); - } - - componentWillUnmount() { - this._reactDisposer?.(); - } - - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { - this._disposers[fieldKey]?.(); - if (ele) { - this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); - } - }; - - 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'); - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ title: 'CompareAnchor:' + this.Document.title, @@ -296,17 +288,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() false, undefined, 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(); - - setTimeout( - action(() => { - this._animating = ''; - }), - 200 - ); + if (!this._childActive) { + this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + setTimeout( action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore + } }) ); } @@ -381,17 +367,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Gets the transcription of an audio recording by sending the * recording to backend. */ - pushInfo = async () => { - const audio = { - file: DocCast(this.Document.audio)[DocData].url, - }; - const response = await axios.post('http://localhost:105/recognize/', audio, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.Document.phoneticTranscription = response.data.transcription; - }; + pushInfo = () => + axios + .post( + 'http://localhost:105/recognize/', // + { file: DocCast(this.Document.audio)[DocData].url }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then(response => { + this.Document.phoneticTranscription = response.data.transcription; + }); /** * Extracts the id of the youtube video url. @@ -408,17 +393,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Gets the transcript of a youtube video by sending the video url to the backend. * @returns transcription of youtube recording */ - youtubeUpload = async () => { - const audio = { - file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)), - }; - const response = await axios.post('http://localhost:105/youtube/', audio, { - headers: { - 'Content-Type': 'application/json', - }, - }); - return response.data.transcription; - }; + youtubeUpload = async () => + axios + .post( + 'http://localhost:105/youtube/', // + { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then(response => response.data.transcription); /** * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer @@ -478,15 +460,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { - const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); - const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text); - const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; + const frontText = RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; + const backText = RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; + const questionText = 'Question: ' + frontText; + const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + backText : ''); this.loading = true; let res = ''; - if (callType !== GPTCallType.CHATCARD || StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text) !== '') { + if (callType !== GPTCallType.CHATCARD || frontText) { try { - res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); + res = await gptAPICall(queryText, callType); if (!res) { console.error('GPT call failed'); } else @@ -624,6 +607,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side; }; + flashcardContextMenu = () => { + const appearance = ContextMenu.Instance.findByDescription('Appearance...'); + const appearanceItems = appearance?.subitems ?? []; + if (this.Document._layout_isFlashcard) { + appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); + } + !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); + }; + testForTextFields = (whichSlot: string) => { const slotData = Doc.Get(this.dataDoc, whichSlot, true); const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string'; @@ -752,17 +744,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div className="input-box"> <textarea value={this._renderSide === this.backKey ? this._outputValue : this._inputValue} - onChange={this.handleInputChange} - onScroll={e => { - e.stopPropagation(); - e.preventDefault(); - }} + onChange={action(e => { + this._inputValue = e.target.value; + })} placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} readOnly={this._renderSide === this.backKey} /> {!this.loading ? null : ( <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + <ReactLoading type="spin" height={30} width={30} color='blue' /> </div> )} </div> @@ -793,12 +783,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ + onContextMenu={this.flashcardContextMenu} onMouseEnter={() => this.hoverFlip(this.backKey)} onMouseLeave={() => this.hoverFlip(this.frontKey)}> {displayBox(this._renderSide, this._props.PanelWidth() - 3)} {this.loading ? ( <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + <ReactLoading type="spin" height={30} width={30} color="blue" /> </div> ) : null} {this.flashcardMenu} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c052a2823..a343b9a39 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -16,12 +16,11 @@ import { List } from '../../../fields/List'; import { PrefetchProxy } from '../../../fields/Proxy'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; -import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -492,21 +491,6 @@ 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', err); - } - }; - onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { if (this._props.dontSelect?.()) return; if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { @@ -569,9 +553,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); } appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' }); - 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' }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 36f06aaf2..29be8d285 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1515,20 +1515,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - const { state, dispatch } = this._editorView; + const { state } = this._editorView; if (!rtfField) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); - if (startupText) { - dispatch(state.tr.insertText(startupText)); - } - const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left')); - if (textAlign && textAlign !== 'left') { + const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign)) || 'left'; + if (textAlign !== 'left') { selectAll(this._editorView.state, tr => { - this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); + this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); }); - this.tryUpdateDoc(true); } + if (startupText) { + this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText)); + } + this.tryUpdateDoc(true); } this._editorView.TextView = this; } @@ -2147,7 +2147,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight < NumCast(this.layoutDoc._height) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} + className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), |