@copilotz/chat-ui 0.9.15 → 0.9.17

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/index.cjs CHANGED
@@ -74,6 +74,9 @@ var defaultChatConfig = {
74
74
  attachFiles: "Attach Files",
75
75
  attachFileTooltip: "Attach file",
76
76
  fileTooLarge: "File too large. Max allowed: {{maxSize}}MB",
77
+ fileProcessError: "Failed to process file",
78
+ dropFiles: "Drop files to attach",
79
+ dropFilesHelp: "Up to {{remaining}} more files",
77
80
  attachmentsCount: "{{count}}/{{max}} attachments",
78
81
  voiceEnter: "Voice input",
79
82
  voiceExit: "Use keyboard",
@@ -4323,6 +4326,9 @@ function formatLabel(template, fallback, values) {
4323
4326
  (_, key) => String(values[key] ?? "")
4324
4327
  );
4325
4328
  }
4329
+ function hasDraggedFiles(dataTransfer) {
4330
+ return Array.from(dataTransfer.types || []).includes("Files");
4331
+ }
4326
4332
  function resolveTargetFromMentions(value, agents) {
4327
4333
  const matches = value.matchAll(/(^|\s)@([\w.-]+)/g);
4328
4334
  for (const match of matches) {
@@ -4586,8 +4592,10 @@ var ChatInput = (0, import_react8.memo)(function ChatInput2({
4586
4592
  const [voiceError, setVoiceError] = (0, import_react8.useState)(null);
4587
4593
  const [activeMention, setActiveMention] = (0, import_react8.useState)(null);
4588
4594
  const [activeMentionIndex, setActiveMentionIndex] = (0, import_react8.useState)(0);
4595
+ const [isDraggingFiles, setIsDraggingFiles] = (0, import_react8.useState)(false);
4589
4596
  const textareaRef = (0, import_react8.useRef)(null);
4590
4597
  const fileInputRef = (0, import_react8.useRef)(null);
4598
+ const dragDepthRef = (0, import_react8.useRef)(0);
4591
4599
  const voiceProviderRef = (0, import_react8.useRef)(null);
4592
4600
  const voiceDraftRef = (0, import_react8.useRef)(null);
4593
4601
  const voiceAppendBaseRef = (0, import_react8.useRef)(null);
@@ -4713,7 +4721,7 @@ var ChatInput = (0, import_react8.memo)(function ChatInput2({
4713
4721
  handleSubmit(e);
4714
4722
  }
4715
4723
  };
4716
- const processFile = async (file) => {
4724
+ const processFile = (0, import_react8.useCallback)(async (file) => {
4717
4725
  if (file.size > maxFileSize) {
4718
4726
  alert(formatLabel(
4719
4727
  config?.labels?.fileTooLarge,
@@ -4778,39 +4786,63 @@ var ChatInput = (0, import_react8.memo)(function ChatInput2({
4778
4786
  newMap.delete(fileId);
4779
4787
  return newMap;
4780
4788
  });
4781
- alert("Failed to process file");
4789
+ alert(config?.labels?.fileProcessError || "Failed to process file");
4782
4790
  return null;
4783
4791
  }
4784
- };
4785
- const handleFileSelect = async (e) => {
4786
- const files = e.target.files;
4787
- if (!files) return;
4792
+ }, [config?.labels?.fileProcessError, config?.labels?.fileTooLarge, maxFileSize, setContext]);
4793
+ const processFiles = (0, import_react8.useCallback)(async (files) => {
4794
+ if (!enableFileUpload || disabled || files.length === 0) return;
4788
4795
  const remainingSlots = maxAttachments - attachments.length;
4789
- const filesToProcess = Array.from(files).slice(0, remainingSlots);
4796
+ if (remainingSlots <= 0) return;
4797
+ const filesToProcess = files.slice(0, remainingSlots);
4798
+ const nextAttachments = [];
4790
4799
  for (const file of filesToProcess) {
4791
4800
  const attachment = await processFile(file);
4792
4801
  if (attachment) {
4793
- onAttachmentsChange([...attachments, attachment]);
4802
+ nextAttachments.push(attachment);
4794
4803
  }
4795
4804
  }
4805
+ if (nextAttachments.length > 0) {
4806
+ onAttachmentsChange([...attachments, ...nextAttachments]);
4807
+ }
4808
+ }, [attachments, disabled, enableFileUpload, maxAttachments, onAttachmentsChange, processFile]);
4809
+ const handleFileSelect = async (e) => {
4810
+ const files = e.target.files;
4811
+ if (!files) return;
4812
+ await processFiles(Array.from(files));
4796
4813
  e.target.value = "";
4797
4814
  };
4798
4815
  const handleDrop = (0, import_react8.useCallback)(async (e) => {
4816
+ if (!enableFileUpload || !hasDraggedFiles(e.dataTransfer)) return;
4799
4817
  e.preventDefault();
4800
- if (!enableFileUpload) return;
4801
- const files = Array.from(e.dataTransfer.files);
4802
- const remainingSlots = maxAttachments - attachments.length;
4803
- const filesToProcess = files.slice(0, remainingSlots);
4804
- for (const file of filesToProcess) {
4805
- const attachment = await processFile(file);
4806
- if (attachment) {
4807
- onAttachmentsChange([...attachments, attachment]);
4808
- }
4818
+ dragDepthRef.current = 0;
4819
+ setIsDraggingFiles(false);
4820
+ await processFiles(Array.from(e.dataTransfer.files));
4821
+ }, [enableFileUpload, processFiles]);
4822
+ const handleDragEnter = (0, import_react8.useCallback)((e) => {
4823
+ if (!enableFileUpload || !hasDraggedFiles(e.dataTransfer)) return;
4824
+ e.preventDefault();
4825
+ dragDepthRef.current += 1;
4826
+ if (attachments.length < maxAttachments) {
4827
+ setIsDraggingFiles(true);
4809
4828
  }
4810
- }, [attachments, enableFileUpload, maxAttachments, onAttachmentsChange]);
4829
+ }, [attachments.length, enableFileUpload, maxAttachments]);
4811
4830
  const handleDragOver = (0, import_react8.useCallback)((e) => {
4831
+ if (!enableFileUpload || !hasDraggedFiles(e.dataTransfer)) return;
4812
4832
  e.preventDefault();
4813
- }, []);
4833
+ e.dataTransfer.dropEffect = attachments.length < maxAttachments ? "copy" : "none";
4834
+ if (attachments.length < maxAttachments) {
4835
+ setIsDraggingFiles(true);
4836
+ }
4837
+ }, [attachments.length, enableFileUpload, maxAttachments]);
4838
+ const handleDragLeave = (0, import_react8.useCallback)((e) => {
4839
+ if (!enableFileUpload || !hasDraggedFiles(e.dataTransfer)) return;
4840
+ e.preventDefault();
4841
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
4842
+ if (dragDepthRef.current === 0) {
4843
+ setIsDraggingFiles(false);
4844
+ }
4845
+ }, [enableFileUpload]);
4814
4846
  const resetVoiceComposerState = (0, import_react8.useCallback)((nextState = "idle") => {
4815
4847
  setVoiceState(nextState);
4816
4848
  setVoiceDraft(null);
@@ -5163,10 +5195,21 @@ var ChatInput = (0, import_react8.memo)(function ChatInput2({
5163
5195
  ) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("form", { onSubmit: handleSubmit, className: "mb-1", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
5164
5196
  "div",
5165
5197
  {
5166
- className: "group/composer flex w-full flex-col gap-2 rounded-3xl border border-border/80 bg-card/95 p-2.5 shadow-sm transition-[border-color,box-shadow,background-color] focus-within:border-ring/60 focus-within:bg-card focus-within:shadow-md focus-within:ring-2 focus-within:ring-ring/15",
5198
+ className: `group/composer relative flex w-full flex-col gap-2 overflow-visible rounded-3xl border border-border/80 bg-card/95 p-2.5 shadow-sm transition-[border-color,box-shadow,background-color] focus-within:border-ring/60 focus-within:bg-card focus-within:shadow-md focus-within:ring-2 focus-within:ring-ring/15 ${isDraggingFiles ? "border-primary/60 bg-primary/5 shadow-md ring-2 ring-primary/15" : ""}`,
5167
5199
  onDrop: handleDrop,
5200
+ onDragEnter: handleDragEnter,
5168
5201
  onDragOver: handleDragOver,
5202
+ onDragLeave: handleDragLeave,
5169
5203
  children: [
5204
+ isDraggingFiles && enableFileUpload && canAddMoreAttachments && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "pointer-events-none absolute inset-0 z-20 flex items-center justify-center rounded-3xl border-2 border-dashed border-primary/60 bg-primary/10 text-primary backdrop-blur-[1px]", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex flex-col items-center gap-2 px-4 text-center", children: [
5205
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "flex h-11 w-11 items-center justify-center rounded-full bg-primary/15", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react12.UploadCloud, { className: "h-5 w-5" }) }),
5206
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-sm font-semibold", children: config?.labels?.dropFiles || "Drop files to attach" }),
5207
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-xs text-muted-foreground", children: formatLabel(
5208
+ config?.labels?.dropFilesHelp,
5209
+ "Up to {{remaining}} more files",
5210
+ { remaining: Math.max(0, maxAttachments - attachments.length) }
5211
+ ) })
5212
+ ] }) }),
5170
5213
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "relative min-w-0", children: [
5171
5214
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5172
5215
  Textarea,
@@ -5193,7 +5236,7 @@ var ChatInput = (0, import_react8.memo)(function ChatInput2({
5193
5236
  rows: 1
5194
5237
  }
5195
5238
  ),
5196
- isMentionMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "absolute bottom-full left-0 right-0 mb-2 overflow-hidden rounded-md border bg-popover shadow-md", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "p-1", children: filteredMentionAgents.map((agent, index) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
5239
+ isMentionMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "absolute bottom-full left-0 right-0 z-30 mb-2 overflow-hidden rounded-md border bg-popover shadow-md", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "p-1", children: filteredMentionAgents.map((agent, index) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
5197
5240
  "button",
5198
5241
  {
5199
5242
  type: "button",