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/DailyJournal.tsx | |
| parent | ccfdf905400cd4b81d8cde0f16bb0e15cd65621b (diff) | |
| parent | 0093370a04348ef38b91252d02ab850f25d753b2 (diff) | |
Merge branch 'master' into agent-paper-main
Diffstat (limited to 'src/client/views/nodes/formattedText/DailyJournal.tsx')
| -rw-r--r-- | src/client/views/nodes/formattedText/DailyJournal.tsx | 248 |
1 files changed, 223 insertions, 25 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, |
