dsh-lcx-codex 0.4.3 → 0.5.0-pre.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/README.md +95 -60
- package/lib/abort-error.js +47 -0
- package/lib/client.js +597 -505
- package/lib/dsh-compat.js +37 -0
- package/lib/dsh-responses.js +92 -48
- package/lib/index.js +174 -105
- package/lib/invocation-policy-scope.js +4 -8
- package/lib/native-checkpoint.js +172 -80
- package/lib/pi-responses-runtime.js +252 -98
- package/lib/pi086-responses-allowlist.js +131 -0
- package/lib/responses-request.js +2 -2
- package/lib/responses-stream.js +62 -11
- package/lib/route.js +23 -32
- package/lib/search-accounting.js +22 -6
- package/lib/search-usage.js +32 -18
- package/lib/service-mutex.js +4 -8
- package/lib/token-budget.js +15 -21
- package/lib/transport.js +6 -6
- package/lib/types/client/search-media.d.ts +11 -2
- package/lib/types/index.d.ts +71 -43
- package/lib/web-search-alpha-history.js +62 -0
- package/lib/web-search-alpha.js +141 -124
- package/lib/web-search-capability.js +1 -1
- package/lib/web-search-hosted.js +29 -17
- package/lib/web-search-ref-store.js +27 -5
- package/package.json +41 -27
package/lib/client.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
(() => {
|
|
3
3
|
// src/client/search-media.ts
|
|
4
4
|
var SEARCH_MEDIA_LIMIT = 60;
|
|
5
|
-
var IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|avif)$/i;
|
|
6
5
|
var VIDEO_EXTENSION = /\.(?:mp4|webm|ogv)$/i;
|
|
6
|
+
var IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|avif)$/i;
|
|
7
|
+
var IMAGE_CDN_HOSTS = /* @__PURE__ */ new Set(["images.unsplash.com", "plus.unsplash.com", "images.pexels.com"]);
|
|
7
8
|
var URL_TOKEN = /https?:\/\/[^\s<>"'`,。;:!?、“”‘’()【】《》]+/gi;
|
|
8
9
|
var REFERENCE_DEFINITION = /^ {0,3}\[([^\]\r\n]+)\]:[ \t]*(?:<([^>\s]+)>|(\S+))(?:[ \t]+.*)?$/gm;
|
|
9
10
|
var REFERENCE_IMAGE = /!\[([^\]\r\n]*)\](?:\s*\[([^\]\r\n]*)\])?/g;
|
|
@@ -174,12 +175,13 @@
|
|
|
174
175
|
return null;
|
|
175
176
|
return parsed;
|
|
176
177
|
}
|
|
177
|
-
function
|
|
178
|
+
function proseMediaItem(value) {
|
|
178
179
|
const parsed = publicHttpUrl(value);
|
|
179
180
|
if (parsed === null) return null;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
if (VIDEO_EXTENSION.test(parsed.pathname)) return { kind: "video", url: parsed.href };
|
|
182
|
+
if (IMAGE_EXTENSION.test(parsed.pathname) || parsed.protocol === "https:" && IMAGE_CDN_HOSTS.has(parsed.hostname) && !/\.(?:svg|html?|js)$/i.test(parsed.pathname))
|
|
183
|
+
return { kind: "image", url: parsed.href };
|
|
184
|
+
return null;
|
|
183
185
|
}
|
|
184
186
|
function referenceLabel(value) {
|
|
185
187
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -226,7 +228,7 @@
|
|
|
226
228
|
}
|
|
227
229
|
return result;
|
|
228
230
|
}
|
|
229
|
-
function
|
|
231
|
+
function extractDirectMediaLinks(text) {
|
|
230
232
|
let visible = stripHtml(stripInlineCode(stripFencedCode(text)));
|
|
231
233
|
const definitions = /* @__PURE__ */ new Map();
|
|
232
234
|
visible.replace(REFERENCE_DEFINITION, (_match, label, angle, bare) => {
|
|
@@ -234,19 +236,16 @@
|
|
|
234
236
|
return _match;
|
|
235
237
|
});
|
|
236
238
|
const nativeImages = /* @__PURE__ */ new Set();
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
239
|
+
const addNative = (value) => {
|
|
240
|
+
const parsed = publicHttpUrl(value);
|
|
241
|
+
if (parsed) nativeImages.add(parsed.href.replace(/#.*$/, ""));
|
|
240
242
|
};
|
|
241
|
-
visible = eraseInlineImages(visible,
|
|
242
|
-
visible = visible.replace(
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return " ".repeat(_match.length);
|
|
248
|
-
}
|
|
249
|
-
);
|
|
243
|
+
visible = eraseInlineImages(visible, addNative);
|
|
244
|
+
visible = visible.replace(REFERENCE_IMAGE, (_match, alt, explicit) => {
|
|
245
|
+
const value = definitions.get(referenceLabel(explicit === void 0 || explicit === "" ? alt : explicit));
|
|
246
|
+
if (value !== void 0) addNative(value);
|
|
247
|
+
return " ".repeat(_match.length);
|
|
248
|
+
});
|
|
250
249
|
const candidates = [];
|
|
251
250
|
visible = collectInlineLinkDestinations(
|
|
252
251
|
visible,
|
|
@@ -254,7 +253,7 @@
|
|
|
254
253
|
);
|
|
255
254
|
visible.replace(
|
|
256
255
|
REFERENCE_LINK,
|
|
257
|
-
(_match,
|
|
256
|
+
(_match, _prefix, label, explicit) => {
|
|
258
257
|
const value = definitions.get(referenceLabel(explicit === "" ? label : explicit));
|
|
259
258
|
if (value !== void 0) candidates.push(value);
|
|
260
259
|
return _match;
|
|
@@ -265,10 +264,10 @@
|
|
|
265
264
|
const result = [];
|
|
266
265
|
const seen = /* @__PURE__ */ new Set();
|
|
267
266
|
for (const candidate of candidates) {
|
|
268
|
-
const item =
|
|
267
|
+
const item = proseMediaItem(candidate);
|
|
269
268
|
if (item === null) continue;
|
|
270
269
|
const key = item.kind === "video" ? item.url : item.url.replace(/#.*$/, "");
|
|
271
|
-
if (
|
|
270
|
+
if (seen.has(key) || item.kind === "image" && nativeImages.has(key)) continue;
|
|
272
271
|
seen.add(key);
|
|
273
272
|
result.push(item);
|
|
274
273
|
if (result.length === SEARCH_MEDIA_LIMIT) break;
|
|
@@ -279,179 +278,200 @@
|
|
|
279
278
|
// src/client/inline-media.ts
|
|
280
279
|
var inlineMediaCss = `
|
|
281
280
|
[data-chat-flow-kind="lcx-search-media"]{display:none!important}
|
|
282
|
-
.lcx-media-
|
|
283
|
-
.lcx-media-
|
|
284
|
-
.lcx-media
|
|
285
|
-
.lcx-media-
|
|
286
|
-
.lcx-media-
|
|
287
|
-
.lcx-media-
|
|
288
|
-
.lcx-media-
|
|
289
|
-
.lcx-media-
|
|
290
|
-
.lcx-
|
|
291
|
-
.lcx-
|
|
292
|
-
.lcx-media-
|
|
293
|
-
.lcx-media-
|
|
294
|
-
.lcx-media-
|
|
295
|
-
|
|
296
|
-
.lcx-media-
|
|
297
|
-
.lcx-media-
|
|
298
|
-
.lcx-media-
|
|
299
|
-
.lcx-media-
|
|
300
|
-
.lcx-media-
|
|
301
|
-
.lcx-media-
|
|
302
|
-
.lcx-media-
|
|
303
|
-
.lcx-media-
|
|
304
|
-
.lcx-media-
|
|
305
|
-
.lcx-media-
|
|
306
|
-
.lcx-media-
|
|
307
|
-
.lcx-media-
|
|
308
|
-
.lcx-media-
|
|
309
|
-
.lcx-media-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border:0;border-radius:50%;background:#ffffff14;color:white;font-size:23px;cursor:pointer}
|
|
310
|
-
.lcx-media-nav{position:absolute;left:16px;top:8px;display:flex;align-items:center;gap:8px;font:12px/20px system-ui;color:#ddd}
|
|
281
|
+
.lcx-media{box-sizing:border-box;display:flex;flex-direction:column;gap:12px;min-width:0;max-width:100%;margin:16px 0 4px;font-family:var(--dsw-font-family,inherit);color:var(--dsw-alias-label-primary,inherit)}
|
|
282
|
+
.lcx-media *{box-sizing:border-box}
|
|
283
|
+
.lcx-media[hidden]{display:none}
|
|
284
|
+
.lcx-media-rail{position:relative;min-width:0;--lcx-h:136px}
|
|
285
|
+
.lcx-media-rail[data-count="1"]{--lcx-h:220px}
|
|
286
|
+
.lcx-media-rail[data-count="2"]{--lcx-h:176px}
|
|
287
|
+
.lcx-media-track{--lcx-fade-l:0px;--lcx-fade-r:0px;display:flex;gap:8px;margin:0;padding:0;list-style:none;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior-x:contain;-webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--lcx-fade-l),#000 calc(100% - var(--lcx-fade-r)),transparent 100%);mask-image:linear-gradient(to right,transparent 0,#000 var(--lcx-fade-l),#000 calc(100% - var(--lcx-fade-r)),transparent 100%)}
|
|
288
|
+
.lcx-media-track::-webkit-scrollbar{display:none}
|
|
289
|
+
.lcx-media-rail[data-more-left] .lcx-media-track{--lcx-fade-l:40px}
|
|
290
|
+
.lcx-media-rail[data-more-right] .lcx-media-track{--lcx-fade-r:40px}
|
|
291
|
+
.lcx-media-tile{position:relative;flex:none;height:var(--lcx-h);aspect-ratio:var(--lcx-ar,4/3);max-width:100%;margin:0;padding:0;scroll-snap-align:start;border-radius:var(--dsw-radius-md,12px);overflow:hidden;background:var(--dsw-alias-bg-skeleton,#0000000a)}
|
|
292
|
+
.lcx-media-tile::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 .5px var(--dsw-alias-border-l2,#0000001a);pointer-events:none}
|
|
293
|
+
.lcx-media-tile[data-state="loading"]{background:linear-gradient(100deg,var(--dsw-alias-bg-skeleton,#0000000a) 30%,var(--dsw-alias-interactive-bg-hover,#2631480f) 50%,var(--dsw-alias-bg-skeleton,#0000000a) 70%) 0 0/300% 100%;animation:lcx-media-shimmer 1.4s ease-in-out infinite}
|
|
294
|
+
@keyframes lcx-media-shimmer{from{background-position:100% 0}to{background-position:0 0}}
|
|
295
|
+
.lcx-media-open{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:none;color:inherit;font:inherit;cursor:zoom-in}
|
|
296
|
+
.lcx-media-open:disabled{cursor:default}
|
|
297
|
+
.lcx-media-open img{display:block;width:100%;height:100%;max-width:none;margin:0;padding:0;border:0;border-radius:0;object-fit:cover;opacity:0;transition:opacity .25s ease,transform .45s cubic-bezier(.2,.7,.2,1)}
|
|
298
|
+
.lcx-media-tile[data-state="loaded"] .lcx-media-open img{opacity:1}
|
|
299
|
+
.lcx-media-tile[data-fit="contain"] .lcx-media-open img{object-fit:contain}
|
|
300
|
+
.lcx-media-tile[data-state="loaded"]:hover .lcx-media-open img{transform:scale(1.04)}
|
|
301
|
+
.lcx-media-fail{position:absolute;inset:0;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#8a93a6)}
|
|
302
|
+
.lcx-media-fail svg{width:22px;height:22px}
|
|
303
|
+
.lcx-media-chip{position:absolute;left:6px;bottom:6px;z-index:1;max-width:calc(100% - 12px);overflow:hidden;padding:2px 8px;border-radius:var(--dsw-radius-xs,6px);background:#00000073;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;font-size:11px;line-height:16px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;text-decoration:none;opacity:0;transform:translateY(3px);transition:opacity .18s ease,transform .18s ease}
|
|
304
|
+
a.lcx-media-chip:hover{text-decoration:underline;text-underline-offset:2px}
|
|
305
|
+
.lcx-media-tile:hover .lcx-media-chip,.lcx-media-tile:focus-within .lcx-media-chip,.lcx-media-tile[data-state="failed"] .lcx-media-chip{opacity:1;transform:none}
|
|
306
|
+
.lcx-media-tile[data-state="failed"] .lcx-media-chip{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;color:var(--dsw-alias-label-tertiary,#8a93a6)}
|
|
307
|
+
.lcx-media-nav{position:absolute;top:50%;z-index:2;display:grid;place-items:center;width:32px;height:32px;margin-top:-16px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f),0 2px 10px #00000021;color:var(--dsw-alias-label-primary,#1f2430);cursor:pointer;opacity:0;transition:opacity .15s ease}
|
|
311
308
|
.lcx-media-nav[hidden]{display:none}
|
|
312
|
-
.lcx-media-nav
|
|
313
|
-
.lcx-media-nav
|
|
314
|
-
.lcx-media-nav
|
|
315
|
-
.lcx-media-
|
|
316
|
-
|
|
309
|
+
.lcx-media-nav[data-dir="prev"]{left:8px}
|
|
310
|
+
.lcx-media-nav[data-dir="next"]{right:8px}
|
|
311
|
+
.lcx-media-nav svg{width:16px;height:16px}
|
|
312
|
+
.lcx-media-rail:hover .lcx-media-nav,.lcx-media-nav:focus-visible{opacity:1}
|
|
313
|
+
.lcx-media-videos{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,210px),1fr));gap:8px;min-width:0}
|
|
314
|
+
.lcx-media-video{display:flex;align-items:center;gap:10px;width:100%;min-width:0;margin:0;padding:6px 12px 6px 6px;border:0;border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-interactive-bg-hover,#2631480f);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}
|
|
315
|
+
.lcx-media-video:hover{background:var(--dsw-alias-interactive-bg-active,#26314817)}
|
|
316
|
+
.lcx-media-play{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:999px;background:var(--dsw-alias-label-primary,#1f2430);color:var(--dsw-alias-label-primary-foreground,#fff);transition:transform .15s ease}
|
|
317
|
+
.lcx-media-video:hover .lcx-media-play{transform:scale(1.06)}
|
|
318
|
+
.lcx-media-play svg{width:16px;height:16px;margin-left:2px}
|
|
319
|
+
.lcx-media-video-text{display:flex;flex-direction:column;min-width:0}
|
|
320
|
+
.lcx-media-video-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;line-height:19px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
|
|
321
|
+
.lcx-media-video-sub{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#8a93a6)}
|
|
322
|
+
.lcx-media-player{grid-column:1/-1;position:relative;width:min(100%,640px);aspect-ratio:16/9;border-radius:var(--dsw-radius-md,12px);overflow:hidden;background:#000}
|
|
323
|
+
.lcx-media-player video{display:block;width:100%;height:100%;max-width:none;margin:0;border:0;object-fit:contain;background:#000}
|
|
324
|
+
.lcx-media-player-close{position:absolute;top:8px;right:8px;z-index:1;display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;border-radius:999px;background:#00000080;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer}
|
|
325
|
+
.lcx-media-player-close svg,.lcx-media-close svg{width:14px;height:14px}
|
|
326
|
+
.lcx-media-player-fail{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:16px;color:#ffffffd9;font-size:13px;line-height:20px;text-align:center}
|
|
327
|
+
.lcx-media-player-fail a{color:#fff;text-decoration:underline;text-underline-offset:3px}
|
|
328
|
+
.lcx-media-open:focus-visible,.lcx-media-chip:focus-visible,.lcx-media-nav:focus-visible,.lcx-media-video:focus-visible,.lcx-media-player-close:focus-visible,.lcx-media-close:focus-visible,.lcx-media-dialog a:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-link,#416de0));outline-offset:2px}
|
|
329
|
+
.lcx-media-open:focus-visible{outline-offset:-2px}
|
|
330
|
+
.lcx-media-dialog{position:fixed;inset:0;box-sizing:border-box;width:100vw;height:100vh;max-width:none;max-height:none;margin:0;padding:48px;border:0;overflow:hidden;background:var(--dsw-alias-bg-mask-1,#0009);-webkit-backdrop-filter:var(--dsw-mask-blur,blur(8px));backdrop-filter:var(--dsw-mask-blur,blur(8px));color:var(--dsw-alias-label-primary,inherit);font-family:var(--dsw-font-family,inherit)}
|
|
331
|
+
.lcx-media-dialog[open]{display:grid;place-items:center}
|
|
332
|
+
.lcx-media-dialog::backdrop{background:transparent}
|
|
333
|
+
.lcx-media-dialog-body{display:flex;flex-direction:column;align-items:center;gap:12px;min-width:0;max-width:min(100%,1600px);max-height:100%}
|
|
334
|
+
.lcx-media-dialog img{display:block;max-width:100%;max-height:calc(100vh - 150px);width:auto;height:auto;object-fit:contain;border-radius:var(--dsw-radius-md,12px);box-shadow:var(--dsw-elevation-prominent,0 10px 40px #0005)}
|
|
335
|
+
.lcx-media-dialog-fail{display:grid;place-items:center;min-width:min(320px,100%);min-height:160px;padding:24px;border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#5a6478);font-size:13px;line-height:20px}
|
|
336
|
+
.lcx-media-dialog-bar{display:flex;align-items:center;gap:14px;max-width:100%;padding:6px 16px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f);font-size:12px;line-height:20px}
|
|
337
|
+
.lcx-media-dialog-caption{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#5a6478)}
|
|
338
|
+
.lcx-media-dialog-bar a{flex:none;color:var(--dsw-alias-link,#416de0);text-decoration:none}
|
|
339
|
+
.lcx-media-dialog-bar a:hover{text-decoration:underline;text-underline-offset:3px}
|
|
340
|
+
.lcx-media-close{position:fixed;top:20px;right:20px;z-index:1;display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f);color:var(--dsw-alias-label-primary,#1f2430);cursor:pointer}
|
|
341
|
+
@media(max-width:520px){.lcx-media-rail{--lcx-h:112px}.lcx-media-rail[data-count="1"]{--lcx-h:184px}.lcx-media-rail[data-count="2"]{--lcx-h:136px}.lcx-media-dialog{padding:16px}.lcx-media-close{top:12px;right:12px}}
|
|
342
|
+
@media(hover:none){.lcx-media-chip{opacity:1;transform:none}.lcx-media-nav{display:none}}
|
|
343
|
+
@media(prefers-reduced-motion:reduce){.lcx-media-track{scroll-behavior:auto}.lcx-media-tile[data-state="loading"]{animation:none}.lcx-media-open img,.lcx-media-chip,.lcx-media-nav,.lcx-media-video,.lcx-media-play{transition:none}.lcx-media-tile[data-state="loaded"]:hover .lcx-media-open img{transform:none}}
|
|
317
344
|
`;
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
var nativeMarkdownImageCss = `
|
|
346
|
+
[data-chat-flow-kind="assistant-step"] p>button:has(>img:only-child){display:block;width:fit-content;max-width:100%;margin:4px 0 8px}
|
|
347
|
+
`;
|
|
348
|
+
var ICON_PLAY = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z" fill="currentColor"/></svg>';
|
|
349
|
+
var ICON_CLOSE = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.5 3.5l9 9m0-9l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
350
|
+
var ICON_BROKEN = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 16l4.5-4.5 3 3 2.5-2.5 7 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="9" cy="9" r="1.3" fill="currentColor"/></svg>';
|
|
351
|
+
var ICON_PREV = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M10 3.5 5.5 8l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
352
|
+
var ICON_NEXT = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
353
|
+
var TILE_ASPECT_MIN = 0.5;
|
|
354
|
+
var TILE_ASPECT_MAX = 2.2;
|
|
355
|
+
var imageKey = (url) => url.replace(/#.*$/, "");
|
|
356
|
+
var hostOf = (url) => {
|
|
357
|
+
try {
|
|
358
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
359
|
+
} catch {
|
|
360
|
+
return url;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var fileNameOf = (url) => {
|
|
364
|
+
try {
|
|
365
|
+
const name = decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) ?? "");
|
|
366
|
+
return name || hostOf(url);
|
|
367
|
+
} catch {
|
|
368
|
+
return url;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
function railImages(items) {
|
|
372
|
+
const seen = /* @__PURE__ */ new Set();
|
|
373
|
+
const result = [];
|
|
374
|
+
for (const item of [...items.filter((value) => value.structured === true), ...items.filter((value) => value.structured !== true)]) {
|
|
375
|
+
if (item.kind !== "image") continue;
|
|
376
|
+
const key = imageKey(item.url);
|
|
377
|
+
if (seen.has(key)) continue;
|
|
378
|
+
seen.add(key);
|
|
379
|
+
result.push(item);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function videoItems(items) {
|
|
321
384
|
const groups = /* @__PURE__ */ new Map();
|
|
322
385
|
for (const item of items) {
|
|
323
|
-
if (item.kind
|
|
386
|
+
if (item.kind !== "video") continue;
|
|
324
387
|
const url = new URL(item.url);
|
|
325
|
-
const identity =
|
|
388
|
+
const identity = JSON.stringify([url.origin, url.pathname.replace(/\.(mp4|webm|ogv)$/i, ""), url.search, url.hash]);
|
|
326
389
|
const group = groups.get(identity) ?? [];
|
|
327
390
|
if (!group.some((value) => value.url === item.url)) group.push(item);
|
|
328
391
|
groups.set(identity, group);
|
|
329
392
|
}
|
|
330
|
-
const result = /* @__PURE__ */ new Map();
|
|
331
393
|
const priority = (url) => ({ mp4: 0, webm: 1, ogv: 2 })[new URL(url).pathname.split(".").at(-1).toLowerCase()] ?? 3;
|
|
332
|
-
|
|
394
|
+
return [...groups.values()].map((group) => {
|
|
333
395
|
group.sort((a, b) => priority(a.url) - priority(b.url));
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
return result;
|
|
396
|
+
return { ...group[0], sources: group.map((value) => value.url) };
|
|
397
|
+
});
|
|
338
398
|
}
|
|
339
399
|
var activeDialogByDocument = /* @__PURE__ */ new WeakMap();
|
|
340
|
-
function
|
|
400
|
+
function inlineMediaAnswer(marker) {
|
|
341
401
|
const row = marker.closest('[data-chat-flow-kind="lcx-search-media"]');
|
|
342
|
-
const answer = row?.previousElementSibling;
|
|
343
402
|
const ElementClass = marker.ownerDocument.defaultView?.HTMLElement;
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
|
|
403
|
+
if (!ElementClass || !row || !row.dataset.chatTurn) return null;
|
|
404
|
+
const group = row.closest("[data-chat-group-key][data-step-process]");
|
|
405
|
+
const column = group?.parentElement;
|
|
406
|
+
if (!group || !(column instanceof ElementClass) || !column.hasAttribute("data-chat-flow") || group.dataset.chatTurn !== row.dataset.chatTurn) return null;
|
|
407
|
+
let answer = null;
|
|
408
|
+
for (const child of column.children) {
|
|
409
|
+
if (child === group) break;
|
|
410
|
+
if (child instanceof ElementClass && child.dataset.chatFlowKind === "assistant-step" && child.dataset.chatGroupPart === "response" && child.dataset.chatTurn === row.dataset.chatTurn) {
|
|
411
|
+
answer = child;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return answer;
|
|
347
415
|
}
|
|
348
|
-
function installInlineMedia(marker, items, labels) {
|
|
349
|
-
|
|
416
|
+
function installInlineMedia(marker, items, labels, host = {}) {
|
|
417
|
+
const resolvedAnswer = inlineMediaAnswer(marker);
|
|
418
|
+
const images = railImages(items), videos = videoItems(items);
|
|
419
|
+
if (!resolvedAnswer || images.length === 0 && videos.length === 0) return () => {
|
|
350
420
|
};
|
|
351
|
-
const
|
|
352
|
-
const answer = row.previousElementSibling;
|
|
421
|
+
const answer = resolvedAnswer;
|
|
353
422
|
const doc = marker.ownerDocument;
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const previews = /* @__PURE__ */ new Map();
|
|
357
|
-
const strips = /* @__PURE__ */ new Map();
|
|
358
|
-
const entries = /* @__PURE__ */ new Map();
|
|
359
|
-
let expanded = false;
|
|
360
|
-
const toggle = doc.createElement("button");
|
|
361
|
-
toggle.type = "button";
|
|
362
|
-
toggle.className = "lcx-media-toggle";
|
|
363
|
-
const toggleRow = doc.createElement("span");
|
|
364
|
-
toggleRow.className = "lcx-media-toggle-row";
|
|
365
|
-
toggleRow.append(toggle);
|
|
366
|
-
toggle.onclick = () => {
|
|
367
|
-
expanded = !expanded;
|
|
368
|
-
updateVisibility();
|
|
369
|
-
toggle.focus({ preventScroll: true });
|
|
370
|
-
};
|
|
371
|
-
const failed = /* @__PURE__ */ new Set();
|
|
423
|
+
const win = doc.defaultView;
|
|
424
|
+
const cleanups = [];
|
|
372
425
|
let disposed = false;
|
|
373
426
|
let frame = 0;
|
|
374
427
|
let ownedClose;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
function
|
|
382
|
-
const all = orderedEntries();
|
|
383
|
-
all.forEach((entry, index) => {
|
|
384
|
-
const hidden = !expanded && index >= VISIBLE_MEDIA_LIMIT;
|
|
385
|
-
if (entry.preview.hidden !== hidden) entry.preview.hidden = hidden;
|
|
386
|
-
if (!hidden) {
|
|
387
|
-
entry.load?.();
|
|
388
|
-
const saved = entries.get(entry.preview);
|
|
389
|
-
if (saved) saved.load = void 0;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
if (all.length <= VISIBLE_MEDIA_LIMIT) {
|
|
393
|
-
toggleRow.remove();
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const text = expanded ? labels.less : labels.more.replace("{count}", String(all.length - VISIBLE_MEDIA_LIMIT));
|
|
397
|
-
if (toggle.textContent !== text) toggle.textContent = text;
|
|
398
|
-
const aria = String(expanded);
|
|
399
|
-
if (toggle.getAttribute("aria-expanded") !== aria) toggle.setAttribute("aria-expanded", aria);
|
|
400
|
-
const parent = all[expanded ? all.length - 1 : VISIBLE_MEDIA_LIMIT - 1].preview.parentElement;
|
|
401
|
-
if (toggleRow.parentElement !== parent || toggleRow !== parent.lastElementChild) parent.append(toggleRow);
|
|
402
|
-
}
|
|
403
|
-
function removePreview(preview) {
|
|
404
|
-
const strip = preview.parentElement;
|
|
405
|
-
entries.get(preview)?.cleanup();
|
|
406
|
-
entries.delete(preview);
|
|
407
|
-
preview.remove();
|
|
408
|
-
if (strip?.classList.contains("lcx-media-strip") && !strip.childElementCount) {
|
|
409
|
-
strip.remove();
|
|
410
|
-
for (const [block, value] of strips) if (value === strip) strips.delete(block);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
function placePreview(anchor, preview) {
|
|
414
|
-
const block = anchor.closest("p,li,td,th,figcaption");
|
|
415
|
-
const owner = block && answer.contains(block) ? block : anchor;
|
|
416
|
-
let strip = strips.get(owner);
|
|
417
|
-
if (!strip?.isConnected) {
|
|
418
|
-
strip = doc.createElement("span");
|
|
419
|
-
strip.className = "lcx-media-strip";
|
|
420
|
-
if (owner === anchor) anchor.after(strip);
|
|
421
|
-
else owner.append(strip);
|
|
422
|
-
strips.set(owner, strip);
|
|
423
|
-
}
|
|
424
|
-
strip.append(preview);
|
|
425
|
-
}
|
|
426
|
-
function open(item, trigger, fail) {
|
|
428
|
+
const token = Math.random().toString(36).slice(2);
|
|
429
|
+
answer.dataset.lcxMediaOwner = token;
|
|
430
|
+
for (const stale of [...answer.children]) if (stale.classList.contains("lcx-media")) stale.remove();
|
|
431
|
+
const root = doc.createElement("div");
|
|
432
|
+
root.className = "lcx-media";
|
|
433
|
+
root.dataset.lcxMedia = "";
|
|
434
|
+
function openDialog(item, trigger, description) {
|
|
427
435
|
activeDialogByDocument.get(doc)?.();
|
|
428
436
|
const dialog = doc.createElement("dialog");
|
|
429
437
|
dialog.className = "lcx-media-dialog";
|
|
430
|
-
dialog.setAttribute("aria-label",
|
|
438
|
+
dialog.setAttribute("aria-label", labels.imageDialog);
|
|
439
|
+
dialog.dataset.kind = "image";
|
|
440
|
+
const body = doc.createElement("div");
|
|
441
|
+
body.className = "lcx-media-dialog-body";
|
|
431
442
|
const close = doc.createElement("button");
|
|
432
443
|
close.type = "button";
|
|
433
444
|
close.className = "lcx-media-close";
|
|
434
|
-
close.
|
|
445
|
+
close.innerHTML = ICON_CLOSE;
|
|
435
446
|
close.setAttribute("aria-label", labels.close);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
media.playsInline = true;
|
|
443
|
-
media.preload = "none";
|
|
444
|
-
}
|
|
445
|
-
if (media instanceof HTMLVideoElement) media.src = item.url;
|
|
447
|
+
const bar = doc.createElement("div");
|
|
448
|
+
bar.className = "lcx-media-dialog-bar";
|
|
449
|
+
const caption = doc.createElement("span");
|
|
450
|
+
caption.className = "lcx-media-dialog-caption";
|
|
451
|
+
caption.textContent = description;
|
|
452
|
+
caption.title = description;
|
|
446
453
|
const source = doc.createElement("a");
|
|
447
454
|
source.href = item.sourceUrl ?? item.url;
|
|
448
455
|
source.target = "_blank";
|
|
449
456
|
source.rel = "noopener noreferrer";
|
|
450
457
|
source.textContent = labels.source;
|
|
458
|
+
bar.append(caption, source);
|
|
459
|
+
let media;
|
|
451
460
|
let closed = false;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
461
|
+
const image = doc.createElement("img");
|
|
462
|
+
image.alt = description;
|
|
463
|
+
image.referrerPolicy = "no-referrer";
|
|
464
|
+
image.src = item.url;
|
|
465
|
+
image.onerror = () => {
|
|
466
|
+
if (closed) return;
|
|
467
|
+
const fail = doc.createElement("div");
|
|
468
|
+
fail.className = "lcx-media-dialog-fail";
|
|
469
|
+
fail.textContent = labels.failed;
|
|
470
|
+
media.onerror = null;
|
|
471
|
+
media.replaceWith(fail);
|
|
472
|
+
media = fail;
|
|
473
|
+
};
|
|
474
|
+
media = image;
|
|
455
475
|
const cleanup = () => {
|
|
456
476
|
if (closed) return;
|
|
457
477
|
closed = true;
|
|
@@ -459,15 +479,7 @@
|
|
|
459
479
|
dialog.oncancel = null;
|
|
460
480
|
dialog.onclick = null;
|
|
461
481
|
dialog.onkeydown = null;
|
|
462
|
-
previous && (previous.onclick = null);
|
|
463
|
-
next && (next.onclick = null);
|
|
464
|
-
media.onload = null;
|
|
465
482
|
media.onerror = null;
|
|
466
|
-
if (media instanceof HTMLVideoElement) {
|
|
467
|
-
media.pause();
|
|
468
|
-
media.removeAttribute("src");
|
|
469
|
-
media.load();
|
|
470
|
-
}
|
|
471
483
|
dialog.remove();
|
|
472
484
|
if (activeDialogByDocument.get(doc) === cleanup) activeDialogByDocument.delete(doc);
|
|
473
485
|
if (ownedClose === cleanup) ownedClose = void 0;
|
|
@@ -481,253 +493,316 @@
|
|
|
481
493
|
cleanup();
|
|
482
494
|
};
|
|
483
495
|
dialog.onclick = (event) => {
|
|
484
|
-
if (event.target === dialog)
|
|
485
|
-
const rect = dialog.getBoundingClientRect();
|
|
486
|
-
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) cleanup();
|
|
487
|
-
}
|
|
496
|
+
if (event.target === dialog) cleanup();
|
|
488
497
|
};
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
498
|
+
dialog.onkeydown = (event) => {
|
|
499
|
+
if (event.key !== "Tab") return;
|
|
500
|
+
const focusable = [...dialog.querySelectorAll("button:not([disabled]),a[href]")];
|
|
501
|
+
if (focusable.length === 0) return;
|
|
502
|
+
const first = focusable[0], last = focusable[focusable.length - 1];
|
|
503
|
+
const active = doc.activeElement;
|
|
504
|
+
if (event.shiftKey && active === first) {
|
|
505
|
+
event.preventDefault();
|
|
506
|
+
last.focus();
|
|
507
|
+
} else if (!event.shiftKey && active === last) {
|
|
508
|
+
event.preventDefault();
|
|
509
|
+
first.focus();
|
|
499
510
|
}
|
|
500
|
-
cleanup();
|
|
501
|
-
fail();
|
|
502
511
|
};
|
|
503
|
-
|
|
512
|
+
body.append(media, bar);
|
|
513
|
+
dialog.append(body, close);
|
|
504
514
|
doc.body.append(dialog);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
previousButton.setAttribute("aria-label", labels.previous);
|
|
515
|
-
nextButton.setAttribute("aria-label", labels.next);
|
|
516
|
-
counter.setAttribute("aria-live", "polite");
|
|
517
|
-
nav.append(previousButton, counter, nextButton);
|
|
518
|
-
dialog.append(nav);
|
|
519
|
-
let currentKey = mediaKey(item);
|
|
520
|
-
const images = () => orderedEntries().filter((entry) => entry.item.kind === "image");
|
|
521
|
-
const select = (index) => {
|
|
515
|
+
dialog.showModal();
|
|
516
|
+
close.focus();
|
|
517
|
+
}
|
|
518
|
+
function openImage(item, trigger, description) {
|
|
519
|
+
activeDialogByDocument.get(doc)?.();
|
|
520
|
+
if (host.openImage) {
|
|
521
|
+
let closed = false;
|
|
522
|
+
let dispose = null;
|
|
523
|
+
const cleanup = () => {
|
|
522
524
|
if (closed) return;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
image.referrerPolicy = "no-referrer";
|
|
529
|
-
image.onerror = () => {
|
|
530
|
-
if (closed || media !== image) return;
|
|
531
|
-
entry.fail();
|
|
532
|
-
const remaining = images();
|
|
533
|
-
if (!remaining.length) {
|
|
534
|
-
cleanup();
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
select(Math.min(index, remaining.length - 1));
|
|
538
|
-
};
|
|
539
|
-
media.onerror = null;
|
|
540
|
-
media.replaceWith(image);
|
|
541
|
-
media = image;
|
|
542
|
-
source.href = entry.item.sourceUrl ?? entry.item.url;
|
|
543
|
-
image.src = entry.item.url;
|
|
544
|
-
counter.textContent = `${index + 1} / ${list.length}`;
|
|
545
|
-
previousButton.disabled = index === 0;
|
|
546
|
-
nextButton.disabled = index === list.length - 1;
|
|
547
|
-
nav.hidden = list.length < 2;
|
|
548
|
-
};
|
|
549
|
-
const move = (offset) => {
|
|
550
|
-
const list = images();
|
|
551
|
-
select(list.findIndex((entry) => mediaKey(entry.item) === currentKey) + offset);
|
|
552
|
-
};
|
|
553
|
-
previousButton.onclick = () => move(-1);
|
|
554
|
-
nextButton.onclick = () => move(1);
|
|
555
|
-
dialog.onkeydown = (event) => {
|
|
556
|
-
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
557
|
-
event.preventDefault();
|
|
558
|
-
move(event.key === "ArrowLeft" ? -1 : 1);
|
|
559
|
-
}
|
|
525
|
+
closed = true;
|
|
526
|
+
dispose?.();
|
|
527
|
+
if (activeDialogByDocument.get(doc) === cleanup) activeDialogByDocument.delete(doc);
|
|
528
|
+
if (ownedClose === cleanup) ownedClose = void 0;
|
|
529
|
+
if (trigger.isConnected) trigger.focus({ preventScroll: true });
|
|
560
530
|
};
|
|
561
|
-
|
|
531
|
+
dispose = host.openImage({ src: item.url, alt: description, labels: { dialog: labels.imageDialog, close: labels.close }, onClose: cleanup });
|
|
532
|
+
if (dispose) {
|
|
533
|
+
ownedClose = cleanup;
|
|
534
|
+
activeDialogByDocument.set(doc, cleanup);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
closed = true;
|
|
562
538
|
}
|
|
563
|
-
|
|
564
|
-
close.focus();
|
|
565
|
-
if (media instanceof HTMLVideoElement) void media.play().catch(() => {
|
|
566
|
-
});
|
|
539
|
+
openDialog(item, trigger, description);
|
|
567
540
|
}
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
541
|
+
if (images.length) {
|
|
542
|
+
const rail = doc.createElement("div");
|
|
543
|
+
rail.className = "lcx-media-rail";
|
|
544
|
+
rail.dataset.count = String(Math.min(images.length, 3));
|
|
545
|
+
const track = doc.createElement("ul");
|
|
546
|
+
track.className = "lcx-media-track";
|
|
547
|
+
track.setAttribute("role", "list");
|
|
548
|
+
track.setAttribute("aria-label", `${labels.heading} \xB7 ${images.length}`);
|
|
549
|
+
const makeNav = (dir) => {
|
|
550
|
+
const button = doc.createElement("button");
|
|
551
|
+
button.type = "button";
|
|
552
|
+
button.className = "lcx-media-nav";
|
|
553
|
+
button.dataset.dir = dir;
|
|
554
|
+
button.hidden = true;
|
|
555
|
+
button.tabIndex = -1;
|
|
556
|
+
button.setAttribute("aria-label", dir === "prev" ? labels.previous : labels.next);
|
|
557
|
+
button.innerHTML = dir === "prev" ? ICON_PREV : ICON_NEXT;
|
|
558
|
+
button.onclick = () => track.scrollBy({ left: (dir === "prev" ? -1 : 1) * Math.max(160, track.clientWidth * 0.8), behavior: "smooth" });
|
|
559
|
+
cleanups.push(() => {
|
|
560
|
+
button.onclick = null;
|
|
561
|
+
});
|
|
562
|
+
return button;
|
|
586
563
|
};
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
564
|
+
const prev = makeNav("prev"), next = makeNav("next");
|
|
565
|
+
const updateEdges = () => {
|
|
566
|
+
if (disposed) return;
|
|
567
|
+
const max = track.scrollWidth - track.clientWidth;
|
|
568
|
+
const left = track.scrollLeft > 2, right = max - track.scrollLeft > 2;
|
|
569
|
+
rail.toggleAttribute("data-more-left", left);
|
|
570
|
+
rail.toggleAttribute("data-more-right", right);
|
|
571
|
+
prev.hidden = !left;
|
|
572
|
+
next.hidden = !right;
|
|
594
573
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
574
|
+
track.addEventListener("scroll", updateEdges, { passive: true });
|
|
575
|
+
cleanups.push(() => track.removeEventListener("scroll", updateEdges));
|
|
576
|
+
const Resize = win?.ResizeObserver;
|
|
577
|
+
if (Resize) {
|
|
578
|
+
const resize = new Resize(updateEdges);
|
|
579
|
+
resize.observe(track);
|
|
580
|
+
cleanups.push(() => resize.disconnect());
|
|
581
|
+
}
|
|
582
|
+
for (const item of images) {
|
|
583
|
+
const domain = hostOf(item.sourceUrl ?? item.url);
|
|
584
|
+
const description = item.caption ?? domain;
|
|
585
|
+
const tile = doc.createElement("li");
|
|
586
|
+
tile.className = "lcx-media-tile";
|
|
587
|
+
tile.dataset.state = "loading";
|
|
588
|
+
tile.dataset.url = item.url;
|
|
589
|
+
const open = doc.createElement("button");
|
|
590
|
+
open.type = "button";
|
|
591
|
+
open.className = "lcx-media-open";
|
|
592
|
+
open.setAttribute("aria-label", `${labels.image} \xB7 ${description}`);
|
|
593
|
+
if (item.caption) open.title = item.caption;
|
|
598
594
|
const image = doc.createElement("img");
|
|
599
|
-
thumbnail = image;
|
|
600
595
|
image.alt = "";
|
|
601
596
|
image.loading = "lazy";
|
|
602
597
|
image.decoding = "async";
|
|
603
598
|
image.referrerPolicy = "no-referrer";
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
599
|
+
image.draggable = false;
|
|
600
|
+
open.append(image);
|
|
601
|
+
let source = item.previewUrl ?? item.url;
|
|
602
|
+
image.onload = () => {
|
|
603
|
+
if (tile.dataset.state !== "loading") return;
|
|
604
|
+
const ratio = image.naturalWidth > 0 && image.naturalHeight > 0 ? image.naturalWidth / image.naturalHeight : 0;
|
|
605
|
+
if (ratio) tile.style.setProperty("--lcx-ar", String(Math.min(TILE_ASPECT_MAX, Math.max(TILE_ASPECT_MIN, ratio))));
|
|
606
|
+
if (ratio && (ratio > TILE_ASPECT_MAX || ratio < TILE_ASPECT_MIN)) tile.dataset.fit = "contain";
|
|
607
|
+
tile.dataset.state = "loaded";
|
|
608
|
+
updateEdges();
|
|
614
609
|
};
|
|
615
|
-
image.
|
|
616
|
-
|
|
610
|
+
image.onerror = () => {
|
|
611
|
+
if (source !== item.url) {
|
|
612
|
+
source = item.url;
|
|
613
|
+
image.src = source;
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
image.onload = null;
|
|
617
|
+
image.onerror = null;
|
|
617
618
|
image.remove();
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
619
|
+
if (item.structured !== true) {
|
|
620
|
+
open.onclick = null;
|
|
621
|
+
open.onfocus = null;
|
|
622
|
+
tile.remove();
|
|
623
|
+
rail.dataset.count = String(Math.min(track.children.length, 3));
|
|
624
|
+
if (track.children.length === 0) {
|
|
625
|
+
rail.remove();
|
|
626
|
+
if (!root.querySelector(".lcx-media-videos")) root.hidden = true;
|
|
627
|
+
}
|
|
628
|
+
updateEdges();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
tile.dataset.state = "failed";
|
|
632
|
+
open.disabled = true;
|
|
633
|
+
open.onclick = null;
|
|
634
|
+
open.setAttribute("aria-label", `${labels.failed} \xB7 ${description}`);
|
|
635
|
+
open.title = labels.failed;
|
|
636
|
+
const fail = doc.createElement("span");
|
|
637
|
+
fail.className = "lcx-media-fail";
|
|
638
|
+
fail.innerHTML = ICON_BROKEN;
|
|
639
|
+
open.append(fail);
|
|
622
640
|
};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
badge.setAttribute("aria-hidden", "true");
|
|
636
|
-
button.append(badge);
|
|
641
|
+
image.src = source;
|
|
642
|
+
open.onclick = () => openImage(item, open, description);
|
|
643
|
+
open.onfocus = () => tile.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
644
|
+
const chip = item.sourceUrl ? doc.createElement("a") : doc.createElement("span");
|
|
645
|
+
chip.className = "lcx-media-chip";
|
|
646
|
+
chip.textContent = domain;
|
|
647
|
+
chip.title = item.caption ? `${item.caption} \xB7 ${domain}` : domain;
|
|
648
|
+
if (item.sourceUrl) {
|
|
649
|
+
const anchor = chip;
|
|
650
|
+
anchor.href = item.sourceUrl;
|
|
651
|
+
anchor.target = "_blank";
|
|
652
|
+
anchor.rel = "noopener noreferrer";
|
|
637
653
|
}
|
|
654
|
+
tile.append(open, chip);
|
|
655
|
+
track.append(tile);
|
|
656
|
+
cleanups.push(() => {
|
|
657
|
+
open.onclick = null;
|
|
658
|
+
open.onfocus = null;
|
|
659
|
+
image.onload = null;
|
|
660
|
+
image.onerror = null;
|
|
661
|
+
});
|
|
638
662
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
entries.set(preview, entry);
|
|
642
|
-
return preview;
|
|
663
|
+
rail.append(track, prev, next);
|
|
664
|
+
root.append(rail);
|
|
643
665
|
}
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
666
|
+
if (videos.length) {
|
|
667
|
+
const section = doc.createElement("div");
|
|
668
|
+
section.className = "lcx-media-videos";
|
|
669
|
+
for (const item of videos) {
|
|
670
|
+
const name = fileNameOf(item.url);
|
|
671
|
+
const format = item.sources.map((value) => /\.(mp4|webm|ogv)$/i.exec(new URL(value).pathname)?.[1]).filter(Boolean).join(" / ").toUpperCase();
|
|
672
|
+
const card = doc.createElement("button");
|
|
673
|
+
card.type = "button";
|
|
674
|
+
card.className = "lcx-media-video";
|
|
675
|
+
card.dataset.url = item.url;
|
|
676
|
+
card.setAttribute("aria-label", `${labels.video} \xB7 ${name}`);
|
|
677
|
+
const play = doc.createElement("span");
|
|
678
|
+
play.className = "lcx-media-play";
|
|
679
|
+
play.setAttribute("aria-hidden", "true");
|
|
680
|
+
play.innerHTML = ICON_PLAY;
|
|
681
|
+
const text = doc.createElement("span");
|
|
682
|
+
text.className = "lcx-media-video-text";
|
|
683
|
+
const title = doc.createElement("span");
|
|
684
|
+
title.className = "lcx-media-video-title";
|
|
685
|
+
title.textContent = name;
|
|
686
|
+
title.title = item.url;
|
|
687
|
+
const sub = doc.createElement("span");
|
|
688
|
+
sub.className = "lcx-media-video-sub";
|
|
689
|
+
sub.textContent = [hostOf(item.url), format].filter(Boolean).join(" \xB7 ");
|
|
690
|
+
text.append(title, sub);
|
|
691
|
+
card.append(play, text);
|
|
692
|
+
let collapse;
|
|
693
|
+
card.onclick = () => {
|
|
694
|
+
const player = doc.createElement("div");
|
|
695
|
+
player.className = "lcx-media-player";
|
|
696
|
+
player.setAttribute("role", "group");
|
|
697
|
+
player.setAttribute("aria-label", `${labels.dialog} \xB7 ${name}`);
|
|
698
|
+
const video = doc.createElement("video");
|
|
699
|
+
video.controls = true;
|
|
700
|
+
video.playsInline = true;
|
|
701
|
+
video.preload = "none";
|
|
702
|
+
const poster = item.previewUrl ?? item.poster;
|
|
703
|
+
if (poster) video.poster = poster;
|
|
704
|
+
const close = doc.createElement("button");
|
|
705
|
+
close.type = "button";
|
|
706
|
+
close.className = "lcx-media-player-close";
|
|
707
|
+
close.innerHTML = ICON_CLOSE;
|
|
708
|
+
close.setAttribute("aria-label", labels.close);
|
|
709
|
+
let sourceIndex = 0;
|
|
710
|
+
let closed = false;
|
|
711
|
+
video.onerror = () => {
|
|
712
|
+
if (closed) return;
|
|
713
|
+
if (sourceIndex + 1 < item.sources.length) {
|
|
714
|
+
sourceIndex += 1;
|
|
715
|
+
video.src = item.sources[sourceIndex];
|
|
716
|
+
video.load();
|
|
717
|
+
void video.play().catch(() => {
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
video.onerror = null;
|
|
722
|
+
video.pause();
|
|
723
|
+
video.removeAttribute("src");
|
|
724
|
+
video.load();
|
|
725
|
+
video.remove();
|
|
726
|
+
const fail = doc.createElement("div");
|
|
727
|
+
fail.className = "lcx-media-player-fail";
|
|
728
|
+
const message = doc.createElement("span");
|
|
729
|
+
message.textContent = labels.videoFailed;
|
|
730
|
+
const link = doc.createElement("a");
|
|
731
|
+
link.href = item.url;
|
|
732
|
+
link.target = "_blank";
|
|
733
|
+
link.rel = "noopener noreferrer";
|
|
734
|
+
link.textContent = labels.source;
|
|
735
|
+
fail.append(message, link);
|
|
736
|
+
player.append(fail);
|
|
737
|
+
};
|
|
738
|
+
collapse = (refocus = true) => {
|
|
739
|
+
if (closed) return;
|
|
740
|
+
closed = true;
|
|
741
|
+
video.onerror = null;
|
|
742
|
+
close.onclick = null;
|
|
743
|
+
player.onkeydown = null;
|
|
744
|
+
video.pause();
|
|
745
|
+
video.removeAttribute("src");
|
|
746
|
+
video.load();
|
|
747
|
+
player.replaceWith(card);
|
|
748
|
+
collapse = void 0;
|
|
749
|
+
if (refocus && card.isConnected) card.focus({ preventScroll: true });
|
|
750
|
+
};
|
|
751
|
+
close.onclick = () => collapse?.();
|
|
752
|
+
player.onkeydown = (event) => {
|
|
753
|
+
if (event.key === "Escape") {
|
|
754
|
+
event.preventDefault();
|
|
755
|
+
collapse?.();
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
video.src = item.url;
|
|
759
|
+
player.append(video, close);
|
|
760
|
+
card.replaceWith(player);
|
|
761
|
+
video.focus({ preventScroll: true });
|
|
762
|
+
void video.play().catch(() => {
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
section.append(card);
|
|
766
|
+
cleanups.push(() => {
|
|
767
|
+
collapse?.(false);
|
|
768
|
+
card.onclick = null;
|
|
769
|
+
});
|
|
653
770
|
}
|
|
654
|
-
|
|
771
|
+
root.append(section);
|
|
655
772
|
}
|
|
656
|
-
|
|
657
|
-
|
|
773
|
+
answer.append(root);
|
|
774
|
+
const keepAttached = () => {
|
|
658
775
|
frame = 0;
|
|
659
776
|
if (disposed) return;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (!canPreview(anchor)) continue;
|
|
664
|
-
let url;
|
|
665
|
-
try {
|
|
666
|
-
url = new URL(anchor.href);
|
|
667
|
-
} catch {
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) continue;
|
|
671
|
-
const baseItem = wanted.get(url.href.replace(/#.*$/, ""));
|
|
672
|
-
const item = wanted.get(url.href) ?? (baseItem?.kind === "image" ? baseItem : void 0);
|
|
673
|
-
if (!item) continue;
|
|
674
|
-
const key = mediaKey(item);
|
|
675
|
-
if (selected.has(key) || failed.has(key)) continue;
|
|
676
|
-
selected.add(key);
|
|
677
|
-
candidates.set(anchor, { item, url });
|
|
678
|
-
}
|
|
679
|
-
for (const [anchor, current] of previews) {
|
|
680
|
-
if (!candidates.has(anchor) || !current.preview.isConnected || current.href !== anchor.href) {
|
|
681
|
-
removePreview(current.preview);
|
|
682
|
-
previews.delete(anchor);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
for (const [anchor, { item, url }] of candidates) {
|
|
686
|
-
if (previews.has(anchor)) continue;
|
|
687
|
-
const preview = createPreview(
|
|
688
|
-
item,
|
|
689
|
-
anchor.textContent?.trim() || url.hostname,
|
|
690
|
-
() => previews.delete(anchor)
|
|
691
|
-
);
|
|
692
|
-
if (!preview) continue;
|
|
693
|
-
placePreview(anchor, preview);
|
|
694
|
-
previews.set(anchor, { preview, href: anchor.href });
|
|
777
|
+
if (answer.dataset.lcxMediaOwner !== token) {
|
|
778
|
+
root.remove();
|
|
779
|
+
return;
|
|
695
780
|
}
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
scan();
|
|
781
|
+
if (root.parentElement !== answer || answer.lastElementChild !== root) answer.append(root);
|
|
782
|
+
};
|
|
699
783
|
const observer = new MutationObserver(() => {
|
|
700
|
-
if (!disposed && !frame) frame = requestAnimationFrame(
|
|
701
|
-
});
|
|
702
|
-
observer.observe(answer, {
|
|
703
|
-
attributes: true,
|
|
704
|
-
attributeFilter: ["data-streaming", "data-turn-process-inline", "href"],
|
|
705
|
-
childList: true,
|
|
706
|
-
subtree: true
|
|
784
|
+
if (!disposed && !frame) frame = requestAnimationFrame(keepAttached);
|
|
707
785
|
});
|
|
786
|
+
observer.observe(answer, { childList: true });
|
|
708
787
|
return () => {
|
|
709
788
|
if (disposed) return;
|
|
710
|
-
disposed = true;
|
|
711
789
|
observer.disconnect();
|
|
712
790
|
if (frame) {
|
|
713
791
|
cancelAnimationFrame(frame);
|
|
714
792
|
frame = 0;
|
|
715
793
|
}
|
|
716
794
|
ownedClose?.();
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
for (const strip of strips.values()) strip.remove();
|
|
723
|
-
strips.clear();
|
|
795
|
+
for (const cleanup of cleanups) cleanup();
|
|
796
|
+
cleanups.length = 0;
|
|
797
|
+
disposed = true;
|
|
798
|
+
if (answer.dataset.lcxMediaOwner === token) delete answer.dataset.lcxMediaOwner;
|
|
799
|
+
root.remove();
|
|
724
800
|
};
|
|
725
801
|
}
|
|
726
802
|
|
|
727
803
|
// src/search-accounting.ts
|
|
728
804
|
var object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
729
805
|
var count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
730
|
-
var zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
731
806
|
var isSearchTool = (name) => name === "web_search" || name === "websearch_gpt_advanced";
|
|
732
807
|
function auxiliaryUsageOf(event, toolName) {
|
|
733
808
|
if (!object(event) || event.type !== "tool/result" || !object(event.data?.meta)) return [];
|
|
@@ -744,38 +819,6 @@
|
|
|
744
819
|
}
|
|
745
820
|
return result;
|
|
746
821
|
}
|
|
747
|
-
function addUsage(base, records) {
|
|
748
|
-
if (!records.length) return base;
|
|
749
|
-
const next = { ...base };
|
|
750
|
-
for (const { usage: u } of records) {
|
|
751
|
-
next.uncachedInputTokens += u.inputTokens;
|
|
752
|
-
next.outputTokens += u.outputTokens;
|
|
753
|
-
next.cacheReadTokens += u.cacheReadTokens;
|
|
754
|
-
next.cacheWriteTokens += u.cacheWriteTokens;
|
|
755
|
-
}
|
|
756
|
-
if (!Object.values(next).every(count)) throw new Error("LCX search usage exceeds safe counters");
|
|
757
|
-
return next;
|
|
758
|
-
}
|
|
759
|
-
function mergeBuckets(base, extra) {
|
|
760
|
-
if (!object(base) || Object.values(extra).every((n) => n === 0)) return base;
|
|
761
|
-
const next = { ...base };
|
|
762
|
-
for (const key of Object.keys(extra)) {
|
|
763
|
-
if (count(base[key])) next[key] = base[key] + extra[key];
|
|
764
|
-
}
|
|
765
|
-
return next;
|
|
766
|
-
}
|
|
767
|
-
function addTurnUsage(base, records) {
|
|
768
|
-
if (!object(base) || !records.length || !count(base.totalTokens)) return base;
|
|
769
|
-
const next = mergeBuckets(base, addUsage(zeroBuckets(), records));
|
|
770
|
-
next.totalTokens = base.totalTokens + records.reduce((n, r) => n + r.usage.totalTokens, 0);
|
|
771
|
-
delete next.reasoningTokens;
|
|
772
|
-
if (Array.isArray(base.routes)) {
|
|
773
|
-
const routes = /* @__PURE__ */ new Map();
|
|
774
|
-
for (const r of [...base.routes, ...records]) routes.set(`${r.provider}\0${r.model}`, { provider: r.provider, model: r.model });
|
|
775
|
-
next.routes = [...routes.values()];
|
|
776
|
-
}
|
|
777
|
-
return next;
|
|
778
|
-
}
|
|
779
822
|
|
|
780
823
|
// src/client/search-usage-ui.ts
|
|
781
824
|
var searchUsageDefinition = {
|
|
@@ -803,64 +846,70 @@
|
|
|
803
846
|
return scope === "turn" && s?.complete ? { kind: "turn", turn: s.turn, key: "lcx-search-usage", value: s.records } : null;
|
|
804
847
|
}
|
|
805
848
|
};
|
|
849
|
+
function tokenCount(value) {
|
|
850
|
+
return String(value);
|
|
851
|
+
}
|
|
852
|
+
function recordsTotal(records) {
|
|
853
|
+
let total = 0;
|
|
854
|
+
for (const record2 of records) {
|
|
855
|
+
const value = record2.usage.totalTokens;
|
|
856
|
+
if (!Number.isSafeInteger(value) || value < 0) continue;
|
|
857
|
+
total += value;
|
|
858
|
+
if (!Number.isSafeInteger(total)) return 0;
|
|
859
|
+
}
|
|
860
|
+
return total;
|
|
861
|
+
}
|
|
862
|
+
function bucketsTotal(value) {
|
|
863
|
+
if (!object(value)) return 0;
|
|
864
|
+
let total = 0;
|
|
865
|
+
for (const key of ["uncachedInputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
|
|
866
|
+
const count2 = value[key];
|
|
867
|
+
if (!Number.isSafeInteger(count2) || count2 < 0) return 0;
|
|
868
|
+
total += count2;
|
|
869
|
+
if (!Number.isSafeInteger(total)) return 0;
|
|
870
|
+
}
|
|
871
|
+
return total;
|
|
872
|
+
}
|
|
873
|
+
function usageView(createElement, scope, total, t) {
|
|
874
|
+
if (total <= 0) return null;
|
|
875
|
+
const label = t?.(scope === "turn" ? "usageTurn" : "usageSession") ?? "Search usage";
|
|
876
|
+
return createElement("span", {
|
|
877
|
+
className: `lcx-search-usage lcx-search-usage-${scope}`,
|
|
878
|
+
"data-lcx-search-usage": scope,
|
|
879
|
+
title: `${label}: ${tokenCount(total)}`
|
|
880
|
+
}, `${label}: ${tokenCount(total)}`);
|
|
881
|
+
}
|
|
882
|
+
function TurnTailUsage({ turn, t }, createElement) {
|
|
883
|
+
if (!createElement) return null;
|
|
884
|
+
return usageView(createElement, "turn", recordsTotal(turn.data.get("lcx-search-usage") ?? []), t);
|
|
885
|
+
}
|
|
886
|
+
function SessionUsage({ useProjection, t }, createElement) {
|
|
887
|
+
if (!createElement) return null;
|
|
888
|
+
const extra = useProjection("lcxSearchUsage");
|
|
889
|
+
const auxiliary = object(extra) ? extra.auxiliary : void 0;
|
|
890
|
+
return usageView(createElement, "session", bucketsTotal(auxiliary), t);
|
|
891
|
+
}
|
|
806
892
|
function installUsageSlots(slots, createElement) {
|
|
893
|
+
const registrations = [
|
|
894
|
+
[
|
|
895
|
+
{ name: "conversation.chat.turnTail", id: "lcx-search-usage-turn", order: 0, locale: "lcx-codex" },
|
|
896
|
+
(props) => TurnTailUsage(props, createElement)
|
|
897
|
+
],
|
|
898
|
+
[
|
|
899
|
+
{ name: "conversation.composer.dock", id: "lcx-search-usage-session", order: 10, locale: "lcx-codex" },
|
|
900
|
+
(props) => SessionUsage(props, createElement)
|
|
901
|
+
]
|
|
902
|
+
];
|
|
807
903
|
const cleanups = [];
|
|
808
|
-
for (const [name,
|
|
809
|
-
["conversation.composer.dock", "stats"],
|
|
810
|
-
["conversation.composer.bar", ""],
|
|
811
|
-
["conversation.chat.node", "turn-tail"]
|
|
812
|
-
]) {
|
|
813
|
-
let effect;
|
|
904
|
+
for (const [name, component] of registrations) {
|
|
814
905
|
try {
|
|
815
|
-
effect = slots.inject(name, () => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
entries = slots.entriesOfSlot(name);
|
|
819
|
-
} catch {
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (!Array.isArray(entries) || entries.length === 0) return;
|
|
823
|
-
const candidates = key === "" ? [entries[0]] : entries.filter((e) => e?.options?.id === key || e?.options?.key === key);
|
|
824
|
-
if (candidates.length !== 1) return;
|
|
825
|
-
const base = candidates[0], original = base?.component;
|
|
826
|
-
if (!base || typeof original !== "function" || !base.options || typeof base.options !== "object") return;
|
|
827
|
-
const descriptor = Object.getOwnPropertyDescriptor(base, "component");
|
|
828
|
-
if (descriptor && descriptor.set === void 0 && descriptor.writable === false) return;
|
|
829
|
-
function WithSearchUsage(props) {
|
|
830
|
-
if (key === "turn-tail") {
|
|
831
|
-
const location = props?.node?.location;
|
|
832
|
-
const records = (location?.kind === "turn" || location?.kind === "step") && location.turn?.data?.get ? location.turn.data.get("lcx-search-usage") : void 0;
|
|
833
|
-
if (!records?.length) return createElement(original, props);
|
|
834
|
-
return createElement(original, { ...props, node: { ...props.node, data: { ...props.node.data, tokenUsage: addTurnUsage(props.node.data.tokenUsage, records) } } });
|
|
835
|
-
}
|
|
836
|
-
if (typeof props?.useProjection !== "function") return createElement(original, props);
|
|
837
|
-
const extra = props.useProjection("lcxSearchUsage");
|
|
838
|
-
const breakdown = props.useProjection("contextBreakdown");
|
|
839
|
-
const useProjection = (projection) => {
|
|
840
|
-
const value = props.useProjection(projection);
|
|
841
|
-
if (projection === "tokenUsage" && extra?.auxiliary) return mergeBuckets(value, extra.auxiliary);
|
|
842
|
-
if (projection === "contextPressure" && extra?.aggregateContext && object(breakdown)) {
|
|
843
|
-
const tokens = breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
844
|
-
if (Number.isSafeInteger(tokens) && tokens >= 0 && object(value)) return { ...value, pressureTokens: tokens, projectedTokens: tokens };
|
|
845
|
-
}
|
|
846
|
-
return value;
|
|
847
|
-
};
|
|
848
|
-
return createElement(original, { ...props, useProjection });
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
base.component = WithSearchUsage;
|
|
852
|
-
} catch {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
if (base.component !== WithSearchUsage) return;
|
|
856
|
-
return () => {
|
|
857
|
-
if (base.component === WithSearchUsage) base.component = original;
|
|
858
|
-
};
|
|
906
|
+
const effect = slots.inject(name.name, () => {
|
|
907
|
+
const dispose = slots.register(name, component);
|
|
908
|
+
return typeof dispose === "function" ? dispose : void 0;
|
|
859
909
|
});
|
|
910
|
+
if (typeof effect === "function") cleanups.push(effect);
|
|
860
911
|
} catch {
|
|
861
|
-
continue;
|
|
862
912
|
}
|
|
863
|
-
if (typeof effect === "function") cleanups.push(effect);
|
|
864
913
|
}
|
|
865
914
|
return () => {
|
|
866
915
|
for (const dispose of cleanups.reverse()) dispose();
|
|
@@ -894,7 +943,7 @@
|
|
|
894
943
|
const DEFAULTS = Object.fromEntries(
|
|
895
944
|
FIELDS.map((field) => [field, false])
|
|
896
945
|
);
|
|
897
|
-
const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}` + inlineMediaCss;
|
|
946
|
+
const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}.lcx-search-usage{display:inline-flex;align-items:center;gap:4px;margin:2px 0;color:var(--dsw-alias-label-tertiary);font:12px/18px system-ui}.lcx-search-usage-session{padding:0 4px}.lcx-search-usage-turn{padding:0 6px}` + inlineMediaCss + nativeMarkdownImageCss;
|
|
898
947
|
function mountCss() {
|
|
899
948
|
if (typeof document === "undefined") return () => {
|
|
900
949
|
};
|
|
@@ -910,6 +959,7 @@
|
|
|
910
959
|
const copy = {
|
|
911
960
|
zh: {
|
|
912
961
|
title: "Responses / Codex \u80FD\u529B",
|
|
962
|
+
summary: "GPT Responses route \u4E0E\u641C\u7D22\u80FD\u529B",
|
|
913
963
|
desc: "LCX \u8DDF\u968F DSH \u5F53\u524D\u4F1A\u8BDD\u9009\u62E9\u7684 GPT Responses route\uFF1B\u6A21\u578B\u3001endpoint \u548C credential \u7EE7\u7EED\u53EA\u7531 DSH \u7BA1\u7406\u3002",
|
|
914
964
|
enabled: "\u542F\u7528 LCX\uFF08\u63A5\u7BA1\u5F53\u524D GPT Responses \u4F1A\u8BDD\uFF09",
|
|
915
965
|
enabledHelp: "\u6B64\u5F00\u5173\u4EC5\u63A5\u7BA1 GPT Responses \u4F1A\u8BDD\uFF1BGrok \u539F\u751F\u641C\u7D22\u7531\u4E0B\u65B9\u72EC\u7ACB\u5F00\u5173\u63A7\u5236\uFF0C\u5176\u4ED6\u975E GPT \u6A21\u578B\u7EE7\u7EED\u4F7F\u7528 DSH \u539F\u751F\u8DEF\u5F84\u3002",
|
|
@@ -920,25 +970,20 @@
|
|
|
920
970
|
alpha: "\u542F\u7528 Alpha command\uFF08websearch_alpha\uFF09",
|
|
921
971
|
alphaHelp: "\u4EC5 capability probe \u5BF9\u5F53\u524D route/schema \u9A8C\u8BC1\u901A\u8FC7\u540E\u624D\u771F\u6B63\u6CE8\u518C\u3002",
|
|
922
972
|
mediaPreview: "\u641C\u7D22\u5A92\u4F53\u9884\u89C8",
|
|
923
|
-
mediaPreviewHelp: "\u5728\u56DE\u7B54\u4E0B\u663E\u793A\
|
|
924
|
-
mediaTitle: "\
|
|
973
|
+
mediaPreviewHelp: "\u5728\u56DE\u7B54\u4E0B\u65B9\u663E\u793A\u641C\u7D22\u8FD4\u56DE\u7684\u56FE\u7247\u3001\u56DE\u7B54\u91CC\u7684\u56FE\u7247\u76F4\u94FE\uFF0C\u4EE5\u53CA\u53EF\u76F4\u63A5\u64AD\u653E\u7684\u89C6\u9891\u76F4\u94FE\uFF1B\u70B9\u51FB\u653E\u5927\u6216\u64AD\u653E\u3002\u56DE\u7B54\u6B63\u6587\u91CC\u7684 Markdown \u56FE\u7247\u7531 DSH \u539F\u751F\u6E32\u67D3\uFF0C\u666E\u901A\u7F51\u9875\u94FE\u63A5\u4FDD\u6301\u4E3A\u94FE\u63A5\u3002\u6B64\u8BBE\u7F6E\u53EA\u6539\u53D8\u754C\u9762\u663E\u793A\u3002",
|
|
974
|
+
mediaTitle: "\u89C6\u9891\u9884\u89C8",
|
|
975
|
+
mediaImagePreview: "\u56FE\u7247\u9884\u89C8",
|
|
925
976
|
mediaPlay: "\u64AD\u653E\u89C6\u9891",
|
|
926
977
|
mediaEnlarge: "\u653E\u5927\u56FE\u7247",
|
|
927
978
|
mediaClose: "\u5173\u95ED\u9884\u89C8",
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
mediaLoading: "\u6B63\u5728\u83B7\u53D6\u5A92\u4F53\u2026",
|
|
934
|
-
mediaUnavailable: "\u6682\u4E0D\u80FD\u9884\u89C8",
|
|
935
|
-
mediaAll: "\u5168\u90E8",
|
|
936
|
-
mediaImages: "\u56FE\u7247",
|
|
937
|
-
mediaVideos: "\u89C6\u9891",
|
|
938
|
-
mediaImage: "\u56FE\u7247",
|
|
939
|
-
mediaVideo: "\u89C6\u9891",
|
|
940
|
-
mediaFilter: "\u5A92\u4F53\u7C7B\u578B",
|
|
979
|
+
mediaHeading: "\u641C\u7D22\u56FE\u7247",
|
|
980
|
+
mediaPrevious: "\u4E0A\u4E00\u7EC4\u56FE\u7247",
|
|
981
|
+
mediaNext: "\u4E0B\u4E00\u7EC4\u56FE\u7247",
|
|
982
|
+
mediaFailed: "\u56FE\u7247\u52A0\u8F7D\u5931\u8D25",
|
|
983
|
+
mediaVideoFailed: "\u89C6\u9891\u65E0\u6CD5\u64AD\u653E",
|
|
941
984
|
mediaOpen: "\u6253\u5F00\u539F\u59CB\u5A92\u4F53",
|
|
985
|
+
usageTurn: "\u672C\u8F6E\u641C\u7D22\u7528\u91CF",
|
|
986
|
+
usageSession: "\u641C\u7D22\u7528\u91CF",
|
|
942
987
|
grokTitle: "Grok \u539F\u751F\u641C\u7D22",
|
|
943
988
|
grokDesc: "\u4F7F\u7528 DSH \u5F53\u524D\u9009\u62E9\u7684 Grok \u6A21\u578B\u53CA\u5176\u670D\u52A1\u914D\u7F6E\uFF1B\u4E0E GPT \u529F\u80FD\u5F00\u5173\u72EC\u7ACB\u3002\u5F00\u542F\u4EFB\u4E00\u641C\u7D22\u540E\uFF0CGrok \u4EC5\u4F7F\u7528\u539F\u751F\u641C\u7D22\uFF0C\u7F51\u9875\u8BFB\u53D6\u548C\u5176\u4ED6\u5DE5\u5177\u4ECD\u53EF\u7528\u3002",
|
|
944
989
|
grokWeb: "\u542F\u7528\u539F\u751F Web Search",
|
|
@@ -952,6 +997,7 @@
|
|
|
952
997
|
},
|
|
953
998
|
en: {
|
|
954
999
|
title: "Responses / Codex capabilities",
|
|
1000
|
+
summary: "GPT Responses route and search capabilities",
|
|
955
1001
|
desc: "LCX follows the GPT Responses route selected by the current DSH session. Model, endpoint and credentials remain DSH-owned.",
|
|
956
1002
|
enabled: "Enable LCX (own current GPT Responses conversation)",
|
|
957
1003
|
enabledHelp: "Applies only to GPT Responses conversations. Grok native search is controlled independently below; other non-GPT models continue through DSH normally.",
|
|
@@ -962,25 +1008,20 @@
|
|
|
962
1008
|
alpha: "Enable Alpha command (websearch_alpha)",
|
|
963
1009
|
alphaHelp: "Registered only after a matching capability probe.",
|
|
964
1010
|
mediaPreview: "Search media previews",
|
|
965
|
-
mediaPreviewHelp: "
|
|
966
|
-
mediaTitle: "
|
|
1011
|
+
mediaPreviewHelp: "Show images returned by search, direct image links in the answer and directly playable video links below the answer; click to enlarge or play. Markdown images in the answer are rendered natively by DSH, and ordinary page links stay links. This setting changes presentation only.",
|
|
1012
|
+
mediaTitle: "Video preview",
|
|
1013
|
+
mediaImagePreview: "Image preview",
|
|
967
1014
|
mediaPlay: "Play video",
|
|
968
1015
|
mediaEnlarge: "Enlarge image",
|
|
969
1016
|
mediaClose: "Close preview",
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
mediaLoading: "Resolving media\u2026",
|
|
976
|
-
mediaUnavailable: "Preview unavailable",
|
|
977
|
-
mediaAll: "All",
|
|
978
|
-
mediaImages: "Photos",
|
|
979
|
-
mediaVideos: "Videos",
|
|
980
|
-
mediaImage: "Photo",
|
|
981
|
-
mediaVideo: "Video",
|
|
982
|
-
mediaFilter: "Media type",
|
|
1017
|
+
mediaHeading: "Search images",
|
|
1018
|
+
mediaPrevious: "Previous images",
|
|
1019
|
+
mediaNext: "Next images",
|
|
1020
|
+
mediaFailed: "Image failed to load",
|
|
1021
|
+
mediaVideoFailed: "Video cannot be played",
|
|
983
1022
|
mediaOpen: "Open original media",
|
|
1023
|
+
usageTurn: "Turn search usage",
|
|
1024
|
+
usageSession: "Search usage",
|
|
984
1025
|
grokTitle: "Grok native search",
|
|
985
1026
|
grokDesc: "Uses the Grok model and provider profile currently selected in DSH, independently of the GPT switch. When either search is enabled, Grok uses native search while page reading and other tools remain available.",
|
|
986
1027
|
grokWeb: "Enable native Web Search",
|
|
@@ -1067,7 +1108,12 @@
|
|
|
1067
1108
|
...context.state,
|
|
1068
1109
|
anchorSeq: match.event.seq,
|
|
1069
1110
|
location: match.location,
|
|
1070
|
-
|
|
1111
|
+
// Ownership (Issue #102): structured provider candidates + direct image/video file links
|
|
1112
|
+
// from the answer text. Markdown images belong to DSH and are never duplicated.
|
|
1113
|
+
items: mergeSearchMedia(
|
|
1114
|
+
context.state.structuredItems,
|
|
1115
|
+
extractDirectMediaLinks(text)
|
|
1116
|
+
),
|
|
1071
1117
|
provider: source.provider,
|
|
1072
1118
|
model: source.model,
|
|
1073
1119
|
ready: true
|
|
@@ -1102,12 +1148,14 @@
|
|
|
1102
1148
|
saveError;
|
|
1103
1149
|
store;
|
|
1104
1150
|
stop;
|
|
1151
|
+
saveEpoch;
|
|
1105
1152
|
constructor(scope) {
|
|
1106
1153
|
this.scope = scope;
|
|
1107
1154
|
this.draft = null;
|
|
1108
1155
|
this.dirty = false;
|
|
1109
1156
|
this.saving = false;
|
|
1110
1157
|
this.saveError = false;
|
|
1158
|
+
this.saveEpoch = 0;
|
|
1111
1159
|
this.store = createSnapshotStore(this.projection());
|
|
1112
1160
|
mediaStore.set({ enabled: this.value().searchMediaPreview });
|
|
1113
1161
|
this.stop = scope.subscribe(() => {
|
|
@@ -1159,10 +1207,19 @@
|
|
|
1159
1207
|
this.saveError = false;
|
|
1160
1208
|
this.publish();
|
|
1161
1209
|
}
|
|
1210
|
+
leavePage() {
|
|
1211
|
+
this.saveEpoch += 1;
|
|
1212
|
+
this.draft = null;
|
|
1213
|
+
this.dirty = false;
|
|
1214
|
+
this.saving = false;
|
|
1215
|
+
this.saveError = false;
|
|
1216
|
+
this.publish();
|
|
1217
|
+
}
|
|
1162
1218
|
async save() {
|
|
1163
1219
|
if (!this.dirty || this.saving || !this.snapshot().writable || this.snapshot().status !== "ready") return;
|
|
1164
1220
|
const next = this.draftValue(), prev = this.value();
|
|
1165
1221
|
const fields = FIELDS.filter((field) => next[field] !== prev[field]);
|
|
1222
|
+
const saveEpoch = ++this.saveEpoch;
|
|
1166
1223
|
this.saving = true;
|
|
1167
1224
|
this.saveError = false;
|
|
1168
1225
|
this.publish();
|
|
@@ -1173,16 +1230,20 @@
|
|
|
1173
1230
|
path: [field],
|
|
1174
1231
|
value: next[field]
|
|
1175
1232
|
})));
|
|
1233
|
+
if (saveEpoch !== this.saveEpoch) return;
|
|
1176
1234
|
if (this.snapshot().status !== "ready" || fields.some((field) => this.value()[field] !== next[field]))
|
|
1177
1235
|
throw new Error("Settings mutation was not confirmed");
|
|
1178
1236
|
}
|
|
1237
|
+
if (saveEpoch !== this.saveEpoch) return;
|
|
1179
1238
|
this.draft = null;
|
|
1180
1239
|
this.dirty = false;
|
|
1181
1240
|
} catch {
|
|
1182
|
-
this.saveError = true;
|
|
1241
|
+
if (saveEpoch === this.saveEpoch) this.saveError = true;
|
|
1183
1242
|
} finally {
|
|
1184
|
-
this.
|
|
1185
|
-
|
|
1243
|
+
if (saveEpoch === this.saveEpoch) {
|
|
1244
|
+
this.saving = false;
|
|
1245
|
+
this.publish();
|
|
1246
|
+
}
|
|
1186
1247
|
}
|
|
1187
1248
|
}
|
|
1188
1249
|
inject() {
|
|
@@ -1194,7 +1255,8 @@
|
|
|
1194
1255
|
setMediaPreview: (value) => this.edit("searchMediaPreview", value),
|
|
1195
1256
|
edit: (field, value) => this.edit(field, value),
|
|
1196
1257
|
save: () => void this.save(),
|
|
1197
|
-
discard: () => this.discard()
|
|
1258
|
+
discard: () => this.discard(),
|
|
1259
|
+
leavePage: () => this.leavePage()
|
|
1198
1260
|
};
|
|
1199
1261
|
}
|
|
1200
1262
|
}
|
|
@@ -1226,7 +1288,13 @@
|
|
|
1226
1288
|
}
|
|
1227
1289
|
function Card(props) {
|
|
1228
1290
|
const t = props.t, s = props.useLcxCard((state) => state), [open, setOpen] = React.useState(false);
|
|
1291
|
+
React.useEffect(
|
|
1292
|
+
() => props.view === "page" ? () => props.leavePage() : void 0,
|
|
1293
|
+
[props.view, props.leavePage]
|
|
1294
|
+
);
|
|
1229
1295
|
if (!s.available) return null;
|
|
1296
|
+
if (props.view === "summary")
|
|
1297
|
+
return React.createElement("span", { className: "lcx-summary" }, t("summary"));
|
|
1230
1298
|
const disabled = !s.writable || s.saving;
|
|
1231
1299
|
return React.createElement(
|
|
1232
1300
|
"li",
|
|
@@ -1329,11 +1397,37 @@
|
|
|
1329
1397
|
) : null
|
|
1330
1398
|
);
|
|
1331
1399
|
}
|
|
1400
|
+
const lightboxHost = {
|
|
1401
|
+
openImage({ src, alt, labels, onClose }) {
|
|
1402
|
+
try {
|
|
1403
|
+
const primitives = require2("@deepseek-ai/dsh-client-ui-primitives");
|
|
1404
|
+
const reactDom = require2("react-dom/client");
|
|
1405
|
+
if (typeof primitives?.ImageLightbox !== "function" || typeof reactDom?.createRoot !== "function") return null;
|
|
1406
|
+
const container = document.createElement("div");
|
|
1407
|
+
container.dataset.lcxMediaLightbox = "";
|
|
1408
|
+
document.body.appendChild(container);
|
|
1409
|
+
const root = reactDom.createRoot(container);
|
|
1410
|
+
root.render(React.createElement(primitives.ImageLightbox, { src, alt, labels, onClose }));
|
|
1411
|
+
let done = false;
|
|
1412
|
+
return () => {
|
|
1413
|
+
if (done) return;
|
|
1414
|
+
done = true;
|
|
1415
|
+
queueMicrotask(() => {
|
|
1416
|
+
root.unmount();
|
|
1417
|
+
container.remove();
|
|
1418
|
+
});
|
|
1419
|
+
};
|
|
1420
|
+
} catch {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1332
1425
|
function InlineMedia({ node, t }) {
|
|
1333
1426
|
const [marker, setMarker] = React.useState(null);
|
|
1334
1427
|
React.useEffect(() => {
|
|
1335
1428
|
if (!marker) return;
|
|
1336
|
-
|
|
1429
|
+
const labels = { image: t("mediaEnlarge"), video: t("mediaPlay"), close: t("mediaClose"), source: t("mediaOpen"), failed: t("mediaFailed"), videoFailed: t("mediaVideoFailed"), heading: t("mediaHeading"), previous: t("mediaPrevious"), next: t("mediaNext"), dialog: t("mediaTitle"), imageDialog: t("mediaImagePreview") };
|
|
1430
|
+
return installInlineMedia(marker, node.data.items, labels, lightboxHost);
|
|
1337
1431
|
}, [marker, node.data.items, t]);
|
|
1338
1432
|
return React.createElement("span", { ref: setMarker, "aria-hidden": true });
|
|
1339
1433
|
}
|
|
@@ -1341,14 +1435,12 @@
|
|
|
1341
1435
|
const enabled = props.useMediaPreview((state) => state?.enabled === true);
|
|
1342
1436
|
return enabled ? React.createElement(InlineMedia, { node: props.node, t: props.t }) : null;
|
|
1343
1437
|
}
|
|
1344
|
-
const inject = ["slots", "locale", "
|
|
1438
|
+
const inject = ["slots", "locale", "configForms", "uiConversation"];
|
|
1345
1439
|
function apply(ctx) {
|
|
1346
|
-
const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.
|
|
1440
|
+
const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.configForms ?? ctx.get("configForms"), locale = ctx.locale ?? ctx.get("locale"), uiConversation = ctx.uiConversation ?? ctx.get("uiConversation");
|
|
1347
1441
|
if (!slots || !svc || !locale || !uiConversation) return;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
|
|
1351
|
-
}
|
|
1442
|
+
ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
|
|
1443
|
+
ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
|
|
1352
1444
|
ctx.effect(mountCss, "lcx-codex styles");
|
|
1353
1445
|
for (const language of ["zh", "en"])
|
|
1354
1446
|
ctx.effect(() => locale.register(NAMESPACE, language, copy[language]), `lcx-codex ${language} dictionary`);
|
|
@@ -1362,14 +1454,14 @@
|
|
|
1362
1454
|
if (typeof cleanup === "function") cleanup();
|
|
1363
1455
|
};
|
|
1364
1456
|
}, label);
|
|
1365
|
-
const controller = new Controller(svc.
|
|
1457
|
+
const controller = new Controller(svc.get(NAMESPACE));
|
|
1366
1458
|
installSlot(
|
|
1367
|
-
"
|
|
1368
|
-
"lcx-codex
|
|
1459
|
+
"plugins.bundle.config",
|
|
1460
|
+
"lcx-codex plugin configuration",
|
|
1369
1461
|
() => slots.register(
|
|
1370
1462
|
{
|
|
1371
|
-
name: "
|
|
1372
|
-
key:
|
|
1463
|
+
name: "plugins.bundle.config",
|
|
1464
|
+
key: "dsh-lcx-codex",
|
|
1373
1465
|
locale: NAMESPACE,
|
|
1374
1466
|
inject: () => controller.inject()
|
|
1375
1467
|
},
|