dsh-plugin-image-tools 0.3.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 ADDED
@@ -0,0 +1,863 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-plugin-image-tools',
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
7
+ var react_jsx_runtime = require('react/jsx-runtime');
8
+ var react = require('react');
9
+ var jsx = react_jsx_runtime.jsx;
10
+ var jsxs = react_jsx_runtime.jsxs;
11
+ var Fragment = react_jsx_runtime.Fragment;
12
+
13
+ // ------------------------------------------------------------------
14
+ // 样式:一次注入 <style>,类名 dshpick-*,全部走主题 CSS 变量。
15
+ // ------------------------------------------------------------------
16
+ var CSS = [
17
+ '.dshpick-frame{padding:14px calc(var(--dsh-composer-side-clearance, 16px) + 16px) 12px;justify-content:center;display:flex}',
18
+ '.dshpick-card{width:100%;max-width:var(--dsh-chat-content-width, 748px);border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);max-height:min(70vh,620px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;padding:0 0 10px;display:flex;overflow:hidden}',
19
+ '.dshpick-card,.dshpick-card *{box-sizing:border-box}',
20
+ '.dshpick-header{flex-shrink:0;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px;display:flex}',
21
+ '.dshpick-headingBlock{min-width:0}',
22
+ '.dshpick-eyebrow{color:var(--dsw-alias-label-tertiary);margin-bottom:5px;font-size:11px;line-height:16px}',
23
+ '.dshpick-title{margin:0;font-size:16px;font-weight:500;line-height:22px}',
24
+ '.dshpick-detail{margin:0 2px 8px 24px;white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}',
25
+ '.dshpick-iconButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}',
26
+ '.dshpick-iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
27
+ '.dshpick-iconButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}',
28
+ '.dshpick-body{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}',
29
+ '.dshpick-grid{flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 24px;display:flex}',
30
+ '.dshpick-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(128px,1fr));gap:10px;padding:10px 24px 4px}',
31
+ '.dshpick-cardBtn{position:relative;min-width:0;color:inherit;text-align:left;cursor:pointer;background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:14px;padding:0;overflow:hidden;display:flex;flex-direction:column;transition:border-color .12s ease,background .12s ease}',
32
+ '.dshpick-cardBtn:hover:not(:disabled){border-color:var(--dsw-alias-interactive-bg-active);background:var(--dsw-alias-interactive-bg-hover)}',
33
+ '.dshpick-cardBtn:disabled{cursor:default;opacity:.6}',
34
+ '.dshpick-cardBtn.dshpick-selected{border-color:var(--dsw-alias-interactive-bg-active);background:var(--dsw-alias-interactive-bg-hover);outline:2px solid var(--dsw-alias-interactive-bg-active);outline-offset:0}',
35
+ '.dshpick-thumb{position:relative;width:100%;aspect-ratio:1/1;background:var(--dsw-alias-interactive-bg-hover);border:none;padding:0;overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:zoom-in}',
36
+ '.dshpick-thumb:focus-visible{outline:2px solid var(--dsw-alias-interactive-bg-active);outline-offset:-2px}',
37
+ '.dshpick-thumb img{width:100%;height:100%;object-fit:cover;display:block}',
38
+ '.dshpick-thumb .dshpick-thumbFallback{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px;padding:8px;text-align:center;overflow:hidden}',
39
+ '.dshpick-zoomHint{position:absolute;top:6px;left:6px;width:22px;height:22px;border-radius:999px;background:rgba(8,10,18,.55);color:#fff;place-items:center;display:grid;pointer-events:none;opacity:0;transition:opacity .12s ease;z-index:1}',
40
+ '.dshpick-thumb:hover .dshpick-zoomHint,.dshpick-thumb:focus-visible .dshpick-zoomHint{opacity:1}',
41
+ '.dshpick-cardBtn:focus-visible{outline:2px solid var(--dsw-alias-interactive-bg-active);outline-offset:2px}',
42
+ '.dshpick-check{position:absolute;top:6px;right:6px;width:20px;height:20px;border-radius:999px;background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary-inverted);place-items:center;display:none;font-size:12px;line-height:1;z-index:1}',
43
+ '.dshpick-cardBtn.dshpick-selected .dshpick-check{display:grid}',
44
+ '.dshpick-cardCopy{padding:8px 10px 10px;min-width:0}',
45
+ '.dshpick-optionLabel{font-size:13px;font-weight:500;line-height:18px;overflow-wrap:anywhere}',
46
+ '.dshpick-badge{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:999px;background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);font-size:10px;line-height:14px;vertical-align:1px}',
47
+ '.dshpick-description{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px;overflow-wrap:anywhere}',
48
+ '.dshpick-option{width:100%;min-height:40px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}',
49
+ '.dshpick-option:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
50
+ '.dshpick-option:disabled{cursor:default}',
51
+ '.dshpick-option.dshpick-optionSelected{background:var(--dsw-alias-interactive-bg-hover)}',
52
+ '.dshpick-number{width:24px;height:24px;flex-shrink:0;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;place-items:center;display:grid;font-size:12px;line-height:1}',
53
+ '.dshpick-checkbox{width:18px;height:18px;flex-shrink:0;border:1px solid var(--dsw-alias-border-l2);border-radius:5px;place-items:center;display:grid}',
54
+ '.dshpick-checkboxChecked{background:var(--dsw-alias-interactive-bg-active);border-color:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary-inverted)}',
55
+ '.dshpick-optionCopy{flex:auto;min-width:0}',
56
+ '.dshpick-optionLine{flex-direction:column;display:flex}',
57
+ '.dshpick-customRow{width:100%;min-height:40px;align-items:center;gap:10px;padding:8px 12px;display:flex}',
58
+ '.dshpick-customInput{flex:auto;min-width:0;color:inherit;background:0 0;border:none;outline:none;font-size:14px;line-height:22px}',
59
+ '.dshpick-customInput::placeholder{color:var(--dsw-alias-label-tertiary)}',
60
+ '.dshpick-customTextarea{width:100%;margin:8px 0 0;resize:none;color:inherit;background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:10px;outline:none;padding:8px 10px;font-size:14px;line-height:22px;font-family:inherit}',
61
+ '.dshpick-customTextarea:focus{border-color:var(--dsw-alias-interactive-bg-active)}',
62
+ '.dshpick-footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 0;display:flex}',
63
+ '.dshpick-pager{flex-shrink:0;align-items:center;gap:6px;display:flex}',
64
+ '.dshpick-progress{color:var(--dsw-alias-label-secondary);white-space:nowrap;word-spacing:-2px;padding:0 4px;font-size:14px;font-weight:500;line-height:24px}',
65
+ '.dshpick-feedback{min-height:16px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;flex:auto;padding:0 8px}',
66
+ '.dshpick-footerActions{flex-shrink:0;align-items:center;gap:12px;display:flex}',
67
+ '.dshpick-btn{appearance:none;cursor:pointer;border-radius:999px;border:1px solid transparent;font-size:13px;line-height:20px;padding:5px 14px;font-family:inherit}',
68
+ '.dshpick-btn:disabled{cursor:default;opacity:.55}',
69
+ '.dshpick-btn-outline{background:0 0;border-color:var(--dsw-alias-border-l2-darkmode-thin);color:var(--dsw-alias-label-primary)}',
70
+ '.dshpick-btn-outline:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
71
+ '.dshpick-btn-primary{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary-inverted)}',
72
+ '.dshpick-btn-primary:hover:not(:disabled){filter:brightness(1.08)}',
73
+ '.dshpick-lightbox{position:fixed;inset:0;z-index:9999;background:rgba(8,10,18,.86);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:52px 24px 28px;cursor:zoom-out}',
74
+ '.dshpick-lightboxClose{position:absolute;top:16px;right:18px;width:34px;height:34px;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;background:rgba(255,255,255,.08);color:#fff;cursor:pointer;place-items:center;display:grid;font-size:16px;line-height:1;padding:0}',
75
+ '.dshpick-lightboxClose:hover{background:rgba(255,255,255,.18)}',
76
+ '.dshpick-lightboxFigure{margin:0;max-width:min(92vw,1100px);display:flex;flex-direction:column;align-items:center;gap:10px}',
77
+ '.dshpick-lightboxFigure img{max-width:100%;max-height:82vh;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 8px 40px rgba(0,0,0,.5)}',
78
+ '.dshpick-lightboxCap{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;text-align:center;overflow-wrap:anywhere}',
79
+ '.dshpick-lightboxCap small{display:block;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:15px;margin-top:2px}',
80
+ '.dshimg-inline{max-width:min(100%,560px);max-height:420px;width:auto;height:auto;border-radius:14px;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-alias-interactive-bg-hover);margin:8px 0;display:block;cursor:zoom-in;object-fit:contain}',
81
+ '.dshimg-inline[data-failed]{opacity:.45;cursor:default;filter:grayscale(1)}',
82
+ '@media (width<=720px){.dshpick-card{border-radius:16px}.dshpick-cards{grid-template-columns:repeat(auto-fill,minmax(104px,1fr))}.dshpick-lightbox{padding:44px 12px 20px}}'
83
+ ].join('');
84
+ var tagId = 'dsh-plugin-image-tools/pick.module.css';
85
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
86
+ var tag = document.createElement('style');
87
+ tag.dataset.plugin = 'dsh-plugin-image-tools';
88
+ tag.dataset.pluginCss = tagId;
89
+ tag.textContent = CSS;
90
+ document.head.appendChild(tag);
91
+ }
92
+
93
+ // ------------------------------------------------------------------
94
+ // detail 标记解析(与服务端 buildPickMarker 对应;标记 JSON 为 ASCII)
95
+ // ------------------------------------------------------------------
96
+ var MARKER_PREFIX = '<!--dsh-pick:v1:';
97
+ var MARKER_SUFFIX = '-->';
98
+
99
+ function decodeBase64Url(value) {
100
+ var b64 = value.replace(/-/g, '+').replace(/_/g, '/');
101
+ while (b64.length % 4 !== 0) b64 += '=';
102
+ var binary = atob(b64);
103
+ var bytes = new Uint8Array(binary.length);
104
+ for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
105
+ return new TextDecoder('utf-8').decode(bytes);
106
+ }
107
+
108
+ /** 解析 detail 标记;无标记返回 null。 */
109
+ function parseMarker(detail) {
110
+ if (typeof detail !== 'string' || !detail.startsWith(MARKER_PREFIX)) return null;
111
+ var end = detail.indexOf(MARKER_SUFFIX, MARKER_PREFIX.length);
112
+ if (end < 0) return null;
113
+ try {
114
+ var data = JSON.parse(decodeBase64Url(detail.slice(MARKER_PREFIX.length, end)));
115
+ if (data === null || typeof data !== 'object' || typeof data.pickId !== 'string') return null;
116
+ var images = Array.isArray(data.images) ? data.images.filter(function (n) { return Number.isInteger(n) && n >= 0; }) : [];
117
+ return { pickId: data.pickId, images: images, human: detail.slice(end + MARKER_SUFFIX.length) };
118
+ } catch (e) {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /** 去掉 label 末尾的推荐标注,返回展示文本与是否推荐(答案值保持原 label)。 */
124
+ function parseRecommendedLabel(label) {
125
+ var suffix = /\s*(?:\((?:recommended|推荐)\)|((?:recommended|推荐)))\s*$/i;
126
+ return suffix.test(label)
127
+ ? { label: label.replace(suffix, ''), recommended: true }
128
+ : { label: label, recommended: false };
129
+ }
130
+
131
+ /** 输入法组合中不触发回车提交。 */
132
+ function isComposing(event) {
133
+ return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
134
+ }
135
+
136
+ // ------------------------------------------------------------------
137
+ // 待答载体(与服务端回答协议一致;同 PendingQuestion 的 wire 编码)
138
+ // ------------------------------------------------------------------
139
+ var PendingChoice = class {
140
+ constructor(wait) { this.wait = wait; }
141
+ get key() { return this.wait.key; }
142
+ get questions() { return this.wait.payload.questions; }
143
+ async answer(answer) {
144
+ var receipt = await this.wait.respond({ ok: true, value: { sessionId: this.wait.sessionId, answer: answer } });
145
+ if (!receipt.accepted) throw new Error('question response rejected: ' + receipt.reason);
146
+ }
147
+ async cancel() {
148
+ var receipt = await this.wait.respond({
149
+ ok: false,
150
+ error: { code: 'cancelled', message: 'the user closed this question request', details: {} }
151
+ });
152
+ if (!receipt.accepted) throw new Error('question cancellation rejected: ' + receipt.reason);
153
+ }
154
+ };
155
+
156
+ // ------------------------------------------------------------------
157
+ // 选项渲染
158
+ // ------------------------------------------------------------------
159
+ function recommendedBadge(display, t) {
160
+ return display.recommended ? jsx('span', { className: 'dshpick-badge', children: t('option.recommended') }) : null;
161
+ }
162
+
163
+ /** 放大查看的放大镜图标(inline SVG,作为 zoomHint 内容)。 */
164
+ var ZOOM_ICON_SVG = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/><line x1="8" y1="11" x2="14" y2="11"/></svg>';
165
+
166
+ /**
167
+ * 图片卡片选项。
168
+ * 结构(合法 HTML,无嵌套 button):
169
+ * div[role=radio|checkbox](整卡可点选中,Enter/Space 同效)
170
+ * └ button.dshpick-thumb(点击放大查看,焦点独立)
171
+ * └ div.dshpick-cardCopy(label/描述)
172
+ */
173
+ function ImageCardBtn(props) {
174
+ var pickId = props.pickId;
175
+ var option = props.option;
176
+ var optionIndex = props.optionIndex;
177
+ var selected = props.selected;
178
+ var multi = props.multi;
179
+ var disabled = props.disabled;
180
+ var t = props.t;
181
+ var display = parseRecommendedLabel(option.label);
182
+ var failed = react.useState(false);
183
+ var failedState = failed[0];
184
+ var setFailed = failed[1];
185
+ var src = '/dsh-plugin-image-tools/' + pickId + '/' + optionIndex;
186
+ var onSelect = function () {
187
+ if (disabled) return;
188
+ props.onSelect();
189
+ };
190
+ var onKeyDown = function (event) {
191
+ if (disabled || event.target !== event.currentTarget) return;
192
+ if (event.key === 'Enter' || event.key === ' ') {
193
+ event.preventDefault();
194
+ props.onSelect();
195
+ }
196
+ };
197
+ return jsxs('div', {
198
+ className: 'dshpick-cardBtn' + (selected ? ' dshpick-selected' : ''),
199
+ role: multi ? 'checkbox' : 'radio',
200
+ 'aria-checked': selected,
201
+ 'aria-label': display.label,
202
+ 'aria-disabled': disabled,
203
+ tabIndex: 0,
204
+ onClick: onSelect,
205
+ onKeyDown: onKeyDown,
206
+ children: [
207
+ jsxs('button', {
208
+ type: 'button',
209
+ className: 'dshpick-thumb',
210
+ 'aria-label': failedState ? t('image.failed') : t('image.zoom') + ':' + display.label,
211
+ title: t('image.zoom'),
212
+ disabled: disabled,
213
+ onClick: function (event) {
214
+ event.stopPropagation();
215
+ if (!failedState) props.onZoom();
216
+ },
217
+ children: [
218
+ failedState
219
+ ? jsx('div', { className: 'dshpick-thumbFallback', children: t('image.failed') })
220
+ : jsx('img', {
221
+ src: src,
222
+ alt: display.label,
223
+ loading: 'lazy',
224
+ onError: function () { setFailed(true); }
225
+ }),
226
+ jsx('span', { className: 'dshpick-zoomHint', 'aria-hidden': 'true', dangerouslySetInnerHTML: { __html: ZOOM_ICON_SVG } }),
227
+ selected && jsx('span', { className: 'dshpick-check', 'aria-hidden': 'true', children: '\u2713' })
228
+ ]
229
+ }),
230
+ jsxs('div', {
231
+ className: 'dshpick-cardCopy',
232
+ children: [
233
+ jsxs('div', { className: 'dshpick-optionLabel', children: [display.label, recommendedBadge(display, t)] }),
234
+ option.description !== void 0 && jsx('div', { className: 'dshpick-description', children: option.description })
235
+ ]
236
+ })
237
+ ]
238
+ }, optionIndex);
239
+ }
240
+
241
+ /** 纯文字选项行(与原生 UI 一致的样式)。 */
242
+ function TextOptionRow(props) {
243
+ var option = props.option;
244
+ var optionIndex = props.optionIndex;
245
+ var selected = props.selected;
246
+ var multi = props.multi;
247
+ var disabled = props.disabled;
248
+ var t = props.t;
249
+ var display = parseRecommendedLabel(option.label);
250
+ return jsxs('button', {
251
+ type: 'button',
252
+ className: 'dshpick-option' + (selected && !multi ? ' dshpick-optionSelected' : ''),
253
+ role: multi ? 'checkbox' : 'radio',
254
+ 'aria-checked': selected,
255
+ 'aria-label': display.label,
256
+ disabled: disabled,
257
+ onClick: function () { props.onClick(); },
258
+ onKeyDown: props.onKeyDown,
259
+ children: [
260
+ multi
261
+ ? jsx('span', {
262
+ className: 'dshpick-checkbox' + (selected ? ' dshpick-checkboxChecked' : ''),
263
+ 'aria-hidden': 'true',
264
+ children: selected ? '\u2713' : null
265
+ })
266
+ : jsx('span', { className: 'dshpick-number', children: optionIndex + 1 }),
267
+ jsxs('span', {
268
+ className: 'dshpick-optionCopy',
269
+ children: [
270
+ jsxs('span', {
271
+ className: 'dshpick-optionLine',
272
+ children: [
273
+ jsxs('span', { className: 'dshpick-optionLabel', children: [display.label, recommendedBadge(display, t)] }),
274
+ option.description !== void 0 && jsx('span', { className: 'dshpick-description', children: option.description })
275
+ ]
276
+ })
277
+ ]
278
+ })
279
+ ]
280
+ }, optionIndex);
281
+ }
282
+
283
+ // ------------------------------------------------------------------
284
+ // 图片放大查看(Lightbox):全屏遮罩 + 大图 + 关闭按钮 / Esc / 点遮罩
285
+ // ------------------------------------------------------------------
286
+ /**
287
+ * 图片放大查看层。zoom 为 null 时渲染 null。
288
+ * 打开期间:锁定 body 滚动、焦点移到关闭按钮、Esc 关闭;
289
+ * 关闭时恢复滚动并把焦点还给触发按钮(触发按钮 ref 由父级持有,这里不处理)。
290
+ * @param {object} props - { zoom: { src, label, description? }|null, onClose, t }。
291
+ */
292
+ function Lightbox(props) {
293
+ var zoom = props.zoom;
294
+ var onClose = props.onClose;
295
+ var t = props.t;
296
+ var closeRef = react.useRef(null);
297
+ react.useEffect(function () {
298
+ if (zoom === null) return;
299
+ var previous = document.body.style.overflow;
300
+ document.body.style.overflow = 'hidden';
301
+ var focusTarget = closeRef.current;
302
+ if (focusTarget !== null && typeof focusTarget.focus === 'function') focusTarget.focus();
303
+ function onKeyDown(event) {
304
+ if (event.key === 'Escape') onClose();
305
+ }
306
+ document.addEventListener('keydown', onKeyDown);
307
+ return function () {
308
+ document.body.style.overflow = previous;
309
+ document.removeEventListener('keydown', onKeyDown);
310
+ };
311
+ }, [zoom, onClose]);
312
+ if (zoom === null) return null;
313
+ return jsxs('div', {
314
+ className: 'dshpick-lightbox',
315
+ role: 'dialog',
316
+ 'aria-modal': 'true',
317
+ 'aria-label': zoom.label,
318
+ onClick: onClose,
319
+ children: [
320
+ jsx('button', {
321
+ type: 'button',
322
+ ref: closeRef,
323
+ className: 'dshpick-lightboxClose',
324
+ 'aria-label': t('image.close'),
325
+ title: t('image.close'),
326
+ onClick: function (event) { event.stopPropagation(); onClose(); },
327
+ children: '\u2715'
328
+ }),
329
+ jsxs('figure', {
330
+ className: 'dshpick-lightboxFigure',
331
+ onClick: function (event) { event.stopPropagation(); },
332
+ children: [
333
+ jsx('img', { src: zoom.src, alt: zoom.label }),
334
+ jsxs('figcaption', {
335
+ className: 'dshpick-lightboxCap',
336
+ children: [
337
+ zoom.label,
338
+ zoom.description !== void 0 && jsx('small', { children: zoom.description })
339
+ ]
340
+ })
341
+ ]
342
+ })
343
+ ]
344
+ });
345
+ }
346
+
347
+ // ------------------------------------------------------------------
348
+ // 回复内嵌图片增强:show_images 返回的 markdown 图片行由核心渲染器
349
+ // 原生显示为 <img src="/dsh-plugin-image-tools/show/<id>/<i>">;这里通过
350
+ // MutationObserver 发现这类图片并做渐进增强:加样式类、悬停提示、
351
+ // 点击放大(复用 .dshpick-lightbox 样式,命令式 DOM,不依赖 React)。
352
+ // 纯 DOM 单节点变更:不插入/移除 React 管理的结构,重渲染时安全。
353
+ // ------------------------------------------------------------------
354
+ var IMAGE_SHOW_PREFIX = '/dsh-plugin-image-tools/show/';
355
+ var ZOOM_CLOSE_LABEL = (typeof document !== 'undefined' && (document.documentElement.lang || '').toLowerCase().indexOf('zh') === 0)
356
+ ? '\u5173\u95ed'
357
+ : 'Close';
358
+
359
+ /** 是否为插件内嵌图片的 src(纯字符串判定,供冒烟测试)。 */
360
+ function isShowImageSrc(src) {
361
+ return typeof src === 'string' && src.indexOf(IMAGE_SHOW_PREFIX) >= 0;
362
+ }
363
+
364
+ /** 打开命令式 Lightbox 放大查看(复用选择卡的 .dshpick-lightbox 样式)。 */
365
+ function openImageZoom(src, caption) {
366
+ if (typeof document === 'undefined') return;
367
+ var existing = document.querySelector('.dshpick-lightbox[data-dshimg-zoom]');
368
+ if (existing !== null) existing.remove();
369
+ var overlay = document.createElement('div');
370
+ overlay.className = 'dshpick-lightbox';
371
+ overlay.dataset.dshimgZoom = '1';
372
+ var close = document.createElement('button');
373
+ close.type = 'button';
374
+ close.className = 'dshpick-lightboxClose';
375
+ close.textContent = '\u2715';
376
+ close.setAttribute('aria-label', ZOOM_CLOSE_LABEL);
377
+ close.title = ZOOM_CLOSE_LABEL;
378
+ var figure = document.createElement('figure');
379
+ figure.className = 'dshpick-lightboxFigure';
380
+ var img = document.createElement('img');
381
+ img.src = src;
382
+ img.alt = caption;
383
+ figure.appendChild(img);
384
+ if (caption !== '') {
385
+ var figcaption = document.createElement('figcaption');
386
+ figcaption.className = 'dshpick-lightboxCap';
387
+ figcaption.textContent = caption;
388
+ figure.appendChild(figcaption);
389
+ }
390
+ overlay.appendChild(close);
391
+ overlay.appendChild(figure);
392
+ var previous = document.body.style.overflow;
393
+ document.body.style.overflow = 'hidden';
394
+ var closeZoom = function () {
395
+ overlay.remove();
396
+ document.body.style.overflow = previous;
397
+ document.removeEventListener('keydown', onKey);
398
+ };
399
+ var onKey = function (event) {
400
+ if (event.key === 'Escape') closeZoom();
401
+ };
402
+ overlay.addEventListener('click', closeZoom);
403
+ figure.addEventListener('click', function (event) { event.stopPropagation(); });
404
+ close.addEventListener('click', function (event) { event.stopPropagation(); closeZoom(); });
405
+ document.addEventListener('keydown', onKey);
406
+ document.body.appendChild(overlay);
407
+ close.focus();
408
+ }
409
+
410
+ /** 升级单个内嵌图片:样式 + 悬停说明 + 点击放大 + 加载失败降级。 */
411
+ function upgradeInlineImage(img) {
412
+ if (img === null || typeof img !== 'object' || img.dataset === void 0) return;
413
+ if (img.dataset.dshimg === '1') return;
414
+ if (typeof img.closest === 'function' && img.closest('.dshpick-lightbox') !== null) return;
415
+ img.dataset.dshimg = '1';
416
+ img.classList.add('dshimg-inline');
417
+ var caption = (img.getAttribute('alt') || '').trim();
418
+ if (caption !== '') img.title = caption;
419
+ var src = img.getAttribute('src') || '';
420
+ if (!isShowImageSrc(src)) return;
421
+ img.addEventListener('click', function (event) {
422
+ event.preventDefault();
423
+ event.stopPropagation();
424
+ openImageZoom(src, caption);
425
+ });
426
+ img.addEventListener('error', function () { img.dataset.failed = '1'; });
427
+ }
428
+
429
+ /**
430
+ * 启动内联图片增强观察器。对 document.body 做 childList+subtree 监听,
431
+ * 用 requestAnimationFrame 合帧扫描(避免高频回调),只处理匹配前缀的 img。
432
+ * @returns {Function|null} 停止函数;无 DOM 环境返回 null。
433
+ */
434
+ function startInlineEnhancer() {
435
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined' || typeof requestAnimationFrame === 'undefined') return null;
436
+ var pending = false;
437
+ var scan = function () {
438
+ pending = false;
439
+ var images = document.querySelectorAll('img[src^="' + IMAGE_SHOW_PREFIX + '"]');
440
+ for (var i = 0; i < images.length; i++) upgradeInlineImage(images[i]);
441
+ };
442
+ var observer = new MutationObserver(function () {
443
+ if (pending) return;
444
+ pending = true;
445
+ requestAnimationFrame(scan);
446
+ });
447
+ observer.observe(document.body, { childList: true, subtree: true });
448
+ scan();
449
+ return function () { observer.disconnect(); };
450
+ }
451
+
452
+ // ------------------------------------------------------------------
453
+ // 主流程:分页多题、图片/文字混排、多选、自定义答案、跳过、取消
454
+ // ------------------------------------------------------------------
455
+ function ImageChoiceFlow(props) {
456
+ var pending = props.pending;
457
+ var t = props.t;
458
+ var questions = pending.questions;
459
+ var markers = questions.map(function (q) { return parseMarker(q.detail); });
460
+ var indexState = react.useState(0);
461
+ var index = indexState[0];
462
+ var setIndex = indexState[1];
463
+ var draftsState = react.useState(function () {
464
+ return questions.map(function () { return { selected: [], custom: '', skipped: false }; });
465
+ });
466
+ var drafts = draftsState[0];
467
+ var setDrafts = draftsState[1];
468
+ var busyState = react.useState(null);
469
+ var busy = busyState[0];
470
+ var setBusy = busyState[1];
471
+ var errorState = react.useState(null);
472
+ var error = errorState[0];
473
+ var setError = errorState[1];
474
+ var zoomState = react.useState(null);
475
+ var zoom = zoomState[0];
476
+ var setZoom = zoomState[1];
477
+ var closeZoom = react.useCallback(function () { setZoom(null); }, []);
478
+
479
+ var question = questions[index];
480
+ var marker = markers[index];
481
+ var draft = drafts[index];
482
+ var hasOptions = (question.options !== void 0 && question.options !== null && question.options.length > 0);
483
+ var hasImages = marker !== null && marker.images.length > 0;
484
+
485
+ var cancelFlow = function () {
486
+ setBusy('cancel');
487
+ setError(null);
488
+ pending.cancel().catch(function (cause) {
489
+ setBusy(null);
490
+ setError({ text: cause instanceof Error ? cause.message : String(cause) });
491
+ });
492
+ };
493
+ var updateDraft = function (update) {
494
+ setDrafts(function (current) {
495
+ return current.map(function (item, itemIndex) { return itemIndex === index ? update(item) : item; });
496
+ });
497
+ setError(null);
498
+ };
499
+ var choose = function (label) {
500
+ updateDraft(function (current) {
501
+ if (question.multiSelect === true) {
502
+ var selected = current.selected.indexOf(label) >= 0
503
+ ? current.selected.filter(function (item) { return item !== label; })
504
+ : current.selected.concat([label]);
505
+ return { selected: selected, custom: '', skipped: false };
506
+ }
507
+ return { selected: [label], custom: '', skipped: false };
508
+ });
509
+ if (question.multiSelect !== true && index < questions.length - 1) setIndex(index + 1);
510
+ };
511
+ var answered = function (item) { return item.selected.length > 0 || item.custom.trim() !== ''; };
512
+ var completed = function (item) { return answered(item) || item.skipped; };
513
+ var submitDrafts = function (values) {
514
+ var missing = values.findIndex(function (item) { return !completed(item); });
515
+ if (missing >= 0) {
516
+ setIndex(missing);
517
+ setError({ key: 'error.incomplete' });
518
+ return;
519
+ }
520
+ var answer = {
521
+ answers: questions.map(function (item, itemIndex) {
522
+ var value = values[itemIndex];
523
+ if (value.skipped) return { id: item.id, selected: [] };
524
+ var custom = value.custom.trim();
525
+ return {
526
+ id: item.id,
527
+ selected: custom === '' || item.multiSelect === true ? value.selected : [],
528
+ ...(custom === '' ? {} : { custom: custom })
529
+ };
530
+ })
531
+ };
532
+ setBusy('answer');
533
+ setError(null);
534
+ pending.answer(answer).catch(function (cause) {
535
+ setBusy(null);
536
+ setError({ text: cause instanceof Error ? cause.message : String(cause) });
537
+ });
538
+ };
539
+ var continueFlow = function () {
540
+ if (!answered(draft)) {
541
+ setError({ key: 'error.unanswered' });
542
+ return;
543
+ }
544
+ if (index < questions.length - 1) {
545
+ setIndex(index + 1);
546
+ setError(null);
547
+ return;
548
+ }
549
+ submitDrafts(drafts);
550
+ };
551
+ var draftCustom = function (event) {
552
+ var value = event.target.value;
553
+ updateDraft(function (current) {
554
+ return {
555
+ selected: question.multiSelect === true ? current.selected : [],
556
+ custom: value,
557
+ skipped: false
558
+ };
559
+ });
560
+ };
561
+ var continueFromCustom = function (event) {
562
+ if (event.key !== 'Enter' || event.shiftKey || isComposing(event)) return;
563
+ event.preventDefault();
564
+ continueFlow();
565
+ };
566
+ var skipQuestion = function () {
567
+ var nextDrafts = drafts.map(function (item, itemIndex) {
568
+ return itemIndex === index ? { selected: [], custom: '', skipped: true } : item;
569
+ });
570
+ setDrafts(nextDrafts);
571
+ setError(null);
572
+ if (index < questions.length - 1) {
573
+ setIndex(index + 1);
574
+ return;
575
+ }
576
+ submitDrafts(nextDrafts);
577
+ };
578
+ var onKeyDownOption = function (event) {
579
+ if (event.key !== 'Enter' || !drafts.every(completed)) return;
580
+ event.preventDefault();
581
+ submitDrafts(drafts);
582
+ };
583
+
584
+ var detailText = marker !== null ? marker.human : (typeof question.detail === 'string' ? question.detail : '');
585
+ var options = question.options !== void 0 && question.options !== null ? question.options : [];
586
+ var imageIndexSet = marker !== null ? marker.images : [];
587
+ var isImageOption = function (optionIndex) { return imageIndexSet.indexOf(optionIndex) >= 0; };
588
+
589
+ return jsx('div', {
590
+ className: 'dshpick-frame',
591
+ 'data-question-key': pending.key,
592
+ children: [
593
+ jsxs('section', {
594
+ className: 'dshpick-card',
595
+ 'aria-labelledby': 'dshpick-q-' + pending.key + '-' + index,
596
+ children: [
597
+ jsxs('header', {
598
+ className: 'dshpick-header',
599
+ children: [
600
+ jsxs('div', {
601
+ className: 'dshpick-headingBlock',
602
+ children: [
603
+ question.header !== void 0 && jsx('div', { className: 'dshpick-eyebrow', children: question.header }),
604
+ jsx('h2', { className: 'dshpick-title', id: 'dshpick-q-' + pending.key + '-' + index, children: question.question })
605
+ ]
606
+ }),
607
+ jsx('button', {
608
+ type: 'button',
609
+ className: 'dshpick-iconButton',
610
+ 'aria-label': t('nav.cancel'),
611
+ title: t('nav.cancel'),
612
+ disabled: busy !== null,
613
+ onClick: cancelFlow,
614
+ children: '\u2715'
615
+ })
616
+ ]
617
+ }),
618
+ jsxs('div', {
619
+ className: 'dshpick-body',
620
+ children: [
621
+ detailText !== '' && jsx('p', { className: 'dshpick-detail', children: detailText }),
622
+ hasImages
623
+ ? jsxs('div', {
624
+ className: 'dshpick-cards',
625
+ role: question.multiSelect === true ? 'group' : 'radiogroup',
626
+ children: options.map(function (option, optionIndex) {
627
+ if (!isImageOption(optionIndex)) return null;
628
+ var selected = draft.selected.indexOf(option.label) >= 0;
629
+ return jsx(ImageCardBtn, {
630
+ pickId: marker.pickId,
631
+ option: option,
632
+ optionIndex: optionIndex,
633
+ selected: selected,
634
+ multi: question.multiSelect === true,
635
+ disabled: busy !== null,
636
+ t: t,
637
+ onSelect: function () { choose(option.label); },
638
+ onZoom: function () {
639
+ setZoom({
640
+ src: '/dsh-plugin-image-tools/' + marker.pickId + '/' + optionIndex,
641
+ label: option.label,
642
+ description: option.description
643
+ });
644
+ }
645
+ }, optionIndex);
646
+ })
647
+ })
648
+ : null,
649
+ jsxs('div', {
650
+ className: 'dshpick-grid',
651
+ role: question.multiSelect === true ? 'group' : 'radiogroup',
652
+ children: [
653
+ options.map(function (option, optionIndex) {
654
+ if (hasImages && isImageOption(optionIndex)) return null;
655
+ var selected = draft.selected.indexOf(option.label) >= 0;
656
+ return jsx(TextOptionRow, {
657
+ option: option,
658
+ optionIndex: optionIndex,
659
+ selected: selected,
660
+ multi: question.multiSelect === true,
661
+ disabled: busy !== null,
662
+ t: t,
663
+ onClick: function () { choose(option.label); },
664
+ onKeyDown: onKeyDownOption
665
+ }, optionIndex);
666
+ }),
667
+ hasOptions
668
+ ? jsxs('div', {
669
+ className: 'dshpick-customRow',
670
+ children: [
671
+ multiInput(draft, question, t, busy, draftCustom, continueFromCustom)
672
+ ]
673
+ })
674
+ : jsx('textarea', {
675
+ autoFocus: true,
676
+ className: 'dshpick-customTextarea',
677
+ value: draft.custom,
678
+ disabled: busy !== null,
679
+ rows: 2,
680
+ placeholder: t('custom.placeholder'),
681
+ onChange: draftCustom,
682
+ onKeyDown: continueFromCustom
683
+ })
684
+ ]
685
+ })
686
+ ]
687
+ }),
688
+ jsxs('footer', {
689
+ className: 'dshpick-footer',
690
+ children: [
691
+ jsxs('div', {
692
+ className: 'dshpick-pager',
693
+ children: [
694
+ jsx('button', {
695
+ type: 'button',
696
+ className: 'dshpick-iconButton',
697
+ 'aria-label': t('nav.prev'),
698
+ disabled: index === 0 || busy !== null,
699
+ onClick: function () { setIndex(index - 1); setError(null); },
700
+ children: '\u2039'
701
+ }),
702
+ jsxs('span', { className: 'dshpick-progress', children: [index + 1, ' / ', questions.length] }),
703
+ jsx('button', {
704
+ type: 'button',
705
+ className: 'dshpick-iconButton',
706
+ 'aria-label': t('nav.next'),
707
+ disabled: index === questions.length - 1 || busy !== null,
708
+ onClick: function () { setIndex(index + 1); setError(null); },
709
+ children: '\u203a'
710
+ })
711
+ ]
712
+ }),
713
+ jsx('div', {
714
+ className: 'dshpick-feedback',
715
+ role: 'status',
716
+ children: error === null ? null : ('key' in error ? t(error.key) : error.text)
717
+ }),
718
+ jsxs('div', {
719
+ className: 'dshpick-footerActions',
720
+ children: [
721
+ jsx('button', {
722
+ type: 'button',
723
+ className: 'dshpick-btn dshpick-btn-outline',
724
+ disabled: busy !== null,
725
+ onClick: skipQuestion,
726
+ children: t('action.skip')
727
+ }),
728
+ jsx('button', {
729
+ type: 'button',
730
+ className: 'dshpick-btn dshpick-btn-primary',
731
+ disabled: busy !== null || !answered(draft),
732
+ onClick: continueFlow,
733
+ children: busy === 'answer' ? t('submitting') : (index === questions.length - 1 ? t('submit') : t('action.next'))
734
+ })
735
+ ]
736
+ })
737
+ ]
738
+ })
739
+ ]
740
+ }, 'dshpick-card'),
741
+ zoom !== null && jsx(Lightbox, { zoom: zoom, onClose: closeZoom, t: t })
742
+ ]
743
+ });
744
+ }
745
+
746
+ /** 多选/单选自定义答案行的前置控件(复刻原生 UI 形状)。 */
747
+ function multiInput(draft, question, t, busy, draftCustom, continueFromCustom) {
748
+ return jsxs(Fragment, {
749
+ children: [
750
+ question.multiSelect === true
751
+ ? jsx('span', {
752
+ className: 'dshpick-checkbox' + (draft.custom !== '' ? ' dshpick-checkboxChecked' : ''),
753
+ 'aria-hidden': 'true',
754
+ children: draft.custom !== '' ? '\u2713' : null
755
+ })
756
+ : jsx('span', { className: 'dshpick-number', 'aria-hidden': 'true', children: '\u270E' }),
757
+ jsx('input', {
758
+ type: 'text',
759
+ className: 'dshpick-customInput',
760
+ value: draft.custom,
761
+ disabled: busy !== null,
762
+ placeholder: t('custom.placeholder'),
763
+ onChange: draftCustom,
764
+ onKeyDown: continueFromCustom
765
+ })
766
+ ]
767
+ });
768
+ }
769
+
770
+ // ------------------------------------------------------------------
771
+ // 组件入口 + 链条目选择器
772
+ // ------------------------------------------------------------------
773
+ function ImageChoiceComposer(props) {
774
+ var pending = react.useMemo(function () { return new PendingChoice(props.matched); }, [props.matched]);
775
+ return jsx(ImageChoiceFlow, { pending: pending, t: props.t }, pending.key);
776
+ }
777
+
778
+ /** 认领“至少一题带图片标记”的 question 交互;其余交给原生 UI。 */
779
+ function selectPickChoice(_ref) {
780
+ var interactions = _ref.interactions;
781
+ for (var i = 0; i < interactions.length; i++) {
782
+ var item = interactions[i];
783
+ if (item.kind !== 'question') continue;
784
+ var payload = item.payload;
785
+ var questions = payload !== null && typeof payload === 'object' && Array.isArray(payload.questions) ? payload.questions : [];
786
+ var claimed = false;
787
+ for (var j = 0; j < questions.length; j++) {
788
+ if (parseMarker(questions[j].detail) !== null) { claimed = true; break; }
789
+ }
790
+ if (claimed) return item;
791
+ }
792
+ return null;
793
+ }
794
+
795
+ // ------------------------------------------------------------------
796
+ // 文案
797
+ // ------------------------------------------------------------------
798
+ var NS = 'pick';
799
+ var zh = {
800
+ 'error.incomplete': '请先完成这道问题。',
801
+ 'error.unanswered': '请选择一个选项或填写自定义答案。',
802
+ 'nav.prev': '上一题',
803
+ 'nav.next': '下一题',
804
+ 'nav.cancel': '放弃整组问题',
805
+ 'option.recommended': '推荐',
806
+ 'custom.placeholder': '输入你的答案',
807
+ 'action.skip': '跳过本题',
808
+ 'action.next': '下一题',
809
+ 'submit': '提交',
810
+ 'submitting': '提交中…',
811
+ 'image.failed': '图片加载失败',
812
+ 'image.zoom': '放大查看',
813
+ 'image.close': '关闭'
814
+ };
815
+ var en = {
816
+ 'error.incomplete': 'Please complete this question first.',
817
+ 'error.unanswered': 'Please select an option or enter a custom answer.',
818
+ 'nav.prev': 'Previous question',
819
+ 'nav.next': 'Next question',
820
+ 'nav.cancel': 'Dismiss all questions',
821
+ 'option.recommended': 'Recommended',
822
+ 'custom.placeholder': 'Type your answer',
823
+ 'action.skip': 'Skip this question',
824
+ 'action.next': 'Next',
825
+ 'submit': 'Submit',
826
+ 'submitting': 'Submitting…',
827
+ 'image.failed': 'Image failed to load',
828
+ 'image.zoom': 'Zoom in',
829
+ 'image.close': 'Close'
830
+ };
831
+
832
+ function apply(ctx) {
833
+ ctx.effect(function () {
834
+ return ctx.locale.register(NS, { zh: zh, en: en });
835
+ }, 'dsh-plugin-image-tools: dictionaries');
836
+ ctx.slots.inject('conversation.composer', function () {
837
+ return ctx.slots.register({
838
+ name: 'conversation.composer',
839
+ select: selectPickChoice,
840
+ locale: NS,
841
+ priority: -100
842
+ }, ImageChoiceComposer);
843
+ });
844
+ ctx.effect(function () {
845
+ return startInlineEnhancer();
846
+ }, 'dsh-plugin-image-tools: inline image enhancer');
847
+ }
848
+
849
+ exports.apply = apply;
850
+ exports.inject = ['slots', 'locale'];
851
+ // 供冒烟测试/复用:标记解析、链条目选择器、载体与组件。
852
+ exports.parseMarker = parseMarker;
853
+ exports.selectPickChoice = selectPickChoice;
854
+ exports.PendingChoice = PendingChoice;
855
+ exports.ImageChoiceComposer = ImageChoiceComposer;
856
+ exports.Lightbox = Lightbox;
857
+ exports.IMAGE_SHOW_PREFIX = IMAGE_SHOW_PREFIX;
858
+ exports.isShowImageSrc = isShowImageSrc;
859
+ exports.upgradeInlineImage = upgradeInlineImage;
860
+ exports.startInlineEnhancer = startInlineEnhancer;
861
+ return module.exports;
862
+ }
863
+ });