@bolloon/bolloon-agent 0.3.1 → 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.
- package/dist/judgeness/auto-add.js +145 -0
- package/dist/judgeness/protocol.js +214 -0
- package/dist/judgeness/rank.js +78 -0
- package/dist/judgeness/reflect.js +93 -0
- package/dist/judgeness/store.js +481 -0
- package/dist/judgeness/types.js +19 -0
- package/dist/judgeness/visibility.js +118 -0
- package/dist/scripts/dedup-session-messages.js +68 -0
- package/dist/web/client-hearth.js +67 -0
- package/dist/web/client.js +309 -41
- package/dist/web/routes-hearth.js +371 -0
- package/dist/web/server.js +356 -10
- package/dist/web/ui/message-renderer.js +13 -2
- package/dist/web/util/dual-mode.js +87 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/web/client.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
2248
|
-
|
|
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"
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
currentPreviewBubble
|
|
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
|
|
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.
|
|
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
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
input.style.
|
|
3012
|
-
|
|
3013
|
-
|
|
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);
|