@luckydye/calendar 1.3.1 → 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/dist/calendar.js +1801 -1317
- package/package.json +1 -1
- package/src/CalDAVSource.ts +52 -19
- package/src/CalendarIntegration.ts +2 -2
- package/src/CalendarInternal.ts +1 -0
- package/src/CalendarView.ts +240 -36
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +2 -1
- package/src/ICal.ts +7 -2
- package/src/InMemorySource.ts +6 -6
- package/src/IndexedDBStorage.ts +3 -0
- package/src/InhouseBookingSource.ts +2 -1
- package/src/app.ts +155 -44
|
@@ -429,7 +429,8 @@ export class GoogleCalendarSource implements CalendarSource {
|
|
|
429
429
|
/**
|
|
430
430
|
* Update an existing event in Google Calendar.
|
|
431
431
|
*/
|
|
432
|
-
async updateEvent(
|
|
432
|
+
async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
|
|
433
|
+
const id = event.id;
|
|
433
434
|
const isRsvpOnly = Object.keys(updates).length === 1 && updates.attendees !== undefined;
|
|
434
435
|
|
|
435
436
|
if (isRsvpOnly) {
|
package/src/ICal.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CalendarCredentials } from './CalendarIntegration.js';
|
|
2
2
|
import type { CalendarEvent, Attendee, Organizer, NotificationConfig } from './CalendarInternal.js';
|
|
3
|
+
import { sanitizeEventDescription } from './DescriptionSanitizer.js';
|
|
3
4
|
|
|
4
5
|
export interface ParseResult {
|
|
5
6
|
events: CalendarEvent[];
|
|
@@ -50,7 +51,9 @@ export function parseICalEventsWithNotifications(icalText: string, color: string
|
|
|
50
51
|
end: (currentEvent.end instanceof Date ? currentEvent.end : currentEvent.start) as Date,
|
|
51
52
|
color,
|
|
52
53
|
calendar,
|
|
53
|
-
description: currentEvent.description
|
|
54
|
+
description: currentEvent.description
|
|
55
|
+
? sanitizeEventDescription(String(currentEvent.description))
|
|
56
|
+
: undefined,
|
|
54
57
|
location: currentEvent.location ? String(currentEvent.location) : undefined,
|
|
55
58
|
url: currentEvent.url ? String(currentEvent.url) : undefined,
|
|
56
59
|
organizer: currentEvent.organizer as Organizer | undefined,
|
|
@@ -203,7 +206,9 @@ export function parseSingleICalEvent(icalText: string): Partial<CalendarEvent> {
|
|
|
203
206
|
if (keyPart.includes("VALUE=DATE")) raw.isAllDay = true;
|
|
204
207
|
break;
|
|
205
208
|
case "DTEND": raw.end = parseICalDate(value, keyPart); break;
|
|
206
|
-
case "DESCRIPTION":
|
|
209
|
+
case "DESCRIPTION":
|
|
210
|
+
raw.description = sanitizeEventDescription(value.replace(/\\n/g, "\n"));
|
|
211
|
+
break;
|
|
207
212
|
case "LOCATION": raw.location = value; break;
|
|
208
213
|
case "URL": raw.url = value; break;
|
|
209
214
|
case "RRULE": raw.rrule = value.trim(); break;
|
package/src/InMemorySource.ts
CHANGED
|
@@ -44,17 +44,17 @@ export class InMemorySource implements CalendarSource {
|
|
|
44
44
|
return newEvent;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async updateEvent(
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
throw new Error(`Event ${id} not found`);
|
|
47
|
+
async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
|
|
48
|
+
const existing = this.events.get(event.id);
|
|
49
|
+
if (!existing) {
|
|
50
|
+
throw new Error(`Event ${event.id} not found`);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const updated = {
|
|
54
|
-
...
|
|
54
|
+
...existing,
|
|
55
55
|
...updates,
|
|
56
56
|
};
|
|
57
|
-
this.events.set(id, updated);
|
|
57
|
+
this.events.set(event.id, updated);
|
|
58
58
|
return updated;
|
|
59
59
|
}
|
|
60
60
|
|
package/src/IndexedDBStorage.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface SerializedEvent {
|
|
|
22
22
|
isAllDay?: boolean;
|
|
23
23
|
reminders?: NotificationConfig[];
|
|
24
24
|
visualStyle?: 'heatmap';
|
|
25
|
+
resourceUrl?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
interface SerializedMetadata {
|
|
@@ -328,6 +329,7 @@ export class IndexedDBStorage implements CalendarStorage {
|
|
|
328
329
|
isAllDay: event.isAllDay,
|
|
329
330
|
reminders: event.reminders,
|
|
330
331
|
visualStyle: event.visualStyle,
|
|
332
|
+
resourceUrl: event.resourceUrl,
|
|
331
333
|
};
|
|
332
334
|
}
|
|
333
335
|
|
|
@@ -378,6 +380,7 @@ export class IndexedDBStorage implements CalendarStorage {
|
|
|
378
380
|
isAllDay: serialized.isAllDay,
|
|
379
381
|
reminders: serialized.reminders,
|
|
380
382
|
visualStyle: serialized.visualStyle,
|
|
383
|
+
resourceUrl: serialized.resourceUrl,
|
|
381
384
|
};
|
|
382
385
|
}
|
|
383
386
|
|
|
@@ -432,11 +432,12 @@ export class InhouseBookingSource implements CalendarSource {
|
|
|
432
432
|
/**
|
|
433
433
|
* Update an existing booking in the Inhouse Booking System.
|
|
434
434
|
*/
|
|
435
|
-
async updateEvent(
|
|
435
|
+
async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
|
|
436
436
|
if (!this.enabled) {
|
|
437
437
|
throw new Error('Inhouse Booking System source is disabled');
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
const id = event.id;
|
|
440
441
|
const parsedId = this.parseEventId(id);
|
|
441
442
|
if (!parsedId) {
|
|
442
443
|
throw new Error(`Invalid event ID format: ${id}`);
|
package/src/app.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
|
|
|
16
16
|
import { queueStatus } from "./StatusMessage.js";
|
|
17
17
|
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
18
18
|
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
19
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
19
20
|
import { registerKeybinds } from "./Keybinds.js";
|
|
20
21
|
import "./StatusBar.js";
|
|
21
22
|
import { NotificationScheduler } from "./NotificationScheduler.js";
|
|
@@ -36,6 +37,136 @@ const calendarElement = document.querySelector(
|
|
|
36
37
|
) as CalendarViewElement;
|
|
37
38
|
const calendar = calendarElement.internal;
|
|
38
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
|
+
|
|
39
170
|
registerKeybinds(
|
|
40
171
|
[
|
|
41
172
|
{ key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
|
|
@@ -109,6 +240,21 @@ calendarElement.addEventListener("event-click", () => {
|
|
|
109
240
|
requestNotificationPermission();
|
|
110
241
|
}, { once: true });
|
|
111
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
|
+
|
|
112
258
|
// Load iCal files
|
|
113
259
|
const integration = new CalendarIntegration(calendar);
|
|
114
260
|
|
|
@@ -277,10 +423,10 @@ async function createCalDAVCalendars(
|
|
|
277
423
|
},
|
|
278
424
|
|
|
279
425
|
async updateEvent(
|
|
280
|
-
|
|
426
|
+
event: CalendarEvent,
|
|
281
427
|
updates: Partial<CalendarEvent>,
|
|
282
428
|
): Promise<CalendarEvent> {
|
|
283
|
-
return caldavSource.updateEvent(
|
|
429
|
+
return caldavSource.updateEvent(event, { ...updates, calendarId: calInfo.url });
|
|
284
430
|
},
|
|
285
431
|
|
|
286
432
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -372,10 +518,10 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
372
518
|
},
|
|
373
519
|
|
|
374
520
|
async updateEvent(
|
|
375
|
-
|
|
521
|
+
event: CalendarEvent,
|
|
376
522
|
updates: Partial<CalendarEvent>,
|
|
377
523
|
): Promise<CalendarEvent> {
|
|
378
|
-
return googleSource.updateEvent(
|
|
524
|
+
return googleSource.updateEvent(event, updates);
|
|
379
525
|
},
|
|
380
526
|
|
|
381
527
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -419,10 +565,10 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
419
565
|
},
|
|
420
566
|
|
|
421
567
|
async updateEvent(
|
|
422
|
-
|
|
568
|
+
event: CalendarEvent,
|
|
423
569
|
updates: Partial<CalendarEvent>,
|
|
424
570
|
): Promise<CalendarEvent> {
|
|
425
|
-
return inhouseSource.updateEvent(
|
|
571
|
+
return inhouseSource.updateEvent(event, updates);
|
|
426
572
|
},
|
|
427
573
|
|
|
428
574
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -458,12 +604,12 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
|
|
|
458
604
|
},
|
|
459
605
|
|
|
460
606
|
async updateEvent(
|
|
461
|
-
|
|
607
|
+
event: CalendarEvent,
|
|
462
608
|
updates: Partial<CalendarEvent>,
|
|
463
609
|
): Promise<CalendarEvent> {
|
|
464
610
|
if (!source.updateEvent)
|
|
465
611
|
throw new Error("Source does not support updateEvent");
|
|
466
|
-
return source.updateEvent(
|
|
612
|
+
return source.updateEvent(event, updates);
|
|
467
613
|
},
|
|
468
614
|
|
|
469
615
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -806,41 +952,6 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
806
952
|
}
|
|
807
953
|
});
|
|
808
954
|
|
|
809
|
-
calendarElement.addEventListener("move-event", async (e) => {
|
|
810
|
-
const { event, start, end } = e.detail;
|
|
811
|
-
|
|
812
|
-
const calendar = findCalendarForEvent(event);
|
|
813
|
-
|
|
814
|
-
if (!calendar) {
|
|
815
|
-
queueStatus("Cannot move event: calendar not found.");
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (!calendar.updateEvent) {
|
|
820
|
-
queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
console.log(
|
|
825
|
-
"Moving event:",
|
|
826
|
-
event.id,
|
|
827
|
-
"from",
|
|
828
|
-
event.start,
|
|
829
|
-
"to",
|
|
830
|
-
start,
|
|
831
|
-
"on calendar:",
|
|
832
|
-
calendar.name,
|
|
833
|
-
);
|
|
834
|
-
try {
|
|
835
|
-
calendarElement.internal.applyEventOptimistically({ ...event, start, end });
|
|
836
|
-
await withMutation(calendar, () => calendar.updateEvent(event.id, { start, end }));
|
|
837
|
-
} catch (error) {
|
|
838
|
-
calendarElement.internal.applyEventOptimistically(event);
|
|
839
|
-
console.error("Failed to move event:", error);
|
|
840
|
-
queueStatus(`Failed to move event: ${error instanceof Error ? error.message : String(error)}`);
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
|
|
844
955
|
calendarElement.addEventListener("update-event", async (e) => {
|
|
845
956
|
const { event, updates } = e.detail;
|
|
846
957
|
|
|
@@ -864,7 +975,7 @@ calendarElement.addEventListener("update-event", async (e) => {
|
|
|
864
975
|
);
|
|
865
976
|
try {
|
|
866
977
|
calendarElement.internal.applyEventOptimistically({ ...event, ...updates });
|
|
867
|
-
await withMutation(calendar, () => calendar.updateEvent(event
|
|
978
|
+
await withMutation(calendar, () => calendar.updateEvent(event, updates));
|
|
868
979
|
|
|
869
980
|
if ("reminders" in updates) {
|
|
870
981
|
const eventWithReminders = { ...event, ...updates };
|