aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-10-11 20:21:33 -0400
committerbobzel <zzzman@gmail.com>2024-10-11 20:21:33 -0400
commitbb8fe2933154c6db70cfe5da1e890535bc9012d4 (patch)
treebee06f8b6649f51388439145dae45213ac729279
parent0f83debd8d2cca04d9fac959c7ed450312ef8d7d (diff)
fixed verticalalign of text boxes on load when existing text was there. fixe scrolling of vertical align textboxes when fitwidth is set. added flashcard contextmenu to comparisonbox and
-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),