aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText/DailyJournal.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-06-26 10:53:54 -0400
committerbobzel <zzzman@gmail.com>2025-06-26 10:53:54 -0400
commitbaae27b205356898c5866a0f095e4ec056e02459 (patch)
tree1b62de5579b8de8be81b6d342a9767f0f379bb91 /src/client/views/nodes/formattedText/DailyJournal.tsx
parentccfdf905400cd4b81d8cde0f16bb0e15cd65621b (diff)
parent0093370a04348ef38b91252d02ab850f25d753b2 (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.tsx248
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,