@luckydye/calendar 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app.ts CHANGED
@@ -12,9 +12,11 @@ import { initializeTheme } from "./Theme.js";
12
12
  import { InMemorySource } from "./InMemorySource.js";
13
13
  import { GoogleCalendarSource } from "./GoogleCalendarSource.js";
14
14
  import { InhouseBookingSource } from "./InhouseBookingSource.js";
15
+ import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
15
16
  import { queueStatus } from "./StatusMessage.js";
16
17
  import { activeCalendarStore } from "./ActiveCalendarStore.js";
17
18
  import type { CalendarEvent } from "./CalendarInternal.js";
19
+ import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
18
20
  import { registerKeybinds } from "./Keybinds.js";
19
21
  import "./StatusBar.js";
20
22
  import { NotificationScheduler } from "./NotificationScheduler.js";
@@ -35,6 +37,136 @@ const calendarElement = document.querySelector(
35
37
  ) as CalendarViewElement;
36
38
  const calendar = calendarElement.internal;
37
39
 
40
+ type PromptApiLanguageModel = {
41
+ availability?: () => Promise<string>;
42
+ create: (options?: unknown) => Promise<{
43
+ promptStreaming: (
44
+ prompt: string,
45
+ options?: unknown,
46
+ ) => Promise<ReadableStream<string>> | ReadableStream<string>;
47
+ destroy?: () => void;
48
+ }>;
49
+ };
50
+
51
+ function getPromptApiLanguageModel(): PromptApiLanguageModel | null {
52
+ const fromGlobal = (globalThis as { LanguageModel?: unknown }).LanguageModel;
53
+ if (fromGlobal) return fromGlobal as PromptApiLanguageModel;
54
+
55
+ const fromWindow = (
56
+ window as { ai?: { languageModel?: unknown } }
57
+ ).ai?.languageModel;
58
+ if (!fromWindow) return null;
59
+ return fromWindow as PromptApiLanguageModel;
60
+ }
61
+
62
+ let summaryAbortController: AbortController | null = null;
63
+ let summarySession: { destroy?: () => void } | null = null;
64
+ let summaryRequestToken = 0;
65
+ let activeSummaryKey: string | null = null;
66
+
67
+ function abortActiveDescriptionSummary(): void {
68
+ if (summaryAbortController) {
69
+ summaryAbortController.abort();
70
+ summaryAbortController = null;
71
+ }
72
+ if (summarySession) {
73
+ summarySession.destroy?.();
74
+ summarySession = null;
75
+ }
76
+ if (activeSummaryKey) {
77
+ calendarElement.cancelDescriptionSummary(activeSummaryKey);
78
+ activeSummaryKey = null;
79
+ }
80
+ summaryRequestToken++;
81
+ }
82
+
83
+ async function streamDescriptionSummary(key: string, description: string): Promise<void> {
84
+ const sanitizedDescription = sanitizeEventDescription(description);
85
+ abortActiveDescriptionSummary();
86
+ activeSummaryKey = key;
87
+ const token = summaryRequestToken;
88
+ calendarElement.startDescriptionSummary(key);
89
+
90
+ const languageModel = getPromptApiLanguageModel();
91
+ if (!languageModel) {
92
+ calendarElement.failDescriptionSummary(key, "Prompt API not available in this browser.");
93
+ activeSummaryKey = null;
94
+ return;
95
+ }
96
+
97
+ const controller = new AbortController();
98
+ summaryAbortController = controller;
99
+ let session: { destroy?: () => void } | null = null;
100
+
101
+ try {
102
+ const availability = await languageModel.availability?.();
103
+ if (token !== summaryRequestToken || key !== activeSummaryKey) return;
104
+ if (availability === "unavailable") {
105
+ throw new Error("Prompt API model is unavailable.");
106
+ }
107
+
108
+ session = await languageModel.create();
109
+ summarySession = session;
110
+ const prompt = [
111
+ "Summarize this calendar event description in 2-3 concise sentences.",
112
+ "Keep critical details like dates, times, links, and action items.",
113
+ "Return plain text only.",
114
+ "",
115
+ sanitizedDescription,
116
+ ].join("\n");
117
+
118
+ const stream = await Promise.resolve(
119
+ session.promptStreaming(prompt, { signal: controller.signal }),
120
+ );
121
+ const reader = stream.getReader();
122
+ try {
123
+ while (true) {
124
+ const { value, done } = await reader.read();
125
+ if (done) break;
126
+ if (
127
+ token !== summaryRequestToken ||
128
+ key !== activeSummaryKey ||
129
+ controller.signal.aborted
130
+ ) {
131
+ return;
132
+ }
133
+ if (value) {
134
+ calendarElement.appendDescriptionSummaryChunk(key, value);
135
+ }
136
+ }
137
+ } finally {
138
+ reader.releaseLock();
139
+ }
140
+
141
+ if (token !== summaryRequestToken || key !== activeSummaryKey) return;
142
+ calendarElement.finishDescriptionSummary(key);
143
+ activeSummaryKey = null;
144
+ } catch (error) {
145
+ if (
146
+ controller.signal.aborted ||
147
+ token !== summaryRequestToken ||
148
+ key !== activeSummaryKey
149
+ ) {
150
+ return;
151
+ }
152
+ calendarElement.failDescriptionSummary(
153
+ key,
154
+ error instanceof Error
155
+ ? error.message
156
+ : "Failed to generate description summary.",
157
+ );
158
+ activeSummaryKey = null;
159
+ } finally {
160
+ if (summaryAbortController === controller) {
161
+ summaryAbortController = null;
162
+ }
163
+ if (summarySession === session) {
164
+ summarySession = null;
165
+ }
166
+ session?.destroy?.();
167
+ }
168
+ }
169
+
38
170
  registerKeybinds(
39
171
  [
40
172
  { key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
@@ -44,7 +176,6 @@ registerKeybinds(
44
176
  { key: "Escape", action: () => calendarElement.escape() },
45
177
  { key: "t", action: () => calendarElement.scrollToToday() },
46
178
  ],
47
- calendarElement,
48
179
  );
49
180
 
50
181
  let workerPromise: Promise<ServiceWorkerRegistration> | undefined;
@@ -80,6 +211,7 @@ function updateStatusBar() {
80
211
 
81
212
  // Update statusbar on relevant events
82
213
  calendarElement.addEventListener("selection-change", updateStatusBar);
214
+ calendarElement.addEventListener("meta-key-change", updateStatusBar);
83
215
 
84
216
  // Update statusbar periodically for current time
85
217
  setInterval(updateStatusBar, 10000); // Every 10 seconds
@@ -108,6 +240,21 @@ calendarElement.addEventListener("event-click", () => {
108
240
  requestNotificationPermission();
109
241
  }, { once: true });
110
242
 
243
+ calendarElement.addEventListener("request-description-summary", (e: Event) => {
244
+ const { key, description } = (e as CustomEvent).detail as {
245
+ key: string;
246
+ description: string;
247
+ };
248
+ void streamDescriptionSummary(key, description);
249
+ });
250
+
251
+ calendarElement.addEventListener("cancel-description-summary", (e: Event) => {
252
+ const { key } = (e as CustomEvent).detail as { key: string };
253
+ if (key === activeSummaryKey) {
254
+ abortActiveDescriptionSummary();
255
+ }
256
+ });
257
+
111
258
  // Load iCal files
112
259
  const integration = new CalendarIntegration(calendar);
113
260
 
@@ -276,10 +423,10 @@ async function createCalDAVCalendars(
276
423
  },
277
424
 
278
425
  async updateEvent(
279
- id: string,
426
+ event: CalendarEvent,
280
427
  updates: Partial<CalendarEvent>,
281
428
  ): Promise<CalendarEvent> {
282
- return caldavSource.updateEvent(id, { ...updates, calendarId: calInfo.url });
429
+ return caldavSource.updateEvent(event, { ...updates, calendarId: calInfo.url });
283
430
  },
284
431
 
285
432
  async deleteEvent(id: string): Promise<void> {
@@ -311,6 +458,29 @@ function createICalCalendar(source: CalendarSource): Calendar {
311
458
  };
312
459
  }
313
460
 
461
+ // Create a Timeseries JSON calendar wrapper
462
+ function createTimeseriesJsonCalendar(source: CalendarSource): Calendar {
463
+ return {
464
+ id: source.id,
465
+ name: source.name,
466
+ color: source.color,
467
+ enabled: source.enabled,
468
+ locked: (source as { locked?: boolean }).locked,
469
+ sourceId: source.id,
470
+ sourceType: "timeseries-json",
471
+
472
+ async fetchEvents(): Promise<CalendarEvent[]> {
473
+ return fetchTimeseriesJsonEvents(
474
+ source.credentials as { url: string; timestampField?: string; titleField?: string },
475
+ source.color,
476
+ source.name,
477
+ source.id,
478
+ );
479
+ },
480
+ // Timeseries JSON calendars are read-only by default
481
+ };
482
+ }
483
+
314
484
  // Create a Google Calendar wrapper
315
485
  function createGoogleCalendar(source: CalendarSource): Calendar {
316
486
  const calendarId = source.credentials.calendarId || "primary";
@@ -348,10 +518,10 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
348
518
  },
349
519
 
350
520
  async updateEvent(
351
- id: string,
521
+ event: CalendarEvent,
352
522
  updates: Partial<CalendarEvent>,
353
523
  ): Promise<CalendarEvent> {
354
- return googleSource.updateEvent(id, updates);
524
+ return googleSource.updateEvent(event, updates);
355
525
  },
356
526
 
357
527
  async deleteEvent(id: string): Promise<void> {
@@ -395,10 +565,10 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
395
565
  },
396
566
 
397
567
  async updateEvent(
398
- id: string,
568
+ event: CalendarEvent,
399
569
  updates: Partial<CalendarEvent>,
400
570
  ): Promise<CalendarEvent> {
401
- return inhouseSource.updateEvent(id, updates);
571
+ return inhouseSource.updateEvent(event, updates);
402
572
  },
403
573
 
404
574
  async deleteEvent(id: string): Promise<void> {
@@ -434,12 +604,12 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
434
604
  },
435
605
 
436
606
  async updateEvent(
437
- id: string,
607
+ event: CalendarEvent,
438
608
  updates: Partial<CalendarEvent>,
439
609
  ): Promise<CalendarEvent> {
440
610
  if (!source.updateEvent)
441
611
  throw new Error("Source does not support updateEvent");
442
- return source.updateEvent(id, updates);
612
+ return source.updateEvent(event, updates);
443
613
  },
444
614
 
445
615
  async deleteEvent(id: string): Promise<void> {
@@ -538,6 +708,21 @@ async function syncCalDAV(force = false) {
538
708
  } catch (error) {
539
709
  console.error(`Sync error for iCal source ${source.name}:`, error);
540
710
  }
711
+ } else if (source.type === "timeseries-json") {
712
+ if (!source.credentials?.url) {
713
+ console.warn(`Skipping Timeseries JSON source ${source.name}: missing URL`);
714
+ continue;
715
+ }
716
+
717
+ const calendar = createTimeseriesJsonCalendar(source);
718
+ loadedCalendars.set(calendar.id, calendar);
719
+ activeCalendarStore.registerCalendar(calendar);
720
+
721
+ try {
722
+ await sync(calendar, { force });
723
+ } catch (error) {
724
+ console.error(`Sync error for Timeseries JSON source ${source.name}:`, error);
725
+ }
541
726
  } else if (source.type === "google") {
542
727
  if (!source.credentials?.accessToken) {
543
728
  console.warn(`Skipping Google Calendar source ${source.name}: missing access token`);
@@ -722,7 +907,7 @@ function showProjectPickerDialog(
722
907
  }
723
908
 
724
909
  calendarElement.addEventListener("create-event", async (e) => {
725
- const { start, end } = e.detail;
910
+ const { start, end, isAllDay } = e.detail;
726
911
  const calendar = getActiveCalendar();
727
912
  if (!calendar) {
728
913
  queueStatus("No calendar selected. Please configure a calendar first.");
@@ -751,6 +936,7 @@ calendarElement.addEventListener("create-event", async (e) => {
751
936
  title: "New Event",
752
937
  start,
753
938
  end,
939
+ isAllDay,
754
940
  calendar: calendar.name,
755
941
  calendarId: calendar.calendarUrl ?? calendar.id,
756
942
  sourceId: calendar.sourceId,
@@ -758,7 +944,7 @@ calendarElement.addEventListener("create-event", async (e) => {
758
944
  lastSynced: new Date(),
759
945
  });
760
946
  try {
761
- await withMutation(calendar, () => calendar.createEvent({ id, title: "New Event", start, end }));
947
+ await withMutation(calendar, () => calendar.createEvent({ id, title: "New Event", start, end, isAllDay }));
762
948
  } catch (error) {
763
949
  calendarElement.internal.removeEventOptimistically(id);
764
950
  console.error("Failed to create event:", error);
@@ -766,41 +952,6 @@ calendarElement.addEventListener("create-event", async (e) => {
766
952
  }
767
953
  });
768
954
 
769
- calendarElement.addEventListener("move-event", async (e) => {
770
- const { event, start, end } = e.detail;
771
-
772
- const calendar = findCalendarForEvent(event);
773
-
774
- if (!calendar) {
775
- queueStatus("Cannot move event: calendar not found.");
776
- return;
777
- }
778
-
779
- if (!calendar.updateEvent) {
780
- queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
781
- return;
782
- }
783
-
784
- console.log(
785
- "Moving event:",
786
- event.id,
787
- "from",
788
- event.start,
789
- "to",
790
- start,
791
- "on calendar:",
792
- calendar.name,
793
- );
794
- try {
795
- calendarElement.internal.applyEventOptimistically({ ...event, start, end });
796
- await withMutation(calendar, () => calendar.updateEvent(event.id, { start, end }));
797
- } catch (error) {
798
- calendarElement.internal.applyEventOptimistically(event);
799
- console.error("Failed to move event:", error);
800
- queueStatus(`Failed to move event: ${error instanceof Error ? error.message : String(error)}`);
801
- }
802
- });
803
-
804
955
  calendarElement.addEventListener("update-event", async (e) => {
805
956
  const { event, updates } = e.detail;
806
957
 
@@ -824,7 +975,7 @@ calendarElement.addEventListener("update-event", async (e) => {
824
975
  );
825
976
  try {
826
977
  calendarElement.internal.applyEventOptimistically({ ...event, ...updates });
827
- await withMutation(calendar, () => calendar.updateEvent(event.id, updates));
978
+ await withMutation(calendar, () => calendar.updateEvent(event, updates));
828
979
 
829
980
  if ("reminders" in updates) {
830
981
  const eventWithReminders = { ...event, ...updates };
@@ -992,6 +1143,7 @@ calendarElement.addEventListener("import-ical", async (e) => {
992
1143
  function updateActiveCalendarColor() {
993
1144
  const calendar = activeCalendarStore.getActiveCalendar();
994
1145
  calendarElement.activeCalendarColor = calendar?.color ?? null;
1146
+ calendarElement.activeCalendarId = activeCalendarStore.getActiveId();
995
1147
  }
996
1148
 
997
1149
  function updateEnabledCalendars() {
@@ -1027,4 +1179,3 @@ function updateLockedCalendars() {
1027
1179
  });
1028
1180
  calendar.setLockedCalendars(lockedCalendarIdentifiers);
1029
1181
  }
1030
-