@bolloon/bolloon-agent 0.3.0 → 0.3.3

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * client-hearth.ts — judgeness 前端模块 (占位实现)
3
+ *
4
+ * 防御期: 接 routes-hearth.ts, 渲染最小骨架 UI (My Hearth / Discover / Visit).
5
+ * 真实样式与组件待相持期接入 bolloon 现有 ui 设计 (HIG 等参考).
6
+ *
7
+ * 这个文件会被 esbuild 打包进 dist/web/client.js (与 src/web/client.ts 一致链路).
8
+ *
9
+ * 部署约定: 本文件 import 的 url 都是 '/api/hearth/*', 与 server.ts 端口 54188 对齐.
10
+ */
11
+ // 默认接受 JSON-LD (agent 头等公民); 人类用 ?view=human 切
12
+ async function fetchHearth(path, opts = {}) {
13
+ const res = await fetch(path, { headers: { Accept: opts.json ? 'application/ld+json' : '*/*' } });
14
+ if (!res.ok)
15
+ throw new Error(`hearth fetch ${path} -> ${res.status}`);
16
+ return await res.json();
17
+ }
18
+ // 三视图 stub: 由主 client.ts 路由到 #/hearth / #/hearth/discover / #/hearth/visit/<pk>
19
+ export async function renderMyHearth(root) {
20
+ root.innerHTML = '<h1>My Hearth</h1><p>Loading…</p>';
21
+ try {
22
+ const data = await fetchHearth('/api/hearth');
23
+ root.innerHTML = `
24
+ <h1>My Hearth</h1>
25
+ <dl>
26
+ <dt>Service</dt><dd>${data.service} v${data.version}</dd>
27
+ <dt>Root</dt><dd><code>${data.rootPath}</code></dd>
28
+ <dt>Description count</dt><dd>${data.descriptionCount}</dd>
29
+ <dt>Visibility channels</dt><dd>${data.visibilityChannels}</dd>
30
+ <dt>Allowlist count</dt><dd>${data.allowlistCount}</dd>
31
+ <dt>Defense mode</dt><dd>${data.defenseMode ? 'ON (write APIs disabled)' : 'OFF (反攻期)'}</dd>
32
+ </dl>
33
+ <p>视图: <a href="#/hearth/discover">Discover</a> · <a href="#/hearth/visit/__self__">Visit self</a></p>`;
34
+ }
35
+ catch (e) {
36
+ root.innerHTML = `<h1>My Hearth</h1><p style="color:red">Error: ${e.message}</p>`;
37
+ }
38
+ }
39
+ export async function renderDiscover(root) {
40
+ root.innerHTML = '<h1>Discover</h1><p>Searching…</p>';
41
+ try {
42
+ const data = await fetchHearth('/api/hearth/discover');
43
+ // data 可能是 JSON-LD @graph 或 list
44
+ const items = Array.isArray(data) ? data : (data['@graph'] ?? []);
45
+ const cards = items.slice(0, 50).map((it) => {
46
+ const id = it['@id'] ?? it.descriptionId ?? '?';
47
+ const vis = it.visibility ?? '?';
48
+ const state = it.openState ?? '?';
49
+ return `<li><code>${id}</code> · <span>${vis}</span> · <span>${state}</span></li>`;
50
+ }).join('');
51
+ root.innerHTML = `<h1>Discover</h1><ul>${cards || '<li>(no results)</li>'}</ul>`;
52
+ }
53
+ catch (e) {
54
+ root.innerHTML = `<h1>Discover</h1><p style="color:red">Error: ${e.message}</p>`;
55
+ }
56
+ }
57
+ export async function renderVisit(root, _pubkey) {
58
+ root.innerHTML = `<h1>Visit</h1><p>Visit 视图待相持期实现 (plan §C2).</p>`;
59
+ }
60
+ // 注册 router hook — 主 client.ts 在启动时调用
61
+ export function registerHearthRoutes(router, getRoot) {
62
+ router.on('/hearth', () => void renderMyHearth(getRoot()));
63
+ router.on('/hearth/discover', () => void renderDiscover(getRoot()));
64
+ router.on('/hearth/visit/:pk', (_p, params) => {
65
+ void renderVisit(getRoot(), params['pk'] ?? '__self__');
66
+ });
67
+ }
@@ -125,6 +125,13 @@
125
125
  node.appendChild(marker);
126
126
  node.appendChild(label2);
127
127
  node.appendChild(args);
128
+ const errContainer = document.createElement("div");
129
+ errContainer.className = "step-timeline-error-wrap";
130
+ errContainer.style.display = "none";
131
+ const errEl = document.createElement("span");
132
+ errEl.className = "step-timeline-error";
133
+ errContainer.appendChild(errEl);
134
+ node.appendChild(errContainer);
128
135
  if (existing[htmlIdx] && existing[htmlIdx] !== node) {
129
136
  listEl.replaceChild(node, existing[htmlIdx]);
130
137
  } else {
@@ -142,6 +149,16 @@
142
149
  argsEl.textContent = argStr;
143
150
  argsEl.style.display = argStr ? "" : "none";
144
151
  }
