aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText/DailyJournal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/formattedText/DailyJournal.tsx')
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx280
1 files changed, 254 insertions, 26 deletions
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index ec1f7a023..871c556e6 100644
--- a/src/client/views/nodes/formattedText/DailyJournal.tsx
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -1,14 +1,22 @@
-import { action, makeObservable, observable } from 'mobx';
+import { makeObservable, action, observable } from 'mobx';
import * as React from 'react';
-import { RichTextField } from '../../../../fields/RichTextField';
import { Docs } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { FieldView, FieldViewProps } from '../FieldView';
import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox';
+import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
+import { RichTextField } from '../../../../fields/RichTextField';
+import { Plugin } from 'prosemirror-state';
+import { RTFCast } from '../../../../fields/Types';
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
+ _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?';
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(DailyJournal, fieldStr);
@@ -18,12 +26,13 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
super(props);
makeObservable(this);
this.journalDate = this.getFormattedDate();
-
- console.log('Constructor: Setting initial title and text...');
- this.setDailyTitle();
- this.setDailyText();
}
+ /**
+ * Method to get the current date in standard format
+ * @returns - date in standard long format
+ */
+
getFormattedDate(): string {
const date = new Date().toLocaleDateString(undefined, {
weekday: 'long',
@@ -35,6 +44,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
return date;
}
+ /**
+ * Method to set the title of the node to the date
+ */
@action
setDailyTitle() {
console.log('setDailyTitle() called...');
@@ -48,43 +60,259 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
console.log('New title after update:', this.dataDoc.title);
}
+ /**
+ * Method to set the standard text of the node (to the current date)
+ */
@action
setDailyText() {
- console.log('setDailyText() called...');
const placeholderText = 'Start writing here...';
- const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`;
+ const dateText = `${this.journalDate}\n`;
console.log('Checking if dataDoc has text field...');
- const styles = {
- bold: true, // Make the journal date bold
- color: 'blue', // Set the journal date color to blue
- fontSize: 18, // Set the font size to 18px for the whole text
- };
-
- console.log('Setting new text field with:', initialText);
- this.dataDoc[this.fieldKey] = RichTextField.textToRtf(
- initialText,
- undefined, // No image DocId
- styles, // Pass the styles object here
- placeholderText.length // The position for text selection
+ this.dataDoc[this.fieldKey] = RichTextField.textToRtfFormat(
+ [
+ { text: 'Journal Entry:', styles: { bold: true, color: 'black', fontSize: 20 } },
+ { text: dateText, styles: { italic: true, color: 'gray', fontSize: 15 } },
+ { text: placeholderText, styles: { fontSize: 14, color: 'gray' } },
+ ],
+ undefined,
+ placeholderText.length
);
console.log('Current text field:', this.dataDoc[this.fieldKey]);
}
+ /**
+ * Tracks user typing text inout into the node, to call the insert predicted
+ * text function when appropriate (i.e. when the user stops typing)
+ */
+
+ @action onTextInput = () => {
+ const editorView = this._ref.current?.EditorView;
+ if (!editorView) return;
+
+ if (this.typingTimeout) clearTimeout(this.typingTimeout);
+
+ this.typingTimeout = setTimeout(() => {
+ this.insertPredictiveQuestion();
+ }, 3500);
+ };
+
+ /**
+ * Inserts predictive text at the end of what the user is typing
+ */
+
+ @action insertPredictiveQuestion = async () => {
+ const editorView = this._ref.current?.EditorView;
+ if (!editorView) return;
+
+ const { state, dispatch } = editorView;
+ const { schema } = state;
+ const { to } = state.selection;
+ const insertPos = to; // cursor position
+
+ const resolvedPos = state.doc.resolve(insertPos);
+ const parentNode = resolvedPos.parent;
+ const indexInParent = resolvedPos.index();
+ const isAtEndOfParent = indexInParent >= parentNode.childCount;
+
+ // Check if there's a line break or paragraph node after the current position
+ let hasNewlineAfter = false;
+ try {
+ const nextNode = parentNode.child(indexInParent);
+ hasNewlineAfter = nextNode.type.name === schema.nodes.hard_break.name || nextNode.type.name === schema.nodes.paragraph.name;
+ } catch {
+ hasNewlineAfter = false;
+ }
+
+ // 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' });
+ const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'lightgray' });
+ const fontItalicsMark = schema.marks.em.create();
+
+ this.predictiveText = ' ...'; // placeholder for now
+
+ 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 res = await gptAPICall(gptPrompt, GPTCallType.COMPLETION);
+ if (!res) return;
+
+ // styled text node
+ const text = ` ... ${res.trim()}`;
+ const predictedText = schema.text(text, [fontSizeMark, fontColorMark, fontItalicsMark]);
+
+ // 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
+ dispatch(transaction);
+
+ this.predictiveText = text;
+ };
+
+ createPredictiveCleanupPlugin = () => {
+ return new Plugin({
+ view: () => {
+ return {
+ update: (view, prevState) => {
+ const { state, dispatch } = view;
+ if (!this.predictiveText) return;
+
+ // Check if doc or selection changed
+ if (!prevState.doc.eq(state.doc) || !prevState.selection.eq(state.selection)) {
+ const found = false;
+ const textToRemove = this.predictiveText;
+
+ state.doc.descendants((node, pos) => {
+ if (node.isText && node.text === textToRemove) {
+ const tr = state.tr.delete(pos, pos + node.nodeSize);
+
+ // Set the desired default marks for future 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]);
+
+ dispatch(tr);
+
+ this.predictiveText = null;
+ return false;
+ }
+ return true;
+ });
+
+ if (!found) {
+ // fallback cleanup
+ this.predictiveText = null;
+ }
+ }
+ },
+ };
+ },
+ });
+ };
+
componentDidMount(): void {
console.log('componentDidMount() triggered...');
- // bcz: This should be moved into Docs.Create.DailyJournalDocument()
- // otherwise, it will override all the text whenever the note is reloaded
- this.setDailyTitle();
- this.setDailyText();
+ console.log('Text: ' + RTFCast(this.Document.text)?.Text);
+
+ const editorView = this._ref.current?.EditorView;
+ if (editorView) {
+ editorView.dom.addEventListener('input', this.onTextInput);
+
+ // Add plugin to state if not already added
+ const cleanupPlugin = this.createPredictiveCleanupPlugin();
+ this._ref.current?.addPlugin(cleanupPlugin);
+ }
+
+ const rawText = RTFCast(this.Document.text)?.Text ?? '';
+ const isTextEmpty = !rawText || rawText === '';
+
+ const currentTitle = this.dataDoc.title || '';
+ const isTitleString = typeof currentTitle === 'string';
+ const isDefaultTitle = isTitleString && currentTitle.includes('Untitled DailyJournal');
+
+ if (isTextEmpty && isDefaultTitle) {
+ console.log('Journal title and text are default. Initializing...');
+ this.setDailyTitle();
+ this.setDailyText();
+ } else {
+ console.log('Journal already has content. Skipping initialization.');
+ }
}
+ componentWillUnmount(): void {
+ const editorView = this._ref.current?.EditorView;
+ if (editorView) {
+ editorView.dom.removeEventListener('input', this.onTextInput);
+ }
+ if (this.typingTimeout) clearTimeout(this.typingTimeout);
+ }
+
+ @action handleGeneratePrompts = async () => {
+ 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);
+
+ if (!rawText.trim()) {
+ alert('Journal is empty! Write something first.');
+ return;
+ }
+
+ try {
+ // Call GPT API to generate prompts
+ const res = await gptAPICall('Generate 1-2 short journal prompts for the following journal entry: ' + rawText, GPTCallType.COMPLETION);
+
+ if (!res) {
+ console.error('GPT call failed.');
+ return;
+ }
+
+ const editorView = this._ref.current?.EditorView;
+ if (!editorView) {
+ console.error('EditorView is not available.');
+ return;
+ } else {
+ const { state, dispatch } = editorView;
+ const { schema } = state;
+
+ // Use available marks
+ const boldMark = schema.marks.strong.create();
+ const italicMark = schema.marks.em.create();
+ const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' });
+ const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'gray' });
+
+ // Create text nodes with formatting
+ const headerText = schema.text('\n\n# Suggested Prompts:\n', [boldMark, italicMark, fontSizeMark, fontColorMark]);
+ const responseText = schema.text(res, [fontSizeMark, fontColorMark]);
+
+ // Insert formatted text
+ const transaction = state.tr.insert(state.selection.from, headerText).insert(state.selection.from + headerText.nodeSize, responseText);
+ dispatch(transaction);
+ }
+ } catch (err) {
+ console.error('Error calling GPT:', err);
+ }
+ };
+
render() {
return (
- <div style={{ background: 'beige', width: '100%', height: '100%' }}>
- <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
+ <div
+ style={{
+ // background: 'beige',
+ width: '100%',
+ height: '100%',
+ backgroundColor: 'beige',
+ backgroundImage: `
+ repeating-linear-gradient(
+ to bottom,
+ rgba(255, 26, 26, 0.2) 0px, rgba(255, 26, 26, 0.2) 1px, /* Thin red stripes */
+ transparent 1px, transparent 20px
+ )
+ `,
+ backgroundSize: '100% 20px',
+ backgroundRepeat: 'repeat',
+ }}>
+ {/* GPT Button */}
+ <button
+ style={{
+ position: 'absolute',
+ bottom: '5px',
+ right: '5px',
+ padding: '5px 10px',
+ backgroundColor: '#9EAD7C',
+ color: 'white',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer',
+ zIndex: 10,
+ }}
+ onClick={this.handleGeneratePrompts}>
+ Prompts
+ </button>
+
+ <FormattedTextBox ref={this._ref} {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
</div>
);
}