aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/StyleProvider.tsx3
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx217
-rw-r--r--src/client/views/nodes/DocumentView.tsx21
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx18
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),