152
+ const errWrap = node.querySelector(".step-timeline-error-wrap");
153
+ const errEl2 = node.querySelector(".step-timeline-error");
154
+ if (step.status === "error" && step.error && errWrap && errEl2) {
155
+ errEl2.textContent = String(step.error);
156
+ errEl2.setAttribute("title", String(step.error));
157
+ errWrap.style.display = "";
158
+ } else if (errWrap && errEl2) {
159
+ errWrap.style.display = "none";
160
+ errEl2.textContent = "";
161
+ }
145
162
  htmlIdx++;
146
163
  }
147
164
  while (listEl.children.length > htmlIdx) {
@@ -356,6 +373,31 @@
356
373
  function parseToolCall(content, ctx) {
357
374
  if (!content) return null;
358
375
  const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, "");
376
+ try {
377
+ const result = /* @__PURE__ */ function _diagProbe() {
378
+ return null;
379
+ }();
380
+ const probe = function _doParse() {
381
+ const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
382
+ if (m1) {
383
+ return { name: m1[1], args: { __probe_invoketag: "1" } };
384
+ }
385
+ const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
386
+ if (m2) {
387
+ return { name: m2[1], args: { __probe_function_calls: "1" } };
388
+ }
389
+ const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
390
+ if (m3) {
391
+ return { name: m3[1], args: { __probe_json_name: "1" } };
392
+ }
393
+ return null;
394
+ }();
395
+ console.warn(
396
+ "[parseToolCall diag] rawLen=" + content.length + " strippedLen=" + strippedContent.length + " probeName=" + (probe?.name ?? "null") + " rawHead=" + JSON.stringify(content.slice(0, 500))
397
+ );
398
+ } catch (diagErr) {
399
+ console.warn("[parseToolCall diag] diag-self-failed:", String(diagErr));
400
+ }
359
401
  const jsonPatterns = [
360
402
  // markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
361
403
  /(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/
@@ -797,7 +839,7 @@
797
839
  "'": "&#39;"
798
840
  })[c]);
799
841
  }
800
- 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) {
801
843
  const messagesEl2 = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
802
844
  const messagesContainers2 = ctx.messagesContainers || /* @__PURE__ */ new Map();
803
845
  const currentChannelId2 = ctx.currentChannelId;
@@ -866,9 +908,20 @@
866
908
  return;
867
909
  }
868
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" });
869
922
  const time = document.createElement("div");
870
923
  time.className = "time";
871
- time.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
924
+ time.textContent = timeLabel;
872
925
  if (type === "ai") {
873
926
  div.appendChild(buildMessageActions(div, rawContent, ctx));
874
927
  }
@@ -2168,16 +2221,27 @@
2168
2221
  const session = await res.json();
2169
2222
  const msgs = session.messages || [];
2170
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
+ });
2171
2232
  const frag = document.createDocumentFragment();
2172
2233
  const tmpContainer = document.createElement("div");
2173
2234
  tmpContainer.style.display = "none";
2174
- for (const msg of msgs) {
2175
- 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);
2176
2237
  }
2177
2238
  while (tmpContainer.firstChild) {
2178
2239
  frag.appendChild(tmpContainer.firstChild);
2179
2240
  }
2180
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
+ }
2181
2245
  } else {
2182
2246
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2183
2247
  }
@@ -2202,9 +2266,21 @@ ${data.error || "channel not found"}`, "error");
2202
2266
  const session = await res.json();
2203
2267
  container.innerHTML = "";
2204
2268
  if (session.messages && session.messages.length > 0) {
2205
- session.messages.forEach((msg) => {
2206
- 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;
2277
+ });
2278
+ deduped.forEach((msg) => {
2279
+ addMessage2(msg.content, msg.type, false, container, msg.metadata?.usedJudgmentIds || [], msg.timestamp);
2207
2280
  });
2281
+ if (deduped.length !== rawMsgs.length) {
2282
+ console.log(`[loadSession-v2] \u53BB\u91CD ${rawMsgs.length - deduped.length} \u6761`);
2283
+ }
2208
2284
  } else {
2209
2285
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2210
2286
  }
@@ -2214,8 +2290,8 @@ ${data.error || "channel not found"}`, "error");
2214
2290
  addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
2215
2291
  }
2216
2292
  }
2217
- function addMessage2(content, type, save = true, container, usedJudgmentIds = []) {
2218
- 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);
2219
2295
  }
