diff options
| author | bobzel <zzzman@gmail.com> | 2025-06-26 10:53:54 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2025-06-26 10:53:54 -0400 |
| commit | baae27b205356898c5866a0f095e4ec056e02459 (patch) | |
| tree | 1b62de5579b8de8be81b6d342a9767f0f379bb91 /src/client/views/nodes/formattedText | |
| parent | ccfdf905400cd4b81d8cde0f16bb0e15cd65621b (diff) | |
| parent | 0093370a04348ef38b91252d02ab850f25d753b2 (diff) | |
Merge branch 'master' into agent-paper-main
Diffstat (limited to 'src/client/views/nodes/formattedText')
11 files changed, 345 insertions, 137 deletions
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index 871c556e6..564609494 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -9,14 +9,22 @@ import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { RichTextField } from '../../../../fields/RichTextField'; import { Plugin } from 'prosemirror-state'; import { RTFCast } from '../../../../fields/Types'; +import { Mark } from 'prosemirror-model'; +import { observer } from 'mobx-react'; export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable journalDate: string; - @observable typingTimeout: NodeJS.Timeout | null = null; // Track typing delay - @observable lastUserText: string = ''; // Store last user-entered text + @observable typingTimeout: NodeJS.Timeout | null = null; // track typing delay + @observable lastUserText: string = ''; // store last user-entered text + @observable isLoadingPrompts: boolean = false; // track if prompts are loading + @observable showPromptMenu = false; + @observable inlinePromptsEnabled = true; + @observable askPromptsEnabled = true; + _ref = React.createRef<FormattedTextBox>(); // reference to the formatted textbox predictiveTextRange: { from: number; to: number } | null = null; // where predictive text starts and ends private predictiveText: string | null = ' ... why?'; + private prePredictiveMarks: Mark[] = []; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(DailyJournal, fieldStr); @@ -40,7 +48,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() month: 'long', day: 'numeric', }); - console.log('getFormattedDate():', date); + // console.log('getFormattedDate():', date); return date; } @@ -49,15 +57,15 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() */ @action setDailyTitle() { - console.log('setDailyTitle() called...'); - console.log('Current title before update:', this.dataDoc.title); + // console.log('setDailyTitle() called...'); + // console.log('Current title before update:', this.dataDoc.title); if (!this.dataDoc.title || this.dataDoc.title !== this.journalDate) { - console.log('Updating title to:', this.journalDate); + // console.log('Updating title to:', this.journalDate); this.dataDoc.title = this.journalDate; } - console.log('New title after update:', this.dataDoc.title); + // console.log('New title after update:', this.dataDoc.title); } /** @@ -68,7 +76,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() const placeholderText = 'Start writing here...'; const dateText = `${this.journalDate}\n`; - console.log('Checking if dataDoc has text field...'); + // console.log('Checking if dataDoc has text field...'); this.dataDoc[this.fieldKey] = RichTextField.textToRtfFormat( [ @@ -80,7 +88,63 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() placeholderText.length ); - console.log('Current text field:', this.dataDoc[this.fieldKey]); + // console.log('Current text field:', this.dataDoc[this.fieldKey]); + } + + /** + * Method to show/hide the prompts menu + */ + @action.bound togglePromptMenu() { + this.showPromptMenu = !this.showPromptMenu; + } + + /** + * Method to toggle on/off inline predictive prompts + */ + @action.bound toggleInlinePrompts() { + this.inlinePromptsEnabled = !this.inlinePromptsEnabled; + } + + /** + * Method to toggle on/off inline /ask prompts + */ + @action.bound toggleAskPrompts() { + this.askPromptsEnabled = !this.askPromptsEnabled; + } + + /** + * Method to handle click on document (to close prompt menu) + * @param e - a click on the document + */ + @action.bound + handleDocumentClick(e: MouseEvent) { + const menu = document.getElementById('prompts-menu'); + const button = document.getElementById('prompts-button'); + if (this.showPromptMenu && menu && !menu.contains(e.target as Node) && button && !button.contains(e.target as Node)) { + this.showPromptMenu = false; + } + } + + /** + * Method to set initial date of document in the calendar view + */ + + @action setInitialDateRange() { + if (!this.dataDoc.$task_dateRange && this.journalDate) { + const parsedDate = new Date(this.journalDate); + if (!isNaN(parsedDate.getTime())) { + const localStart = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate()); + const localEnd = new Date(localStart); // same day + + this.dataDoc.$task_dateRange = `${localStart.toISOString()}|${localEnd.toISOString()}`; + this.dataDoc.$task_allDay = true; + this.dataDoc.$task = ''; // needed only to make the keyvalue view look good. + + // console.log('Set task_dateRange and task_allDay on journal (from local date):', this.dataDoc.$task_dateRange); + } else { + // console.log('Could not parse journalDate:', this.journalDate); + } + } } /** @@ -94,8 +158,26 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this.typingTimeout) clearTimeout(this.typingTimeout); - this.typingTimeout = setTimeout(() => { + const { state } = editorView; + const cursorPos = state.selection.from; + + // characters before cursor + const triggerText = state.doc.textBetween(Math.max(0, cursorPos - 4), cursorPos); + + if (triggerText === '/ask' && this.askPromptsEnabled) { + // remove /ask text + const tr = state.tr.delete(cursorPos - 4, cursorPos); + editorView.dispatch(tr); + + // insert predicted question this.insertPredictiveQuestion(); + return; + } + + this.typingTimeout = setTimeout(() => { + if (this.inlinePromptsEnabled) { + this.insertPredictiveQuestion(); + } }, 3500); }; @@ -129,28 +211,38 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() // Only insert if we're at end of node, or there's a newline node after if (!isAtEndOfParent && !hasNewlineAfter) return; - const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' }); + // Save current marks at cursor + const currentMarks = state.storedMarks || resolvedPos.marks(); + this.prePredictiveMarks = [...currentMarks]; + + // color and italics are preset for predictive question, font and size are adaptive const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'lightgray' }); const fontItalicsMark = schema.marks.em.create(); + const fontSizeMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontSize'); + const fontFamilyMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontFamily'); // if applicable - this.predictiveText = ' ...'; // placeholder for now + this.predictiveText = ' ...'; // placeholder const fullTextUpToCursor = state.doc.textBetween(0, state.selection.to, '\n', '\n'); - const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word question that continues the user's thought:\n\n"${fullTextUpToCursor}"`; + const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word reflective question that continues the user's thought:\n\n"${fullTextUpToCursor}"`; const res = await gptAPICall(gptPrompt, GPTCallType.COMPLETION); if (!res) return; // styled text node const text = ` ... ${res.trim()}`; - const predictedText = schema.text(text, [fontSizeMark, fontColorMark, fontItalicsMark]); + const predictedText = schema.text(text, [fontColorMark, fontItalicsMark, ...(fontSizeMark ? [fontSizeMark] : []), ...(fontFamilyMark ? [fontFamilyMark] : [])]); // Insert styled text at cursor position - const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks([state.schema.marks.pFontColor.create({ fontColor: 'gray' })]); // should probably instead inquire marks before predictive prompt + const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks(this.prePredictiveMarks); dispatch(transaction); this.predictiveText = text; }; + /** + * Method to remove the predictive question upon type/click + * @returns - once predictive text is found, or all text has been checked + */ createPredictiveCleanupPlugin = () => { return new Plugin({ view: () => { @@ -168,15 +260,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (node.isText && node.text === textToRemove) { const tr = state.tr.delete(pos, pos + node.nodeSize); - // Set the desired default marks for future input + // default marks for input const fontSizeMark = state.schema.marks.pFontSize.create({ fontSize: '14px' }); const fontColorMark = state.schema.marks.pFontColor.create({ fontColor: 'gray' }); tr.setStoredMarks([]); - tr.setStoredMarks([fontSizeMark, fontColorMark]); + if (this.prePredictiveMarks.length > 0) { + tr.setStoredMarks(this.prePredictiveMarks); + } else { + tr.setStoredMarks([fontSizeMark, fontColorMark]); + } dispatch(tr); this.predictiveText = null; + this.prePredictiveMarks = []; return false; } return true; @@ -194,8 +291,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() }; componentDidMount(): void { - console.log('componentDidMount() triggered...'); - console.log('Text: ' + RTFCast(this.Document.text)?.Text); + // console.log('componentDidMount() triggered...'); + document.addEventListener('mousedown', this.handleDocumentClick); + // console.log('Text: ' + RTFCast(this.Document.text)?.Text); const editorView = this._ref.current?.EditorView; if (editorView) { @@ -214,15 +312,17 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() const isDefaultTitle = isTitleString && currentTitle.includes('Untitled DailyJournal'); if (isTextEmpty && isDefaultTitle) { - console.log('Journal title and text are default. Initializing...'); + // console.log('Journal title and text are default. Initializing...'); this.setDailyTitle(); this.setDailyText(); + this.setInitialDateRange(); } else { - console.log('Journal already has content. Skipping initialization.'); + // console.log('Journal already has content. Skipping initialization.'); } } componentWillUnmount(): void { + document.removeEventListener('mousedown', this.handleDocumentClick); const editorView = this._ref.current?.EditorView; if (editorView) { editorView.dom.removeEventListener('input', this.onTextInput); @@ -230,10 +330,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this.typingTimeout) clearTimeout(this.typingTimeout); } + /** + * Method to generate pormpts via GPT + * @returns - if failed + */ @action handleGeneratePrompts = async () => { + if (this.isLoadingPrompts) { + return; + } + + this.isLoadingPrompts = true; + const rawText = RTFCast(this.Document.text)?.Text ?? ''; - console.log('Extracted Journal Text:', rawText); - console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text); + // console.log('Extracted Journal Text:', rawText); + // console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text); if (!rawText.trim()) { alert('Journal is empty! Write something first.'); @@ -273,9 +383,15 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() } } catch (err) { console.error('Error calling GPT:', err); + } finally { + this.isLoadingPrompts = false; } }; + /** + * Method to render the styled DailyJournal + * @returns - the HTML component for the journal + */ render() { return ( <div @@ -296,6 +412,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() }}> {/* GPT Button */} <button + id="prompts-button" style={{ position: 'absolute', bottom: '5px', @@ -308,9 +425,88 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() cursor: 'pointer', zIndex: 10, }} - onClick={this.handleGeneratePrompts}> + onClick={this.togglePromptMenu}> Prompts </button> + {this.showPromptMenu && ( + <div + id="prompts-menu" + style={{ + position: 'absolute', + bottom: '45px', + right: '5px', + backgroundColor: 'white', + border: '1px solid #ccc', + borderRadius: '4px', + padding: '10px', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + zIndex: 20, + minWidth: '170px', + maxWidth: 'fit-content', + overflow: 'auto', + }}> + <div + style={{ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginBottom: '10px', + }}> + <label + style={{ + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '14px', + justifyContent: 'flex-end', + width: '100%', + }}> + /ask + <input type="checkbox" checked={this.askPromptsEnabled} onChange={this.toggleAskPrompts} style={{ margin: 0 }} /> + </label> + </div> + + <div + style={{ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginBottom: '10px', + }}> + <label + style={{ + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '14px', + justifyContent: 'flex-end', + width: '100%', + }}> + Inline Prompting + <input type="checkbox" checked={this.inlinePromptsEnabled} onChange={this.toggleInlinePrompts} style={{ margin: 0 }} /> + </label> + </div> + + <button + onClick={() => { + this.showPromptMenu = false; + this.handleGeneratePrompts(); + }} + disabled={this.isLoadingPrompts} + style={{ + backgroundColor: '#9EAD7C', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: this.isLoadingPrompts ? 'not-allowed' : 'pointer', + opacity: this.isLoadingPrompts ? 0.6 : 1, + padding: '5px 10px', + float: 'right', + }}> + Generate Prompts + </button> + </div> + )} <FormattedTextBox ref={this._ref} {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} /> </div> @@ -318,8 +514,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() } } +const ObservedDailyJournal = observer(DailyJournal); + Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, { - layout: { view: DailyJournal, dataField: 'text' }, + layout: { view: ObservedDailyJournal, dataField: 'text' }, options: { acl: '', _height: 35, diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 3734ad9cc..3ef3c2cef 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -17,7 +17,7 @@ min-width: 12px; position: relative; display: inline-block; - margin: 0; + margin: 0px; transform: scale(0.7); background-color: rgba(155, 155, 155, 0.24); } @@ -53,7 +53,7 @@ .dashFieldView, .dashFieldView-active { .dashFieldView-select { - height: 10p; + height: 100%; font-size: 12px; background: transparent; opacity: 0; @@ -61,7 +61,7 @@ } } -.dashFieldView { +.dashFieldView-active { &:hover { .dashFieldView-select { opacity: unset; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 7ea5d1fcf..6e3bdc5e8 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -262,6 +262,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi render() { return ( <div + // eslint-disable-next-line no-use-before-define className={`dashFieldView${this.isRowActive() ? '-active' : ''}`} ref={this._fieldRef} style={{ diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss index b0c17e56e..602135a30 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.scss +++ b/src/client/views/nodes/formattedText/EquationEditor.scss @@ -32,7 +32,7 @@ margin-left: -1px; position: relative; z-index: 1; - padding: 0; + padding: 0px; display: -moz-inline-box; display: inline-block; } @@ -128,8 +128,8 @@ .mq-math-mode * { font-size: inherit; line-height: inherit; - margin: 0; - padding: 0; + margin: 0px; + padding: 0px; border-color: black; -webkit-user-select: none; -moz-user-select: none; @@ -178,7 +178,7 @@ margin-left: 0.1em; } .mq-math-mode .mq-roman var.mq-f { - margin: 0; + margin: 0px; } .mq-math-mode big { font-size: 200%; @@ -323,7 +323,7 @@ padding: 0.1em; } .mq-math-mode .mq-sqrt-prefix { - padding-top: 0; + padding-top: 0px; position: relative; top: 0.1em; vertical-align: top; diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx index 48efa6e63..23d273523 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.tsx +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -72,6 +72,10 @@ class EquationEditor extends Component<EquationEditorProps> { this.mathField.latex(value || ''); } + componentDidUpdate(prevProps: Readonly<EquationEditorProps>): void { + !prevProps.value && this.mathField.latex(this.props.value || ''); + } + render() { return <span ref={this.element} style={{ border: '0px', boxShadow: 'None' }} />; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 547a2efa8..d5e566226 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -46,7 +46,7 @@ } audiotag { - left: 0; + left: 0px; position: absolute; cursor: pointer; border-radius: 10px; @@ -62,7 +62,7 @@ audiotag:hover { .formattedTextBox { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-color: global.$medium-gray; box-sizing: border-box; @@ -77,14 +77,14 @@ audiotag:hover { width: 100%; position: relative; transform-origin: left top; - top: 0; - left: 0; + top: 0px; + left: 0px; } .formattedTextBox-cont { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-radius: inherit; border-color: global.$medium-gray; @@ -111,7 +111,7 @@ audiotag:hover { .answer-tooltip { font-size: 15px; padding: 2px; - max-width: 150; + max-width: 150px; line-height: 150%; position: relative; } @@ -122,10 +122,10 @@ audiotag:hover { position: absolute; color: white; background: black; - right: 0; - bottom: 0; - width: 15; - height: 22; + right: 0px; + bottom: 0px; + width: 15px; + height: 22px; cursor: default; } @@ -139,8 +139,8 @@ audiotag:hover { .formattedTextBox-sidebar-handle { position: absolute; - top: 0; - right: 0; + top: 0px; + right: 0px; width: 20px; height: 20px; font-size: 11px; @@ -168,7 +168,7 @@ audiotag:hover { height: 100%; display: inline-block; position: absolute; - right: 0; + right: 0px; overflow: hidden; .collectionfreeformview-container { @@ -302,8 +302,8 @@ footnote::before { position: absolute; top: -0.5em; content: ' '; - height: 0; - width: 0; + height: 0px; + width: 0px; } .formattedTextBox-inlineComment { @@ -346,7 +346,7 @@ footnote::before { .prosemirror-linkBtn { background: unset; color: unset; - padding: 0; + padding: 0px; text-transform: unset; letter-spacing: unset; font-size: unset; @@ -357,7 +357,7 @@ footnote::before { background-color: dimgray; margin-top: 1.5em; z-index: 1; - padding: 5; + padding: 5px; border-radius: 2px; } .prosemirror-hrefoptions { @@ -396,7 +396,7 @@ footnote::before { blockquote { padding: 10px 10px; font-size: smaller; - margin: 0; + margin: 0px; font-style: italic; background: lightgray; border-left: solid 2px dimgray; @@ -415,7 +415,7 @@ footnote::before { p { font-family: inherit; } - margin-left: 0; + margin-left: 0px; } .bullet1 { p { @@ -439,7 +439,7 @@ footnote::before { display: inline-block; font-family: inherit; } - margin-left: 0; + margin-left: 0px; background-color: inherit; } .decimal2-ol { @@ -506,7 +506,7 @@ footnote::before { display: inline-block; font-family: inherit; } - margin-left: 0; + margin-left: 0px; padding-left: 1.2em; background-color: inherit; } @@ -661,7 +661,7 @@ footnote::before { .formattedTextBox-cont { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-radius: inherit; border-color: global.$medium-gray; @@ -706,7 +706,7 @@ footnote::before { height: 100%; display: inline-block; position: absolute; - right: 0; + right: 0px; .collectionfreeformview-container { position: relative; @@ -832,8 +832,8 @@ footnote::before { position: absolute; top: -0.5em; content: ' '; - height: 0; - width: 0; + height: px; + width: 0px; } .formattedTextBox-inlineComment { @@ -892,7 +892,7 @@ footnote::before { display: inline; font-family: inherit; } - margin-left: 0; + margin-left: 0px; } .decimal2-ol { counter-reset: deci2; @@ -952,7 +952,7 @@ footnote::before { display: inline; font-family: inherit; } - margin-left: 0; + margin-left: 0px; padding-left: 1.2em; } .multi2-ol { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 57720baae..255ee1afe 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -98,7 +98,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoadChar = ''; - public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection + public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection + private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; private _curHighlights = new ObservableSet<string>(['Audio Tags']); @@ -270,13 +271,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + const sourceAnchorCreator = () => this.getAnchor(true); + const docView = this.DocumentView?.(); - docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { @@ -304,6 +308,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; + autoTag = () => { + const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]); + if (rawText && !this.Document.$tags_chat) { + const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL; + + gptAPICall(rawText, callType).then( + action(desc => { + // Split GPT response into tokens and push individually & clear existing tags + this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/)); + this.Document._layout_showTags = true; + }) + ); + } + }; + leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); @@ -365,6 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); + if (textChange) this.dataDoc.$tags_chat = undefined; this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false unchanged = false; } @@ -425,10 +445,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const newAutoLinks = new Set<Doc>(); const oldAutoLinks = Doc.Links(this.Document).filter( link => - ((!Doc.isTemplateForField(this.Document) && - ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || - (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && + ((!Doc.IsTemplateForField(this.Document) && + ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this.EditorView?.state.doc.textContent) { @@ -1068,17 +1088,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return anchorDoc ?? this.Document; } + showBorderRounding = returnTrue; getView = (doc: Doc, options: FocusViewOptions) => { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (!this.SidebarShown) { - this.toggleSidebar(false); - options.didMove = true; - } - setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); + return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } - return new Promise<Opt<DocumentView>>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res)); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; @@ -1145,13 +1160,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return undefined; }; - // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. - // Since we also monitor all component height changes, this will update the document's height. - resetNativeHeight = action((scrollHeight: number) => { - this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; - if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight; - }); - addPlugin = (plugin: Plugin) => { const editorView = this.EditorView; if (editorView) { @@ -1182,21 +1190,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight), + ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on - () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), - ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { + () => ({ + border: this._props.PanelHeight(), + scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']), + sidebarHeight: this.sidebarHeight, + textHeight: this.textHeight, + layoutAutoHeight: this.layout_autoHeight, + marginsHeight: this.layout_autoHeightMargins, + }), + ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && // layoutAutoHeight && newHeight && - (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) && + (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) && !this._props.dontRegisterView ) { + if (NumCast(this.layoutDoc.nativeHeight)) { + this.layoutDoc._nativeHeight = scrollHeight; + } this._props.setHeight?.(newHeight); } }, @@ -1219,7 +1237,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; - return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; + return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) }; }, incomingValue => { if (this.EditorView && this.ApplyingChange !== this.fieldKey) { @@ -1525,7 +1543,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const { $from } = this.EditorView.state.selection; - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }); const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; if (selLoadChar === 'Enter') { @@ -1536,6 +1554,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } if (selectOnLoad) { + this._liveTextUndo = FormattedTextBox.LiveTextUndo; + FormattedTextBox.LiveTextUndo = undefined; this.EditorView!.focus(); } if (this._props.isContentActive()) this.prepareForTyping(); @@ -1552,7 +1572,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const { text, paragraph } = schema.nodes; const selNode = this.EditorView.state.selection.$anchor.node(); if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { - const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; + const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })]; this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); } } @@ -1565,8 +1585,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB removeStyleSheet(this._userStyleSheetElement); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); - FormattedTextBox.LiveTextUndo?.end(); - FormattedTextBox.LiveTextUndo = undefined; + this._liveTextUndo?.end(); this.unhighlightSearchTerms(); this.EditorView?.destroy(); RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); @@ -1688,7 +1707,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document editorView.focus(); - editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); + // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); } } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) { editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos))); @@ -1770,7 +1789,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) { const stordMarks = this.EditorView?.state.storedMarks?.slice(); - if (!(this.EditorView?.state.selection instanceof NodeSelection)) { + if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') { this.autoLink(); if (this.EditorView?.state.tr) { const tr = stordMarks?.reduce((tr2, m) => { @@ -1795,8 +1814,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this.endUndoTypingBatch(); - FormattedTextBox.LiveTextUndo?.end(); - FormattedTextBox.LiveTextUndo = undefined; + this._liveTextUndo?.end(); // if the text box blurs and none of its contents are focused(), then pass the blur along setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); @@ -1817,7 +1835,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return; } if (this._enteringStyle && 'tix!'.includes(e.key)) { - const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??'; const node = state.selection.$from.nodeAfter; const start = state.selection.from; const end = state.selection.to; @@ -1826,9 +1843,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB StopEvent(e); _editorView.dispatch( state.tr - .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) })) + .removeMark(start, end, schema.marks.user_mark) // + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })) ); return; } @@ -1861,9 +1877,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB break; default: if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) { - const modified = Math.floor(Date.now() / 1000); - const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified); - _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); + const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark); + _editorView.dispatch( + state.tr + .removeStoredMark(schema.marks.user_mark) // + .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })) + ); } break; } @@ -1887,14 +1906,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (this.EditorView && children && !SnappingManager.IsDragging) { - const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; + const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; - const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children); + const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins; const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation @@ -2110,6 +2129,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; + setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel); + setScrollRef = (r: HTMLDivElement | null) => (this._scrollRef = r); render() { TraceMobx(); const scale = this.nativeScaling(); @@ -2124,7 +2145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ) : styleFromLayout?.height === '0px' ? null : ( <div className="formattedTextBox" - ref={r => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)} + ref={this.setRef} style={{ ...(this._props.dontScale ? {} @@ -2162,9 +2183,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onDoubleClick={this.onDoubleClick}> <div className="formattedTextBox-outer" - ref={r => { - this._scrollRef = r; - }} + ref={this.setScrollRef} style={{ width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined, diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index bc0810f22..92f3e3290 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -11,8 +11,8 @@ -webkit-transform: translateX(-50%); transform: translateX(-50%); box-shadow: 3px 3px 1.5px grey; - max-width: 400; - max-height: 235; + max-width: 400px; + max-height: 235px; height: max-content; .formattedTextBox-tooltipText { height: max-content; @@ -22,26 +22,26 @@ .formattedTextBox-tooltip:before { content: ''; - height: 0; - width: 0; + height: 0px; + width: 0px; position: absolute; left: 50%; margin-left: -5px; bottom: -6px; border: 5px solid transparent; - border-bottom-width: 0; + border-bottom-width: 0px; border-top-color: silver; } .formattedTextBox-tooltip:after { content: ''; - height: 0; - width: 0; + height: 0px; + width: 0px; position: absolute; left: 50%; margin-left: -5px; bottom: -4.5px; border: 5px solid transparent; - border-bottom-width: 0; + border-bottom-width: 0px; border-top-color: white; } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index fcc816447..7c747de1e 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -23,7 +23,7 @@ .dropdown { position: absolute; top: 35px; - left: 0; + left: 0px; background-color: #323232; color: global.$light-gray; border: 1px solid #4d4d4d; @@ -47,7 +47,7 @@ } &:last-child { - margin-bottom: 0; + margin-bottom: 0px; } } } diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss index 87320943d..8980a93a2 100644 --- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss +++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss @@ -195,7 +195,7 @@ left: 1px; width: 24px; height: 4px; - margin-top: 0; + margin-top: 0px; } } @@ -221,7 +221,7 @@ display: inline-block; width: 1em; height: 1em; - stroke-width: 0; + stroke-width: 0px; stroke: currentColor; fill: currentColor; margin-right: 15px; @@ -231,7 +231,7 @@ display: inline-block; width: 1em; height: 1em; - stroke-width: 3; + stroke-width: 3px; fill: greenyellow; margin-right: 15px; } @@ -270,7 +270,7 @@ &.ProseMirror-menu-dropdown { width: 10px; height: 25px; - margin: 0; + margin: 0px; padding: 0 2px; background-color: #323232; text-align: center; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index b7dae1ca3..333ee6be8 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = { return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0]; }, }, - // the id of the user who entered the text - user_tag: { - attrs: { - userid: { default: '' }, - modified: { default: 'when?' }, // 1 second intervals since 1970 - tag: { default: '' }, - }, - group: 'inline', - inclusive: false, - toDOM: node => { - const uid = node.attrs.userid.replace('.', '').replace('@', ''); - return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0]; - }, - }, // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { |
