@bolloon/bolloon-agent 0.3.1 → 0.3.4

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.
@@ -839,7 +839,7 @@
839
839
  "'": "'"
840
840
  })[c]);
841
841
  }
842
- function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
842
+ function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }, timestamp) {
843
843
  const messagesEl2 = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
844
844
  const messagesContainers2 = ctx.messagesContainers || /* @__PURE__ */ new Map();
845
845
  const currentChannelId2 = ctx.currentChannelId;
@@ -908,9 +908,20 @@
908
908
  return;
909
909
  }
910
910
  const rawContent = segments.filter((s) => s.type === "text" || s.type === "final").map((s) => s.content || "").join("\n");
911
+ let timeLabel = "";
912
+ try {
913
+ if (timestamp !== void 0 && timestamp !== null) {
914
+ const d = timestamp instanceof Date ? timestamp : new Date(timestamp);
915
+ if (!isNaN(d.getTime())) {
916
+ timeLabel = d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
917
+ }
918
+ }
919
+ } catch {
920
+ }
921
+ if (!timeLabel) timeLabel = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
911
922
  const time = document.createElement("div");
912
923
  time.className = "time";
913
- time.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
924
+ time.textContent = timeLabel;
914
925
  if (type === "ai") {
915
926
  div.appendChild(buildMessageActions(div, rawContent, ctx));
916
927
  }
@@ -2210,16 +2221,27 @@
2210
2221
  const session = await res.json();
2211
2222
  const msgs = session.messages || [];
2212
2223
  if (msgs.length > 0) {
2224
+ let lastType = null;
2225
+ let lastContent = null;
2226
+ const dedupedMsgs = msgs.filter((m) => {
2227
+ const same = lastType === m.type && lastContent === m.content;
2228
+ lastType = m.type;
2229
+ lastContent = m.content;
2230
+ return !same;
2231
+ });
2213
2232
  const frag = document.createDocumentFragment();
2214
2233
  const tmpContainer = document.createElement("div");
2215
2234
  tmpContainer.style.display = "none";
2216
- for (const msg of msgs) {
2217
- addMessage2(msg.content, msg.type, false, tmpContainer, msg.metadata?.usedJudgmentIds || []);
2235
+ for (const msg of dedupedMsgs) {
2236
+ addMessage2(msg.content, msg.type, false, tmpContainer, msg.metadata?.usedJudgmentIds || [], msg.timestamp);
2218
2237
  }
2219
2238
  while (tmpContainer.firstChild) {
2220
2239
  frag.appendChild(tmpContainer.firstChild);
2221
2240
  }
2222
2241
  container.appendChild(frag);
2242
+ if (dedupedMsgs.length !== msgs.length) {
2243
+ console.log(`[loadSession] \u53BB\u91CD ${msgs.length - dedupedMsgs.length} \u6761\u76F8\u90BB\u91CD\u590D\u6D88\u606F (${msgs.length} \u2192 ${dedupedMsgs.length})`);
2244
+ }
2223
2245
  } else {
2224
2246
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2225
2247
  }
@@ -2244,9 +2266,21 @@ ${data.error || "channel not found"}`, "error");
2244
2266
  const session = await res.json();
2245
2267
  container.innerHTML = "";
2246
2268
  if (session.messages && session.messages.length > 0) {
2247
- session.messages.forEach((msg) => {
2248
- addMessage2(msg.content, msg.type, false, container, msg.metadata?.usedJudgmentIds || []);
2269
+ const rawMsgs = session.messages;
2270
+ let lastType = null;
2271
+ let lastContent = null;
2272
+ const deduped = rawMsgs.filter((m) => {
2273
+ const same = lastType === m.type && lastContent === m.content;
2274
+ lastType = m.type;
2275
+ lastContent = m.content;
2276
+ return !same;
2249
2277
  });
2278
+ deduped.forEach((msg) => {
2279
+ addMessage2(msg.content, msg.type, false, container, msg.metadata?.usedJudgmentIds || [], msg.timestamp);
2280
+ });
2281
+ if (deduped.length !== rawMsgs.length) {
2282
+ console.log(`[loadSession-v2] \u53BB\u91CD ${rawMsgs.length - deduped.length} \u6761`);
2283
+ }
2250
2284
  } else {
2251
2285
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2252
2286
  }
@@ -2256,8 +2290,8 @@ ${data.error || "channel not found"}`, "error");
2256
2290
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2257
2291
  }
