@yfwu2020/dsh-selection-explain 0.10.6 → 0.11.1

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";
@@ -441,12 +445,97 @@ var CSS = [
441
445
  "background:transparent;color:var(--dsw-alias-label-secondary,inherit);opacity:.5;font:inherit;font-size:11px;",
442
446
  "line-height:1;display:inline-flex;align-items:center;justify-content:center}",
443
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}",
444
511
  // 气泡里的引用块(用户消息):引用在上、问题在下 —— 和发出去给模型的那份顺序一致
445
512
  ".dsh-sel-bq{display:flex;flex-direction:column;gap:3px;margin-bottom:6px}",
446
513
  ".dsh-sel-bqitem{padding:3px 8px;border-radius:8px;border-left:2px solid color-mix(in srgb,var(--sel-a1,#0d9488) 45%,transparent);",
447
514
  "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14));font-size:11.5px;line-height:1.55;",
448
515
  "opacity:.82;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
449
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}",
450
539
  // 「引用整条」:贴在助手气泡末尾,**平时不出现**(正文优先),鼠标移到这条消息上才浮出来。
451
540
  // 键盘 Tab 也能到(:focus-visible 同样点亮),触屏上另有浮标那条路。
452
541
  ".dsh-sel-bubquote{margin-top:7px;display:inline-flex;align-items:center;gap:4px;padding:2px 9px;",
@@ -1128,6 +1217,21 @@ function micIcon() {
1128
1217
  svg.appendChild(stem);
1129
1218
  return svg;
1130
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
+ }
1131
1235
  function sendIcon() {
1132
1236
  var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1133
1237
  svg.setAttribute("viewBox", "0 0 16 16");
@@ -1178,6 +1282,34 @@ function quoteIcon() {
1178
1282
  }
1179
1283
  return svg;
1180
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
+ }
1181
1313
 
1182
1314
  // src/client/index.js
1183
1315
  var import_react = __toESM(require("react"), 1);
@@ -3935,7 +4067,12 @@ function createQuotesController(env) {
3935
4067
  function exportTurns() {
3936
4068
  var out = [];
3937
4069
  for (var i = 0; i < env.state.turns.length; i += 1) {
3938
- 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
+ });
3939
4076
  }
3940
4077
  return out;
3941
4078
  }
@@ -3954,6 +4091,383 @@ function createQuotesController(env) {
3954
4091
  };
3955
4092
  }
3956
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
+
3957
4471
  // src/client/bridge.js
