@yfwu2020/dsh-selection-explain 0.10.5 → 0.11.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.
package/lib/client.js CHANGED
@@ -117,6 +117,10 @@ var PILL_POS = "/selection-explain/api/pill";
117
117
  var PANEL_SIZE = "/selection-explain/api/panel";
118
118
  var HISTORY = "/selection-explain/api/history";
119
119
  var MODELS = "/selection-explain/api/models";
120
+ var ATTACH = "/selection-explain/api/attach";
121
+ var ATTACHMENT = "/selection-explain/api/attachment";
122
+ var ATTACH_MAX_ITEMS = 10;
123
+ var ATTACH_MAX_ITEM_BYTES = 20 * 1024 * 1024;
120
124
  var SETTINGS = "/selection-explain/api/settings";
121
125
  var ROUTE = "/selection-explain/api/route";
122
126
  var SPEECH_API = "/selection-explain/api/speech";
@@ -204,6 +208,10 @@ var CSS = [
204
208
  ".dsh-sel-layer{position:fixed;inset:0;z-index:70;pointer-events:none;--sel-a1:#0d9488;--sel-a1-text:#0f766e;--sel-a2:#0d9488;--sel-a2-text:#0f766e;",
205
209
  "--sel-fill:#0f766e;--sel-fill-fg:#ffffff;",
206
210
  "--sel-a2-soft:color-mix(in srgb,var(--sel-a2) 12%,transparent);--sel-a1-soft:color-mix(in srgb,var(--sel-a1) 10%,transparent)}",
211
+ // DSH 桌面将 body>:not(#root) 设为 no-drag;全屏透明根会因此屏蔽整个窗口拖拽。
212
+ // important 只用于根的拖拽属性,覆盖宿主带 #root 的选择器;空白与装饰保持中性。
213
+ ".dsh-sel-layer{-webkit-app-region:initial!important}",
214
+ ".dsh-sel-layer>:is(.dsh-sel-btn,.dsh-sel-panel,.dsh-sel-pill,.dsh-sel-history,.dsh-sel-vcard){-webkit-app-region:no-drag}",
207
215
  ".dsh-sel-layer[data-full-view] .dsh-sel-pill{display:none!important}",
208
216
  ".dsh-sel-layer[data-theme=dark]{--sel-a1:#4ecdc4;--sel-a1-text:#6fe3d8;--sel-a2:#4ecdc4;--sel-a2-text:#6fe3d8;",
209
217
  "--sel-fill:#4ecdc4;--sel-fill-fg:#052b26;",
@@ -437,12 +445,97 @@ var CSS = [
437
445
  "background:transparent;color:var(--dsw-alias-label-secondary,inherit);opacity:.5;font:inherit;font-size:11px;",
438
446
  "line-height:1;display:inline-flex;align-items:center;justify-content:center}",
439
447
  ".dsh-sel-quotechip-x:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.16))}",
448
+ // ── 附件(composer 第一行,和引用同栏)──
449
+ // 引用是竖着排的卡片(有出处 + 一段文字),附件是横着一排的缩略图 —— 两种形状,
450
+ // 所以是**两条独立的条**、共用 ask 的第一行区域,而不是把附件塞进引用卡片里。
451
+ // 横向溢出就滚动(附件最多 10 件,小窗宽度装不下是常态),滚动条藏起来不占高度。
452
+ ".dsh-sel-attach{display:none;flex-direction:row;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;",
453
+ "scrollbar-width:none;padding:1px 0 0}",
454
+ '.dsh-sel-attach[data-show="1"]{display:flex}',
455
+ ".dsh-sel-attach::-webkit-scrollbar{width:0;height:0;display:none}",
456
+ // 40px 见方:比主会话的 64px 小一圈(小窗的 composer 整体是收着的:按钮 26、输入 13px)
457
+ ".dsh-sel-thumb{position:relative;flex:0 0 auto;width:40px;height:40px;border-radius:9px;overflow:hidden;",
458
+ "border:1px solid var(--dsw-alias-border-l3,rgba(140,140,140,.35));background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14));",
459
+ "display:grid;place-items:center;color:var(--dsw-alias-label-secondary,inherit)}",
460
+ ".dsh-sel-thumb img{width:100%;height:100%;object-fit:cover;display:block}",
461
+ ".dsh-sel-thumb svg{width:15px;height:15px;opacity:.55}",
462
+ '.dsh-sel-thumb[data-state="error"]{border-color:color-mix(in srgb,var(--sel-bad,#e5534b) 70%,transparent)}',
463
+ '.dsh-sel-thumb[data-state="error"] svg{opacity:.9;color:var(--sel-bad,#e5534b)}',
464
+ // 转圈 + 不定态进度条:fetch 拿不到上传进度,所以**不假装知道百分比**
465
+ // (主会话在"首次报告字节之前"用的也是不定态)
466
+ ".dsh-sel-attachspin{position:absolute;inset:0;display:grid;place-items:center;background:rgba(0,0,0,.42)}",
467
+ ".dsh-sel-attachspin svg{width:16px;height:16px;opacity:1;color:var(--sel-a1,#0d9488);animation:dsh-sel-spin .9s linear infinite}",
468
+ ".dsh-sel-attachbar{position:absolute;left:0;right:0;bottom:0;height:3px;background:rgba(140,140,140,.35);overflow:hidden}",
469
+ ".dsh-sel-attachbar i{display:block;height:100%;width:38%;background:var(--sel-a1,#0d9488);animation:dsh-sel-indeterminate 1.1s ease-in-out infinite}",
470
+ "@keyframes dsh-sel-indeterminate{0%{transform:translateX(-100%)}100%{transform:translateX(280%)}}",
471
+ // 移除:缩略图/文件卡右上角。**常驻可见**(触摸设备上没有 hover,藏起来就删不掉了)
472
+ // 兜底色必须是**中性灰 + 白字**:主题变量缺席时(非 DSH 宿主 / 测试页)
473
+ // 原来的 `bg-layer-2 → #3a3f46` 配 `label-primary → CanvasText`(浅色方案下是黑)
474
+ // 会画成"深底黑叉",实拍就是一坨看不出是什么的黑点。
475
+ ".dsh-sel-attachx{position:absolute;top:-5px;right:-5px;width:15px;height:15px;padding:0;border-radius:50%;",
476
+ "border:1px solid var(--dsw-alias-bg-layer-1,Canvas);background:var(--dsw-alias-bg-layer-2,rgba(120,120,120,.92));",
477
+ "color:var(--dsw-alias-label-primary,#fff);font:inherit;font-size:9px;line-height:1;cursor:pointer;",
478
+ "display:grid;place-items:center;opacity:.85}",
479
+ ".dsh-sel-attachx:hover{opacity:1}",
480
+ // 文件卡:和缩略图同高(40px),名字一行、扩展名 + 字节数一行小字
481
+ ".dsh-sel-fcard{position:relative;display:inline-flex;align-items:center;gap:7px;flex:0 0 auto;height:40px;",
482
+ "padding:0 7px 0 6px;border-radius:9px;border:1px solid var(--dsw-alias-border-l3,rgba(140,140,140,.35));",
483
+ "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.1));font-size:11.5px;line-height:1.35}",
484
+ '.dsh-sel-fcard[data-state="error"]{border-color:color-mix(in srgb,var(--sel-bad,#e5534b) 70%,transparent)}',
485
+ ".dsh-sel-fico{flex:0 0 auto;width:22px;height:22px;border-radius:6px;display:grid;place-items:center;",
486
+ "background:linear-gradient(160deg,#3f6ee0,#7aa2ff);color:#fff}",
487
+ ".dsh-sel-fico svg{width:13px;height:13px;opacity:1}",
488
+ ".dsh-sel-fname{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
489
+ ".dsh-sel-fmeta{font-size:10.5px;opacity:.6;letter-spacing:.3px;white-space:nowrap}",
490
+ ".dsh-sel-fretry{flex:0 0 auto;height:16px;padding:0 6px;border-radius:8px;cursor:pointer;font:inherit;font-size:10.5px;",
491
+ "line-height:14px;color:var(--sel-bad,#e5534b);border:1px solid color-mix(in srgb,var(--sel-bad,#e5534b) 50%,transparent);",
492
+ "background:transparent}",
493
+ ".dsh-sel-fretry:hover{background:color-mix(in srgb,var(--sel-bad,#e5534b) 12%,transparent)}",
494
+ // 附件出错时的说明行(贴在附件条下面,一行小字)
495
+ ".dsh-sel-attachnote{display:none;margin:0;padding:0 2px;font-size:11px;line-height:1.6;color:var(--sel-warn-fg,#b45309)}",
496
+ '.dsh-sel-attachnote[data-show="1"]{display:block}',
497
+ // + 入口:圆形浅底(和主会话 composer 的 + 同款),放在工具行**最左**
498
+ ".dsh-sel-add{background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14));color:var(--dsw-alias-label-primary,CanvasText)}",
499
+ ".dsh-sel-add:hover{background:var(--dsw-alias-interactive-bg-active,rgba(140,140,140,.22))}",
500
+ '.dsh-sel-add[data-busy="1"]{opacity:.6}',
501
+ // 拖放遮罩:铺满面板(面板本身 overflow:hidden,所以内缩 6px 的圆角块正好贴住内容区)
502
+ ".dsh-sel-drop{position:absolute;inset:6px;z-index:9;display:none;flex-direction:column;align-items:center;",
503
+ "justify-content:center;gap:9px;border:2px dashed var(--sel-a1,#0d9488);border-radius:13px;",
504
+ "background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,Canvas) 88%,transparent);pointer-events:none}",
505
+ '.dsh-sel-panel[data-dropping="1"] .dsh-sel-drop{display:flex}',
506
+ ".dsh-sel-drop-art{width:52px;height:52px;border-radius:14px;display:grid;place-items:center;",
507
+ "background:var(--sel-a1-soft);color:var(--sel-a1,#0d9488)}",
508
+ ".dsh-sel-drop-art svg{width:25px;height:25px}",
509
+ ".dsh-sel-drop-title{font-size:14px;font-weight:600}",
510
+ ".dsh-sel-drop-hint{font-size:11.5px;opacity:.7}",
440
511
  // 气泡里的引用块(用户消息):引用在上、问题在下 —— 和发出去给模型的那份顺序一致
441
512
  ".dsh-sel-bq{display:flex;flex-direction:column;gap:3px;margin-bottom:6px}",
442
513
  ".dsh-sel-bqitem{padding:3px 8px;border-radius:8px;border-left:2px solid color-mix(in srgb,var(--sel-a1,#0d9488) 45%,transparent);",
443
514
  "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14));font-size:11.5px;line-height:1.55;",
444
515
  "opacity:.82;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
445
516
  ".dsh-sel-bqitem b{font-weight:600;opacity:.75;margin-right:5px}",
517
+ // 气泡里的附件:图片是缩略图(点开原图),文件是一张卡(点一下下载)。
518
+ // 和输入框上面那条 rail 是**同一个东西的两个阶段**(发送前 / 发送后),
519
+ // 所以尺寸、圆角、配色都对齐:缩略图 40px、文件卡同高。
520
+ ".dsh-sel-bqattach{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:6px}",
521
+ // ⚠️ flex:0 0 auto 会让这个盒子的宽度由**图片自己**撑出来,于是图片上的 max-width:100%
522
+ // 变成"相对于自己"(循环引用)→ 一张 810px 宽的图会照原样渲染并溢出气泡(实测量到 810x180)。
523
+ // 必须允许它收缩(flex:0 1 auto + min-width:0),百分比才有一个确定的参照。
524
+ ".dsh-sel-bqimgwrap{flex:0 1 auto;min-width:0;max-width:100%;display:block;border-radius:9px;overflow:hidden;line-height:0;",
525
+ "border:1px solid var(--dsw-alias-border-l3,rgba(140,140,140,.35))}",
526
+ // 上限 180px 高、最宽占满气泡:小窗本来就窄,缩略图不能把气泡撑爆
527
+ ".dsh-sel-bqimg{display:block;max-width:100%;max-height:180px;width:auto;height:auto;border-radius:8px;cursor:zoom-in}",
528
+ ".dsh-sel-bqimgfail{display:inline-block;padding:5px 9px;border-radius:8px;font-size:11.5px;opacity:.7;",
529
+ "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14))}",
530
+ ".dsh-sel-bqfile{display:inline-flex;align-items:center;gap:7px;max-width:100%;padding:5px 9px;border-radius:9px;",
531
+ "text-decoration:none;color:inherit;cursor:pointer;font-size:11.5px;line-height:1.35;",
532
+ "border:1px solid var(--dsw-alias-border-l3,rgba(140,140,140,.35));",
533
+ "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.1))}",
534
+ ".dsh-sel-bqfile:hover{border-color:color-mix(in srgb,var(--sel-a1,#0d9488) 45%,transparent)}",
535
+ ".dsh-sel-bqfileext{flex:0 0 auto;font-size:9.5px;font-weight:700;letter-spacing:.4px;padding:0 5px;",
536
+ "border-radius:5px;line-height:15px;background:var(--sel-fill,#0f766e);color:var(--sel-fill-fg,#fff)}",
537
+ ".dsh-sel-bqfilename{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px}",
538
+ ".dsh-sel-bqfilemeta{flex:0 0 auto;font-size:10.5px;opacity:.6}",
446
539
  // 「引用整条」:贴在助手气泡末尾,**平时不出现**(正文优先),鼠标移到这条消息上才浮出来。
447
540
  // 键盘 Tab 也能到(:focus-visible 同样点亮),触屏上另有浮标那条路。
448
541
  ".dsh-sel-bubquote{margin-top:7px;display:inline-flex;align-items:center;gap:4px;padding:2px 9px;",
@@ -1124,6 +1217,21 @@ function micIcon() {
1124
1217
  svg.appendChild(stem);
1125
1218
  return svg;
1126
1219
  }
1220
+ function spinnerIcon() {
1221
+ var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1222
+ svg.setAttribute("viewBox", "0 0 16 16");
1223
+ svg.setAttribute("width", "14");
1224
+ svg.setAttribute("height", "14");
1225
+ svg.setAttribute("fill", "none");
1226
+ svg.setAttribute("stroke", "currentColor");
1227
+ svg.setAttribute("stroke-width", "1.7");
1228
+ svg.setAttribute("stroke-linecap", "round");
1229
+ svg.setAttribute("aria-hidden", "true");
1230
+ var arc = document.createElementNS("http://www.w3.org/2000/svg", "path");
1231
+ arc.setAttribute("d", "M8 1.8a6.2 6.2 0 1 1-4.4 1.8");
1232
+ svg.appendChild(arc);
1233
+ return svg;
1234
+ }
1127
1235
  function sendIcon() {
1128
1236
  var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1129
1237
  svg.setAttribute("viewBox", "0 0 16 16");
@@ -1174,6 +1282,34 @@ function quoteIcon() {
1174
1282
  }
1175
1283
  return svg;
1176
1284
  }
1285
+ function plusIcon() {
1286
+ var svg = iconCanvas(14);
1287
+ iconPath(svg, "M8 3.4v9.2M3.4 8h9.2");
1288
+ return svg;
1289
+ }
1290
+ function docIcon() {
1291
+ var svg = iconCanvas(14);
1292
+ iconPath(svg, "M4.3 1.9h4.4l3 3v9.2H4.3z");
1293
+ iconPath(svg, "M8.5 1.9v3.2h3.2");
1294
+ return svg;
1295
+ }
1296
+ function imageIcon() {
1297
+ var svg = iconCanvas(14);
1298
+ iconPath(svg, "M2.6 3.2h10.8v9.6H2.6z");
1299
+ var dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
1300
+ dot.setAttribute("cx", "5.7");
1301
+ dot.setAttribute("cy", "6.3");
1302
+ dot.setAttribute("r", "0.9");
1303
+ svg.appendChild(dot);
1304
+ iconPath(svg, "M2.6 10.9l3.1-2.9 2.5 2.3 2.3-2.1 2.9 2.7");
1305
+ return svg;
1306
+ }
1307
+ function retryIcon() {
1308
+ var svg = iconCanvas(13);
1309
+ iconPath(svg, "M12.6 8a4.6 4.6 0 1 1-1.6-3.5");
1310
+ iconPath(svg, "M12.9 2.6v3.1h-3.1");
1311
+ return svg;
1312
+ }
1177
1313
 
1178
1314
  // src/client/index.js
1179
1315
  var import_react = __toESM(require("react"), 1);
@@ -3931,7 +4067,12 @@ function createQuotesController(env) {
3931
4067
  function exportTurns() {
3932
4068
  var out = [];
3933
4069
  for (var i = 0; i < env.state.turns.length; i += 1) {
3934
- out.push({ role: env.state.turns[i].role, text: sentTextOf(env.state.turns[i]) });
4070
+ var turn = env.state.turns[i];
4071
+ out.push({
4072
+ role: turn.role,
4073
+ text: sentTextOf(turn),
4074
+ ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
4075
+ });
3935
4076
  }
3936
4077
  return out;
3937
4078
  }
@@ -3950,6 +4091,383 @@ function createQuotesController(env) {
3950
4091
  };
3951
4092
  }
3952
4093
 
4094
+ // src/client/attach.js
4095
+ function encodeAttachmentRef(ref) {
4096
+ var json = JSON.stringify(ref || {});
4097
+ if (typeof TextEncoder === "function") {
4098
+ var bytes = new TextEncoder().encode(json);
4099
+ var binary = "";
4100
+ for (var i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
4101
+ json = binary;
4102
+ }
4103
+ return btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4104
+ }
4105
+ function attachmentUrl(kind, ref, download) {
4106
+ return ATTACHMENT + "?kind=" + (kind === "file" ? "file" : "image") + "&ref=" + encodeAttachmentRef(ref) + (download ? "&download=1" : "");
4107
+ }
4108
+ function formatBytes(bytes) {
4109
+ var value = typeof bytes === "number" && isFinite(bytes) ? bytes : 0;
4110
+ if (value < 1024) return value + " B";
4111
+ if (value < 1024 * 1024) return (value / 1024).toFixed(1) + " KB";
4112
+ return (value / 1024 / 1024).toFixed(1) + " MB";
4113
+ }
4114
+ function extensionOf(name) {
4115
+ var match = /\.([A-Za-z0-9]+)$/.exec(String(name || ""));
4116
+ return match ? match[1].toUpperCase() : "FILE";
4117
+ }
4118
+ function fileToBase64(file) {
4119
+ return new Promise(function(resolve, reject) {
4120
+ var reader = new FileReader();
4121
+ reader.onload = function() {
4122
+ var result = String(reader.result || "");
4123
+ var comma = result.indexOf(",");
4124
+ resolve(comma >= 0 ? result.slice(comma + 1) : "");
4125
+ };
4126
+ reader.onerror = function() {
4127
+ reject(reader.error || new Error("\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25"));
4128
+ };
4129
+ reader.readAsDataURL(file);
4130
+ });
4131
+ }
4132
+ function looksLikeDirectory(file) {
4133
+ return file && file.size === 0 && !file.type && !/\.[A-Za-z0-9]+$/.test(String(file.name || ""));
4134
+ }
4135
+ function createAttachController(env) {
4136
+ var seq = 0;
4137
+ function items() {
4138
+ return env.state.attachments;
4139
+ }
4140
+ function note(text) {
4141
+ env.state.attachNote = text || "";
4142
+ renderNote();
4143
+ }
4144
+ function renderNote() {
4145
+ if (!env.attachNote) return;
4146
+ env.attachNote.textContent = env.state.attachNote || "";
4147
+ env.attachNote.setAttribute("data-show", env.state.attachNote ? "1" : "0");
4148
+ }
4149
+ function busy() {
4150
+ var list = items();
4151
+ for (var i = 0; i < list.length; i += 1) if (list[i].status === "uploading") return true;
4152
+ return false;
4153
+ }
4154
+ function payload() {
4155
+ var out = [];
4156
+ var list = items();
4157
+ for (var i = 0; i < list.length; i += 1) {
4158
+ if (list[i].status === "done" && list[i].ref) out.push({ kind: list[i].kind, ref: list[i].ref });
4159
+ }
4160
+ return out;
4161
+ }
4162
+ function describe(explicit) {
4163
+ var list = explicit || items();
4164
+ var names = [];
4165
+ for (var i = 0; i < list.length; i += 1) {
4166
+ if (list[i].status !== "done" || !list[i].ref) continue;
4167
+ names.push(list[i].name + (list[i].kind === "image" ? "\uFF08\u56FE\u7247\uFF09" : "\uFF08\u6587\u4EF6\uFF09"));
4168
+ }
4169
+ if (names.length === 0) return "";
4170
+ return "\u3010\u672C\u6761\u6D88\u606F\u5E26\u4E86\u9644\u4EF6\u3011" + names.join("\u3001");
4171
+ }
4172
+ function clear() {
4173
+ var list = items();
4174
+ for (var i = 0; i < list.length; i += 1) revoke(list[i]);
4175
+ env.state.attachments = [];
4176
+ env.state.attachNote = "";
4177
+ render();
4178
+ }
4179
+ function revoke(item) {
4180
+ if (item && item.url && typeof URL !== "undefined" && URL.revokeObjectURL) {
4181
+ try {
4182
+ URL.revokeObjectURL(item.url);
4183
+ } catch (error) {
4184
+ }
4185
+ }
4186
+ }
4187
+ function remove(id) {
4188
+ var list = items();
4189
+ for (var i = 0; i < list.length; i += 1) {
4190
+ if (list[i].id === id) {
4191
+ revoke(list[i]);
4192
+ list.splice(i, 1);
4193
+ break;
4194
+ }
4195
+ }
4196
+ env.state.attachNote = "";
4197
+ render();
4198
+ env.refreshAskState();
4199
+ }
4200
+ function addFiles(files) {
4201
+ var list = items();
4202
+ var incoming = [];
4203
+ for (var i = 0; i < files.length; i += 1) incoming.push(files[i]);
4204
+ if (incoming.length === 0) return;
4205
+ var rejected = [];
4206
+ for (var j = 0; j < incoming.length; j += 1) {
4207
+ var file = incoming[j];
4208
+ if (looksLikeDirectory(file)) {
4209
+ rejected.push("\u300C" + (file.name || "\u672A\u547D\u540D") + "\u300D\u662F\u6587\u4EF6\u5939\uFF0C\u4E0D\u652F\u6301");
4210
+ continue;
4211
+ }
4212
+ if (list.length >= ATTACH_MAX_ITEMS) {
4213
+ rejected.push("\u4E00\u6B21\u6700\u591A\u5E26 " + ATTACH_MAX_ITEMS + " \u4EF6\u9644\u4EF6");
4214
+ break;
4215
+ }
4216
+ if (file.size > ATTACH_MAX_ITEM_BYTES) {
4217
+ rejected.push("\u300C" + file.name + "\u300D\u8D85\u8FC7\u5355\u4EF6\u4E0A\u9650 " + formatBytes(ATTACH_MAX_ITEM_BYTES));
4218
+ continue;
4219
+ }
4220
+ list.push(createItem(file));
4221
+ }
4222
+ note(rejected.join("\uFF1B"));
4223
+ render();
4224
+ env.refreshAskState();
4225
+ var pending = list.filter(function(item) {
4226
+ return item.status === "uploading" && !item.started;
4227
+ });
4228
+ var chain = Promise.resolve();
4229
+ pending.forEach(function(item) {
4230
+ chain = chain.then(function() {
4231
+ return upload(item);
4232
+ });
4233
+ });
4234
+ }
4235
+ function createItem(file) {
4236
+ var isImage = /^image\//.test(String(file.type || ""));
4237
+ var item = {
4238
+ id: ++seq,
4239
+ kind: isImage ? "image" : "file",
4240
+ name: file.name || (isImage ? "\u7C98\u8D34\u7684\u56FE\u7247" : "\u672A\u547D\u540D\u6587\u4EF6"),
4241
+ size: typeof file.size === "number" ? file.size : 0,
4242
+ mediaType: file.type || "application/octet-stream",
4243
+ file,
4244
+ // 缩略图先用本地 object URL 显示(不必等上传完),失败时回落到图标
4245
+ url: isImage && typeof URL !== "undefined" && URL.createObjectURL ? URL.createObjectURL(file) : "",
4246
+ status: "uploading",
4247
+ started: false,
4248
+ ref: null,
4249
+ error: ""
4250
+ };
4251
+ return item;
4252
+ }
4253
+ function upload(item) {
4254
+ if (item.started) return Promise.resolve();
4255
+ item.started = true;
4256
+ item.status = "uploading";
4257
+ item.error = "";
4258
+ render();
4259
+ return fileToBase64(item.file).then(function(data) {
4260
+ return fetch(ATTACH, {
4261
+ method: "POST",
4262
+ headers: { "content-type": "application/json" },
4263
+ body: JSON.stringify({
4264
+ items: [{ kind: item.kind, name: item.name, mediaType: item.mediaType, data }]
4265
+ })
4266
+ });
4267
+ }).then(function(response) {
4268
+ return response.json().catch(function() {
4269
+ return {};
4270
+ }).then(function(body) {
4271
+ if (!response.ok || !body || body.ok !== true) {
4272
+ throw new Error(body && body.error || "HTTP " + response.status);
4273
+ }
4274
+ var first = body.items && body.items[0];
4275
+ if (!first || !first.ref) throw new Error("\u4E0A\u4F20\u540E\u6CA1\u6709\u62FF\u5230\u9644\u4EF6\u5F15\u7528");
4276
+ item.ref = first.ref;
4277
+ item.status = "done";
4278
+ item.file = null;
4279
+ if (first.ref.name) item.name = String(first.ref.name);
4280
+ if (typeof first.ref.bytes === "number") item.size = first.ref.bytes;
4281
+ if (env.state.attachNote.indexOf(item.name) >= 0) note("");
4282
+ });
4283
+ }).catch(function(error) {
4284
+ item.status = "error";
4285
+ item.error = String(error && error.message || error);
4286
+ note("\u300C" + item.name + "\u300D" + item.error);
4287
+ }).then(function() {
4288
+ render();
4289
+ env.refreshAskState();
4290
+ });
4291
+ }
4292
+ function retry(id) {
4293
+ var list = items();
4294
+ for (var i = 0; i < list.length; i += 1) {
4295
+ if (list[i].id === id && list[i].status === "error") {
4296
+ if (!list[i].file) {
4297
+ note("\u8FD9\u4E00\u4EF6\u5DF2\u7ECF\u6CA1\u6CD5\u91CD\u8BD5\u4E86\uFF0C\u8BF7\u91CD\u65B0\u6DFB\u52A0");
4298
+ return;
4299
+ }
4300
+ list[i].started = false;
4301
+ upload(list[i]);
4302
+ return;
4303
+ }
4304
+ }
4305
+ }
4306
+ function render() {
4307
+ var box = env.attachBox;
4308
+ if (!box) return;
4309
+ var list = items();
4310
+ box.textContent = "";
4311
+ box.setAttribute("data-show", list.length > 0 ? "1" : "0");
4312
+ for (var i = 0; i < list.length; i += 1) {
4313
+ box.appendChild(list[i].kind === "image" ? thumbNode(list[i]) : fileNode(list[i]));
4314
+ }
4315
+ renderNote();
4316
+ }
4317
+ function removeButton(item) {
4318
+ var button = el("button", "dsh-sel-attachx", "\u2715");
4319
+ button.type = "button";
4320
+ button.title = "\u79FB\u9664\u300C" + item.name + "\u300D";
4321
+ button.setAttribute("aria-label", "\u79FB\u9664\u9644\u4EF6 " + item.name);
4322
+ listen(button, "click", function(event) {
4323
+ event.stopPropagation();
4324
+ remove(item.id);
4325
+ });
4326
+ return button;
4327
+ }
4328
+ function thumbNode(item) {
4329
+ var node = el("span", "dsh-sel-thumb");
4330
+ node.setAttribute("data-state", item.status);
4331
+ node.title = item.name;
4332
+ if (item.url) {
4333
+ var img = document.createElement("img");
4334
+ img.src = item.url;
4335
+ img.alt = item.name;
4336
+ node.appendChild(img);
4337
+ } else {
4338
+ node.appendChild(imageIcon());
4339
+ }
4340
+ if (item.status === "uploading") {
4341
+ var spin = el("span", "dsh-sel-attachspin");
4342
+ spin.appendChild(spinnerIcon());
4343
+ node.appendChild(spin);
4344
+ var bar = el("span", "dsh-sel-attachbar");
4345
+ bar.appendChild(el("i"));
4346
+ node.appendChild(bar);
4347
+ } else if (item.status === "error") {
4348
+ var retryBox = el("span", "dsh-sel-attachspin");
4349
+ retryBox.style.background = "transparent";
4350
+ retryBox.title = "\u4E0A\u4F20\u5931\u8D25\uFF0C\u70B9\u51FB\u91CD\u8BD5";
4351
+ var retryButton = el("button", "dsh-sel-attachx", "");
4352
+ retryButton.type = "button";
4353
+ retryButton.style.position = "static";
4354
+ retryButton.style.width = "20px";
4355
+ retryButton.style.height = "20px";
4356
+ retryButton.setAttribute("aria-label", "\u91CD\u8BD5\u4E0A\u4F20 " + item.name);
4357
+ retryButton.appendChild(retryIcon());
4358
+ listen(retryButton, "click", function(event) {
4359
+ event.stopPropagation();
4360
+ retry(item.id);
4361
+ });
4362
+ retryBox.appendChild(retryButton);
4363
+ node.appendChild(retryBox);
4364
+ }
4365
+ node.appendChild(removeButton(item));
4366
+ return node;
4367
+ }
4368
+ function fileNode(item) {
4369
+ var node = el("span", "dsh-sel-fcard");
4370
+ node.setAttribute("data-state", item.status);
4371
+ var icon = el("span", "dsh-sel-fico");
4372
+ icon.appendChild(docIcon());
4373
+ node.appendChild(icon);
4374
+ node.appendChild(el("span", "dsh-sel-fname", item.name));
4375
+ var meta = extensionOf(item.name) + " \xB7 " + formatBytes(item.size);
4376
+ if (item.status === "uploading") meta = "\u6B63\u5728\u4E0A\u4F20\u2026";
4377
+ node.appendChild(el("span", "dsh-sel-fmeta", meta));
4378
+ if (item.status === "error") {
4379
+ var retryButton = el("button", "dsh-sel-fretry", "\u91CD\u8BD5");
4380
+ retryButton.type = "button";
4381
+ listen(retryButton, "click", function(event) {
4382
+ event.stopPropagation();
4383
+ retry(item.id);
4384
+ });
4385
+ node.appendChild(retryButton);
4386
+ }
4387
+ node.appendChild(removeButton(item));
4388
+ return node;
4389
+ }
4390
+ function pick() {
4391
+ if (!env.attachInput) return;
4392
+ env.attachInput.value = "";
4393
+ env.attachInput.click();
4394
+ }
4395
+ function handlePaste(event) {
4396
+ var files = event.clipboardData && event.clipboardData.files;
4397
+ if (!files || files.length === 0) return;
4398
+ event.preventDefault();
4399
+ addFiles(files);
4400
+ env.setStatus("\u5DF2\u4ECE\u526A\u8D34\u677F\u6DFB\u52A0 " + files.length + " \u4EF6\u9644\u4EF6");
4401
+ }
4402
+ function isFileDrag(event) {
4403
+ var transfer = event.dataTransfer;
4404
+ if (!transfer) return false;
4405
+ var types = transfer.types;
4406
+ if (!types) return false;
4407
+ for (var i = 0; i < types.length; i += 1) {
4408
+ if (types[i] === "Files") return true;
4409
+ }
4410
+ return false;
4411
+ }
4412
+ function wireDrop() {
4413
+ var depth = 0;
4414
+ var panel = env.panel;
4415
+ if (!panel) return;
4416
+ function show() {
4417
+ panel.setAttribute("data-dropping", "1");
4418
+ }
4419
+ function hide() {
4420
+ depth = 0;
4421
+ panel.removeAttribute("data-dropping");
4422
+ }
4423
+ listen(panel, "dragenter", function(event) {
4424
+ if (!isFileDrag(event)) return;
4425
+ event.preventDefault();
4426
+ event.stopPropagation();
4427
+ depth += 1;
4428
+ show();
4429
+ });
4430
+ listen(panel, "dragover", function(event) {
4431
+ if (!isFileDrag(event)) return;
4432
+ event.preventDefault();
4433
+ event.stopPropagation();
4434
+ if (event.dataTransfer.dropEffect !== void 0) event.dataTransfer.dropEffect = "copy";
4435
+ });
4436
+ listen(panel, "dragleave", function(event) {
4437
+ if (!isFileDrag(event)) return;
4438
+ event.stopPropagation();
4439
+ depth -= 1;
4440
+ if (depth <= 0) hide();
4441
+ });
4442
+ listen(panel, "drop", function(event) {
4443
+ if (!isFileDrag(event)) return;
4444
+ event.preventDefault();
4445
+ event.stopPropagation();
4446
+ hide();
4447
+ var files = event.dataTransfer.files;
4448
+ if (files && files.length > 0) {
4449
+ addFiles(files);
4450
+ env.setStatus("\u5DF2\u6DFB\u52A0 " + files.length + " \u4EF6\u9644\u4EF6");
4451
+ }
4452
+ });
4453
+ }
4454
+ return {
4455
+ items,
4456
+ addFiles,
4457
+ remove,
4458
+ clear,
4459
+ retry,
4460
+ render,
4461
+ busy,
4462
+ payload,
4463
+ describe,
4464
+ note,
4465
+ pick,
4466
+ handlePaste,
4467
+ wireDrop
4468
+ };
4469
+ }
4470
+
3953
4471
  // src/client/bridge.js
3954
4472
  function bridgeBody() {
3955
4473
  if (window.__dshSelBridge) return;
@@ -4511,6 +5029,7 @@ function createPanelController(env) {
4511
5029
  env.state.turns = [];
4512
5030
  env.state.toolDigest = "";
4513
5031
  env.clearQuotes();
5032
+ env.clearAttachments();
4514
5033
  env.state.parts = cached && cached.parts || { translation: "", detail: "" };
4515
5034
  env.state.stage = "";
4516
5035
  env.state.raw = "";
@@ -6335,7 +6854,9 @@ function createHistoryController(env) {
6335
6854
  if (turn.role === "user") {
6336
6855
  var usig = (turn.text || "") + "|" + (turn.quotes || []).map(function(item) {
6337
6856
  return item.text;
6338
- }).join("\0");
6857
+ }).join("\0") + "|" + (turn.attachments || []).map(function(item) {
6858
+ return (item.kind || "") + ":" + String(item.ref && item.ref.attachmentId || "");
6859
+ }).join(",");
6339
6860
  if (node.__sig !== usig) {
6340
6861
  node.__sig = usig;
6341
6862
  fillUserBubble(node, turn);
@@ -6389,6 +6910,10 @@ function createHistoryController(env) {
6389
6910
  function fillUserBubble(node, turn) {
6390
6911
  node.textContent = "";
6391
6912
  var quotes = turn.quotes || [];
6913
+ var attachments = turn.attachments || [];
6914
+ if (attachments.length > 0) {
6915
+ node.appendChild(attachmentStrip(attachments));
6916
+ }
6392
6917
  if (quotes.length > 0) {
6393
6918
  var box = el("div", "dsh-sel-bq");
6394
6919
  for (var i = 0; i < quotes.length; i += 1) {
@@ -6400,7 +6925,57 @@ function createHistoryController(env) {
6400
6925
  }
6401
6926
  node.appendChild(box);
6402
6927
  }
6403
- if (turn.text) node.appendChild(el("div", null, turn.text));
6928
+ var display = displayTextOf(turn);
6929
+ if (display) node.appendChild(el("div", null, display));
6930
+ }
6931
+ function attachmentStrip(attachments) {
6932
+ var strip = el("div", "dsh-sel-bqattach");
6933
+ for (var i = 0; i < attachments.length; i += 1) {
6934
+ var item = attachments[i] || {};
6935
+ var ref = item.ref || {};
6936
+ if (item.kind === "image") {
6937
+ strip.appendChild(attachmentImage(ref));
6938
+ } else {
6939
+ strip.appendChild(attachmentFile(ref));
6940
+ }
6941
+ }
6942
+ return strip;
6943
+ }
6944
+ function attachmentImage(ref) {
6945
+ var link = document.createElement("a");
6946
+ link.className = "dsh-sel-bqimgwrap";
6947
+ link.href = attachmentUrl("image", ref);
6948
+ link.target = "_blank";
6949
+ link.rel = "noreferrer noopener";
6950
+ link.title = "\u70B9\u51FB\u67E5\u770B\u539F\u56FE" + (ref.name ? "\uFF1A" + ref.name : "");
6951
+ var img = document.createElement("img");
6952
+ img.className = "dsh-sel-bqimg";
6953
+ img.src = link.href;
6954
+ img.alt = ref.name || "\u56FE\u7247";
6955
+ img.loading = "lazy";
6956
+ img.addEventListener("error", function() {
6957
+ var box = el("span", "dsh-sel-bqimgfail", "\u56FE\u7247\u5DF2\u4E0D\u5728\u9644\u4EF6\u4ED3\u91CC");
6958
+ if (link.parentNode) link.parentNode.replaceChild(box, link);
6959
+ });
6960
+ link.appendChild(img);
6961
+ return link;
6962
+ }
6963
+ function attachmentFile(ref) {
6964
+ var link = document.createElement("a");
6965
+ link.className = "dsh-sel-bqfile";
6966
+ link.href = attachmentUrl("file", ref, true);
6967
+ link.rel = "noreferrer noopener";
6968
+ link.title = "\u4E0B\u8F7D\uFF1A" + (ref.name || "\u6587\u4EF6");
6969
+ link.appendChild(el("span", "dsh-sel-bqfileext", ref.name ? extensionOf(ref.name) : "FILE"));
6970
+ link.appendChild(el("span", "dsh-sel-bqfilename", ref.name || "\u6587\u4EF6"));
6971
+ if (typeof ref.bytes === "number") link.appendChild(el("span", "dsh-sel-bqfilemeta", formatBytes(ref.bytes)));
6972
+ return link;
6973
+ }
6974
+ function displayTextOf(turn) {
6975
+ var text = turn.text || "";
6976
+ if (!text) return "";
6977
+ if (!turn.attachments || turn.attachments.length === 0) return text;
6978
+ return text.replace(/^【本条消息带了附件】[^\n]*\n?/, "");
6404
6979
  }
6405
6980
  function bubbleFor(turn) {
6406
6981
  var bubble = el("div", "dsh-sel-bubble " + (turn.role === "user" ? "dsh-sel-bubble-user" : "dsh-sel-bubble-bot"));
@@ -6535,7 +7110,10 @@ function createConversationController(env) {
6535
7110
  env.askSend.setAttribute("aria-label", generating ? "\u505C\u6B62" : "\u53D1\u9001");
6536
7111
  env.askSend.title = generating ? "\u505C\u6B62\u751F\u6210\uFF08\u70B9\u4E00\u4E0B\u6253\u65AD\u8FD9\u6B21\u8F93\u51FA\uFF09" : "\u53D1\u9001\uFF08Enter\uFF09";
6537
7112
  }
6538
- env.askSend.disabled = generating ? false : !String(env.askBox.value || "").trim() && env.state.quotes.length === 0;
7113
+ var waitingAttachments = env.attachmentsBusy();
7114
+ env.askSend.disabled = generating ? false : waitingAttachments || !String(env.askBox.value || "").trim() && env.state.quotes.length === 0 && env.state.attachments.length === 0;
7115
+ var placeholder = env.state.attachments.length > 0 ? "\u5C31\u8FD9\u6BB5\u6587\u5B57\u7EE7\u7EED\u8FFD\u95EE\u2026\uFF08Enter \u53D1\u9001\uFF0CShift+Enter \u6362\u884C\uFF09" : "\u5C31\u8FD9\u6BB5\u6587\u5B57\u7EE7\u7EED\u8FFD\u95EE\u2026\uFF08\u53EF\u62D6\u5165\u56FE\u7247 / \u6587\u4EF6\uFF0C\u6216 \u2318V \u7C98\u8D34\uFF09";
7116
+ if (env.askBox.placeholder !== placeholder) env.askBox.placeholder = placeholder;
6539
7117
  }
6540
7118
  function stopCurrent() {
6541
7119
  if (env.state.askRequest) {
@@ -6568,7 +7146,12 @@ function createConversationController(env) {
6568
7146
  }
6569
7147
  var asked = String(question || "").trim();
6570
7148
  var quotes = env.state.quotes.slice();
6571
- if (!asked && quotes.length === 0 || !env.state.payload) return;
7149
+ var attachments = env.attachmentsPayload();
7150
+ if (!asked && quotes.length === 0 && attachments.length === 0 || !env.state.payload) return;
7151
+ if (env.attachmentsBusy()) {
7152
+ env.setStatus("\u9644\u4EF6\u8FD8\u5728\u4E0A\u4F20\uFF0C\u7A0D\u7B49\u4E00\u4E0B\u2026");
7153
+ return;
7154
+ }
6572
7155
  if (env.state.resolvingQuotes) {
6573
7156
  env.setStatus("\u6B63\u5728\u53D6\u5F15\u7528\u7684\u4E0A\u4E0B\u6587\u2026");
6574
7157
  return;
@@ -6582,24 +7165,31 @@ function createConversationController(env) {
6582
7165
  env.renderQuotes();
6583
7166
  refreshAskState();
6584
7167
  }
7168
+ var attachmentsNote = env.attachmentsDescribe();
7169
+ if (attachments.length > 0) {
7170
+ env.clearAttachments();
7171
+ refreshAskState();
7172
+ }
6585
7173
  if (quotes.length === 0) {
6586
- sendAsk(asked, quotes);
7174
+ sendAsk(asked, quotes, attachments, attachmentsNote);
6587
7175
  return;
6588
7176
  }
6589
7177
  env.state.resolvingQuotes = true;
6590
7178
  env.resolveQuoteContexts(quotes).then(function(resolved) {
6591
7179
  env.state.resolvingQuotes = false;
6592
- sendAsk(asked, resolved);
7180
+ sendAsk(asked, resolved, attachments, attachmentsNote);
6593
7181
  });
6594
7182
  }
6595
- function sendAsk(asked, quotes) {
7183
+ function sendAsk(asked, quotes, attachments, attachmentsNote) {
6596
7184
  var payload = env.state.payload;
6597
7185
  if (!payload) return;
7186
+ var withAttachments = attachments || [];
6598
7187
  var sent = env.composeQuestion(quotes, asked);
7188
+ if (attachmentsNote) sent = attachmentsNote + "\n" + sent;
6599
7189
  if (env.state.turns.length === 0 && (env.state.raw.trim() || env.state.parts.translation || env.state.parts.detail)) {
6600
7190
  env.state.turns.push({ role: "assistant", text: env.seedHistoryFromExplanation(), seed: true, hidden: true });
6601
7191
  }
6602
- env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent });
7192
+ env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments });
6603
7193
  var answerTurn = {
6604
7194
  role: "assistant",
6605
7195
  text: "",
@@ -6632,7 +7222,11 @@ function createConversationController(env) {
6632
7222
  var historyTurns = env.state.turns.slice(0, env.state.turns.length - 2);
6633
7223
  var history = historyTurns.map(function(turn, index) {
6634
7224
  var keepRaw = env.FOLD_HISTORY_KEEP_LATEST && index === historyTurns.length - 1;
6635
- return { role: turn.role, text: env.historyTextOf(turn, keepRaw) };
7225
+ return {
7226
+ role: turn.role,
7227
+ text: env.historyTextOf(turn, keepRaw),
7228
+ ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
7229
+ };
6636
7230
  });
6637
7231
  env.state.stopped = false;
6638
7232
  var askController = new AbortController();
@@ -6679,6 +7273,8 @@ function createConversationController(env) {
6679
7273
  sessionId: env.panelOrCurrentSessionId(),
6680
7274
  question: sent,
6681
7275
  history,
7276
+ // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
7277
+ ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
6682
7278
  // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
6683
7279
  ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
6684
7280
  ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
@@ -8561,6 +9157,14 @@ function apply(ctx) {
8561
9157
  * 只属于**下一条**提问(发送即清空);关掉面板不清 —— 回来接着写还看得见。
8562
9158
  */
8563
9159
  quotes: [],
9160
+ /**
9161
+ * 待发送的附件:[{ id, kind:'image'|'file', name, size, mediaType, file, url, status, ref, error }]。
9162
+ * 和引用同一条规矩 —— 只属于**下一条**追问,发送即清空;
9163
+ * 区别是上传状态挂在每一项上(逐件上传、逐件重试,见 attach.js)。
9164
+ */
9165
+ attachments: [],
9166
+ /** 附件条下面那行提示("文件夹不支持""一次最多 10 件"之类),空串就不显示。 */
9167
+ attachNote: "",
8564
9168
  /** 最近一次"可以引用"的选区:[{ text, label, rect, source }](浮标据此显示)。 */
8565
9169
  quoteSelection: null,
8566
9170
  /** 正在取引用的会话上下文(这期间再按 Enter 不能空发一条)。 */
@@ -8681,14 +9285,25 @@ function apply(ctx) {
8681
9285
  var askRow = el("div", "dsh-sel-ask");
8682
9286
  var quotesBox = el("div", "dsh-sel-quotes");
8683
9287
  askRow.appendChild(quotesBox);
9288
+ var attachBox = el("div", "dsh-sel-attach");
9289
+ askRow.appendChild(attachBox);
9290
+ var attachNote = el("p", "dsh-sel-attachnote");
9291
+ askRow.appendChild(attachNote);
9292
+ var attachInput = el("input");
9293
+ attachInput.type = "file";
9294
+ attachInput.multiple = true;
9295
+ attachInput.style.display = "none";
9296
+ attachInput.setAttribute("aria-hidden", "true");
9297
+ attachInput.tabIndex = -1;
9298
+ askRow.appendChild(attachInput);
8684
9299
  var askBox = el("textarea", "dsh-sel-askbox");
8685
9300
  askBox.rows = 1;
8686
9301
  askBox.placeholder = "\u5C31\u8FD9\u6BB5\u6587\u5B57\u7EE7\u7EED\u8FFD\u95EE\u2026\uFF08Enter \u53D1\u9001\uFF0CShift+Enter \u6362\u884C\uFF09";
8687
9302
  var webMode = el("span", "dsh-sel-pref");
8688
- webMode.appendChild(el("span", "dsh-sel-preftext", "\u8F93\u51FA\u504F\u597D"));
9303
+ webMode.title = "\u8F93\u51FA\u504F\u597D\uFF1AMarkdown\uFF08\u9ED8\u8BA4\u6587\u5B57\u56DE\u7B54\uFF09/ \u7F51\u9875\uFF08\u590D\u6742\u95EE\u9898\u751F\u6210 HTML \u9875\u9762\uFF09";
8689
9304
  var prefSeg = el("span", "dsh-sel-seg");
8690
9305
  prefSeg.setAttribute("role", "radiogroup");
8691
- prefSeg.setAttribute("aria-label", "\u8F93\u51FA\u504F\u597D");
9306
+ prefSeg.setAttribute("aria-label", "\u8F93\u51FA\u504F\u597D\uFF1AMarkdown \u6216\u7F51\u9875");
8692
9307
  var prefPill = el("span", "dsh-sel-segpill");
8693
9308
  var cellMd = el("button", "dsh-sel-segcell");
8694
9309
  cellMd.type = "button";
@@ -8769,7 +9384,13 @@ function apply(ctx) {
8769
9384
  captureRow.appendChild(voiceActionSlot);
8770
9385
  captureRow.appendChild(voiceStop);
8771
9386
  var askTools = el("div", "dsh-sel-asktools");
9387
+ var addButton = el("button", "dsh-sel-iconbtn dsh-sel-add");
9388
+ addButton.type = "button";
9389
+ addButton.setAttribute("aria-label", "\u6DFB\u52A0\u56FE\u7247\u6216\u6587\u4EF6");
9390
+ addButton.title = "\u6DFB\u52A0\u56FE\u7247\u6216\u6587\u4EF6\uFF08\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7C98\u8D34\u6216\u62D6\u8FDB\u6765\uFF09";
9391
+ addButton.appendChild(plusIcon());
8772
9392
  var askSpacer = el("span", "dsh-sel-askspace");
9393
+ askTools.appendChild(addButton);
8773
9394
  askTools.appendChild(webMode);
8774
9395
  askTools.appendChild(askSpacer);
8775
9396
  askTools.appendChild(modelPill);
@@ -8833,6 +9454,14 @@ function apply(ctx) {
8833
9454
  panel.appendChild(head);
8834
9455
  panel.appendChild(body);
8835
9456
  panel.appendChild(askRow);
9457
+ var dropMask = el("div", "dsh-sel-drop");
9458
+ dropMask.setAttribute("aria-hidden", "true");
9459
+ var dropArt = el("div", "dsh-sel-drop-art");
9460
+ dropArt.appendChild(plusIcon());
9461
+ dropMask.appendChild(dropArt);
9462
+ dropMask.appendChild(el("div", "dsh-sel-drop-title", "\u677E\u5F00\u5373\u6DFB\u52A0\u4E3A\u8FD9\u6761\u8FFD\u95EE\u7684\u9644\u4EF6"));
9463
+ dropMask.appendChild(el("div", "dsh-sel-drop-hint", "\u56FE\u7247\u76F4\u63A5\u7ED9\u6A21\u578B\u770B \xB7 \u6587\u4EF6\u5B58\u6210\u53EA\u8BFB\u8DEF\u5F84\u6309\u9700\u8BFB\u53D6"));
9464
+ panel.appendChild(dropMask);
8836
9465
  layer.appendChild(panel);
8837
9466
  var settingsController = createSettingsController({
8838
9467
  get state() {
@@ -9121,6 +9750,9 @@ function apply(ctx) {
9121
9750
  get clearQuotes() {
9122
9751
  return quotesController.clearQuotes;
9123
9752
  },
9753
+ get clearAttachments() {
9754
+ return attachController.clear;
9755
+ },
9124
9756
  get renderTurns() {
9125
9757
  return historyController.renderTurns;
9126
9758
  },
@@ -9514,6 +10146,18 @@ function apply(ctx) {
9514
10146
  get composeQuestion() {
9515
10147
  return quotesController.composeQuestion;
9516
10148
  },
10149
+ get attachmentsPayload() {
10150
+ return attachController.payload;
10151
+ },
10152
+ get attachmentsBusy() {
10153
+ return attachController.busy;
10154
+ },
10155
+ get attachmentsDescribe() {
10156
+ return attachController.describe;
10157
+ },
10158
+ get clearAttachments() {
10159
+ return attachController.clear;
10160
+ },
9517
10161
  get seedHistoryFromExplanation() {
9518
10162
  return historyController.seedHistoryFromExplanation;
9519
10163
  },
@@ -9626,6 +10270,29 @@ function apply(ctx) {
9626
10270
  return panelController.buildPayload;
9627
10271
  }
9628
10272
  });
10273
+ var attachController = createAttachController({
10274
+ get state() {
10275
+ return state;
10276
+ },
10277
+ get panel() {
10278
+ return panel;
10279
+ },
10280
+ get attachBox() {
10281
+ return attachBox;
10282
+ },
10283
+ get attachNote() {
10284
+ return attachNote;
10285
+ },
10286
+ get attachInput() {
10287
+ return attachInput;
10288
+ },
10289
+ get setStatus() {
10290
+ return panelController.setStatus;
10291
+ },
10292
+ get refreshAskState() {
10293
+ return conversationController.refreshAskState;
10294
+ }
10295
+ });
9629
10296
  var modelsController = createModelsController({
9630
10297
  get state() {
9631
10298
  return state;
@@ -10158,7 +10825,7 @@ function apply(ctx) {
10158
10825
  event.preventDefault();
10159
10826
  event.stopPropagation();
10160
10827
  var question = askBox.value;
10161
- if (!question.trim() && state.quotes.length === 0) return;
10828
+ if (!question.trim() && state.quotes.length === 0 && state.attachments.length === 0) return;
10162
10829
  conversationController.ask(question);
10163
10830
  });
10164
10831
  var offAskInput = listen(askBox, "input", function() {
@@ -10171,9 +10838,21 @@ function apply(ctx) {
10171
10838
  return;
10172
10839
  }
10173
10840
  var question = askBox.value;
10174
- if (!question.trim() && state.quotes.length === 0) return;
10841
+ if (!question.trim() && state.quotes.length === 0 && state.attachments.length === 0) return;
10175
10842
  conversationController.ask(question);
10176
10843
  });
10844
+ var offAttachPick = listen(attachInput, "change", function() {
10845
+ attachController.addFiles(attachInput.files || []);
10846
+ attachInput.value = "";
10847
+ });
10848
+ var offAttachButton = listen(addButton, "click", function(event) {
10849
+ event.stopPropagation();
10850
+ attachController.pick();
10851
+ });
10852
+ var offAttachPaste = listen(panel, "paste", function(event) {
10853
+ attachController.handlePaste(event);
10854
+ });
10855
+ attachController.wireDrop();
10177
10856
  var offMic = listen(micButton, "click", function(event) {
10178
10857
  event.stopPropagation();
10179
10858
  voiceController.startVoice();
@@ -10769,6 +11448,9 @@ function apply(ctx) {
10769
11448
  offAskKey();
10770
11449
  offAskInput();
10771
11450
  offAskSend();
11451
+ offAttachPick();
11452
+ offAttachButton();
11453
+ offAttachPaste();
10772
11454
  offMic();
10773
11455
  offVoiceCancel();
10774
11456
  offVoiceStop();