2258
2292
  }
2259
- function addMessage2(content, type, save = true, container, usedJudgmentIds = []) {
2260
- return MR_addMessage(content, type, save, container, usedJudgmentIds, getRendererCtx());
2293
+ function addMessage2(content, type, save = true, container, usedJudgmentIds = [], timestamp = void 0) {
2294
+ return MR_addMessage(content, type, save, container, usedJudgmentIds, getRendererCtx(), timestamp);
2261
2295
  }
2262
2296
  function finalizeTimelineAsMessage2() {
2263
2297
  return MR_finalizeTimelineAsMessage(getRendererCtx());
@@ -2435,13 +2469,13 @@ ${data.error || "channel not found"}`, "error");
2435
2469
  const container = messagesContainers.get(targetChannelId) || messagesEl;
2436
2470
  if (msg.type === "ai") {
2437
2471
  if (!MR_hasStreamingText()) {
2438
- addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || []);
2472
+ addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || [], msg.timestamp);
2439
2473
  } else {
2440
2474
  MR_replaceStreamingText?.(msg.content);
2441
2475
  MR_finalizeTimelineAsMessage(getRendererCtx());
2442
2476
  }
2443
2477
  } else if (msg.type === "user") {
2444
- if (msg.source === "remote" || msg.source === "local") {
2478
+ if (msg.source === "remote") {
2445
2479
  addMessage2(msg.content, "user", true, container);
2446
2480
  }
2447
2481
  }
@@ -2526,22 +2560,19 @@ ${data.error || "channel not found"}`, "error");
2526
2560
  addMessage2(data.content, "user", true, container);
2527
2561
  }
2528
2562
  } else if (data.type === "ai") {
2563
+ const allPreviews = container.querySelectorAll(".message-ai.preview");
2564
+ allPreviews.forEach((el) => el.remove());
2565
+ currentPreviewBubble = null;
2529
2566
  addMessage2(data.content || "", "ai", true, container, lastUsedJudgmentIds || []);
2530
- if (currentPreviewBubble) {
2531
- currentPreviewBubble.remove();
2532
- currentPreviewBubble = null;
2533
- }
2534
2567
  } else if (data.type === "reply-preview") {
2535
2568
  const previewContent = data.content || "";
2536
- if (!currentPreviewBubble) {
2537
- currentPreviewBubble = addMessage2(previewContent, "ai", true, container, []);
2538
- if (currentPreviewBubble) {
2539
- currentPreviewBubble.classList.add("preview");
2540
- }
2541
- } else {
2542
- currentPreviewBubble.replaceWith(addMessage2(previewContent, "ai", true, container, []));
2543
- const msgs = container.querySelectorAll(".message-ai.preview");
2544
- currentPreviewBubble = msgs.length > 0 ? msgs[msgs.length - 1] : null;
2569
+ const oldPreviews = container.querySelectorAll(".message-ai.preview");
2570
+ oldPreviews.forEach((el) => el.remove());
2571
+ addMessage2(previewContent, "ai", false, container, []);
2572
+ const newPreview = container.querySelector(".message-ai:not(.preview):last-of-type") || container.lastElementChild;
2573
+ if (newPreview) {
2574
+ newPreview.classList.add("preview");
2575
+ currentPreviewBubble = newPreview;
2545
2576
  }
2546
2577
  } else if (data.type === "stream") {
2547
2578
  if (false) handleStreamTokenEvent(data);
@@ -2615,6 +2646,10 @@ ${data.error || "channel not found"}`, "error");
2615
2646
  persistLastMessageToServer("user", text);
2616
2647
  const channel = channels.find((c) => c.id === currentChannelId);
2617
2648
  const channelDid = channel?.did || "";
2649
+ const attachmentsForSend = pendingAttachments.slice();
2650
+ pendingAttachments = [];
2651
+ const chipsEl = document.getElementById("input-attachment-chips");
2652
+ if (chipsEl) chipsEl.innerHTML = "";
2618
2653
  console.log("[\u53D1\u9001\u6D88\u606F] \u9891\u9053 DID:", channelDid);
2619
2654
  try {
2620
2655
  const res = await fetch("/message", {
@@ -2623,7 +2658,9 @@ ${data.error || "channel not found"}`, "error");
2623
2658
  body: JSON.stringify({
2624
2659
  text,
2625
2660
  channelId: currentChannelId,
2626
- channelDid
2661
+ channelDid,
2662
+ attachments: attachmentsForSend
2663
+ // 后端解析为 LLM contextHint
2627
2664
  })
2628
2665
  });
