@whoz-oss/coday-web 0.13.3 → 0.15.0

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 CHANGED
@@ -114,7 +114,7 @@ var ToolRequestEvent = class _ToolRequestEvent extends CodayEvent {
114
114
  this.length = this.args.length + this.name.length + this.toolRequestId.length + 20;
115
115
  }
116
116
  buildResponse(output) {
117
- return new ToolResponseEvent({ output, toolRequestId: this.toolRequestId });
117
+ return new ToolResponseEvent({ output, toolRequestId: this.toolRequestId, parentKey: this.timestamp });
118
118
  }
119
119
  /**
120
120
  * Renders the tool request as a single line string with truncation
@@ -134,7 +134,33 @@ var ToolResponseEvent = class _ToolResponseEvent extends CodayEvent {
134
134
  super(event, _ToolResponseEvent.type);
135
135
  this.toolRequestId = event.toolRequestId || this.timestamp || (/* @__PURE__ */ new Date()).toISOString();
136
136
  this.output = event.output;
137
- this.length = this.output.length + this.toolRequestId.length + 20;
137
+ if (typeof this.output === "string") {
138
+ this.length = this.output.length + this.toolRequestId.length + 20;
139
+ } else {
140
+ if (this.output.type === "text") {
141
+ this.length = this.output.content.length + this.toolRequestId.length + 20;
142
+ } else if (this.output.type === "image") {
143
+ const tokens = (this.output.width ?? 0) * (this.output.height ?? 0) / 750;
144
+ this.length = (tokens ? tokens * 3.5 : this.output.content.length) + this.toolRequestId.length + 20;
145
+ } else {
146
+ this.length = this.toolRequestId.length + 20;
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Get the text content as a string for backward compatibility
152
+ */
153
+ getTextOutput() {
154
+ if (typeof this.output === "string") {
155
+ return this.output;
156
+ }
157
+ if (this.output.type === "text") {
158
+ return this.output.content;
159
+ }
160
+ if (this.output.type === "image") {
161
+ return `[Image: ${this.output.mimeType}]`;
162
+ }
163
+ return "";
138
164
  }
139
165
  /**
140
166
  * Renders the tool response as a single line string with truncation
@@ -142,8 +168,10 @@ var ToolResponseEvent = class _ToolResponseEvent extends CodayEvent {
142
168
  * @returns A formatted string representation
143
169
  */
144
170
  toSingleLineString(maxLength = 50) {
145
- const truncatedOutput = truncateText(this.output, maxLength);
146
- return `\u2B91 ${truncatedOutput}`;
171
+ const textOutput = this.getTextOutput();
172
+ const truncatedOutput = truncateText(textOutput, maxLength);
173
+ const imageIndicator = typeof this.output !== "string" && this.output.type === "image" ? " [image]" : "";
174
+ return `\u2B91 ${truncatedOutput}${imageIndicator}`;
147
175
  }
148
176
  };
149
177
  var ProjectSelectedEvent = class _ProjectSelectedEvent extends CodayEvent {
@@ -171,7 +199,19 @@ var MessageEvent = class _MessageEvent extends CodayEvent {
171
199
  this.role = event.role;
172
200
  this.name = event.name;
173
201
  this.content = event.content;
174
- this.length = this.content.length + this.role.length + this.name.length + 20;
202
+ this.length = this.content.map((content) => {
203
+ if (content.type === "text") {
204
+ return content.content.length;
205
+ }
206
+ if (content.type === "image") {
207
+ const tokens = (content.width || 0) * (content.height || 0) / 750;
208
+ return tokens ? tokens * 3.5 : content.content.length;
209
+ }
210
+ return 0;
211
+ }).reduce((sum, length) => sum + length, 0);
212
+ }
213
+ getTextContent() {
214
+ return this.content.filter((c) => c.type === "text").map((c) => c.content).join("\n");
175
215
  }
176
216
  };
177
217
  var eventTypeToClassMap = {
@@ -222,7 +262,7 @@ var SpeechToTextareaComponent = class {
222
262
  this.chatTextarea = chatTextarea;
223
263
  this.submitButton = submitButton;
224
264
  this.initializeVoiceInput();
225
- window.addEventListener("voiceLanguageChanged", (event) => {
265
+ window.addEventListener("voiceLanguageChanged", (_event) => {
226
266
  this.updateRecognitionLanguage();
227
267
  });
228
268
  }
@@ -719,6 +759,71 @@ var ChoiceSelectComponent = class {
719
759
  }
720
760
  };
721
761
 
762
+ // apps/web/client/image-upload/image-upload-handler.ts
763
+ var ImageUploadHandler = class {
764
+ constructor(clientId2) {
765
+ this.clientId = clientId2;
766
+ }
767
+ maxFileSize = 5 * 1024 * 1024;
768
+ // 5MB
769
+ supportedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
770
+ /**
771
+ * Validates an image file
772
+ */
773
+ validateFile(file) {
774
+ if (!this.supportedTypes.includes(file.type)) {
775
+ throw new Error(`Unsupported file type: ${file.type}`);
776
+ }
777
+ if (file.size > this.maxFileSize) {
778
+ throw new Error(`File too large: ${(file.size / 1024 / 1024).toFixed(1)}MB exceeds 5MB limit`);
779
+ }
780
+ }
781
+ /**
782
+ * Converts a File to base64 string
783
+ */
784
+ async fileToBase64(file) {
785
+ return new Promise((resolve, reject) => {
786
+ const reader = new FileReader();
787
+ reader.onload = () => {
788
+ const result = reader.result;
789
+ if (!result) {
790
+ reject(new Error("Failed to read file"));
791
+ return;
792
+ }
793
+ const base64 = result.split(",")[1];
794
+ if (!base64) {
795
+ reject(new Error("Invalid file format"));
796
+ return;
797
+ }
798
+ resolve(base64);
799
+ };
800
+ reader.onerror = reject;
801
+ reader.readAsDataURL(file);
802
+ });
803
+ }
804
+ /**
805
+ * Uploads an image file to the server
806
+ */
807
+ async uploadImage(file) {
808
+ this.validateFile(file);
809
+ const content = await this.fileToBase64(file);
810
+ const response = await fetch(`/api/files/upload`, {
811
+ method: "POST",
812
+ headers: { "Content-Type": "application/json" },
813
+ body: JSON.stringify({
814
+ clientId: this.clientId,
815
+ content,
816
+ mimeType: file.type,
817
+ filename: file.name
818
+ })
819
+ });
820
+ if (!response.ok) {
821
+ const error = await response.json();
822
+ throw new Error(error.error || "Upload failed");
823
+ }
824
+ }
825
+ };
826
+
722
827
  // apps/web/client/chat-history/chat-history.component.ts
723
828
  var PARAGRAPH_MIN_LENGTH = 80;
724
829
  var MAX_PARAGRAPHS = 3;
@@ -737,6 +842,8 @@ var ChatHistoryComponent = class {
737
842
  window.addEventListener("voiceReadFullTextChanged", (event) => {
738
843
  this.readFullText = event.detail;
739
844
  });
845
+ this.imageUploadHandler = new ImageUploadHandler(this.getClientId());
846
+ this.setupDragAndDrop();
740
847
  setInterval(() => {
741
848
  this.checkStateConsistency();
742
849
  }, 1e3);
@@ -749,9 +856,18 @@ var ChatHistoryComponent = class {
749
856
  onStopCallback;
750
857
  readFullText = false;
751
858
  currentPlayingButton = null;
859
+ imageUploadHandler;
752
860
  handle(event) {
753
861
  this.history.set(event.timestamp, event);
754
- if (event instanceof TextEvent) {
862
+ if (event instanceof MessageEvent) {
863
+ if (event.role === "user") {
864
+ this.addUserMessage(event);
865
+ } else {
866
+ this.voiceSynthesis.stopSpeech();
867
+ this.resetAllPlayButtons();
868
+ this.addAssistantMessage(event);
869
+ }
870
+ } else if (event instanceof TextEvent) {
755
871
  if (event.speaker) {
756
872
  this.voiceSynthesis.stopSpeech();
757
873
  this.resetAllPlayButtons();
@@ -934,6 +1050,131 @@ var ChatHistoryComponent = class {
934
1050
  this.resetAllPlayButtons();
935
1051
  }
936
1052
  }
1053
+ addUserMessage(event) {
1054
+ const newEntry = this.createRichMessageElement(event);
1055
+ newEntry.classList.add("text", "right");
1056
+ const buttonContainer = document.createElement("div");
1057
+ buttonContainer.classList.add("message-button-container");
1058
+ const textContent = this.extractTextContent(event);
1059
+ if (textContent) {
1060
+ const playButton = this.createPlayButton(textContent);
1061
+ buttonContainer.appendChild(playButton);
1062
+ }
1063
+ const copyButton = document.createElement("button");
1064
+ copyButton.classList.add("copy-button");
1065
+ copyButton.title = "Copy raw message";
1066
+ copyButton.textContent = "\u{1F4CB}";
1067
+ copyButton.addEventListener("click", (event2) => {
1068
+ event2.stopPropagation();
1069
+ this.copyToClipboard(textContent);
1070
+ const clickedButton = event2.currentTarget;
1071
+ if (clickedButton) {
1072
+ document.querySelectorAll(".copy-button.active").forEach((btn) => {
1073
+ btn.classList.remove("active");
1074
+ btn.textContent = "\u{1F4CB}";
1075
+ });
1076
+ clickedButton.classList.add("active");
1077
+ clickedButton.textContent = "\u2713";
1078
+ setTimeout(() => {
1079
+ clickedButton.classList.remove("active");
1080
+ clickedButton.textContent = "\u{1F4CB}";
1081
+ }, 2e3);
1082
+ }
1083
+ });
1084
+ buttonContainer.appendChild(copyButton);
1085
+ newEntry.appendChild(buttonContainer);
1086
+ this.appendMessageElement(newEntry);
1087
+ }
1088
+ addAssistantMessage(event) {
1089
+ const newEntry = this.createRichMessageElement(event);
1090
+ newEntry.classList.add("text", "left");
1091
+ newEntry.addEventListener("click", () => {
1092
+ this.voiceSynthesis.stopSpeech();
1093
+ });
1094
+ const buttonContainer = document.createElement("div");
1095
+ buttonContainer.classList.add("message-button-container");
1096
+ const textContent = this.extractTextContent(event);
1097
+ if (textContent) {
1098
+ const playButton = this.createPlayButton(textContent);
1099
+ buttonContainer.appendChild(playButton);
1100
+ }
1101
+ const copyButton = document.createElement("button");
1102
+ copyButton.classList.add("copy-button");
1103
+ copyButton.title = "Copy raw response";
1104
+ copyButton.textContent = "\u{1F4CB}";
1105
+ copyButton.addEventListener("click", (event2) => {
1106
+ event2.stopPropagation();
1107
+ this.copyToClipboard(textContent);
1108
+ const clickedButton = event2.currentTarget;
1109
+ if (clickedButton) {
1110
+ document.querySelectorAll(".copy-button.active").forEach((btn) => {
1111
+ btn.classList.remove("active");
1112
+ btn.textContent = "\u{1F4CB}";
1113
+ });
1114
+ clickedButton.classList.add("active");
1115
+ clickedButton.textContent = "\u2713";
1116
+ setTimeout(() => {
1117
+ clickedButton.classList.remove("active");
1118
+ clickedButton.textContent = "\u{1F4CB}";
1119
+ }, 2e3);
1120
+ }
1121
+ });
1122
+ buttonContainer.appendChild(copyButton);
1123
+ newEntry.appendChild(buttonContainer);
1124
+ this.appendMessageElement(newEntry);
1125
+ const audioEnabled = getPreference("voiceAnnounceEnabled", false) || false;
1126
+ if (audioEnabled && this.isMessageRecentEnoughForAnnouncement(event.timestamp)) {
1127
+ this.announceText(textContent);
1128
+ }
1129
+ }
1130
+ createRichMessageElement(event) {
1131
+ const newEntry = document.createElement("div");
1132
+ newEntry.classList.add("message");
1133
+ const speakerElement = document.createElement("div");
1134
+ speakerElement.classList.add("speaker");
1135
+ speakerElement.textContent = event.name;
1136
+ newEntry.appendChild(speakerElement);
1137
+ const contentContainer = document.createElement("div");
1138
+ contentContainer.classList.add("message-content");
1139
+ event.content.forEach((content) => {
1140
+ if (content.type === "text") {
1141
+ const textDiv = document.createElement("div");
1142
+ textDiv.classList.add("text-part");
1143
+ const parsed = marked.parse(content.content);
1144
+ if (parsed instanceof Promise) {
1145
+ parsed.then((html) => {
1146
+ textDiv.innerHTML = html;
1147
+ });
1148
+ } else {
1149
+ textDiv.innerHTML = parsed;
1150
+ }
1151
+ contentContainer.appendChild(textDiv);
1152
+ } else if (content.type === "image") {
1153
+ const img = document.createElement("img");
1154
+ img.src = `data:${content.mimeType};base64,${content.content}`;
1155
+ img.alt = content.source || "Image";
1156
+ img.classList.add("message-image");
1157
+ img.style.maxWidth = "100%";
1158
+ img.style.height = "auto";
1159
+ img.style.margin = "8px 0";
1160
+ img.style.borderRadius = "4px";
1161
+ img.style.cursor = "pointer";
1162
+ img.addEventListener("click", (e) => {
1163
+ e.stopPropagation();
1164
+ window.open(img.src, "_blank");
1165
+ });
1166
+ contentContainer.appendChild(img);
1167
+ }
1168
+ });
1169
+ newEntry.appendChild(contentContainer);
1170
+ return newEntry;
1171
+ }
1172
+ /**
1173
+ * Extract all text content from a MessageEvent for voice synthesis and copying
1174
+ */
1175
+ extractTextContent(event) {
1176
+ return event.content.filter((content) => content.type === "text").map((content) => content.content).join("\n\n");
1177
+ }
937
1178
  createMessageElement(content, speaker) {
938
1179
  const newEntry = document.createElement("div");
939
1180
  newEntry.classList.add("message");
@@ -1032,6 +1273,75 @@ var ChatHistoryComponent = class {
1032
1273
  return true;
1033
1274
  }
1034
1275
  }
1276
+ setupDragAndDrop() {
1277
+ this.chatHistory.addEventListener("dragenter", this.handleDragEnter.bind(this));
1278
+ this.chatHistory.addEventListener("dragover", this.handleDragOver.bind(this));
1279
+ this.chatHistory.addEventListener("dragleave", this.handleDragLeave.bind(this));
1280
+ this.chatHistory.addEventListener("drop", this.handleDrop.bind(this));
1281
+ }
1282
+ handleDragEnter(e) {
1283
+ e.preventDefault();
1284
+ if (this.hasImageFiles(e.dataTransfer)) {
1285
+ this.chatHistory.classList.add("drag-over");
1286
+ }
1287
+ }
1288
+ handleDragOver(e) {
1289
+ e.preventDefault();
1290
+ if (this.hasImageFiles(e.dataTransfer)) {
1291
+ e.dataTransfer.dropEffect = "copy";
1292
+ }
1293
+ }
1294
+ handleDragLeave(e) {
1295
+ if (e.target === this.chatHistory) {
1296
+ this.chatHistory.classList.remove("drag-over");
1297
+ }
1298
+ }
1299
+ async handleDrop(e) {
1300
+ e.preventDefault();
1301
+ this.chatHistory.classList.remove("drag-over");
1302
+ const files = Array.from(e.dataTransfer?.files || []);
1303
+ const imageFiles = files.filter((f) => f.type.startsWith("image/"));
1304
+ for (const file of imageFiles) {
1305
+ try {
1306
+ this.showUploadStatus(`Uploading ${file.name}...`);
1307
+ await this.imageUploadHandler.uploadImage(file);
1308
+ this.hideUploadStatus();
1309
+ } catch (error) {
1310
+ console.error("Upload error:", error);
1311
+ this.showUploadError(`Failed to upload ${file.name}: ${error.message}`);
1312
+ }
1313
+ }
1314
+ }
1315
+ hasImageFiles(dataTransfer) {
1316
+ if (!dataTransfer) return false;
1317
+ return Array.from(dataTransfer.types).includes("Files");
1318
+ }
1319
+ showUploadStatus(message) {
1320
+ this.hideUploadStatus();
1321
+ const statusDiv = document.createElement("div");
1322
+ statusDiv.classList.add("upload-status");
1323
+ statusDiv.textContent = message;
1324
+ statusDiv.id = "upload-status";
1325
+ this.chatHistory.appendChild(statusDiv);
1326
+ this.scrollToBottom();
1327
+ }
1328
+ hideUploadStatus() {
1329
+ const existing = document.getElementById("upload-status");
1330
+ if (existing) {
1331
+ existing.remove();
1332
+ }
1333
+ }
1334
+ showUploadError(message) {
1335
+ this.hideUploadStatus();
1336
+ const errorDiv = document.createElement("div");
1337
+ errorDiv.classList.add("upload-status", "error");
1338
+ errorDiv.textContent = message;
1339
+ this.chatHistory.appendChild(errorDiv);
1340
+ this.scrollToBottom();
1341
+ setTimeout(() => {
1342
+ errorDiv.remove();
1343
+ }, 5e3);
1344
+ }
1035
1345
  announceText(text) {
1036
1346
  const mode = getPreference("voiceMode", "speech") || "speech";
1037
1347
  if (mode === "notification") {
@@ -1142,7 +1452,7 @@ var VoiceSynthesisComponent = class {
1142
1452
  }
1143
1453
  }
1144
1454
  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, '"');
1455
+ 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
1456
  processed = this.addNaturalPunctuation(processed);
1147
1457
  return processed.replace(/\s+/g, " ").trim();
1148
1458
  }
@@ -1234,6 +1544,9 @@ var VoiceSynthesisComponent = class {
1234
1544
  this.stopSpeech();
1235
1545
  const langCode = this.selectedVoice.lang.slice(0, 2);
1236
1546
  const testText = TestTexts[langCode] || TestTexts["en"];
1547
+ if (!testText) {
1548
+ return;
1549
+ }
1237
1550
  this.speak(testText);
1238
1551
  }, 100);
1239
1552
  }