2220
2296
  function finalizeTimelineAsMessage2() {
2221
2297
  return MR_finalizeTimelineAsMessage(getRendererCtx());
@@ -2393,13 +2469,13 @@ ${data.error || "channel not found"}`, "error");
2393
2469
  const container = messagesContainers.get(targetChannelId) || messagesEl;
2394
2470
  if (msg.type === "ai") {
2395
2471
  if (!MR_hasStreamingText()) {
2396
- addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || []);
2472
+ addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || [], msg.timestamp);
2397
2473
  } else {
2398
2474
  MR_replaceStreamingText?.(msg.content);
2399
2475
  MR_finalizeTimelineAsMessage(getRendererCtx());
2400
2476
  }
2401
2477
  } else if (msg.type === "user") {
2402
- if (msg.source === "remote" || msg.source === "local") {
2478
+ if (msg.source === "remote") {
2403
2479
  addMessage2(msg.content, "user", true, container);
2404
2480
  }
2405
2481
  }
@@ -2484,22 +2560,19 @@ ${data.error || "channel not found"}`, "error");
2484
2560
  addMessage2(data.content, "user", true, container);
2485
2561
  }
2486
2562
  } else if (data.type === "ai") {
2563
+ const allPreviews = container.querySelectorAll(".message-ai.preview");
2564
+ allPreviews.forEach((el) => el.remove());
2565
+ currentPreviewBubble = null;
2487
2566
  addMessage2(data.content || "", "ai", true, container, lastUsedJudgmentIds || []);
2488
- if (currentPreviewBubble) {
2489
- currentPreviewBubble.remove();
2490
- currentPreviewBubble = null;
2491
- }
2492
2567
  } else if (data.type === "reply-preview") {
2493
2568
  const previewContent = data.content || "";
2494
- if (!currentPreviewBubble) {
2495
- currentPreviewBubble = addMessage2(previewContent, "ai", true, container, []);
2496
- if (currentPreviewBubble) {
2497
- currentPreviewBubble.classList.add("preview");
2498
- }
2499
- } else {
2500
- currentPreviewBubble.replaceWith(addMessage2(previewContent, "ai", true, container, []));
2501
- const msgs = container.querySelectorAll(".message-ai.preview");
2502
- 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;
2503
2576
  }
2504
2577
  } else if (data.type === "stream") {
2505
2578
  if (false) handleStreamTokenEvent(data);
@@ -2573,6 +2646,10 @@ ${data.error || "channel not found"}`, "error");
2573
2646
  persistLastMessageToServer("user", text);
2574
2647
  const channel = channels.find((c) => c.id === currentChannelId);
2575
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 = "";
2576
2653
  console.log("[\u53D1\u9001\u6D88\u606F] \u9891\u9053 DID:", channelDid);
2577
2654
  try {
2578
2655
  const res = await fetch("/message", {
@@ -2581,7 +2658,9 @@ ${data.error || "channel not found"}`, "error");
2581
2658
  body: JSON.stringify({
2582
2659
  text,
2583
2660
  channelId: currentChannelId,
2584
- channelDid
2661
+ channelDid,
2662
+ attachments: attachmentsForSend
2663
+ // 后端解析为 LLM contextHint
2585
2664
  })
2586
2665
  });
2587
2666
  if (!res.ok) {
@@ -2940,11 +3019,103 @@ ${data.error || "channel not found"}`, "error");
2940
3019
  }, true);
2941
3020
  }
2942
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);
2943
3109
  if (input && inputArea) {
2944
3110
  const onDragOver = (e) => {
2945
- 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) {
2946
3116
  e.preventDefault();
2947
- e.dataTransfer.dropEffect = "copy";
3117
+ e.stopPropagation();
3118
+ e.dataTransfer.dropEffect = hasFiles ? "copy" : "copy";
2948
3119
  inputArea.classList.add("drop-target");
2949
3120
  }
2950
3121
  };
@@ -2953,27 +3124,166 @@ ${data.error || "channel not found"}`, "error");
2953
3124
  inputArea.classList.remove("drop-target");
2954
3125
  }
2955
3126
  };
2956
- const onDrop = (e) => {
3127
+ const onDrop = async (e) => {
2957
3128
  inputArea.classList.remove("drop-target");
2958
- const raw = e.dataTransfer.getData("application/x-bolloon-judgment");
2959
- if (!raw) return;
2960
- e.preventDefault();
2961
- try {
2962
- const { id, decision } = JSON.parse(raw);
2963
- const prefix = input.value.trim() ? input.value.trim() + "\n" : "";
2964
- input.value = `${prefix}\u6309\u6211\u7684\u5224\u65AD #${id?.substring(0, 8) || ""} \u6267\u884C: ${decision}`;
2965
- input.focus();
2966
- input.style.transition = "box-shadow 0.3s";
2967
- input.style.boxShadow = "0 0 0 2px #2563eb";
2968
- setTimeout(() => {
2969
- input.style.boxShadow = "";
2970
- }, 800);
2971
- } 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;
2972
3234
  }
2973
3235
  };
2974
3236
  inputArea.addEventListener("dragover", onDragOver);
2975
3237
  inputArea.addEventListener("dragleave", onDragLeave);
2976
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);
2977
3287
  }
2978
3288
  if (themeToggle) {
2979
3289
  themeToggle.addEventListener("click", toggleTheme);