clay-server 2.5.0 → 2.6.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.
@@ -6,6 +6,8 @@ var ctx;
6
6
  // --- State ---
7
7
  var pendingImages = []; // [{data: base64, mediaType: "image/png"}]
8
8
  var pendingPastes = []; // [{text: string, preview: string}]
9
+ var pendingFiles = []; // [{name: string, path: string}]
10
+ var uploadingCount = 0;
9
11
  var slashActiveIdx = -1;
10
12
  var slashFiltered = [];
11
13
  var isComposing = false;
@@ -23,7 +25,8 @@ export var builtinCommands = [
23
25
  export function sendMessage() {
24
26
  var text = ctx.inputEl.value.trim();
25
27
  var images = pendingImages.slice();
26
- if (!text && images.length === 0 && pendingPastes.length === 0) return;
28
+ if (!text && images.length === 0 && pendingPastes.length === 0 && pendingFiles.length === 0) return;
29
+ if (uploadingCount > 0) return; // wait for uploads to finish
27
30
  hideSlashMenu();
28
31
  if (ctx.hideSuggestionChips) ctx.hideSuggestionChips();
29
32
 
@@ -78,6 +81,13 @@ export function sendMessage() {
78
81
  return;
79
82
  }
80
83
 
84
+ // Prepend file paths to text
85
+ var files = pendingFiles.slice();
86
+ if (files.length > 0) {
87
+ var filePaths = files.map(function (f) { return "[Uploaded file: " + f.path + "]"; }).join("\n");
88
+ text = text ? filePaths + "\n\n" + text : filePaths;
89
+ }
90
+
81
91
  var pastes = pendingPastes.map(function (p) { return p.text; });
82
92
  ctx.addUserMessage(text, images.length > 0 ? images : null, pastes.length > 0 ? pastes : null);
83
93
 
@@ -183,6 +193,7 @@ function removePendingImage(idx) {
183
193
  export function clearPendingImages() {
184
194
  pendingImages = [];
185
195
  pendingPastes = [];
196
+ pendingFiles = [];
186
197
  renderInputPreviews();
187
198
  }
188
199
 
@@ -191,10 +202,15 @@ function removePendingPaste(idx) {
191
202
  renderInputPreviews();
192
203
  }
193
204
 
205
+ function removePendingFile(idx) {
206
+ pendingFiles.splice(idx, 1);
207
+ renderInputPreviews();
208
+ }
209
+
194
210
  function renderInputPreviews() {
195
211
  var bar = ctx.imagePreviewBar;
196
212
  bar.innerHTML = "";
197
- if (pendingImages.length === 0 && pendingPastes.length === 0) {
213
+ if (pendingImages.length === 0 && pendingPastes.length === 0 && pendingFiles.length === 0 && uploadingCount === 0) {
198
214
  bar.classList.remove("visible");
199
215
  return;
200
216
  }
@@ -222,6 +238,45 @@ function renderInputPreviews() {
222
238
  })(i);
223
239
  }
224
240
 
241
+ // File chips
242
+ for (var fi = 0; fi < pendingFiles.length; fi++) {
243
+ (function (idx) {
244
+ var chip = document.createElement("div");
245
+ chip.className = "file-chip";
246
+ var icon = document.createElement("span");
247
+ icon.className = "file-chip-icon";
248
+ icon.innerHTML = iconHtml("file");
249
+ var nameSpan = document.createElement("span");
250
+ nameSpan.className = "file-chip-name";
251
+ nameSpan.textContent = pendingFiles[idx].name;
252
+ var removeBtn = document.createElement("button");
253
+ removeBtn.className = "file-chip-remove";
254
+ removeBtn.innerHTML = iconHtml("x");
255
+ removeBtn.addEventListener("click", function (e) {
256
+ e.stopPropagation();
257
+ removePendingFile(idx);
258
+ });
259
+ chip.appendChild(icon);
260
+ chip.appendChild(nameSpan);
261
+ chip.appendChild(removeBtn);
262
+ bar.appendChild(chip);
263
+ })(fi);
264
+ }
265
+
266
+ // Uploading indicator
267
+ if (uploadingCount > 0) {
268
+ var chip = document.createElement("div");
269
+ chip.className = "file-chip file-chip-uploading";
270
+ var spinner = document.createElement("span");
271
+ spinner.className = "file-chip-spinner";
272
+ var label = document.createElement("span");
273
+ label.className = "file-chip-name";
274
+ label.textContent = "Uploading" + (uploadingCount > 1 ? " (" + uploadingCount + ")" : "") + "...";
275
+ chip.appendChild(spinner);
276
+ chip.appendChild(label);
277
+ bar.appendChild(chip);
278
+ }
279
+
225
280
  // Pasted content chips
226
281
  for (var j = 0; j < pendingPastes.length; j++) {
227
282
  (function (idx) {
@@ -253,6 +308,46 @@ function renderInputPreviews() {
253
308
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB
254
309
  var RESIZE_MAX_DIM = 1920;
255
310
  var RESIZE_QUALITY = 0.85;
311
+ var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
312
+
313
+ // --- File upload ---
314
+ function uploadFile(file) {
315
+ if (file.size > MAX_UPLOAD_BYTES) {
316
+ if (ctx.addSystemMessage) ctx.addSystemMessage("File too large (max 50MB): " + file.name, true);
317
+ return;
318
+ }
319
+ uploadingCount++;
320
+ renderInputPreviews();
321
+ var reader = new FileReader();
322
+ reader.onload = function (ev) {
323
+ var dataUrl = ev.target.result;
324
+ var commaIdx = dataUrl.indexOf(",");
325
+ var b64 = commaIdx !== -1 ? dataUrl.substring(commaIdx + 1) : "";
326
+
327
+ var xhr = new XMLHttpRequest();
328
+ xhr.open("POST", ctx.basePath + "api/upload");
329
+ xhr.setRequestHeader("Content-Type", "application/json");
330
+ xhr.onload = function () {
331
+ uploadingCount--;
332
+ if (xhr.status === 200) {
333
+ try {
334
+ var resp = JSON.parse(xhr.responseText);
335
+ pendingFiles.push({ name: resp.name || file.name, path: resp.path });
336
+ } catch (e) {}
337
+ } else {
338
+ if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + file.name, true);
339
+ }
340
+ renderInputPreviews();
341
+ };
342
+ xhr.onerror = function () {
343
+ uploadingCount--;
344
+ if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + file.name, true);
345
+ renderInputPreviews();
346
+ };
347
+ xhr.send(JSON.stringify({ name: file.name, data: b64 }));
348
+ };
349
+ reader.readAsDataURL(file);
350
+ }
256
351
 
257
352
  function readImageBlob(blob) {
258
353
  var reader = new FileReader();
@@ -356,26 +451,10 @@ export function handleInputSync(text) {
356
451
  isRemoteInput = false;
357
452
  }
358
453
 
359
- // --- Attach menu ---
360
- var attachMenuOpen = false;
361
-
362
- function toggleAttachMenu() {
363
- var menu = document.getElementById("attach-menu");
364
- if (!menu) return;
365
- attachMenuOpen = !attachMenuOpen;
366
- menu.classList.toggle("hidden", !attachMenuOpen);
367
- }
368
-
369
- function closeAttachMenu() {
370
- var menu = document.getElementById("attach-menu");
371
- if (menu) menu.classList.add("hidden");
372
- attachMenuOpen = false;
373
- }
374
-
375
454
  function createFileInput(accept, capture, multiple) {
376
455
  var input = document.createElement("input");
377
456
  input.type = "file";
378
- input.accept = accept;
457
+ if (accept) input.accept = accept;
379
458
  if (capture) input.setAttribute("capture", capture);
380
459
  if (multiple) input.multiple = true;
381
460
  input.style.display = "none";
@@ -386,6 +465,8 @@ function createFileInput(accept, capture, multiple) {
386
465
  for (var i = 0; i < input.files.length; i++) {
387
466
  if (input.files[i].type.indexOf("image/") === 0) {
388
467
  readImageBlob(input.files[i]);
468
+ } else {
469
+ uploadFile(input.files[i]);
389
470
  }
390
471
  }
391
472
  }
@@ -399,47 +480,24 @@ function createFileInput(accept, capture, multiple) {
399
480
  export function initInput(_ctx) {
400
481
  ctx = _ctx;
401
482
 
402
- // Attach button
403
- var isTouchDevice = "ontouchstart" in window;
404
- var attachBtn = document.getElementById("attach-btn");
405
- if (attachBtn) {
406
- attachBtn.addEventListener("click", function (e) {
483
+ // File (clip) button — opens file picker for all types
484
+ var attachFileBtn = document.getElementById("attach-file-btn");
485
+ if (attachFileBtn) {
486
+ attachFileBtn.addEventListener("click", function (e) {
407
487
  e.stopPropagation();
408
- // Desktop: skip menu, open file picker directly
409
- if (!isTouchDevice) {
410
- createFileInput("image/*", null, true);
411
- return;
412
- }
413
- toggleAttachMenu();
488
+ createFileInput(null, null, true);
414
489
  });
415
490
  }
416
491
 
417
- var cameraBtn = document.getElementById("attach-camera");
418
- if (cameraBtn) {
419
- cameraBtn.addEventListener("click", function () {
420
- closeAttachMenu();
421
- createFileInput("image/*", "environment");
422
- });
423
- }
424
-
425
- var photosBtn = document.getElementById("attach-photos");
426
- if (photosBtn) {
427
- photosBtn.addEventListener("click", function () {
428
- closeAttachMenu();
492
+ // Image button — opens image picker (OS handles camera/gallery choice)
493
+ var attachImageBtn = document.getElementById("attach-image-btn");
494
+ if (attachImageBtn) {
495
+ attachImageBtn.addEventListener("click", function (e) {
496
+ e.stopPropagation();
429
497
  createFileInput("image/*", null, true);
430
498
  });
431
499
  }
432
500
 
433
- // Close attach menu when clicking outside
434
- document.addEventListener("click", function (e) {
435
- if (attachMenuOpen) {
436
- var wrap = document.getElementById("attach-wrap");
437
- if (wrap && !wrap.contains(e.target)) {
438
- closeAttachMenu();
439
- }
440
- }
441
- });
442
-
443
501
  // Paste handler
444
502
  document.addEventListener("paste", function (e) {
445
503
  var cd = e.clipboardData;
@@ -453,6 +511,9 @@ export function initInput(_ctx) {
453
511
  if (cd.files[i].type.indexOf("image/") === 0) {
454
512
  found = true;
455
513
  readImageBlob(cd.files[i]);
514
+ } else if (cd.files[i].name) {
515
+ found = true;
516
+ uploadFile(cd.files[i]);
456
517
  }
457
518
  }
458
519
  }
@@ -466,6 +527,12 @@ export function initInput(_ctx) {
466
527
  found = true;
467
528
  readImageBlob(blob);
468
529
  }
530
+ } else if (cd.items[i].kind === "file") {
531
+ var fileBlob = cd.items[i].getAsFile();
532
+ if (fileBlob && fileBlob.name) {
533
+ found = true;
534
+ uploadFile(fileBlob);
535
+ }
469
536
  }
470
537
  }
471
538
  }
@@ -559,10 +626,6 @@ export function initInput(_ctx) {
559
626
  return;
560
627
  }
561
628
  e.preventDefault();
562
- // If suggestion chips are visible, accept chip instead of sending
563
- if (ctx.acceptSuggestionChip && ctx.acceptSuggestionChip()) {
564
- return;
565
- }
566
629
  sendMessage();
567
630
  }
568
631
  });