@whoz-oss/coday-web 0.13.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/client/app.js ADDED
@@ -0,0 +1,1560 @@
1
+ "use strict";
2
+
3
+ // libs/coday-events/src/lib/coday-events.ts
4
+ function truncateText(text, maxLength = 80) {
5
+ if (text.length <= maxLength) return text;
6
+ try {
7
+ if (text.trim().startsWith("{") || text.trim().startsWith("[")) {
8
+ const obj = JSON.parse(text);
9
+ const simplified = JSON.stringify(obj);
10
+ if (simplified.length <= maxLength) return simplified;
11
+ return simplified.substring(0, maxLength) + `...(${simplified.length} chars)`;
12
+ }
13
+ } catch (e) {
14
+ }
15
+ return text.substring(0, maxLength) + "...";
16
+ }
17
+ var CodayEvent = class {
18
+ constructor(event, type) {
19
+ this.type = type;
20
+ if (!event.timestamp) {
21
+ const randomSuffix = Math.random().toString(36).substring(2, 7);
22
+ this.timestamp = `${(/* @__PURE__ */ new Date()).toISOString()}-${randomSuffix}`;
23
+ } else {
24
+ this.timestamp = event.timestamp;
25
+ }
26
+ this.parentKey = event.parentKey;
27
+ this.length = 0;
28
+ }
29
+ timestamp;
30
+ parentKey;
31
+ static type;
32
+ length;
33
+ };
34
+ var QuestionEvent = class extends CodayEvent {
35
+ invite;
36
+ constructor(event, type) {
37
+ super(event, type);
38
+ this.invite = event.invite;
39
+ }
40
+ buildAnswer(answer) {
41
+ return new AnswerEvent({ answer, parentKey: this.timestamp, invite: this.invite });
42
+ }
43
+ };
44
+ var HeartBeatEvent = class _HeartBeatEvent extends CodayEvent {
45
+ static type = "heartbeat";
46
+ constructor(event) {
47
+ super(event, _HeartBeatEvent.type);
48
+ }
49
+ };
50
+ var InviteEvent = class _InviteEvent extends QuestionEvent {
51
+ defaultValue;
52
+ static type = "invite";
53
+ constructor(event) {
54
+ super(event, _InviteEvent.type);
55
+ this.defaultValue = event.defaultValue;
56
+ }
57
+ };
58
+ var AnswerEvent = class _AnswerEvent extends CodayEvent {
59
+ answer;
60
+ invite;
61
+ static type = "answer";
62
+ constructor(event) {
63
+ super(event, _AnswerEvent.type);
64
+ this.answer = event.answer ?? "No answer";
65
+ this.invite = event.invite;
66
+ }
67
+ };
68
+ var TextEvent = class _TextEvent extends CodayEvent {
69
+ speaker;
70
+ text;
71
+ static type = "text";
72
+ constructor(event) {
73
+ super(event, _TextEvent.type);
74
+ this.speaker = event.speaker;
75
+ this.text = event.text;
76
+ }
77
+ };
78
+ var WarnEvent = class _WarnEvent extends CodayEvent {
79
+ warning;
80
+ static type = "warn";
81
+ constructor(event) {
82
+ super(event, _WarnEvent.type);
83
+ this.warning = event.warning;
84
+ }
85
+ };
86
+ var ErrorEvent = class _ErrorEvent extends CodayEvent {
87
+ error;
88
+ static type = "error";
89
+ constructor(event) {
90
+ super(event, _ErrorEvent.type);
91
+ this.error = event.error;
92
+ }
93
+ };
94
+ var ChoiceEvent = class _ChoiceEvent extends QuestionEvent {
95
+ options;
96
+ optionalQuestion;
97
+ static type = "choice";
98
+ constructor(event) {
99
+ super(event, _ChoiceEvent.type);
100
+ this.options = event.options;
101
+ this.optionalQuestion = event.optionalQuestion;
102
+ }
103
+ };
104
+ var ToolRequestEvent = class _ToolRequestEvent extends CodayEvent {
105
+ toolRequestId;
106
+ name;
107
+ args;
108
+ static type = "tool_request";
109
+ constructor(event) {
110
+ super(event, _ToolRequestEvent.type);
111
+ this.toolRequestId = event.toolRequestId ?? this.timestamp;
112
+ this.name = event.name;
113
+ this.args = event.args;
114
+ this.length = this.args.length + this.name.length + this.toolRequestId.length + 20;
115
+ }
116
+ buildResponse(output) {
117
+ return new ToolResponseEvent({ output, toolRequestId: this.toolRequestId });
118
+ }
119
+ /**
120
+ * Renders the tool request as a single line string with truncation
121
+ * @param maxLength Maximum length for the arguments before truncation
122
+ * @returns A formatted string representation
123
+ */
124
+ toSingleLineString(maxLength = 50) {
125
+ const truncatedArgs = truncateText(this.args, maxLength);
126
+ return `\u{1F527} ${this.name}(${truncatedArgs})`;
127
+ }
128
+ };
129
+ var ToolResponseEvent = class _ToolResponseEvent extends CodayEvent {
130
+ toolRequestId;
131
+ output;
132
+ static type = "tool_response";
133
+ constructor(event) {
134
+ super(event, _ToolResponseEvent.type);
135
+ this.toolRequestId = event.toolRequestId || this.timestamp || (/* @__PURE__ */ new Date()).toISOString();
136
+ this.output = event.output;
137
+ this.length = this.output.length + this.toolRequestId.length + 20;
138
+ }
139
+ /**
140
+ * Renders the tool response as a single line string with truncation
141
+ * @param maxLength Maximum length for the output before truncation
142
+ * @returns A formatted string representation
143
+ */
144
+ toSingleLineString(maxLength = 50) {
145
+ const truncatedOutput = truncateText(this.output, maxLength);
146
+ return `\u2B91 ${truncatedOutput}`;
147
+ }
148
+ };
149
+ var ProjectSelectedEvent = class _ProjectSelectedEvent extends CodayEvent {
150
+ projectName;
151
+ static type = "project_selected";
152
+ constructor(event) {
153
+ super(event, _ProjectSelectedEvent.type);
154
+ this.projectName = event.projectName;
155
+ }
156
+ };
157
+ var ThinkingEvent = class _ThinkingEvent extends CodayEvent {
158
+ static type = "thinking";
159
+ static debounce = 5e3;
160
+ constructor(event) {
161
+ super(event, _ThinkingEvent.type);
162
+ }
163
+ };
164
+ var MessageEvent = class _MessageEvent extends CodayEvent {
165
+ role;
166
+ name;
167
+ content;
168
+ static type = "message";
169
+ constructor(event) {
170
+ super(event, _MessageEvent.type);
171
+ this.role = event.role;
172
+ this.name = event.name;
173
+ this.content = event.content;
174
+ this.length = this.content.length + this.role.length + this.name.length + 20;
175
+ }
176
+ };
177
+ var eventTypeToClassMap = {
178
+ [MessageEvent.type]: MessageEvent,
179
+ [AnswerEvent.type]: AnswerEvent,
180
+ [ChoiceEvent.type]: ChoiceEvent,
181
+ [ErrorEvent.type]: ErrorEvent,
182
+ [HeartBeatEvent.type]: HeartBeatEvent,
183
+ [InviteEvent.type]: InviteEvent,
184
+ [ProjectSelectedEvent.type]: ProjectSelectedEvent,
185
+ [ToolRequestEvent.type]: ToolRequestEvent,
186
+ [ToolResponseEvent.type]: ToolResponseEvent,
187
+ [TextEvent.type]: TextEvent,
188
+ [ThinkingEvent.type]: ThinkingEvent,
189
+ [WarnEvent.type]: WarnEvent
190
+ };
191
+ function buildCodayEvent(data) {
192
+ const Clazz = eventTypeToClassMap[data.type];
193
+ return Clazz ? new Clazz(data) : void 0;
194
+ }
195
+
196
+ // apps/web/client/utils/preferences.ts
197
+ var STORAGE_KEY = "coday-preferences";
198
+ function getPreference(key, defaultValue) {
199
+ try {
200
+ const stored = localStorage.getItem(STORAGE_KEY);
201
+ if (!stored) return defaultValue;
202
+ const preferences = JSON.parse(stored);
203
+ return preferences[key] !== void 0 ? preferences[key] : defaultValue;
204
+ } catch {
205
+ return defaultValue;
206
+ }
207
+ }
208
+ function setPreference(key, value) {
209
+ try {
210
+ const stored = localStorage.getItem(STORAGE_KEY);
211
+ const preferences = stored ? JSON.parse(stored) : {};
212
+ preferences[key] = value;
213
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
214
+ } catch (error) {
215
+ console.error("Failed to set preference:", error);
216
+ }
217
+ }
218
+
219
+ // apps/web/client/chat-textarea/speech-to-textarea.component.ts
220
+ var SpeechToTextareaComponent = class {
221
+ constructor(chatTextarea, submitButton) {
222
+ this.chatTextarea = chatTextarea;
223
+ this.submitButton = submitButton;
224
+ this.initializeVoiceInput();
225
+ window.addEventListener("voiceLanguageChanged", (event) => {
226
+ this.updateRecognitionLanguage();
227
+ });
228
+ }
229
+ // Voice input properties
230
+ recognition = null;
231
+ voiceButton = null;
232
+ isRecording = false;
233
+ sessionHadTranscript = false;
234
+ pendingLineBreaksTimeout = null;
235
+ initializeVoiceInput() {
236
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
237
+ console.log("speechRecognition", SpeechRecognition);
238
+ if (!SpeechRecognition) return;
239
+ this.recognition = new SpeechRecognition();
240
+ this.recognition.continuous = true;
241
+ this.recognition.interimResults = true;
242
+ this.recognition.maxAlternatives = 1;
243
+ this.recognition.lang = this.getSelectedLanguage();
244
+ try {
245
+ if ("grammars" in this.recognition) {
246
+ this.recognition.serviceURI = void 0;
247
+ }
248
+ } catch (e) {
249
+ console.log("Advanced speech recognition parameters not supported");
250
+ }
251
+ this.recognition.onresult = (event) => {
252
+ let finalTranscript = "";
253
+ for (let i = event.resultIndex; i < event.results.length; i++) {
254
+ const transcript = event.results[i][0].transcript;
255
+ if (event.results[i].isFinal) {
256
+ finalTranscript += transcript;
257
+ }
258
+ }
259
+ if (finalTranscript) {
260
+ const processedTranscript = this.improveTranscriptPunctuation(finalTranscript.trim());
261
+ this.sessionHadTranscript = true;
262
+ console.log("onresult ending, appending to textarea", this.isRecording);
263
+ this.appendToTextarea(processedTranscript);
264
+ }
265
+ };
266
+ this.recognition.onend = () => {
267
+ this.isRecording = false;
268
+ console.log("Recognition ended. Was recording:", this.isRecording);
269
+ this.updateVoiceButtonState();
270
+ };
271
+ this.recognition.onerror = (event) => {
272
+ console.error("Speech recognition error:", event.error);
273
+ console.log("Error details:", {
274
+ error: event.error,
275
+ message: event.message,
276
+ timeStamp: event.timeStamp
277
+ });
278
+ this.isRecording = false;
279
+ this.clearPendingLineBreaks();
280
+ this.updateVoiceButtonState();
281
+ };
282
+ this.recognition.onstart = () => {
283
+ console.log("Recognition started successfully");
284
+ };
285
+ this.recognition.onspeechstart = () => {
286
+ console.log("Speech detected");
287
+ };
288
+ this.recognition.onspeechend = () => {
289
+ console.log("Speech ended");
290
+ };
291
+ this.createVoiceControls();
292
+ }
293
+ createVoiceControls() {
294
+ const buttonContainer = this.submitButton.parentElement;
295
+ if (!buttonContainer) return;
296
+ this.voiceButton = document.createElement("button");
297
+ this.voiceButton.type = "button";
298
+ this.voiceButton.className = "voice-button";
299
+ this.voiceButton.tabIndex = 0;
300
+ this.voiceButton.innerHTML = "\u{1F3A4}";
301
+ this.voiceButton.title = "Maintenir clic souris, touch, ou barre espace pour parler";
302
+ this.voiceButton.style.marginRight = "8px";
303
+ buttonContainer.insertBefore(this.voiceButton, this.submitButton);
304
+ this.voiceButton.addEventListener("mousedown", (e) => {
305
+ e.preventDefault();
306
+ this.startRecording();
307
+ });
308
+ this.voiceButton.addEventListener("mouseup", () => {
309
+ this.stopRecording();
310
+ });
311
+ this.voiceButton.addEventListener("mouseleave", () => {
312
+ this.stopRecording();
313
+ });
314
+ this.voiceButton.addEventListener("touchstart", (e) => {
315
+ e.preventDefault();
316
+ this.startRecording();
317
+ });
318
+ this.voiceButton.addEventListener("touchend", (e) => {
319
+ e.preventDefault();
320
+ this.stopRecording();
321
+ });
322
+ this.voiceButton.addEventListener("keydown", (e) => {
323
+ if (e.code === "Space" || e.key === " " || e.keyCode === 32) {
324
+ e.preventDefault();
325
+ if (!this.isRecording) {
326
+ this.startRecording();
327
+ }
328
+ }
329
+ });
330
+ this.voiceButton.addEventListener("keyup", (e) => {
331
+ if (e.code === "Space" || e.key === " " || e.keyCode === 32) {
332
+ e.preventDefault();
333
+ this.stopRecording();
334
+ }
335
+ });
336
+ }
337
+ async startRecording() {
338
+ if (!this.recognition || this.isRecording) return;
339
+ try {
340
+ this.isRecording = true;
341
+ this.sessionHadTranscript = false;
342
+ this.clearPendingLineBreaks();
343
+ this.updateVoiceButtonState();
344
+ this.recognition.start();
345
+ } catch (error) {
346
+ console.error("Failed to start recording:", error);
347
+ this.isRecording = false;
348
+ this.updateVoiceButtonState();
349
+ }
350
+ }
351
+ stopRecording() {
352
+ if (!this.recognition || !this.isRecording) {
353
+ console.log("stopRecording not recording");
354
+ return;
355
+ }
356
+ try {
357
+ console.log("stopRecording recognition stop");
358
+ this.recognition.stop();
359
+ this.schedulePendingLineBreaks();
360
+ } catch (error) {
361
+ console.error("Failed to stop recording:", error);
362
+ }
363
+ }
364
+ updateVoiceButtonState() {
365
+ if (!this.voiceButton) return;
366
+ if (this.isRecording) {
367
+ this.voiceButton.classList.add("recording");
368
+ this.voiceButton.innerHTML = "\u{1F534}";
369
+ this.voiceButton.title = "Recording... (release to stop)";
370
+ } else {
371
+ this.voiceButton.classList.remove("recording");
372
+ this.voiceButton.innerHTML = "\u{1F3A4}";
373
+ this.voiceButton.title = "Maintenir clic souris, touch, ou barre espace pour parler";
374
+ }
375
+ }
376
+ /**
377
+ * Get language from global preferences
378
+ */
379
+ getSelectedLanguage() {
380
+ return getPreference("voiceLanguage", "en-US") ?? "en-US";
381
+ }
382
+ /**
383
+ * Update speech recognition language
384
+ */
385
+ updateRecognitionLanguage() {
386
+ if (this.recognition) {
387
+ const selectedLang = this.getSelectedLanguage();
388
+ this.recognition.lang = selectedLang;
389
+ console.log("Language changed to:", selectedLang);
390
+ }
391
+ }
392
+ /**
393
+ * Improve punctuation from the rather raw transcript
394
+ */
395
+ improveTranscriptPunctuation(text) {
396
+ let improved = text;
397
+ if (!/[.!?]$/.test(improved.trim())) {
398
+ improved = improved.trim() + ".";
399
+ }
400
+ improved = improved.charAt(0).toUpperCase() + improved.slice(1);
401
+ improved = improved.replace(/\s+/g, " ");
402
+ improved = improved + " ";
403
+ return improved;
404
+ }
405
+ appendToTextarea(text) {
406
+ const currentValue = this.chatTextarea.value;
407
+ const newValue = currentValue ? `${currentValue}${text}` : text;
408
+ this.chatTextarea.value = newValue;
409
+ this.chatTextarea.focus();
410
+ const length = this.chatTextarea.value.length;
411
+ this.chatTextarea.setSelectionRange(length, length);
412
+ }
413
+ /**
414
+ * Schedule line breaks to be added after a delay, allowing time for pending transcripts
415
+ */
416
+ schedulePendingLineBreaks() {
417
+ this.clearPendingLineBreaks();
418
+ this.pendingLineBreaksTimeout = window.setTimeout(() => {
419
+ if (this.sessionHadTranscript) {
420
+ console.log("Adding line breaks after transcript session");
421
+ this.appendToTextarea("\n\n");
422
+ this.sessionHadTranscript = false;
423
+ }
424
+ this.pendingLineBreaksTimeout = null;
425
+ }, 500);
426
+ }
427
+ /**
428
+ * Clear any pending line break timeouts
429
+ */
430
+ clearPendingLineBreaks() {
431
+ if (this.pendingLineBreaksTimeout) {
432
+ clearTimeout(this.pendingLineBreaksTimeout);
433
+ this.pendingLineBreaksTimeout = null;
434
+ }
435
+ }
436
+ };
437
+
438
+ // apps/web/client/chat-textarea/chat-textarea.component.ts
439
+ var MINIMUM_SPEECH_LENGTH = 50;
440
+ var ChatTextareaComponent = class {
441
+ constructor(postEvent2, voiceSynthesis2) {
442
+ this.postEvent = postEvent2;
443
+ this.voiceSynthesis = voiceSynthesis2;
444
+ this.os = this.detectOS();
445
+ this.chatForm = document.getElementById("chat-form");
446
+ this.chatTextarea = document.getElementById("chat-input");
447
+ this.chatLabel = document.getElementById("chat-label");
448
+ this.submitButton = document.getElementById("send-button");
449
+ new SpeechToTextareaComponent(this.chatTextarea, this.submitButton);
450
+ this.chatForm.onsubmit = async (event) => {
451
+ event.preventDefault();
452
+ await this.submit();
453
+ };
454
+ this.chatTextarea.addEventListener("keydown", async (e) => {
455
+ if (this.inviteEvent?.defaultValue) return;
456
+ const useEnterToSend2 = getPreference("useEnterToSend", false);
457
+ if (useEnterToSend2) {
458
+ if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
459
+ e.preventDefault();
460
+ await this.submit();
461
+ }
462
+ } else {
463
+ if (e.key === "Enter" && (this.os === "mac" && e.metaKey || this.os === "non-mac" && e.ctrlKey)) {
464
+ e.preventDefault();
465
+ await this.submit();
466
+ }
467
+ }
468
+ if (e.key === "ArrowUp") {
469
+ if (this.isCursorAtFirstLine()) {
470
+ e.preventDefault();
471
+ this.navigateHistory("up");
472
+ }
473
+ } else if (e.key === "ArrowDown") {
474
+ if (this.isCursorAtLastLine()) {
475
+ e.preventDefault();
476
+ this.navigateHistory("down");
477
+ }
478
+ }
479
+ });
480
+ this.updateSendButtonLabel();
481
+ window.addEventListener("storage", (event) => {
482
+ if (event.key === "coday-preferences") {
483
+ this.updateSendButtonLabel();
484
+ }
485
+ });
486
+ window.addEventListener("voiceModeChanged", (event) => {
487
+ console.log("[CHAT-TEXTAREA] Voice mode changed to:", event.detail);
488
+ });
489
+ window.addEventListener("voiceAnnounceEnabledChanged", (event) => {
490
+ console.log("[CHAT-TEXTAREA] Voice announce enabled changed to:", event.detail);
491
+ });
492
+ }
493
+ chatForm;
494
+ chatTextarea;
495
+ chatLabel;
496
+ submitButton;
497
+ inviteEvent;
498
+ os;
499
+ // Command history for arrow key navigation
500
+ promptHistory = [];
501
+ historyIndex = -1;
502
+ tempInput = "";
503
+ /**
504
+ * Detect operating system for keyboard shortcuts
505
+ */
506
+ detectOS() {
507
+ return navigator.platform.toLowerCase().includes("mac") ? "mac" : "non-mac";
508
+ }
509
+ /**
510
+ * Update the send button label based on preferences and OS
511
+ */
512
+ updateSendButtonLabel() {
513
+ const useEnterToSend2 = getPreference("useEnterToSend", false);
514
+ if (!this.submitButton) return;
515
+ if (useEnterToSend2) {
516
+ this.submitButton.innerHTML = "SEND <br/><br/>enter";
517
+ } else {
518
+ if (this.os === "mac") {
519
+ this.submitButton.innerHTML = "SEND <br/><br/>\u2318 + enter";
520
+ } else {
521
+ this.submitButton.innerHTML = "SEND <br/><br/>Ctrl + enter";
522
+ }
523
+ }
524
+ }
525
+ handle(event) {
526
+ if (event instanceof InviteEvent) {
527
+ this.inviteEvent = event;
528
+ const parsed = marked.parse(this.inviteEvent.invite);
529
+ if (parsed instanceof Promise) {
530
+ parsed.then((html) => {
531
+ this.chatLabel.innerHTML = html;
532
+ this.setupLabelClickHandler();
533
+ });
534
+ } else {
535
+ this.chatLabel.innerHTML = parsed;
536
+ this.setupLabelClickHandler();
537
+ }
538
+ this.chatForm.style.display = "block";
539
+ this.chatTextarea.focus();
540
+ if (this.inviteEvent.defaultValue) {
541
+ console.log(`handling defaultValue: ${this.inviteEvent.defaultValue}`);
542
+ this.chatTextarea.value = this.inviteEvent.defaultValue;
543
+ }
544
+ const audioEnabled = getPreference("voiceAnnounceEnabled", false) || false;
545
+ if (audioEnabled) {
546
+ this.announceLabel(this.inviteEvent.invite);
547
+ }
548
+ }
549
+ }
550
+ /**
551
+ * Check if cursor is at the first line of text
552
+ */
553
+ isCursorAtFirstLine() {
554
+ const text = this.chatTextarea.value;
555
+ const cursorPos = this.chatTextarea.selectionStart;
556
+ if (cursorPos === 0) return true;
557
+ const textBeforeCursor = text.substring(0, cursorPos);
558
+ return textBeforeCursor.indexOf("\n") === -1;
559
+ }
560
+ /**
561
+ * Check if cursor is at the last line of text
562
+ */
563
+ isCursorAtLastLine() {
564
+ const text = this.chatTextarea.value;
565
+ const cursorPos = this.chatTextarea.selectionStart;
566
+ if (cursorPos === text.length) return true;
567
+ const textAfterCursor = text.substring(cursorPos);
568
+ return textAfterCursor.indexOf("\n") === -1;
569
+ }
570
+ /**
571
+ * Navigate through command history using up/down arrows
572
+ */
573
+ navigateHistory(direction) {
574
+ if (this.promptHistory.length === 0) return;
575
+ if (direction === "up" && this.historyIndex === -1) {
576
+ this.tempInput = this.chatTextarea.value;
577
+ }
578
+ if (direction === "up" && this.historyIndex < this.promptHistory.length - 1) {
579
+ this.historyIndex++;
580
+ this.chatTextarea.value = this.promptHistory[this.promptHistory.length - 1 - this.historyIndex];
581
+ this.moveCursorToEnd();
582
+ } else if (direction === "down" && this.historyIndex > -1) {
583
+ this.historyIndex--;
584
+ if (this.historyIndex === -1) {
585
+ this.chatTextarea.value = this.tempInput;
586
+ } else {
587
+ this.chatTextarea.value = this.promptHistory[this.promptHistory.length - 1 - this.historyIndex];
588
+ }
589
+ this.moveCursorToEnd();
590
+ }
591
+ }
592
+ /**
593
+ * Move cursor to the end of the textarea
594
+ */
595
+ moveCursorToEnd() {
596
+ const length = this.chatTextarea.value.length;
597
+ this.chatTextarea.setSelectionRange(length, length);
598
+ }
599
+ async submit() {
600
+ const inputValue = this.chatTextarea.value.trim();
601
+ const answer = this.inviteEvent?.buildAnswer(inputValue);
602
+ this.chatTextarea.value = "";
603
+ if (!answer) {
604
+ return;
605
+ }
606
+ try {
607
+ this.chatForm.style.display = "none";
608
+ const response = await this.postEvent(answer);
609
+ if (response.ok) {
610
+ if (inputValue && (this.promptHistory.length === 0 || this.promptHistory[this.promptHistory.length - 1] !== inputValue)) {
611
+ this.promptHistory.push(inputValue);
612
+ }
613
+ this.historyIndex = -1;
614
+ this.tempInput = "";
615
+ } else {
616
+ this.chatForm.style.display = "block";
617
+ this.chatTextarea.focus();
618
+ console.error("Failed to send message.");
619
+ }
620
+ } catch (error) {
621
+ console.error("Error occurred while sending message:", error);
622
+ }
623
+ }
624
+ setupLabelClickHandler() {
625
+ this.chatLabel.addEventListener("click", () => {
626
+ this.voiceSynthesis.stopSpeech();
627
+ });
628
+ this.chatLabel.style.cursor = "pointer";
629
+ this.chatLabel.title = "Click to stop speech";
630
+ }
631
+ announceLabel(text) {
632
+ const mode = getPreference("voiceMode", "speech") || "speech";
633
+ if (mode === "notification" || text.length <= MINIMUM_SPEECH_LENGTH) {
634
+ this.voiceSynthesis.ding();
635
+ } else {
636
+ this.voiceSynthesis.speak(text);
637
+ }
638
+ }
639
+ };
640
+
641
+ // apps/web/client/choice-select/choice-select.component.ts
642
+ var MINIMUM_SPEECH_LENGTH2 = 50;
643
+ var ChoiceSelectComponent = class {
644
+ constructor(postEvent2, voiceSynthesis2) {
645
+ this.postEvent = postEvent2;
646
+ this.voiceSynthesis = voiceSynthesis2;
647
+ this.choiceForm = document.getElementById("choice-form");
648
+ this.choiceSelect = document.getElementById("choice-select");
649
+ this.choiceLabel = document.getElementById("choices-label");
650
+ this.choiceForm.onsubmit = async (event) => {
651
+ event.preventDefault();
652
+ const answer = this.choiceEvent?.buildAnswer(this.choiceSelect.value);
653
+ if (!answer) {
654
+ return;
655
+ }
656
+ try {
657
+ this.choiceForm.style.display = "none";
658
+ const response = await this.postEvent(answer);
659
+ if (!response.ok) {
660
+ this.choiceForm.style.display = "block";
661
+ this.choiceSelect.focus();
662
+ console.error("Failed to send message.");
663
+ }
664
+ } catch (error) {
665
+ console.error("Error occurred while sending message:", error);
666
+ }
667
+ };
668
+ window.addEventListener("voiceModeChanged", (event) => {
669
+ console.log("[CHOICE-SELECT] Voice mode changed to:", event.detail);
670
+ });
671
+ window.addEventListener("voiceAnnounceEnabledChanged", (event) => {
672
+ console.log("[CHOICE-SELECT] Voice announce enabled changed to:", event.detail);
673
+ });
674
+ }
675
+ choiceForm;
676
+ choiceSelect;
677
+ choiceLabel;
678
+ choiceEvent;
679
+ handle(choiceEvent) {
680
+ if (!(choiceEvent instanceof ChoiceEvent)) {
681
+ return;
682
+ }
683
+ this.choiceEvent = choiceEvent;
684
+ const questionText = this.choiceEvent?.optionalQuestion ? marked.parse(this.choiceEvent.optionalQuestion) : "";
685
+ const inviteText = marked.parse(this.choiceEvent?.invite || "");
686
+ const updateLabel = async () => {
687
+ const [questionHtml, inviteHtml] = await Promise.all([
688
+ questionText instanceof Promise ? questionText : Promise.resolve(questionText),
689
+ inviteText instanceof Promise ? inviteText : Promise.resolve(inviteText)
690
+ ]);
691
+ this.choiceLabel.innerHTML = questionHtml + " " + inviteHtml;
692
+ this.setupLabelClickHandler();
693
+ const audioEnabled = getPreference("voiceAnnounceEnabled", false) || false;
694
+ if (audioEnabled) {
695
+ const fullText = (this.choiceEvent?.optionalQuestion || "") + " " + (this.choiceEvent?.invite || "");
696
+ const plainText = this.voiceSynthesis.extractPlainText(fullText);
697
+ this.announceLabel(plainText);
698
+ }
699
+ };
700
+ updateLabel().catch(console.error);
701
+ this.choiceSelect.innerHTML = this.choiceEvent?.options.map((option) => `<option value="${option}">${option}</option>`).join("");
702
+ this.choiceForm.style.display = "block";
703
+ this.choiceSelect.focus();
704
+ }
705
+ setupLabelClickHandler() {
706
+ this.choiceLabel.addEventListener("click", () => {
707
+ this.voiceSynthesis.stopSpeech();
708
+ });
709
+ this.choiceLabel.style.cursor = "pointer";
710
+ this.choiceLabel.title = "Click to stop speech";
711
+ }
712
+ announceLabel(text) {
713
+ const mode = getPreference("voiceMode", "speech") || "speech";
714
+ if (mode === "notification" || text.length <= MINIMUM_SPEECH_LENGTH2) {
715
+ this.voiceSynthesis.ding();
716
+ } else {
717
+ this.voiceSynthesis.speak(text);
718
+ }
719
+ }
720
+ };
721
+
722
+ // apps/web/client/chat-history/chat-history.component.ts
723
+ var PARAGRAPH_MIN_LENGTH = 80;
724
+ var MAX_PARAGRAPHS = 3;
725
+ var MESSAGE_FRESHNESS_THRESHOLD = 5 * 60 * 1e3;
726
+ var ChatHistoryComponent = class {
727
+ constructor(onStopCallback, voiceSynthesis2) {
728
+ this.voiceSynthesis = voiceSynthesis2;
729
+ this.chatHistory = document.getElementById("chat-history");
730
+ this.thinkingDots = document.getElementById("thinking-dots");
731
+ this.stopButton = this.thinkingDots.querySelector(".stop-button");
732
+ this.onStopCallback = onStopCallback;
733
+ if (this.stopButton) {
734
+ this.stopButton.addEventListener("click", () => this.onStopCallback());
735
+ }
736
+ this.readFullText = getPreference("voiceReadFullText", false) || false;
737
+ window.addEventListener("voiceReadFullTextChanged", (event) => {
738
+ this.readFullText = event.detail;
739
+ });
740
+ setInterval(() => {
741
+ this.checkStateConsistency();
742
+ }, 1e3);
743
+ }
744
+ chatHistory;
745
+ thinkingDots;
746
+ stopButton;
747
+ history = /* @__PURE__ */ new Map();
748
+ thinkingTimeout;
749
+ onStopCallback;
750
+ readFullText = false;
751
+ currentPlayingButton = null;
752
+ handle(event) {
753
+ this.history.set(event.timestamp, event);
754
+ if (event instanceof TextEvent) {
755
+ if (event.speaker) {
756
+ this.voiceSynthesis.stopSpeech();
757
+ this.resetAllPlayButtons();
758
+ this.addText(event.text, event.speaker, event.timestamp);
759
+ } else {
760
+ this.addTechnical(event.text);
761
+ }
762
+ }
763
+ if (event instanceof AnswerEvent) {
764
+ this.addAnswer(event.answer, event.invite);
765
+ }
766
+ if (event instanceof ErrorEvent) {
767
+ const errorMessage = JSON.stringify(event.error);
768
+ this.addError(errorMessage);
769
+ }
770
+ if (event instanceof WarnEvent) {
771
+ const warnMessage = JSON.stringify(event.warning);
772
+ this.addError(warnMessage, "Warning");
773
+ }
774
+ if (event instanceof ThinkingEvent) {
775
+ this.setThinking(true);
776
+ }
777
+ if (event instanceof ToolRequestEvent) {
778
+ this.addToolRequest(event);
779
+ }
780
+ if (event instanceof ToolResponseEvent) {
781
+ this.addToolResponse(event);
782
+ }
783
+ }
784
+ setThinking(value) {
785
+ if (!this.thinkingDots) {
786
+ return;
787
+ }
788
+ clearTimeout(this.thinkingTimeout);
789
+ if (value) {
790
+ this.thinkingTimeout = setTimeout(() => {
791
+ this.thinkingDots.classList.toggle("visible", false);
792
+ }, ThinkingEvent.debounce + 1e3);
793
+ } else {
794
+ this.scrollToBottom();
795
+ }
796
+ this.thinkingDots.classList.toggle("visible", value);
797
+ }
798
+ scrollToBottom() {
799
+ if (this.chatHistory) {
800
+ this.chatHistory.scrollTo(0, this.chatHistory.scrollHeight);
801
+ }
802
+ }
803
+ addTechnical(text) {
804
+ const newEntry = this.createMessageElement(text, void 0);
805
+ newEntry.classList.add("technical");
806
+ this.appendMessageElement(newEntry);
807
+ }
808
+ addText(text, speaker, messageTimestamp) {
809
+ const newEntry = this.createMessageElement(text, speaker);
810
+ newEntry.classList.add("text", "left");
811
+ newEntry.addEventListener("click", () => {
812
+ this.voiceSynthesis.stopSpeech();
813
+ });
814
+ const buttonContainer = document.createElement("div");
815
+ buttonContainer.classList.add("message-button-container");
816
+ const playButton = this.createPlayButton(text);
817
+ buttonContainer.appendChild(playButton);
818
+ const copyButton = document.createElement("button");
819
+ copyButton.classList.add("copy-button");
820
+ copyButton.title = "Copy raw response";
821
+ copyButton.textContent = "\u{1F4CB}";
822
+ copyButton.addEventListener("click", (event) => {
823
+ event.stopPropagation();
824
+ this.copyToClipboard(text);
825
+ const clickedButton = event.currentTarget;
826
+ if (clickedButton) {
827
+ document.querySelectorAll(".copy-button.active").forEach((btn) => {
828
+ btn.classList.remove("active");
829
+ btn.textContent = "\u{1F4CB}";
830
+ });
831
+ clickedButton.classList.add("active");
832
+ clickedButton.textContent = "\u2713";
833
+ setTimeout(() => {
834
+ clickedButton.classList.remove("active");
835
+ clickedButton.textContent = "\u{1F4CB}";
836
+ }, 2e3);
837
+ }
838
+ });
839
+ buttonContainer.appendChild(copyButton);
840
+ newEntry.appendChild(buttonContainer);
841
+ this.appendMessageElement(newEntry);
842
+ const audioEnabled = getPreference("voiceAnnounceEnabled", false) || false;
843
+ if (speaker && audioEnabled && this.isMessageRecentEnoughForAnnouncement(messageTimestamp)) {
844
+ this.announceText(text);
845
+ }
846
+ }
847
+ addAnswer(answer, speaker) {
848
+ const newEntry = this.createMessageElement(answer, speaker);
849
+ newEntry.classList.add("text", "right");
850
+ const buttonContainer = document.createElement("div");
851
+ buttonContainer.classList.add("message-button-container");
852
+ const playButton = this.createPlayButton(answer);
853
+ buttonContainer.appendChild(playButton);
854
+ const copyButton = document.createElement("button");
855
+ copyButton.classList.add("copy-button");
856
+ copyButton.title = "Copy raw message";
857
+ copyButton.textContent = "\u{1F4CB}";
858
+ copyButton.addEventListener("click", (event) => {
859
+ event.stopPropagation();
860
+ this.copyToClipboard(answer);
861
+ const clickedButton = event.currentTarget;
862
+ if (clickedButton) {
863
+ document.querySelectorAll(".copy-button.active").forEach((btn) => {
864
+ btn.classList.remove("active");
865
+ btn.textContent = "\u{1F4CB}";
866
+ });
867
+ clickedButton.classList.add("active");
868
+ clickedButton.textContent = "\u2713";
869
+ setTimeout(() => {
870
+ clickedButton.classList.remove("active");
871
+ clickedButton.textContent = "\u{1F4CB}";
872
+ }, 2e3);
873
+ }
874
+ });
875
+ buttonContainer.appendChild(copyButton);
876
+ newEntry.appendChild(buttonContainer);
877
+ this.appendMessageElement(newEntry);
878
+ }
879
+ copyToClipboard(text) {
880
+ navigator.clipboard.writeText(text).catch((err) => console.error("Failed to copy text: ", err));
881
+ }
882
+ createPlayButton(text) {
883
+ const playButton = document.createElement("button");
884
+ playButton.classList.add("play-button");
885
+ playButton.title = "Play message";
886
+ playButton.textContent = "\u25B6\uFE0F";
887
+ playButton.addEventListener("click", (event) => {
888
+ event.stopPropagation();
889
+ this.togglePlayback(text, playButton);
890
+ });
891
+ return playButton;
892
+ }
893
+ togglePlayback(text, button) {
894
+ if (this.voiceSynthesis.isSpeaking() && this.currentPlayingButton === button) {
895
+ console.log("[CHAT] Stopping current playback");
896
+ this.voiceSynthesis.stopSpeech();
897
+ this.resetPlayButton(button);
898
+ this.currentPlayingButton = null;
899
+ } else {
900
+ console.log("[CHAT] Starting new playback, stopping previous if any");
901
+ this.voiceSynthesis.stopSpeech();
902
+ this.resetAllPlayButtons();
903
+ const plainText = this.voiceSynthesis.extractPlainText(text);
904
+ const onEndCallback = () => {
905
+ console.log("[CHAT] Playback ended, resetting button");
906
+ if (this.currentPlayingButton === button) {
907
+ this.resetPlayButton(button);
908
+ this.currentPlayingButton = null;
909
+ }
910
+ };
911
+ this.voiceSynthesis.speak(plainText, onEndCallback);
912
+ this.currentPlayingButton = button;
913
+ button.textContent = "\u23F8\uFE0F";
914
+ button.title = "Stop playback";
915
+ console.log("[CHAT] Button set to pause state");
916
+ }
917
+ }
918
+ resetPlayButton(button) {
919
+ button.textContent = "\u25B6\uFE0F";
920
+ button.title = "Play message";
921
+ }
922
+ resetAllPlayButtons() {
923
+ const allPlayButtons = document.querySelectorAll(".play-button");
924
+ allPlayButtons.forEach((btn) => {
925
+ const button = btn;
926
+ button.textContent = "\u25B6\uFE0F";
927
+ button.title = "Play message";
928
+ });
929
+ this.currentPlayingButton = null;
930
+ }
931
+ checkStateConsistency() {
932
+ if (!this.voiceSynthesis.isSpeaking() && this.currentPlayingButton) {
933
+ console.log("[CHAT] State inconsistency detected: no speech but button still active, fixing...");
934
+ this.resetAllPlayButtons();
935
+ }
936
+ }
937
+ createMessageElement(content, speaker) {
938
+ const newEntry = document.createElement("div");
939
+ newEntry.classList.add("message");
940
+ if (speaker) {
941
+ const speakerElement = document.createElement("div");
942
+ speakerElement.classList.add("speaker");
943
+ speakerElement.textContent = speaker;
944
+ newEntry.appendChild(speakerElement);
945
+ const parsed = marked.parse(content);
946
+ parsed instanceof Promise ? parsed.then((html) => newEntry.appendChild(this.buildTextElement(html))) : newEntry.appendChild(this.buildTextElement(parsed));
947
+ } else {
948
+ const parsed = marked.parse(content);
949
+ parsed instanceof Promise ? parsed.then((html) => newEntry.innerHTML = html) : newEntry.innerHTML = parsed;
950
+ }
951
+ return newEntry;
952
+ }
953
+ appendMessageElement(element) {
954
+ this.chatHistory?.appendChild(element);
955
+ }
956
+ buildTextElement(innerHTML) {
957
+ const textElement = document.createElement("div");
958
+ textElement.innerHTML = innerHTML;
959
+ return textElement;
960
+ }
961
+ addError(error, level = "Error") {
962
+ this.setThinking(false);
963
+ const errorEntry = document.createElement("div");
964
+ const errorIcon = document.createElement("span");
965
+ errorIcon.textContent = level === "Error" ? "\u274C" : "\u26A0\uFE0F";
966
+ errorEntry.appendChild(errorIcon);
967
+ const errorText = document.createElement("span");
968
+ errorText.textContent = `${level}: ${error}`;
969
+ errorEntry.appendChild(errorText);
970
+ errorEntry.style.color = level === "Error" ? "#e74c3c" : "#e7ab3c";
971
+ errorEntry.style.background = level === "Error" ? "#ffeeee" : "#fffaee";
972
+ errorEntry.style.padding = "10px";
973
+ errorEntry.style.margin = "10px 0";
974
+ errorEntry.style.borderRadius = "4px";
975
+ errorEntry.style.border = `1px solid ${level === "Error" ? "#e74c3c" : "#e7ab3c"}`;
976
+ this.chatHistory?.appendChild(errorEntry);
977
+ this.scrollToBottom();
978
+ }
979
+ getClientId() {
980
+ const params = new URLSearchParams(window.location.search);
981
+ return params.get("clientId") || "";
982
+ }
983
+ createViewFullLink(eventId) {
984
+ const clientId2 = this.getClientId();
985
+ if (!clientId2) return null;
986
+ const link = document.createElement("a");
987
+ link.href = `/api/event/${eventId}?clientId=${clientId2}`;
988
+ link.textContent = "view";
989
+ link.target = "_blank";
990
+ link.style.marginLeft = "5px";
991
+ link.style.fontSize = "0.9em";
992
+ link.style.color = "var(--color-link)";
993
+ return link;
994
+ }
995
+ addToolRequest(event) {
996
+ const element = document.createElement("div");
997
+ element.classList.add("message", "technical");
998
+ const container = document.createElement("span");
999
+ const messageText = event.toSingleLineString();
1000
+ container.appendChild(document.createTextNode(messageText));
1001
+ const link = this.createViewFullLink(event.timestamp);
1002
+ if (link) {
1003
+ container.appendChild(link);
1004
+ }
1005
+ element.appendChild(container);
1006
+ this.appendMessageElement(element);
1007
+ }
1008
+ addToolResponse(event) {
1009
+ const element = document.createElement("div");
1010
+ element.classList.add("message", "technical");
1011
+ const container = document.createElement("span");
1012
+ const messageText = event.toSingleLineString();
1013
+ container.appendChild(document.createTextNode(messageText));
1014
+ const link = this.createViewFullLink(event.timestamp);
1015
+ if (link) {
1016
+ container.appendChild(link);
1017
+ }
1018
+ element.appendChild(container);
1019
+ this.appendMessageElement(element);
1020
+ }
1021
+ isMessageRecentEnoughForAnnouncement(messageTimestamp) {
1022
+ if (!messageTimestamp) {
1023
+ return true;
1024
+ }
1025
+ try {
1026
+ const isoDatePart = messageTimestamp.split("-").slice(0, -1).join("-");
1027
+ const messageTime = new Date(isoDatePart).getTime();
1028
+ const now = Date.now();
1029
+ const timeDiff = now - messageTime;
1030
+ return timeDiff <= MESSAGE_FRESHNESS_THRESHOLD;
1031
+ } catch (error) {
1032
+ return true;
1033
+ }
1034
+ }
1035
+ announceText(text) {
1036
+ const mode = getPreference("voiceMode", "speech") || "speech";
1037
+ if (mode === "notification") {
1038
+ this.voiceSynthesis.ding();
1039
+ return;
1040
+ }
1041
+ let plainText = this.voiceSynthesis.extractPlainText(text);
1042
+ let textToSpeak;
1043
+ if (this.readFullText) {
1044
+ textToSpeak = plainText;
1045
+ } else {
1046
+ const speech = plainText.split("\n").reduce(
1047
+ (acc, value) => {
1048
+ if (acc.paragraphs >= MAX_PARAGRAPHS) {
1049
+ return acc;
1050
+ } else {
1051
+ const paragraphIncrement = value.length > PARAGRAPH_MIN_LENGTH ? 1 : 0;
1052
+ return {
1053
+ paragraphs: acc.paragraphs + paragraphIncrement,
1054
+ text: acc.text + "\n" + value
1055
+ };
1056
+ }
1057
+ },
1058
+ { paragraphs: 0, text: "" }
1059
+ );
1060
+ textToSpeak = speech.text;
1061
+ }
1062
+ if (!textToSpeak.trim()) {
1063
+ console.log("[VOICE] No text to speak after processing!");
1064
+ return;
1065
+ }
1066
+ this.voiceSynthesis.speak(textToSpeak);
1067
+ }
1068
+ };
1069
+
1070
+ // apps/web/client/header/header.component.ts
1071
+ var HeaderComponent = class {
1072
+ headerTitle = document.getElementById("header-title");
1073
+ handle(event) {
1074
+ if (event instanceof ProjectSelectedEvent) {
1075
+ const title = event.projectName || `Coday`;
1076
+ document.title = title;
1077
+ this.headerTitle.innerHTML = title;
1078
+ }
1079
+ }
1080
+ };
1081
+
1082
+ // apps/web/client/voice-synthesis/voice-synthesis.component.ts
1083
+ var TestTexts = {
1084
+ fr: "Bonjour, ceci est un test de la voix s\xE9lectionn\xE9e.",
1085
+ en: "Hello, this is a test of the selected voice.",
1086
+ es: "Hola, esta es una prueba de la voz seleccionada.",
1087
+ de: "Hallo, dies ist ein Test der ausgew\xE4hlten Stimme.",
1088
+ it: "Ciao, questo \xE8 un test della voce selezionata.",
1089
+ pt: "Ol\xE1, este \xE9 um teste da voz selecionada.",
1090
+ ja: "\u3053\u3093\u306B\u3061\u306F\u3001\u3053\u308C\u306F\u9078\u629E\u3055\u308C\u305F\u97F3\u58F0\u306E\u30C6\u30B9\u30C8\u3067\u3059\u3002",
1091
+ ko: "\uC548\uB155\uD558\uC138\uC694, \uC774\uAC83\uC740 \uC120\uD0DD\uB41C \uC74C\uC131\uC758 \uD14C\uC2A4\uD2B8\uC785\uB2C8\uB2E4.",
1092
+ zh: "\u4F60\u597D\uFF0C\u8FD9\u662F\u6240\u9009\u8BED\u97F3\u7684\u6D4B\u8BD5\u3002",
1093
+ ru: "\u041F\u0440\u0438\u0432\u0435\u0442, \u044D\u0442\u043E \u0442\u0435\u0441\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u0433\u043E\u043B\u043E\u0441\u0430.",
1094
+ ar: "\u0645\u0631\u062D\u0628\u0627\u060C \u0647\u0630\u0627 \u0627\u062E\u062A\u0628\u0627\u0631 \u0644\u0644\u0635\u0648\u062A \u0627\u0644\u0645\u062D\u062F\u062F."
1095
+ };
1096
+ var VoiceSynthesisComponent = class {
1097
+ voices;
1098
+ currentUtterance = null;
1099
+ currentOnEndCallback = null;
1100
+ volume = 0.8;
1101
+ rate = 1.2;
1102
+ language = "en";
1103
+ selectedVoice;
1104
+ constructor() {
1105
+ if (!("speechSynthesis" in window)) {
1106
+ console.warn("Speech synthesis not available");
1107
+ this.voices = new Promise((resolve) => resolve([]));
1108
+ return;
1109
+ }
1110
+ this.voices = new Promise((resolve, reject) => {
1111
+ const immediateVoices = speechSynthesis.getVoices();
1112
+ if (immediateVoices.length) {
1113
+ resolve(immediateVoices);
1114
+ } else {
1115
+ const handle = setTimeout(() => {
1116
+ reject("no voices found");
1117
+ }, 2e3);
1118
+ speechSynthesis.addEventListener("voiceschanged", () => {
1119
+ const delayedVoices = speechSynthesis.getVoices();
1120
+ if (delayedVoices.length) {
1121
+ clearTimeout(handle);
1122
+ resolve(delayedVoices);
1123
+ }
1124
+ });
1125
+ }
1126
+ });
1127
+ }
1128
+ ding() {
1129
+ try {
1130
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1131
+ const oscillator = audioContext.createOscillator();
1132
+ const gainNode = audioContext.createGain();
1133
+ oscillator.connect(gainNode);
1134
+ gainNode.connect(audioContext.destination);
1135
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
1136
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
1137
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
1138
+ oscillator.start(audioContext.currentTime);
1139
+ oscillator.stop(audioContext.currentTime + 0.1);
1140
+ } catch (error) {
1141
+ console.error("Notification sound failed:", error);
1142
+ }
1143
+ }
1144
+ extractPlainText(text) {
1145
+ let processed = text.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|⭐/gu, "").replace(/```[\s\S]*?```/g, "code block").replace(/`([^`]+)`/g, "$1").replace(/\*\*\*(.*?)\*\*\*/g, "$1").replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/~~(.*?)~~/g, "$1").replace(/#{1,6}\s*(.*)/g, "$1").replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1").replace(/https?:\/\/[^\s]+/g, "link").replace(/&nbsp;/g, " ").replace(/&amp;/g, "and").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
1146
+ processed = this.addNaturalPunctuation(processed);
1147
+ return processed.replace(/\s+/g, " ").trim();
1148
+ }
1149
+ addNaturalPunctuation(text) {
1150
+ return text.replace(/\n+/g, "\n").split("\n").map((line) => this.addPeriodIfNeeded(line.trim())).filter((line) => line.length > 0).join(" ");
1151
+ }
1152
+ addPeriodIfNeeded(line) {
1153
+ if (!line) return "";
1154
+ if (!/[.!?;:]$/.test(line)) {
1155
+ return line + ".";
1156
+ }
1157
+ return line;
1158
+ }
1159
+ speak(markdown, onEnd) {
1160
+ if (!this.selectedVoice) {
1161
+ console.warn("no voice selected");
1162
+ return;
1163
+ }
1164
+ this.stopSpeech();
1165
+ const text = this.extractPlainText(markdown);
1166
+ const utterance = new SpeechSynthesisUtterance(text);
1167
+ utterance.voice = this.selectedVoice;
1168
+ utterance.lang = this.selectedVoice.lang;
1169
+ utterance.volume = this.volume;
1170
+ utterance.rate = this.rate;
1171
+ this.currentOnEndCallback = onEnd || null;
1172
+ if (onEnd) {
1173
+ utterance.onend = () => {
1174
+ if (this.currentOnEndCallback === onEnd) {
1175
+ this.currentOnEndCallback = null;
1176
+ onEnd();
1177
+ }
1178
+ };
1179
+ utterance.onerror = () => {
1180
+ if (this.currentOnEndCallback === onEnd) {
1181
+ this.currentOnEndCallback = null;
1182
+ onEnd();
1183
+ }
1184
+ };
1185
+ }
1186
+ this.currentUtterance = utterance;
1187
+ speechSynthesis.speak(utterance);
1188
+ console.log("[VOICE-COMPONENT] Started speaking:", text.substring(0, 50) + "...");
1189
+ }
1190
+ async getVoices() {
1191
+ const langCode = this.language.slice(0, 2);
1192
+ const matchingVoices = (await this.voices)?.filter((voice) => voice.lang.toLowerCase().startsWith(langCode))?.sort((a, b) => {
1193
+ if (a.localService && !b.localService) return -1;
1194
+ if (!a.localService && b.localService) return 1;
1195
+ return a.name.localeCompare(b.name);
1196
+ }) ?? [];
1197
+ console.log("matching voices", matchingVoices, langCode);
1198
+ return matchingVoices.map((voice) => ({
1199
+ name: voice.name,
1200
+ lang: voice.lang,
1201
+ localService: voice.localService,
1202
+ default: voice.default,
1203
+ displayName: this.formatVoiceDisplayName(voice)
1204
+ }));
1205
+ }
1206
+ formatVoiceDisplayName(voice) {
1207
+ const localMarker = voice.localService ? "\u{1F3E0}" : "\u2601\uFE0F";
1208
+ const defaultMarker = voice.default ? " \u2B50" : "";
1209
+ return `${localMarker} ${voice.name} (${voice.lang})${defaultMarker}`;
1210
+ }
1211
+ async setSelectedVoice(voice) {
1212
+ if (!voice) {
1213
+ this.selectedVoice = void 0;
1214
+ return;
1215
+ }
1216
+ const [voiceName, voiceLang] = voice.split("|");
1217
+ if (!voiceName || !voiceLang) {
1218
+ this.selectedVoice = void 0;
1219
+ return;
1220
+ }
1221
+ const foundVoice = (await this.voices)?.find((v) => v.name === voiceName && v.lang === voiceLang);
1222
+ if (foundVoice) {
1223
+ this.selectedVoice = foundVoice;
1224
+ } else {
1225
+ this.selectedVoice = void 0;
1226
+ }
1227
+ }
1228
+ testSelectedVoice() {
1229
+ setTimeout(() => {
1230
+ if (!this.selectedVoice) {
1231
+ console.log(`[VOICE] no voice selected`);
1232
+ return;
1233
+ }
1234
+ this.stopSpeech();
1235
+ const langCode = this.selectedVoice.lang.slice(0, 2);
1236
+ const testText = TestTexts[langCode] || TestTexts["en"];
1237
+ this.speak(testText);
1238
+ }, 100);
1239
+ }
1240
+ stopSpeech() {
1241
+ if (this.currentUtterance || speechSynthesis.speaking) {
1242
+ speechSynthesis.cancel();
1243
+ this.currentUtterance = null;
1244
+ this.currentOnEndCallback = null;
1245
+ console.log("[VOICE-COMPONENT] Speech stopped");
1246
+ }
1247
+ }
1248
+ isSpeaking() {
1249
+ return speechSynthesis.speaking;
1250
+ }
1251
+ };
1252
+
1253
+ // apps/web/client/preferences.ts
1254
+ var voiceSynthesis = new VoiceSynthesisComponent();
1255
+ var voiceAnnounceToggle = document.getElementById("voice-announce-toggle");
1256
+ var voiceModeSelect = document.getElementById("voice-mode-select");
1257
+ var voiceSelectionContainer = document.getElementById("voice-selection-container");
1258
+ var voiceSelect = document.getElementById("voice-select");
1259
+ var voiceLanguageSelect = document.getElementById("voice-language-select");
1260
+ var voiceRateSelect = document.getElementById("voice-rate-select");
1261
+ var voiceVolumeSlider = document.getElementById("voice-volume-slider");
1262
+ var volumeDisplay = document.getElementById("volume-display");
1263
+ var voiceVolumeControl = document.getElementById("voice-volume-control");
1264
+ var voiceReadFullToggle = document.getElementById("voice-read-full-toggle");
1265
+ var voiceOptions = document.getElementById("voice-options");
1266
+ var savedVoiceLanguage = getPreference("voiceLanguage", "en-US") || "en-US";
1267
+ voiceLanguageSelect.value = savedVoiceLanguage;
1268
+ voiceSynthesis.language = savedVoiceLanguage;
1269
+ var voiceAnnounceEnabled = getPreference("voiceAnnounceEnabled", false);
1270
+ voiceAnnounceToggle.checked = voiceAnnounceEnabled !== void 0 ? voiceAnnounceEnabled : false;
1271
+ var savedVoiceMode = getPreference("voiceMode", "speech") || "speech";
1272
+ voiceModeSelect.value = savedVoiceMode;
1273
+ var selectedVoice = getPreference("selectedVoice", "");
1274
+ voiceSynthesis.setSelectedVoice(selectedVoice);
1275
+ var savedSpeechSpeed = getPreference("speechSpeed", "1.2") || "1.2";
1276
+ voiceRateSelect.value = savedSpeechSpeed;
1277
+ voiceSynthesis.rate = parseFloat(savedSpeechSpeed);
1278
+ var savedSpeechVolume = getPreference("speechVolume", 80) || 80;
1279
+ voiceVolumeSlider.value = savedSpeechVolume.toString();
1280
+ volumeDisplay.textContent = `${savedSpeechVolume}%`;
1281
+ voiceSynthesis.volume = savedSpeechVolume / 100;
1282
+ var readFullText = getPreference("voiceReadFullText", false);
1283
+ voiceReadFullToggle.checked = readFullText !== void 0 ? readFullText : false;
1284
+ function updateVoiceSelectionVisibility() {
1285
+ const showVoiceSelection = voiceAnnounceToggle.checked && voiceModeSelect.value === "speech";
1286
+ voiceSelectionContainer.style.display = showVoiceSelection ? "block" : "none";
1287
+ voiceVolumeControl.style.display = showVoiceSelection ? "flex" : "none";
1288
+ if (showVoiceSelection) {
1289
+ populateVoiceSelect();
1290
+ }
1291
+ }
1292
+ function updateVoiceOptionsVisibility() {
1293
+ voiceOptions.style.display = voiceAnnounceToggle.checked ? "block" : "none";
1294
+ voiceVolumeControl.style.display = voiceAnnounceToggle.checked && voiceModeSelect.value === "speech" ? "flex" : "none";
1295
+ updateVoiceSelectionVisibility();
1296
+ }
1297
+ async function populateVoiceSelect() {
1298
+ try {
1299
+ voiceSelect.innerHTML = '<option value="">Loading voices...</option>';
1300
+ const voices = await voiceSynthesis.getVoices();
1301
+ console.log("[VOICE] Populate voice select with", voices.length, "voices for language:", voiceSynthesis.language);
1302
+ voiceSelect.innerHTML = "";
1303
+ if (voices.length === 0) {
1304
+ voiceSelect.innerHTML = '<option value="">No voices available for this language</option>';
1305
+ console.warn("[VOICE] No voices found for language:", voiceSynthesis.language);
1306
+ return;
1307
+ }
1308
+ voices.forEach((voice) => {
1309
+ const option = document.createElement("option");
1310
+ option.value = `${voice.name}|${voice.lang}`;
1311
+ option.textContent = voice.displayName;
1312
+ voiceSelect.appendChild(option);
1313
+ });
1314
+ const savedVoice = voiceSynthesis.selectedVoice;
1315
+ if (savedVoice) {
1316
+ const savedValue = `${savedVoice.name}|${savedVoice.lang}`;
1317
+ voiceSelect.value = savedValue;
1318
+ console.log("[VOICE] Restored saved voice preference:", savedVoice);
1319
+ } else {
1320
+ console.log("[VOICE] No saved voice preference found");
1321
+ }
1322
+ } catch (error) {
1323
+ console.error("[VOICE] Error populating voice select:", error);
1324
+ voiceSelect.innerHTML = '<option value="">Error loading voices</option>';
1325
+ }
1326
+ }
1327
+ voiceLanguageSelect.addEventListener("change", () => {
1328
+ setPreference("voiceLanguage", voiceLanguageSelect.value);
1329
+ voiceSynthesis.language = voiceLanguageSelect.value;
1330
+ populateVoiceSelect();
1331
+ window.dispatchEvent(new CustomEvent("voiceLanguageChanged", { detail: voiceLanguageSelect.value }));
1332
+ });
1333
+ voiceAnnounceToggle.addEventListener("change", () => {
1334
+ setPreference("voiceAnnounceEnabled", voiceAnnounceToggle.checked);
1335
+ updateVoiceOptionsVisibility();
1336
+ if (voiceAnnounceToggle.checked) {
1337
+ testAudioAnnouncement();
1338
+ }
1339
+ window.dispatchEvent(new CustomEvent("voiceAnnounceEnabledChanged", { detail: voiceAnnounceToggle.checked }));
1340
+ });
1341
+ voiceModeSelect.addEventListener("change", () => {
1342
+ setPreference("voiceMode", voiceModeSelect.value);
1343
+ updateVoiceSelectionVisibility();
1344
+ testAudioAnnouncement();
1345
+ window.dispatchEvent(new CustomEvent("voiceModeChanged", { detail: voiceModeSelect.value }));
1346
+ });
1347
+ voiceSelect.addEventListener("change", () => {
1348
+ const selectedValue = voiceSelect.value;
1349
+ if (selectedValue) {
1350
+ setPreference("selectedVoice", selectedValue);
1351
+ voiceSynthesis.setSelectedVoice(selectedValue);
1352
+ voiceSynthesis.testSelectedVoice();
1353
+ } else {
1354
+ setPreference("selectedVoice", "");
1355
+ }
1356
+ });
1357
+ voiceRateSelect.addEventListener("change", () => {
1358
+ const selectedRate = voiceRateSelect.value;
1359
+ setPreference("speechSpeed", selectedRate);
1360
+ voiceSynthesis.rate = parseFloat(selectedRate);
1361
+ voiceSynthesis.testSelectedVoice();
1362
+ });
1363
+ voiceVolumeSlider.addEventListener("input", () => {
1364
+ const volumeValue = parseInt(voiceVolumeSlider.value);
1365
+ volumeDisplay.textContent = `${volumeValue}%`;
1366
+ setPreference("speechVolume", volumeValue);
1367
+ voiceSynthesis.volume = volumeValue / 100;
1368
+ });
1369
+ voiceReadFullToggle.addEventListener("change", () => {
1370
+ setPreference("voiceReadFullText", voiceReadFullToggle.checked);
1371
+ window.dispatchEvent(new CustomEvent("voiceReadFullTextChanged", { detail: voiceReadFullToggle.checked }));
1372
+ });
1373
+ function testAudioAnnouncement() {
1374
+ const mode = voiceModeSelect.value;
1375
+ if (mode === "notification") {
1376
+ try {
1377
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1378
+ const oscillator = audioContext.createOscillator();
1379
+ const gainNode = audioContext.createGain();
1380
+ oscillator.connect(gainNode);
1381
+ gainNode.connect(audioContext.destination);
1382
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
1383
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
1384
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
1385
+ oscillator.start(audioContext.currentTime);
1386
+ oscillator.stop(audioContext.currentTime + 0.1);
1387
+ } catch (error) {
1388
+ console.error("[VOICE] Notification sound test failed:", error);
1389
+ }
1390
+ } else if (mode === "speech") {
1391
+ voiceSynthesis.testSelectedVoice();
1392
+ }
1393
+ }
1394
+ function initializePreferences() {
1395
+ updateVoiceOptionsVisibility();
1396
+ setTimeout(() => {
1397
+ populateVoiceSelect();
1398
+ }, 100);
1399
+ }
1400
+ if (document.readyState === "loading") {
1401
+ document.addEventListener("DOMContentLoaded", initializePreferences);
1402
+ } else {
1403
+ initializePreferences();
1404
+ }
1405
+
1406
+ // apps/web/client/app.ts
1407
+ function debugLog(context, ...args) {
1408
+ console.log(`[DEBUG ${context}]`, ...args);
1409
+ }
1410
+ function getOrCreateClientId() {
1411
+ const params = new URLSearchParams(window.location.search);
1412
+ let clientId2 = params.get("clientId");
1413
+ if (!clientId2) {
1414
+ clientId2 = Math.random().toString(36).substring(2, 15);
1415
+ const newUrl = new URL(window.location.href);
1416
+ newUrl.searchParams.set("clientId", clientId2);
1417
+ window.history.pushState({}, "", newUrl);
1418
+ }
1419
+ return clientId2;
1420
+ }
1421
+ var clientId = getOrCreateClientId();
1422
+ debugLog("INIT", `Session started with clientId: ${clientId}`);
1423
+ function postEvent(event) {
1424
+ debugLog("API", "Posting event:", event);
1425
+ return fetch(`/api/message?clientId=${clientId}`, {
1426
+ method: "POST",
1427
+ headers: {
1428
+ "Content-Type": "application/json"
1429
+ },
1430
+ body: JSON.stringify(event)
1431
+ });
1432
+ }
1433
+ var handleStop = () => {
1434
+ debugLog("API", "Stopping execution");
1435
+ fetch(`/api/stop?clientId=${clientId}`, { method: "POST" }).catch(
1436
+ (error) => console.error("Error stopping execution:", error)
1437
+ );
1438
+ };
1439
+ var chatHistory = new ChatHistoryComponent(handleStop, voiceSynthesis);
1440
+ var chatInputComponent = new ChatTextareaComponent(postEvent, voiceSynthesis);
1441
+ var choiceInputComponent = new ChoiceSelectComponent(postEvent, voiceSynthesis);
1442
+ var components = [chatInputComponent, choiceInputComponent, chatHistory, new HeaderComponent()];
1443
+ var eventSource = null;
1444
+ var reconnectAttempts = 0;
1445
+ var MAX_RECONNECT_ATTEMPTS = 3;
1446
+ var RECONNECT_DELAY = 2e3;
1447
+ function setupEventSource() {
1448
+ debugLog("SSE", "Setting up new EventSource");
1449
+ if (eventSource) {
1450
+ debugLog("SSE", "Closing existing EventSource");
1451
+ eventSource.close();
1452
+ }
1453
+ eventSource = new EventSource(`/events?clientId=${clientId}`);
1454
+ eventSource.onmessage = (event) => {
1455
+ debugLog("SSE", "Received message:", event.data);
1456
+ reconnectAttempts = 0;
1457
+ try {
1458
+ const data = JSON.parse(event.data);
1459
+ const codayEvent = buildCodayEvent(data);
1460
+ if (codayEvent) {
1461
+ debugLog("EVENT", "Processing event:", codayEvent);
1462
+ components.forEach((c) => c.handle(codayEvent));
1463
+ }
1464
+ } catch (error) {
1465
+ console.error("Could not parse event", event);
1466
+ }
1467
+ };
1468
+ eventSource.onopen = () => {
1469
+ debugLog("SSE", "Connection established");
1470
+ reconnectAttempts = 0;
1471
+ };
1472
+ eventSource.onerror = (error) => {
1473
+ debugLog("SSE", "EventSource error:", error);
1474
+ if (eventSource?.readyState === EventSource.CLOSED) {
1475
+ debugLog("SSE", "Connection closed");
1476
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
1477
+ debugLog("SSE", `Attempting reconnect ${reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS}`);
1478
+ components.forEach(
1479
+ (c) => c.handle(
1480
+ new ErrorEvent({
1481
+ error: new Error(
1482
+ `Connection lost. Attempting to reconnect (${reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})...`
1483
+ )
1484
+ })
1485
+ )
1486
+ );
1487
+ setTimeout(() => {
1488
+ reconnectAttempts++;
1489
+ setupEventSource();
1490
+ }, RECONNECT_DELAY);
1491
+ } else {
1492
+ debugLog("SSE", "Max reconnection attempts reached");
1493
+ components.forEach(
1494
+ (c) => c.handle(new ErrorEvent({ error: new Error("Connection lost permanently. Please refresh the page.") }))
1495
+ );
1496
+ }
1497
+ }
1498
+ };
1499
+ }
1500
+ var optionsButton = document.getElementById("options-button");
1501
+ var optionsPanel = document.getElementById("options-panel");
1502
+ var enterToSendToggle = document.getElementById("enter-to-send-toggle");
1503
+ var themeLight = document.getElementById("theme-light");
1504
+ var themeDark = document.getElementById("theme-dark");
1505
+ var themeSystem = document.getElementById("theme-system");
1506
+ var useEnterToSend = getPreference("useEnterToSend", false);
1507
+ enterToSendToggle.checked = useEnterToSend !== void 0 ? useEnterToSend : false;
1508
+ function applyTheme() {
1509
+ const savedTheme = getPreference("theme", "light");
1510
+ themeLight.checked = savedTheme === "light";
1511
+ themeDark.checked = savedTheme === "dark";
1512
+ themeSystem.checked = savedTheme === "system";
1513
+ if (savedTheme === "system") {
1514
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
1515
+ if (prefersDark) {
1516
+ document.documentElement.setAttribute("data-theme", "dark");
1517
+ } else {
1518
+ document.documentElement.removeAttribute("data-theme");
1519
+ }
1520
+ } else if (savedTheme === "dark") {
1521
+ document.documentElement.setAttribute("data-theme", "dark");
1522
+ } else {
1523
+ document.documentElement.removeAttribute("data-theme");
1524
+ }
1525
+ }
1526
+ applyTheme();
1527
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
1528
+ const savedTheme = getPreference("theme", "light");
1529
+ if (savedTheme === "system") {
1530
+ if (e.matches) {
1531
+ document.documentElement.setAttribute("data-theme", "dark");
1532
+ } else {
1533
+ document.documentElement.removeAttribute("data-theme");
1534
+ }
1535
+ }
1536
+ });
1537
+ optionsButton.addEventListener("click", (event) => {
1538
+ event.stopPropagation();
1539
+ const isVisible = optionsPanel.style.display === "block";
1540
+ optionsPanel.style.display = isVisible ? "none" : "flex";
1541
+ });
1542
+ document.addEventListener("click", (event) => {
1543
+ if (!optionsPanel.contains(event.target) && event.target !== optionsButton) {
1544
+ optionsPanel.style.display = "none";
1545
+ }
1546
+ });
1547
+ enterToSendToggle.addEventListener("change", () => {
1548
+ setPreference("useEnterToSend", enterToSendToggle.checked);
1549
+ });
1550
+ document.querySelectorAll('input[name="theme"]').forEach((input) => {
1551
+ input.addEventListener("change", (e) => {
1552
+ const target = e.target;
1553
+ if (target.checked) {
1554
+ setPreference("theme", target.value);
1555
+ applyTheme();
1556
+ }
1557
+ });
1558
+ });
1559
+ setupEventSource();
1560
+ //# sourceMappingURL=app.js.map