2629
2666
  if (!res.ok) {
@@ -2982,11 +3019,103 @@ ${data.error || "channel not found"}`, "error");
2982
3019
  }, true);
2983
3020
  }
2984
3021
  var inputArea = document.querySelector(".input-area");
3022
+ var pendingAttachments = [];
3023
+ function fileToBase64Local(file) {
3024
+ return new Promise((resolve2, reject) => {
3025
+ const r = new FileReader();
3026
+ r.onerror = () => reject(r.error || new Error("FileReader error"));
3027
+ r.onload = () => {
3028
+ const s = String(r.result || "");
3029
+ const idx = s.indexOf(",");
3030
+ resolve2(idx >= 0 ? s.slice(idx + 1) : s);
3031
+ };
3032
+ r.readAsDataURL(file);
3033
+ });
3034
+ }
3035
+ function appendAttachmentChip(name) {
3036
+ let chipsEl = document.getElementById("input-attachment-chips");
3037
+ if (!chipsEl) {
3038
+ chipsEl = document.createElement("div");
3039
+ chipsEl.id = "input-attachment-chips";
3040
+ chipsEl.style.cssText = "padding:8px 12px 4px;display:flex;gap:8px;flex-wrap:wrap;font-size:13px;";
3041
+ if (inputArea && inputArea.parentNode) inputArea.parentNode.insertBefore(chipsEl, inputArea);
3042
+ }
3043
+ const chip = document.createElement("span");
3044
+ chip.className = "attach-chip";
3045
+ chip.style.cssText = "background:linear-gradient(135deg,#dbeafe,#e0e7ff);border:1px solid #6366f1;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;color:#3730a3;font-weight:500;box-shadow:0 1px 3px rgba(99,102,241,0.18);animation:attach-pop-in 0.25s ease-out;";
3046
+ chip.textContent = `\u{1F4CE} ${name}`;
3047
+ chip.title = "\u9644\u4EF6\u5DF2\u52A0\u5165\u672C\u6761\u6D88\u606F";
3048
+ chipsEl.appendChild(chip);
3049
+ return chipsEl;
3050
+ }
3051
+ function ensureFileDropOverlay() {
3052
+ let overlay = document.getElementById("file-drop-overlay");
3053
+ if (overlay) return overlay;
3054
+ overlay = document.createElement("div");
3055
+ overlay.id = "file-drop-overlay";
3056
+ overlay.style.cssText = "position:fixed;inset:0;background:rgba(99,102,241,0.18);backdrop-filter:blur(2px);z-index:9998;display:none;align-items:center;justify-content:center;pointer-events:none;transition:opacity 0.18s;";
3057
+ overlay.innerHTML = `
3058
+ <div style="
3059
+ background:white;
3060
+ border:3px dashed #6366f1;
3061
+ border-radius:18px;
3062
+ padding:48px 64px;
3063
+ box-shadow:0 24px 60px rgba(99,102,241,0.25);
3064
+ color:#4338ca;
3065
+ text-align:center;
3066
+ max-width:520px;
3067
+ animation:attach-pulse 1.4s ease-in-out infinite;
3068
+ ">
3069
+ <div style="font-size:64px;line-height:1;margin-bottom:16px;">\u{1F4E5}</div>
3070
+ <div style="font-size:22px;font-weight:600;margin-bottom:8px;">\u677E\u5F00\u4E0A\u4F20\u5230 Bolloon</div>
3071
+ <div style="font-size:14px;color:#64748b;">\u62D6\u5165\u6587\u4EF6\u7ACB\u5373\u4F5C\u4E3A\u9644\u4EF6\u53D1\u7ED9 AI, \u6700\u5927 10MB/\u6587\u4EF6</div>
3072
+ </div>
3073
+ `;
3074
+ document.body.appendChild(overlay);
3075
+ return overlay;
3076
+ }
3077
+ function ensureAttachStyles() {
3078
+ if (document.getElementById("attach-style-tag")) return;
3079
+ const s = document.createElement("style");
3080
+ s.id = "attach-style-tag";
3081
+ s.textContent = `
3082
+ @keyframes attach-pop-in {
3083
+ 0% { transform: scale(0.6); opacity: 0; }
3084
+ 60% { transform: scale(1.08); opacity: 1; }
3085
+ 100% { transform: scale(1); opacity: 1; }
3086
+ }
3087
+ @keyframes attach-pulse {
3088
+ 0%, 100% { transform: scale(1); box-shadow: 0 24px 60px rgba(99,102,241,0.25); }
3089
+ 50% { transform: scale(1.04); box-shadow: 0 30px 80px rgba(99,102,241,0.4); }
3090
+ }
3091
+ @keyframes attach-success-flash {
3092
+ 0% { background: linear-gradient(135deg,#dcfce7,#bbf7d0); }
3093
+ 100% { background: linear-gradient(135deg,#dbeafe,#e0e7ff); }
3094
+ }
3095
+ .drop-target {
3096
+ outline: 3px dashed #6366f1 !important;
3097
+ outline-offset: 4px !important;
3098
+ background-color: rgba(99,102,241,0.08) !important;
3099
+ }
3100
+ `;
3101
+ document.head.appendChild(s);
3102
+ }
3103
+ ensureAttachStyles();
3104
+ var _dropStyle = document.createElement("style");
3105
+ _dropStyle.textContent = `
3106
+ body.file-drag-active { outline: 4px dashed #6366f1; outline-offset: -8px; }
3107
+ `;
3108
+ document.head.appendChild(_dropStyle);
2985
3109
  if (input && inputArea) {
2986
3110
  const onDragOver = (e) => {
2987
- if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes("application/x-bolloon-judgment")) {
3111
+ if (!e.dataTransfer) return;
3112
+ const types = Array.from(e.dataTransfer.types || []);
3113
+ const hasJudgment = types.includes("application/x-bolloon-judgment");
3114
+ const hasFiles = types.includes("Files");
3115
+ if (hasJudgment || hasFiles) {
2988
3116
  e.preventDefault();
2989
- e.dataTransfer.dropEffect = "copy";
3117
+ e.stopPropagation();
3118
+ e.dataTransfer.dropEffect = hasFiles ? "copy" : "copy";
2990
3119
  inputArea.classList.add("drop-target");
2991
3120
  }
2992
3121
  };
@@ -2995,27 +3124,166 @@ ${data.error || "channel not found"}`, "error");
2995
3124
  inputArea.classList.remove("drop-target");
2996
3125
  }
2997
3126
  };
2998
- const onDrop = (e) => {
3127
+ const onDrop = async (e) => {
2999
3128
  inputArea.classList.remove("drop-target");
3000
- const raw = e.dataTransfer.getData("application/x-bolloon-judgment");
3001
- if (!raw) return;
3002
- e.preventDefault();
3003
- try {
3004
- const { id, decision } = JSON.parse(raw);
3005
- const prefix = input.value.trim() ? input.value.trim() + "\n" : "";
3006
- input.value = `${prefix}\u6309\u6211\u7684\u5224\u65AD #${id?.substring(0, 8) || ""} \u6267\u884C: ${decision}`;
3007
- input.focus();
3008
- input.style.transition = "box-shadow 0.3s";
3009
- input.style.boxShadow = "0 0 0 2px #2563eb";
3010
- setTimeout(() => {
3011
- input.style.boxShadow = "";
3012
- }, 800);
3013
- } catch {
3129
+ if (!e.dataTransfer) return;
3130
+ const types = Array.from(e.dataTransfer.types || []);
3131
+ if (types.includes("application/x-bolloon-judgment")) {
3132
+ const raw = e.dataTransfer.getData("application/x-bolloon-judgment");
3133
+ if (!raw) return;
3134
+ e.preventDefault();
3135
+ try {
3136
+ const { id, decision } = JSON.parse(raw);
3137
+ const prefix = input.value.trim() ? input.value.trim() + "\n" : "";
3138
+ input.value = `${prefix}\u6309\u6211\u7684\u5224\u65AD #${id?.substring(0, 8) || ""} \u6267\u884C: ${decision}`;
3139
+ input.focus();
3140
+ input.style.transition = "box-shadow 0.3s";
3141
+ input.style.boxShadow = "0 0 0 2px #2563eb";
3142
+ setTimeout(() => {
3143
+ input.style.boxShadow = "";
3144
+ }, 800);
3145
+ } catch {
3146
+ }
3147
+ return;
3148
+ }
3149
+ if (types.includes("Files")) {
3150
+ e.preventDefault();
3151
+ const ov = document.getElementById("file-drop-overlay");
3152
+ if (ov) ov.style.display = "none";
3153
+ document.body.classList.remove("file-drag-active");
3154
+ const files = Array.from(e.dataTransfer.files || []);
3155
+ if (files.length === 0) return;
3156
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
3157
+ const fmtSize = (b) => b < 1024 ? `${b}B` : b < 1024 * 1024 ? `${(b / 1024).toFixed(1)}KB` : `${(b / 1024 / 1024).toFixed(2)}MB`;
3158
+ if (typeof showSimpleToast === "function") {
3159
+ showSimpleToast(`\u{1F4E5} \u6536\u5230 ${files.length} \u4E2A\u6587\u4EF6 (${fmtSize(totalBytes)}), \u4E0A\u4F20\u4E2D\u2026`);
3160
+ }
3161
+ const chipsEl = appendAttachmentChip(`\u23F3 \u4E0A\u4F20\u4E2D ${files.length} \u4E2A (${fmtSize(totalBytes)})\u2026`);
3162
+ inputArea.classList.add("drop-target");
3163
+ setTimeout(() => inputArea.classList.remove("drop-target"), 600);
3164
+ for (const file of files) {
3165
+ try {
3166
+ const progressChip = document.createElement("span");
3167
+ progressChip.style.cssText = "background:#fef3c7;color:#92400e;border:1px solid #fbbf24;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;font-weight:500;animation:attach-pop-in 0.25s ease-out;";
3168
+ progressChip.innerHTML = `\u23F3 <strong>${file.name}</strong> \u8BFB\u53D6\u4E2D\u2026`;
3169
+ chipsEl.appendChild(progressChip);
3170
+ const dataB64 = await fileToBase64Local(file);
3171
+ progressChip.innerHTML = `\u23F3 <strong>${file.name}</strong> \u4E0A\u4F20\u4E2D (${fmtSize(file.size)})\u2026`;
3172
+ const res = await fetch("/api/attachments/upload", {
3173
+ method: "POST",
3174
+ headers: { "Content-Type": "application/json" },
3175
+ body: JSON.stringify({
3176
+ filename: file.name,
3177
+ mimeType: file.type || "application/octet-stream",
3178
+ content: dataB64
3179
+ })
3180
+ });
3181
+ if (!res.ok) {
3182
+ const errText = await res.text().catch(() => "");
3183
+ throw new Error(`HTTP ${res.status}: ${errText}`);
3184
+ }
3185
+ const out = await res.json();
3186
+ if (!out.ok) throw new Error(out.error || "upload failed");
3187
+ pendingAttachments.push({
3188
+ attachmentId: out.attachmentId,
3189
+ filename: out.filename,
3190
+ mimeType: out.mimeType,
3191
+ size: out.size,
3192
+ url: out.url
3193
+ });
3194
+ const prefix = input.value ? input.value + "\n" : "";
3195
+ input.value = `${prefix}\u{1F4CE} ${out.filename}`;
3196
+ input.focus();
3197
+ progressChip.style.background = "linear-gradient(135deg,#dcfce7,#bbf7d0)";
3198
+ progressChip.style.borderColor = "#16a34a";
3199
+ progressChip.style.color = "#15803d";
3200
+ progressChip.innerHTML = `\u2705 <strong>${out.filename}</strong> (${fmtSize(out.size)})`;
3201
+ input.style.transition = "box-shadow 0.4s ease, transform 0.2s ease";
3202
+ input.style.boxShadow = "0 0 0 3px #16a34a, 0 0 18px rgba(34,197,94,0.4)";
3203
+ input.style.transform = "scale(1.005)";
3204
+ setTimeout(() => {
3205
+ input.style.boxShadow = "0 0 0 2px #16a34a";
3206
+ input.style.transform = "";
3207
+ }, 200);
3208
+ setTimeout(() => {
3209
+ input.style.boxShadow = "";
3210
+ }, 1200);
3211
+ if (typeof showSimpleToast === "function") {
3212
+ showSimpleToast(`\u2705 \u9644\u4EF6 ${out.filename} (${fmtSize(out.size)}) \u5DF2\u4E0A\u4F20, \u7B49\u5F85\u53D1\u9001`);
3213
+ }
3214
+ } catch (err) {
3215
+ console.error("[drag-file] \u4E0A\u4F20\u5931\u8D25:", err);
3216
+ if (chipsEl) {
3217
+ const failChip = document.createElement("span");
3218
+ failChip.style.cssText = "background:#fee2e2;color:#b91c1c;border:1px solid #ef4444;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;font-weight:500;animation:attach-pop-in 0.25s ease-out;";
3219
+ failChip.innerHTML = `\u274C <strong>${file.name}</strong>: ${err?.message || "\u4E0A\u4F20\u5931\u8D25"}`;
3220
+ chipsEl.appendChild(failChip);
3221
+ }
3222
+ if (typeof showSimpleToast === "function") {
3223
+ showSimpleToast(`\u274C ${file.name} \u4E0A\u4F20\u5931\u8D25: ${err?.message || ""}`);
3224
+ }
3225
+ }
3226
+ }
3227
+ if (pendingAttachments.length > 0) {
3228
+ const ready = document.createElement("span");
3229
+ ready.style.cssText = "background:#f1f5f9;color:#0f172a;border:1px dashed #94a3b8;border-radius:12px;padding:4px 10px;display:inline-flex;align-items:center;gap:6px;font-size:12px;font-style:italic;";
3230
+ ready.innerHTML = `\u{1F4E8} \u5171 ${pendingAttachments.length} \u4E2A\u9644\u4EF6\u5F85\u53D1\u9001, \u6309 \u21A9 \u53D1\u9001`;
3231
+ chipsEl.appendChild(ready);
3232
+ }
3233
+ return;
3014
3234
  }
3015
3235
  };
3016
3236
  inputArea.addEventListener("dragover", onDragOver);
3017
3237
  inputArea.addEventListener("dragleave", onDragLeave);
3018
3238
  inputArea.addEventListener("drop", onDrop);
3239
+ let pageDragDepth = 0;
3240
+ const onPageDragEnter = (e) => {
3241
+ if (!e.dataTransfer) return;
3242
+ const types = Array.from(e.dataTransfer.types || []);
3243
+ if (types.includes("Files")) {
3244
+ e.preventDefault();
3245
+ pageDragDepth++;
3246
+ const overlay = ensureFileDropOverlay();
3247
+ overlay.style.display = "flex";
3248
+ document.body.classList.add("file-drag-active");
3249
+ }
3250
+ };
3251
+ const onPageDragOver = (e) => {
3252
+ if (!e.dataTransfer) return;
3253
+ const types = Array.from(e.dataTransfer.types || []);
3254
+ if (types.includes("Files")) {
3255
+ e.preventDefault();
3256
+ }
3257
+ };
3258
+ const onPageDragLeave = (e) => {
3259
+ if (!e.dataTransfer) return;
3260
+ const types = Array.from(e.dataTransfer.types || []);
3261
+ if (types.includes("Files")) {
3262
+ pageDragDepth = Math.max(0, pageDragDepth - 1);
3263
+ if (pageDragDepth === 0) {
3264
+ const overlay = document.getElementById("file-drop-overlay");
3265
+ if (overlay) overlay.style.display = "none";
3266
+ document.body.classList.remove("file-drag-active");
3267
+ }
3268
+ }
3269
+ };
3270
+ window.addEventListener("dragenter", onPageDragEnter);
3271
+ window.addEventListener("dragover", onPageDragOver);
3272
+ window.addEventListener("dragleave", onPageDragLeave);
3273
+ const onPageDrop = (e) => {
3274
+ if (!e.dataTransfer) return;
3275
+ const types = Array.from(e.dataTransfer.types || []);
3276
+ if (types.includes("Files") && e.target !== input && !inputArea?.contains(e.target)) {
3277
+ e.preventDefault();
3278
+ }
3279
+ if (types.includes("Files")) {
3280
+ pageDragDepth = 0;
3281
+ const overlay = document.getElementById("file-drop-overlay");
3282
+ if (overlay) overlay.style.display = "none";
3283
+ document.body.classList.remove("file-drag-active");
3284
+ }
3285
+ };
3286
+ window.addEventListener("drop", onPageDrop);
3019
3287
  }
3020
3288
  if (themeToggle) {
3021
3289
  themeToggle.addEventListener("click", toggleTheme);