diff options
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 3 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 74 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 4 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/TaskManagerTask.scss | 72 | ||||
-rw-r--r-- | src/client/views/nodes/TaskManagerTask.tsx | 251 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.scss | 19 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.tsx | 68 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/DailyJournal.tsx | 61 |
9 files changed, 516 insertions, 38 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index cef44e999..a73d8ba59 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -44,8 +44,9 @@ export enum DocumentType { SCRIPTDB = 'scriptdb', // database of scripts GROUPDB = 'groupdb', // database of groups + JOURNAL = 'journal', // AARAV ADD JOURNAL + TASK = 'task', // AARAV ADD TASK SCRAPBOOK = 'scrapbook', - JOURNAL = 'journal', // AARAV ADD } export enum CollectionViewType { // general collections diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a4a668085..217967c52 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -11,7 +11,7 @@ import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; -import { ScriptCast, StrCast } from '../../fields/Types'; +import { DocCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField'; import { SharingPermissions } from '../../fields/util'; import { PointData } from '../../pen-gestures/GestureTypes'; @@ -526,6 +526,16 @@ export class DocumentOptions { ai_firefly_seed?: number; ai_firefly_prompt?: string; + // AARAV ADD DOC OPTIONS -- TASK MANAGER + + /** Task start date/time picker (metadata and default) */ + startTime?: DateInfo | DateField = new DateInfo('start date and time', /*filterable*/ false); + /** Task end date/time picker (metadata and default) */ + endTime?: DateInfo | DateField = new DateInfo('end date and time', /*filterable*/ false); + /** Treat this as an all-day task (metadata and default) */ + allDay?: BoolInfo | boolean = new BoolInfo('all-day task', /*filterable*/ false); + /** Whether the task is completed */ + completed?: BoolInfo | boolean = new BoolInfo('whether the task is completed', /*filterable*/ false); /** * JSON‐stringified slot configuration for ScrapbookBox */ @@ -584,6 +594,30 @@ export namespace Docs { options: { acl: '' }, }, ], + + // AARAV ADD // + [ + DocumentType.JOURNAL, + { + layout: { view: EmptyBox, dataField: 'text' }, + options: { + title: 'Daily Journal', + acl_Guest: SharingPermissions.View, + }, + }, + ], + + [ + DocumentType.TASK, + { + layout: { view: EmptyBox, dataField: 'text' }, + options: { + title: 'Task', + acl_Guest: SharingPermissions.View, + }, + }, + ], + // AARAV ADD // ]); const suffix = 'Proto'; @@ -938,31 +972,6 @@ export namespace Docs { // AARAV ADD // export function DailyJournalDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { - // const getFormattedDate = () => { - // const date = new Date().toLocaleDateString(undefined, { - // weekday: 'long', - // year: 'numeric', - // month: 'long', - // day: 'numeric', - // }); - // return date; - // }; - - // const getDailyText = () => { - // const placeholderText = 'Start writing here...'; - // const dateText = `${getFormattedDate()}`; - - // return 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 - // ); - // }; - return InstanceFromProto( Prototypes.get(DocumentType.JOURNAL), '', @@ -975,7 +984,18 @@ export namespace Docs { ); } - // AARAV ADD // + export function TaskDocument(text = '', options: DocumentOptions = {}, fieldKey = 'text') { + return InstanceFromProto( + Prototypes.get(DocumentType.TASK), + '', + { + title: '', + ...options, + }, + undefined, + fieldKey + ); + } export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) { const linkDoc = InstanceFromProto( diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8f4d568ab..1288abd3e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -407,7 +407,8 @@ pie title Minerals in my tap water {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300, }}, // AARAV ADD // {key: "DailyJournal",creator:opts => Docs.Create.DailyJournalDocument("", opts),opts: { _width: 300, _height: 300, }}, - {key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}}, + {key: "Task", creator: opts => Docs.Create.TaskDocument("", opts), opts: { _width: 250, _height: 100, _layout_autoHeight: true, title: "Task", }}, + {key: "Scrapbook", creator: opts => Docs.Create.ScrapbookDocument([], opts), opts: { _width: 300, _height: 300}}, //{key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}}, {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }}, {key: "MetaNote", creator: metaNoteTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, @@ -453,6 +454,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, { toolTip: "Tap or drag to create a scrapbook template", title: "Scrapbook", icon: "palette", dragFactory: doc.emptyScrapbook as Doc,clickFactory:DocCast(doc.emptyScrapbook), }, { toolTip: "Tap or drag to create a journal entry", title: "Journal", icon: "book", dragFactory:doc.emptyDailyJournal as Doc,clickFactory: DocCast(doc.emptyDataJournal), }, + { toolTip: "Tap or drag to create a task", title: "Task", icon: "check-square", dragFactory: doc.emptyTask as Doc, clickFactory: DocCast(doc.emptyTask), }, { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon:"person-chalkboard",dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc, clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "MetaNote", icon: "window-maximize", dragFactory: doc.emptyMetaNote as Doc, clickFactory: DocCast(doc.emptyMetaNote), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b884eb8c8..ecbef3497 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -65,6 +65,7 @@ import { PresBox, PresSlideBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; import { StickerPalette } from './smartdraw/StickerPalette'; +import { TaskManagerTask } from './nodes/TaskManagerTask'; import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; dotenv.config(); @@ -120,6 +121,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; StickerPalette: StickerPalette, FormattedTextBox, DailyJournal, // AARAV + TaskManagerTask, // AARAV ImageBox, FontIconBox, LabelBox, diff --git a/src/client/views/nodes/TaskManagerTask.scss b/src/client/views/nodes/TaskManagerTask.scss new file mode 100644 index 000000000..0fcc2f955 --- /dev/null +++ b/src/client/views/nodes/TaskManagerTask.scss @@ -0,0 +1,72 @@ +.task-manager-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 10px; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.task-manager-title { + width: 100%; + font-size: 1.25rem; + font-weight: 600; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} + +.task-manager-description { + width: 100%; + font-size: 1rem; + padding: 8px 10px; + border: 1px solid #ccc; + border-radius: 6px; + min-height: 40px; + box-sizing: border-box; + vertical-align: top; + text-align: start; + resize: none; + line-height: 1.4; + resize: none; + flex-grow: 1 +} + +.task-manager-checkboxes { + display: flex; + align-items: center; + gap: 16px; +} + +.task-manager-allday, .task-manager-complete { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.95rem; +} + +.task-manager-times { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.task-manager-times label { + display: flex; + flex-direction: column; + font-size: 0.9rem; + font-weight: 500; + gap: 4px; +} + +input[type="datetime-local"] { + width: 100%; + font-size: 0.9rem; + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} diff --git a/src/client/views/nodes/TaskManagerTask.tsx b/src/client/views/nodes/TaskManagerTask.tsx new file mode 100644 index 000000000..2d2444275 --- /dev/null +++ b/src/client/views/nodes/TaskManagerTask.tsx @@ -0,0 +1,251 @@ +import { action, observable, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { FieldView } from './FieldView'; +import { DateField } from '../../../fields/DateField'; +import { Doc } from '../../../fields/Doc'; + +import './TaskManagerTask.scss'; + +interface TaskManagerProps { + Document: Doc; +} + +@observer +export class TaskManagerTask extends React.Component<TaskManagerProps> { + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(TaskManagerTask, fieldStr); + } + + @action + updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { + this.props.Document.text = e.target.value; + }; + + @action + updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.title = e.target.value; + }; + + @action + updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.allDay = e.target.checked; + + if (e.target.checked) { + delete this.props.Document.startTime; + delete this.props.Document.endTime; + } + + this.setTaskDateRange(); + }; + + @action + updateStart = (e: React.ChangeEvent<HTMLInputElement>) => { + const newStart = new Date(e.target.value); + + this.props.Document.startTime = new DateField(newStart); + + const endDate = this.props.Document.endTime instanceof DateField ? this.props.Document.endTime.date : undefined; + if (endDate && newStart > endDate) { + // Alert user + alert('Start time cannot be after end time. End time has been adjusted.'); + + // Fix end time + const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000); + this.props.Document.endTime = new DateField(adjustedEnd); + } + + this.setTaskDateRange(); + }; + + + + @action + updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => { + const newEnd = new Date(e.target.value); + + this.props.Document.endTime = new DateField(newEnd); + + const startDate = this.props.Document.startTime instanceof DateField ? this.props.Document.startTime.date : undefined; + if (startDate && newEnd < startDate) { + // Alert user + alert('End time cannot be before start time. Start time has been adjusted.'); + + // Fix start time + const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000); + this.props.Document.startTime = new DateField(adjustedStart); + } + + this.setTaskDateRange(); + }; + + + + + @action + setTaskDateRange() { + const doc = this.props.Document; + + if (doc.allDay) { + // All-day task → use date only + if (!doc.title) return; + + const parsedDate = new Date(doc.title as string); + if (!isNaN(parsedDate.getTime())) { + const localStart = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate()); + const localEnd = new Date(localStart); + doc.date_range = `${localStart.toISOString()}|${localEnd.toISOString()}`; + doc.allDay = true; + } + } else { + // Timed task → use full startTime and endTime + const startField = doc.startTime; + const endField = doc.endTime; + const startDate = startField instanceof DateField ? startField.date : null; + const endDate = endField instanceof DateField ? endField.date : null; + + if (startDate && endDate && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) { + doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`; + doc.allDay = false; + } else { + console.warn('startTime or endTime is invalid'); + } + } + } + + @action + toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.completed = e.target.checked; + }; + + constructor(props: TaskManagerProps) { + super(props); + makeObservable(this); + } + + componentDidMount() { + this.setTaskDateRange(); + } + + + render() { + + function toLocalDateTimeString(date: Date): string { + const pad = (n: number) => n.toString().padStart(2, '0'); + return ( + date.getFullYear() + + '-' + + pad(date.getMonth() + 1) + + '-' + + pad(date.getDate()) + + 'T' + + pad(date.getHours()) + + ':' + + pad(date.getMinutes()) + ); + } + + const doc = this.props.Document; + + const taskDesc = typeof doc.text === 'string' ? doc.text : ''; + const taskTitle = typeof doc.title === 'string' ? doc.title : ''; + const allDay = !!doc.allDay; + const isCompleted = !!this.props.Document.completed; + + const startTime = doc.startTime instanceof DateField && doc.startTime.date instanceof Date + ? toLocalDateTimeString(doc.startTime.date) + : ''; + + const endTime = doc.endTime instanceof DateField && doc.endTime.date instanceof Date + ? toLocalDateTimeString(doc.endTime.date) + : ''; + + + + return ( + <div className="task-manager-container"> + <input + className="task-manager-title" + type="text" + placeholder="Task Title" + value={taskTitle} + onChange={this.updateTitle} + disabled={isCompleted} + style={{opacity: isCompleted ? 0.7 : 1,}} + /> + + <textarea + className="task-manager-description" + placeholder="What’s your task?" + value={taskDesc} + onChange={this.updateText} + disabled={isCompleted} + style={{opacity: isCompleted ? 0.7 : 1,}} + /> + + <div className="task-manager-checkboxes"> + + <label className="task-manager-allday" style={{opacity: isCompleted ? 0.7 : 1,}}> + <input + type="checkbox" + checked={allDay} + onChange={this.updateAllDay} + disabled={isCompleted} + /> + All day + </label> + + <label className="task-manager-complete"> + <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} /> + Complete + </label> + + </div> + + {!allDay && ( + <div + className="task-manager-times" + style={{ opacity: isCompleted ? 0.7 : 1 }} + > + <label> + Start: + <input + type="datetime-local" + value={startTime} + onChange={this.updateStart} + disabled={isCompleted} + /> + </label> + <label> + End: + <input + type="datetime-local" + value={endTime} + onChange={this.updateEnd} + disabled={isCompleted} + /> + </label> + </div> + )} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.TASK, { + layout: { view: TaskManagerTask, dataField: 'text' }, + options: { + acl: '', + _height: 35, + _xMargin: 10, + _yMargin: 10, + _layout_autoHeight: true, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsCheckSquare', // or whatever icon you like + }, +}); diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss index f8ac4b2d1..f5d3613e3 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.scss +++ b/src/client/views/nodes/calendarBox/CalendarBox.scss @@ -23,3 +23,22 @@ } } } + +// AARAV ADD + +/* Existing styles */ +.fc-event.mother { + font-weight: 500; + border-radius: 4px; + padding: 2px 4px; + border-width: 2px; + } + + /* New styles for completed tasks */ + .fc-event.completed-task { + opacity: 1; + filter: grayscale(70%) brightness(90%); + text-decoration: line-through; + color: #ffffff; + } +
\ No newline at end of file diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index fa863e123..84fa25dba 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,4 +1,5 @@ import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; +import { EventResizeDoneArg } from '@fullcalendar/interaction'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import multiMonthPlugin from '@fullcalendar/multimonth'; @@ -17,6 +18,7 @@ import { ContextMenu } from '../../ContextMenu'; import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; import './CalendarBox.scss'; +import { DateField } from '../../../../fields/DateField'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @@ -66,6 +68,7 @@ export class CalendarBox extends CollectionSubView() { @computed get calendarEvents(): EventSourceInput | undefined { return this.childDocs.map(doc => { const { start, end } = dateRangeStrToDates(StrCast(doc.date_range)); + const isCompleted = BoolCast(doc.completed); // AARAV ADD return { title: StrCast(doc.title), start, @@ -74,7 +77,7 @@ export class CalendarBox extends CollectionSubView() { startEditable: true, endEditable: true, allDay: BoolCast(doc.allDay), - classNames: ['mother'], // will determine the style + classNames: ['mother', isCompleted ? 'completed-task' : ''], // will determine the style editable: true, // subject to change in the future backgroundColor: this.eventToColor(doc), borderColor: this.eventToColor(doc), @@ -105,7 +108,11 @@ export class CalendarBox extends CollectionSubView() { // TODO: Return a different color based on the event type eventToColor = (event: Doc): string => { - return 'red' + event; + const docType = StrCast(event.type); + if (docType === 'task') { + return '#20B2AA'; // teal for tasks + } + return 'red'; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -123,9 +130,30 @@ export class CalendarBox extends CollectionSubView() { return false; }; - handleEventDrop = (arg: EventDropArg) => { + handleEventDrop = (arg: EventDropArg | EventResizeDoneArg) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); - doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); + // doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); + if (!doc || !arg.event.start) return; + + // get the new start and end dates + const startDate = new Date(arg.event.start); + const endDate = new Date(arg.event.end ?? arg.event.start); + + // update date range, time range, and all day status + doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`; + + const allDayStatus = arg.event.allDay ?? false; + if (doc.allDay !== allDayStatus) { + doc.allDay = allDayStatus; + } + + if (doc.allDay) { + delete doc.startTime; + delete doc.endTime; + } else { + doc.startTime = new DateField(startDate); + doc.endTime = new DateField(endDate); + } }; handleEventClick = (arg: EventClickArg) => { @@ -171,7 +199,39 @@ export class CalendarBox extends CollectionSubView() { events: this.calendarEvents, eventClick: this.handleEventClick, eventDrop: this.handleEventDrop, + eventResize: this.handleEventDrop, eventDidMount: arg => { + const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + if (!doc) return; + + if (doc.type === 'task') { + const checkButton = document.createElement('button'); + checkButton.innerText = doc.completed ? '✅' : '⬜'; + checkButton.style.position = 'absolute'; + checkButton.style.right = '5px'; + checkButton.style.top = '50%'; + checkButton.style.transform = 'translateY(-50%)'; + checkButton.style.background = 'transparent'; + checkButton.style.border = 'none'; + checkButton.style.cursor = 'pointer'; + checkButton.style.fontSize = '18px'; + checkButton.style.zIndex = '1000'; + checkButton.style.padding = '0'; + checkButton.style.margin = '0'; + + checkButton.onclick = ev => { + ev.stopPropagation(); + doc.completed = !doc.completed; + this._calendar?.refetchEvents(); + }; + + // Make sure the parent box is positioned relative + arg.el.style.position = 'relative'; + arg.el.appendChild(checkButton); + } + + // (keep your other pointerup/contextmenu handlers here) + arg.el.addEventListener('pointerdown', ev => { ev.button && ev.stopPropagation(); }); diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index 871c556e6..d6d30dc13 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -9,6 +9,7 @@ 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'; export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable journalDate: string; @@ -17,6 +18,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() _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); @@ -84,6 +86,27 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() } /** + * Method to set initial date of document in the calendar view + */ + + @action setInitialDateRange() { + if (!this.dataDoc.date_range && 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.date_range = `${localStart.toISOString()}|${localEnd.toISOString()}`; + this.dataDoc.allDay = true; + + console.log('Set date_range and allDay on journal (from local date):', this.dataDoc.date_range); + } else { + console.log('Could not parse journalDate:', this.journalDate); + } + } + } + + /** * Tracks user typing text inout into the node, to call the insert predicted * text function when appropriate (i.e. when the user stops typing) */ @@ -94,6 +117,22 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this.typingTimeout) clearTimeout(this.typingTimeout); + 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') { + // remove /ask text + const tr = state.tr.delete(cursorPos - 4, cursorPos); + editorView.dispatch(tr); + + // insert predicted question + this.insertPredictiveQuestion(); + return; + } + this.typingTimeout = setTimeout(() => { this.insertPredictiveQuestion(); }, 3500); @@ -129,11 +168,17 @@ 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}"`; @@ -142,10 +187,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() // 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; @@ -172,11 +217,16 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() 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; @@ -217,6 +267,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() console.log('Journal title and text are default. Initializing...'); this.setDailyTitle(); this.setDailyText(); + this.setInitialDateRange(); } else { console.log('Journal already has content. Skipping initialization.'); } |