3958
4472
  function bridgeBody() {
3959
4473
  if (window.__dshSelBridge) return;
@@ -4501,7 +5015,7 @@ function createPanelController(env) {
4501
5015
  env.state.selection = null;
4502
5016
  }
4503
5017
  function openPanelWithInner(text, context, label, anchor, keyContext, options) {
4504
- env.state.voiceAsk = !!(options && options.askFirst);
5018
+ env.state.skipInterpretation = !!(options && options.askFirst);
4505
5019
  env.panelSessionId = currentSessionId();
4506
5020
  env.state.payload = buildPayload(text, context, label);
4507
5021
  env.state.kind = env.state.payload.kind;
@@ -4515,6 +5029,7 @@ function createPanelController(env) {
4515
5029
  env.state.turns = [];
4516
5030
  env.state.toolDigest = "";
4517
5031
  env.clearQuotes();
5032
+ env.clearAttachments();
4518
5033
  env.state.parts = cached && cached.parts || { translation: "", detail: "" };
4519
5034
  env.state.stage = "";
4520
5035
  env.state.raw = "";
@@ -4535,6 +5050,7 @@ function createPanelController(env) {
4535
5050
  }
4536
5051
  env.renderTurns();
4537
5052
  if (cached) {
5053
+ env.state.skipInterpretation = false;
4538
5054
  env.state.turns = (cached.turns || []).slice();
4539
5055
  env.state.phase = "done";
4540
5056
  env.state.stage = "";
@@ -4825,7 +5341,7 @@ function createPanelController(env) {
4825
5341
  for (var i = 0; i < env.state.turns.length; i += 1) {
4826
5342
  if (env.state.turns[i].role === "user") asked = true;
4827
5343
  }
4828
- env.expandButton.style.display = !hasDetail && translationReady && !asked && !env.state.voiceAsk ? "" : "none";
5344
+ env.expandButton.style.display = !hasDetail && translationReady && !asked && !env.state.skipInterpretation ? "" : "none";
4829
5345
  env.expandButton.disabled = false;
4830
5346
  return asked;
4831
5347
  }
@@ -4845,9 +5361,17 @@ function createPanelController(env) {
4845
5361
  var stage = env.state.stage;
4846
5362
  var selected = env.state.payload && env.state.payload.text ? env.state.payload.text : env.state.selection ? env.state.selection.text : "";
4847
5363
  var sections = splitSections(env.visibleRaw());
4848
- env.translationSection.root.style.display = env.state.voiceAsk ? "none" : "";
4849
5364
  var live = stripEchoPrefix(stage === "detail" ? "" : sections.translation || sections.other, selected);
4850
5365
  var liveDetail = (sections.detail || sections.other || "").trim();
5366
+ var askedSoFar = false;
5367
+ for (var qi = 0; qi < env.state.turns.length; qi += 1) {
5368
+ if (env.state.turns[qi].role === "user") {
5369
+ askedSoFar = true;
5370
+ break;
5371
+ }
5372
+ }
5373
+ var emptyInterpretation = !env.state.parts.translation && !env.state.parts.detail && !live.trim();
5374
+ env.translationSection.root.style.display = env.state.skipInterpretation || emptyInterpretation && askedSoFar ? "none" : "";
4851
5375
  if (env.state.parts.translation) {
4852
5376
  renderRich(env.translationSection.content, stripEchoPrefix(env.state.parts.translation, selected), { callout: true, settled: true });
4853
5377
  } else if (stage !== "detail" && live.trim()) {
@@ -4908,7 +5432,7 @@ function createPanelController(env) {
4908
5432
  var surfaceTheme = env.currentSurfaceTheme();
4909
5433
  env.layer.setAttribute("data-theme", surfaceTheme);
4910
5434
  env.panel.setAttribute("data-theme", surfaceTheme);
4911
- env.askRow.style.display = wide || translationReady || env.state.voiceAsk || env.state.quotes.length > 0 ? "" : "none";
5435
+ env.askRow.style.display = wide || translationReady || env.state.skipInterpretation || env.state.quotes.length > 0 ? "" : "none";
4912
5436
  syncExpandCta();
4913
5437
  env.closeButton.title = "\u5173\u95ED\uFF08Esc\uFF09";
4914
5438
  if (env.state.phase !== "error" && env.state.phase !== "paused" && env.retryButton.parentNode) {
@@ -5962,7 +6486,11 @@ function createHistoryController(env) {
5962
6486
  parts: env.state.parts,
5963
6487
  turns,
5964
6488
  toolDigest: env.state.toolDigest,
5965
- pinned: !!(options && options.pinned)
6489
+ pinned: !!(options && options.pinned),
6490
+ // 这段对话有没有解读,必须跟着条目落盘:回放时靠它决定画不画「翻译」一节。
6491
+ // 不落盘的话,语音卡那条"跳过翻译与详解、直接追问"的对话下次开出来,
6492
+ // 会顶着一张空翻译卡片("这次没有返回内容")——用户实测。
6493
+ skipped: env.state.skipInterpretation === true
5966
6494
  })
5967
6495
  }).catch(function() {
5968
6496
  });
@@ -5972,6 +6500,16 @@ function createHistoryController(env) {
5972
6500
  function applyHistoryEntry(entry, source) {
5973
6501
  var parts = entry.parts || {};
5974
6502
  env.state.parts = { translation: parts.translation || "", detail: parts.detail || "" };
6503
+ var replayTurns = Array.isArray(entry.turns) ? entry.turns : [];
6504
+ var hasAskTurn = false;
6505
+ for (var step = 0; step < replayTurns.length; step += 1) {
6506
+ if (replayTurns[step] && replayTurns[step].role === "user") {
6507
+ hasAskTurn = true;
6508
+ break;
6509
+ }
6510
+ }
6511
+ var noInterpretation = !String(parts.translation || "").trim() && !String(parts.detail || "").trim();
6512
+ env.state.skipInterpretation = entry.skipped === true || entry.skipped === void 0 && noInterpretation && hasAskTurn;
5975
6513
  env.state.turns = Array.isArray(entry.turns) ? entry.turns.slice() : [];
5976
6514
  env.clearQuotes();
5977
6515
  env.state.toolDigest = typeof entry.toolDigest === "string" ? entry.toolDigest : "";
@@ -6339,7 +6877,9 @@ function createHistoryController(env) {
6339
6877
  if (turn.role === "user") {
6340
6878
  var usig = (turn.text || "") + "|" + (turn.quotes || []).map(function(item) {
6341
6879
  return item.text;
6342
- }).join("\0");
6880
+ }).join("\0") + "|" + (turn.attachments || []).map(function(item) {
6881
+ return (item.kind || "") + ":" + String(item.ref && item.ref.attachmentId || "");
6882
+ }).join(",");
6343
6883
  if (node.__sig !== usig) {
6344
6884
  node.__sig = usig;
6345
6885
  fillUserBubble(node, turn);
@@ -6393,6 +6933,10 @@ function createHistoryController(env) {
6393
6933
  function fillUserBubble(node, turn) {
6394
6934
  node.textContent = "";
6395
6935
  var quotes = turn.quotes || [];
6936
+ var attachments = turn.attachments || [];
6937
+ if (attachments.length > 0) {
6938
+ node.appendChild(attachmentStrip(attachments));
6939
+ }
6396
6940
  if (quotes.length > 0) {
6397
6941
  var box = el("div", "dsh-sel-bq");
6398
6942
  for (var i = 0; i < quotes.length; i += 1) {
@@ -6404,7 +6948,57 @@ function createHistoryController(env) {
6404
6948
  }
6405
6949
  node.appendChild(box);
6406
6950
  }
6407
- if (turn.text) node.appendChild(el("div", null, turn.text));
6951
+ var display = displayTextOf(turn);
6952
+ if (display) node.appendChild(el("div", null, display));
6953
+ }
6954
+ function attachmentStrip(attachments) {
6955
+ var strip = el("div", "dsh-sel-bqattach");
6956
+ for (var i = 0; i < attachments.length; i += 1) {
6957
+ var item = attachments[i] || {};
6958
+ var ref = item.ref || {};
6959
+ if (item.kind === "image") {
6960
+ strip.appendChild(attachmentImage(ref));
6961
+ } else {
6962
+ strip.appendChild(attachmentFile(ref));
6963
+ }
6964
+ }
6965
+ return strip;
6966
+ }
6967
+ function attachmentImage(ref) {
6968
+ var link = document.createElement("a");
6969
+ link.className = "dsh-sel-bqimgwrap";
6970
+ link.href = attachmentUrl("image", ref);
6971
+ link.target = "_blank";
6972
+ link.rel = "noreferrer noopener";
6973
+ link.title = "\u70B9\u51FB\u67E5\u770B\u539F\u56FE" + (ref.name ? "\uFF1A" + ref.name : "");
6974
+ var img = document.createElement("img");
6975
+ img.className = "dsh-sel-bqimg";
6976
+ img.src = link.href;
6977
+ img.alt = ref.name || "\u56FE\u7247";
6978
+ img.loading = "lazy";
6979
+ img.addEventListener("error", function() {
6980
+ var box = el("span", "dsh-sel-bqimgfail", "\u56FE\u7247\u5DF2\u4E0D\u5728\u9644\u4EF6\u4ED3\u91CC");
6981
+ if (link.parentNode) link.parentNode.replaceChild(box, link);
6982
+ });
6983
+ link.appendChild(img);
6984
+ return link;
6985
+ }
6986
+ function attachmentFile(ref) {
6987
+ var link = document.createElement("a");
6988
+ link.className = "dsh-sel-bqfile";
6989
+ link.href = attachmentUrl("file", ref, true);
6990
+ link.rel = "noreferrer noopener";
6991
+ link.title = "\u4E0B\u8F7D\uFF1A" + (ref.name || "\u6587\u4EF6");
6992
+ link.appendChild(el("span", "dsh-sel-bqfileext", ref.name ? extensionOf(ref.name) : "FILE"));
6993
+ link.appendChild(el("span", "dsh-sel-bqfilename", ref.name || "\u6587\u4EF6"));
6994
+ if (typeof ref.bytes === "number") link.appendChild(el("span", "dsh-sel-bqfilemeta", formatBytes(ref.bytes)));
6995
+ return link;
6996
+ }
6997
+ function displayTextOf(turn) {
6998
+ var text = turn.text || "";
6999
+ if (!text) return "";
7000
+ if (!turn.attachments || turn.attachments.length === 0) return text;
7001
+ return text.replace(/^【本条消息带了附件】[^\n]*\n?/, "");
6408
7002
  }
6409
7003
  function bubbleFor(turn) {
6410
7004
  var bubble = el("div", "dsh-sel-bubble " + (turn.role === "user" ? "dsh-sel-bubble-user" : "dsh-sel-bubble-bot"));
@@ -6539,7 +7133,10 @@ function createConversationController(env) {
6539
7133
  env.askSend.setAttribute("aria-label", generating ? "\u505C\u6B62" : "\u53D1\u9001");
6540
7134
  env.askSend.title = generating ? "\u505C\u6B62\u751F\u6210\uFF08\u70B9\u4E00\u4E0B\u6253\u65AD\u8FD9\u6B21\u8F93\u51FA\uFF09" : "\u53D1\u9001\uFF08Enter\uFF09";
6541
7135
  }
6542
- env.askSend.disabled = generating ? false : !String(env.askBox.value || "").trim() && env.state.quotes.length === 0;
7136
+ var waitingAttachments = env.attachmentsBusy();
7137
+ env.askSend.disabled = generating ? false : waitingAttachments || !String(env.askBox.value || "").trim() && env.state.quotes.length === 0 && env.state.attachments.length === 0;
7138
+ 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";
7139
+ if (env.askBox.placeholder !== placeholder) env.askBox.placeholder = placeholder;
6543
7140
  }
6544
7141
  function stopCurrent() {
6545
7142
  if (env.state.askRequest) {
@@ -6572,7 +7169,12 @@ function createConversationController(env) {
6572
7169
  }
6573
7170
  var asked = String(question || "").trim();
6574
7171
  var quotes = env.state.quotes.slice();
6575
- if (!asked && quotes.length === 0 || !env.state.payload) return;
7172
+ var attachments = env.attachmentsPayload();
7173
+ if (!asked && quotes.length === 0 && attachments.length === 0 || !env.state.payload) return;
7174
+ if (env.attachmentsBusy()) {
7175
+ env.setStatus("\u9644\u4EF6\u8FD8\u5728\u4E0A\u4F20\uFF0C\u7A0D\u7B49\u4E00\u4E0B\u2026");
7176
+ return;
7177
+ }
6576
7178
  if (env.state.resolvingQuotes) {
6577
7179
  env.setStatus("\u6B63\u5728\u53D6\u5F15\u7528\u7684\u4E0A\u4E0B\u6587\u2026");
6578
7180
  return;
@@ -6586,24 +7188,31 @@ function createConversationController(env) {
6586
7188
  env.renderQuotes();
6587
7189
  refreshAskState();
6588
7190
  }
7191
+ var attachmentsNote = env.attachmentsDescribe();
7192
+ if (attachments.length > 0) {
7193
+ env.clearAttachments();
7194
+ refreshAskState();
7195
+ }
6589
7196
  if (quotes.length === 0) {
6590
- sendAsk(asked, quotes);
7197
+ sendAsk(asked, quotes, attachments, attachmentsNote);
6591
7198
  return;
6592
7199
  }
6593
7200
  env.state.resolvingQuotes = true;
6594
7201
  env.resolveQuoteContexts(quotes).then(function(resolved) {
6595
7202
  env.state.resolvingQuotes = false;
6596
- sendAsk(asked, resolved);
7203
+ sendAsk(asked, resolved, attachments, attachmentsNote);
6597
7204
  });
6598
7205
  }
6599
- function sendAsk(asked, quotes) {
7206
+ function sendAsk(asked, quotes, attachments, attachmentsNote) {
6600
7207
  var payload = env.state.payload;
6601
7208
  if (!payload) return;
7209
+ var withAttachments = attachments || [];
6602
7210
  var sent = env.composeQuestion(quotes, asked);
7211
+ if (attachmentsNote) sent = attachmentsNote + "\n" + sent;
6603
7212
  if (env.state.turns.length === 0 && (env.state.raw.trim() || env.state.parts.translation || env.state.parts.detail)) {
6604
7213
  env.state.turns.push({ role: "assistant", text: env.seedHistoryFromExplanation(), seed: true, hidden: true });
6605
7214
  }
6606
- env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent });
7215
+ env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments });
6607
7216
  var answerTurn = {
6608
7217
  role: "assistant",
6609
7218
  text: "",
@@ -6636,7 +7245,11 @@ function createConversationController(env) {
6636
7245
  var historyTurns = env.state.turns.slice(0, env.state.turns.length - 2);
6637
7246
  var history = historyTurns.map(function(turn, index) {
6638
7247
  var keepRaw = env.FOLD_HISTORY_KEEP_LATEST && index === historyTurns.length - 1;
6639
- return { role: turn.role, text: env.historyTextOf(turn, keepRaw) };
7248
+ return {
7249
+ role: turn.role,
7250
+ text: env.historyTextOf(turn, keepRaw),
7251
+ ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
7252
+ };
6640
7253
  });
6641
7254
  env.state.stopped = false;
6642
7255
  var askController = new AbortController();
@@ -6683,6 +7296,8 @@ function createConversationController(env) {
6683
7296
  sessionId: env.panelOrCurrentSessionId(),
6684
7297
  question: sent,
6685
7298
  history,
7299
+ // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
7300
+ ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
6686
7301
  // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
6687
7302
  ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
6688
7303
  ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
@@ -8556,6 +9171,14 @@ function apply(ctx) {
8556
9171
  payload: null,
8557
9172
  /** 已生成的两节内容(分阶段产出)。 */
8558
9173
  parts: { translation: "", detail: "" },
9174
+ /**
9175
+ * 这段对话**没有解读**(语音卡那条"跳过翻译与详解、直接追问"的路):
9176
+ * 翻译节整节不画,追问输入框直接可见。
9177
+ *
9178
+ * 语义单位是"一段对话"而不是"一次开窗":落盘时写进历史条目的 skipped,
9179
+ * 回放/缓存命中时按它复原(否则同一个词再开一次,空翻译卡片又冒出来 —— 用户实测)。
9180
+ */
9181
+ skipInterpretation: false,
8559
9182
  /** 当前阶段:'translation' | 'detail' | 'chat'。 */
8560
9183
  stage: "",
8561
9184
  /** 追问轮次:[{ role: 'user' | 'assistant', text }],只活在面板/内存里(临时)。 */
@@ -8565,6 +9188,14 @@ function apply(ctx) {
8565
9188
  * 只属于**下一条**提问(发送即清空);关掉面板不清 —— 回来接着写还看得见。
8566
9189
  */
8567
9190
  quotes: [],
9191
+ /**
9192
+ * 待发送的附件:[{ id, kind:'image'|'file', name, size, mediaType, file, url, status, ref, error }]。
9193
+ * 和引用同一条规矩 —— 只属于**下一条**追问,发送即清空;
9194
+ * 区别是上传状态挂在每一项上(逐件上传、逐件重试,见 attach.js)。
9195
+ */
9196
+ attachments: [],
9197
+ /** 附件条下面那行提示("文件夹不支持""一次最多 10 件"之类),空串就不显示。 */
9198
+ attachNote: "",
8568
9199
  /** 最近一次"可以引用"的选区:[{ text, label, rect, source }](浮标据此显示)。 */
8569
9200
  quoteSelection: null,
8570
9201
  /** 正在取引用的会话上下文(这期间再按 Enter 不能空发一条)。 */
@@ -8685,14 +9316,25 @@ function apply(ctx) {
8685
9316
  var askRow = el("div", "dsh-sel-ask");
8686
9317
  var quotesBox = el("div", "dsh-sel-quotes");
8687
9318
  askRow.appendChild(quotesBox);
9319
+ var attachBox = el("div", "dsh-sel-attach");
9320
+ askRow.appendChild(attachBox);
9321
+ var attachNote = el("p", "dsh-sel-attachnote");
9322
+ askRow.appendChild(attachNote);
9323
+ var attachInput = el("input");
9324
+ attachInput.type = "file";
9325
+ attachInput.multiple = true;
9326
+ attachInput.style.display = "none";
9327
+ attachInput.setAttribute("aria-hidden", "true");
9328
+ attachInput.tabIndex = -1;
9329
+ askRow.appendChild(attachInput);
8688
9330
  var askBox = el("textarea", "dsh-sel-askbox");
8689
9331
  askBox.rows = 1;
8690
9332
  askBox.placeholder = "\u5C31\u8FD9\u6BB5\u6587\u5B57\u7EE7\u7EED\u8FFD\u95EE\u2026\uFF08Enter \u53D1\u9001\uFF0CShift+Enter \u6362\u884C\uFF09";
8691
9333
  var webMode = el("span", "dsh-sel-pref");
8692
- webMode.appendChild(el("span", "dsh-sel-preftext", "\u8F93\u51FA\u504F\u597D"));
9334
+ 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";
8693
9335
  var prefSeg = el("span", "dsh-sel-seg");
8694
9336
  prefSeg.setAttribute("role", "radiogroup");
8695
- prefSeg.setAttribute("aria-label", "\u8F93\u51FA\u504F\u597D");
9337
+ prefSeg.setAttribute("aria-label", "\u8F93\u51FA\u504F\u597D\uFF1AMarkdown \u6216\u7F51\u9875");
8696
9338
  var prefPill = el("span", "dsh-sel-segpill");
8697
9339
  var cellMd = el("button", "dsh-sel-segcell");
8698
9340
  cellMd.type = "button";
@@ -8773,7 +9415,13 @@ function apply(ctx) {
8773
9415
  captureRow.appendChild(voiceActionSlot);
8774
9416
  captureRow.appendChild(voiceStop);
8775
9417
  var askTools = el("div", "dsh-sel-asktools");
9418
+ var addButton = el("button", "dsh-sel-iconbtn dsh-sel-add");
9419
+ addButton.type = "button";
9420
+ addButton.setAttribute("aria-label", "\u6DFB\u52A0\u56FE\u7247\u6216\u6587\u4EF6");
9421
+ addButton.title = "\u6DFB\u52A0\u56FE\u7247\u6216\u6587\u4EF6\uFF08\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7C98\u8D34\u6216\u62D6\u8FDB\u6765\uFF09";
9422
+ addButton.appendChild(plusIcon());
8776
9423
  var askSpacer = el("span", "dsh-sel-askspace");
9424
+ askTools.appendChild(addButton);
8777
9425
  askTools.appendChild(webMode);
8778
9426
  askTools.appendChild(askSpacer);
8779
9427
  askTools.appendChild(modelPill);
@@ -8837,6 +9485,14 @@ function apply(ctx) {
8837
9485
  panel.appendChild(head);
8838
9486
  panel.appendChild(body);
8839
9487
  panel.appendChild(askRow);
9488
+ var dropMask = el("div", "dsh-sel-drop");
9489
+ dropMask.setAttribute("aria-hidden", "true");
9490
+ var dropArt = el("div", "dsh-sel-drop-art");
9491
+ dropArt.appendChild(plusIcon());
9492
+ dropMask.appendChild(dropArt);
9493
+ dropMask.appendChild(el("div", "dsh-sel-drop-title", "\u677E\u5F00\u5373\u6DFB\u52A0\u4E3A\u8FD9\u6761\u8FFD\u95EE\u7684\u9644\u4EF6"));
9494
+ 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"));
9495
+ panel.appendChild(dropMask);
8840
9496
  layer.appendChild(panel);
8841
9497
  var settingsController = createSettingsController({
8842
9498
  get state() {
@@ -9125,6 +9781,9 @@ function apply(ctx) {
9125
9781
  get clearQuotes() {
9126
9782
  return quotesController.clearQuotes;
9127
9783
  },
9784
+ get clearAttachments() {
9785
+ return attachController.clear;
9786
+ },
9128
9787
  get renderTurns() {
9129
9788
  return historyController.renderTurns;
9130
9789
  },
@@ -9518,6 +10177,18 @@ function apply(ctx) {
9518
10177
  get composeQuestion() {
9519
10178
  return quotesController.composeQuestion;
9520
10179
  },
10180
+ get attachmentsPayload() {
10181
+ return attachController.payload;
10182
+ },
10183
+ get attachmentsBusy() {
10184
+ return attachController.busy;
10185
+ },
10186
+ get attachmentsDescribe() {
10187
+ return attachController.describe;
10188
+ },
10189
+ get clearAttachments() {
10190
+ return attachController.clear;
10191
+ },
9521
10192
  get seedHistoryFromExplanation() {
9522
10193
  return historyController.seedHistoryFromExplanation;
9523
10194
  },
@@ -9630,6 +10301,29 @@ function apply(ctx) {
9630
10301
  return panelController.buildPayload;
9631
10302
  }
9632
10303
  });
10304
+ var attachController = createAttachController({
10305
+ get state() {
10306
+ return state;
10307
+ },
10308
+ get panel() {
10309
+ return panel;
10310
+ },
10311
+ get attachBox() {
10312
+ return attachBox;
10313
+ },
10314
+ get attachNote() {
10315
+ return attachNote;
10316
+ },
10317
+ get attachInput() {
10318
+ return attachInput;
10319
+ },
10320
+ get setStatus() {
10321
+ return panelController.setStatus;
10322
+ },
10323
+ get refreshAskState() {
10324
+ return conversationController.refreshAskState;
10325
+ }
10326
+ });
9633
10327
  var modelsController = createModelsController({
9634
10328
  get state() {
9635
10329
  return state;
@@ -10162,7 +10856,7 @@ function apply(ctx) {
10162
10856
  event.preventDefault();
10163
10857
  event.stopPropagation();
10164
10858
  var question = askBox.value;
10165
- if (!question.trim() && state.quotes.length === 0) return;
10859
+ if (!question.trim() && state.quotes.length === 0 && state.attachments.length === 0) return;
10166
10860
  conversationController.ask(question);
10167
10861
  });
10168
10862
  var offAskInput = listen(askBox, "input", function() {
@@ -10175,9 +10869,21 @@ function apply(ctx) {
10175
10869
  return;
10176
10870
  }
10177
10871
  var question = askBox.value;
10178
- if (!question.trim() && state.quotes.length === 0) return;
10872
+ if (!question.trim() && state.quotes.length === 0 && state.attachments.length === 0) return;
10179
10873
  conversationController.ask(question);
10180
10874
  });
10875
+ var offAttachPick = listen(attachInput, "change", function() {
10876
+ attachController.addFiles(attachInput.files || []);
10877
+ attachInput.value = "";
10878
+ });
10879
+ var offAttachButton = listen(addButton, "click", function(event) {
10880
+ event.stopPropagation();
10881
+ attachController.pick();
10882
+ });
10883
+ var offAttachPaste = listen(panel, "paste", function(event) {
10884
+ attachController.handlePaste(event);
10885
+ });
10886
+ attachController.wireDrop();
10181
10887
  var offMic = listen(micButton, "click", function(event) {
10182
10888
  event.stopPropagation();
10183
10889
  voiceController.startVoice();
@@ -10773,6 +11479,9 @@ function apply(ctx) {
10773
11479
  offAskKey();
10774
11480
  offAskInput();
10775
11481
  offAskSend();
11482
+ offAttachPick();
11483
+ offAttachButton();
11484
+ offAttachPaste();
10776
11485
  offMic();
10777
11486
  offVoiceCancel();
10778
11487
  offVoiceStop();