loopengine 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/playground.js +308 -1
- package/package.json +1 -1
package/dist/web/playground.js
CHANGED
|
@@ -175,6 +175,96 @@ export const playgroundHtml = `<!doctype html>
|
|
|
175
175
|
.question-answer-row input { flex: 1; font-size: 12px; }
|
|
176
176
|
.question-answer-row button { font-size: 12px; padding: 5px 12px; }
|
|
177
177
|
.question-status { font-size: 12px; font-style: italic; color: light-dark(#666, #999); }
|
|
178
|
+
/* An assistant message's rendered markdown (renderMessageMarkdown) —
|
|
179
|
+
tight margins since .msg-body already pads the bubble; a nested
|
|
180
|
+
paragraph/heading with normal margins would double up on that. */
|
|
181
|
+
.msg-body p { margin: 6px 0; }
|
|
182
|
+
.msg-body p:first-child { margin-top: 0; }
|
|
183
|
+
.msg-body p:last-child { margin-bottom: 0; }
|
|
184
|
+
.msg-body h1, .msg-body h2, .msg-body h3, .msg-body h4 { margin: 10px 0 4px; font-size: 14px; }
|
|
185
|
+
.msg-body code { font-family: ui-monospace, monospace; font-size: 12px; background: light-dark(#f3f3f4, #333); padding: 1px 4px; border-radius: 4px; }
|
|
186
|
+
.msg-body pre { background: light-dark(#f3f3f4, #26262b); border-radius: 6px; padding: 8px; overflow-x: auto; }
|
|
187
|
+
.msg-body pre code { background: none; padding: 0; }
|
|
188
|
+
.msg-body a { color: light-dark(#2563eb, #60a5fa); }
|
|
189
|
+
/* An image immediately followed by a download link (see
|
|
190
|
+
renderMessageMarkdown's own doc comment) merges into this — full
|
|
191
|
+
bubble width, a corner download icon that's a plain <a href> (a real
|
|
192
|
+
Content-Disposition: attachment response, so a plain click already
|
|
193
|
+
downloads correctly with no JS or same-origin requirement), and the
|
|
194
|
+
image itself opens openImageLightbox on click rather than
|
|
195
|
+
navigating anywhere. */
|
|
196
|
+
/* inline-block, not block — the wrap's own box needs to shrink to the
|
|
197
|
+
image's actual rendered width (capped by max-height below, so a
|
|
198
|
+
landscape shot is much narrower than the full chat bubble) so the
|
|
199
|
+
corner download icon, positioned relative to this wrap, lands on the
|
|
200
|
+
image's real visible edge instead of floating in empty space beside
|
|
201
|
+
a narrower image inside a full-width box. */
|
|
202
|
+
.msg-preview-wrap { position: relative; display: inline-block; max-width: 100%; margin: 6px 0; }
|
|
203
|
+
/* .msg-plain-img (no download link — see renderMessageMarkdown's own
|
|
204
|
+
doc comment) gets the same capped size and click-to-zoom as the
|
|
205
|
+
merged .msg-preview-img, just without the corner download icon and
|
|
206
|
+
without a wrap element of its own (nothing needs to be positioned
|
|
207
|
+
relative to it) — see the chatPane click handler below and
|
|
208
|
+
openImageLightbox's own handling of a missing downloadHref. */
|
|
209
|
+
.msg-preview-img, .msg-plain-img { display: block; max-width: 100%; max-height: 260px; width: auto; height: auto; border-radius: 8px; cursor: zoom-in; }
|
|
210
|
+
.msg-plain-img { margin: 6px 0; }
|
|
211
|
+
.msg-download-icon {
|
|
212
|
+
position: absolute;
|
|
213
|
+
top: 8px;
|
|
214
|
+
right: 8px;
|
|
215
|
+
width: 30px;
|
|
216
|
+
height: 30px;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
background: rgba(0, 0, 0, 0.6);
|
|
221
|
+
color: #fff;
|
|
222
|
+
border-radius: 50%;
|
|
223
|
+
text-decoration: none;
|
|
224
|
+
font-size: 15px;
|
|
225
|
+
line-height: 1;
|
|
226
|
+
}
|
|
227
|
+
.msg-download-icon:hover { background: rgba(0, 0, 0, 0.8); }
|
|
228
|
+
.lightbox-overlay {
|
|
229
|
+
display: none;
|
|
230
|
+
position: fixed;
|
|
231
|
+
inset: 0;
|
|
232
|
+
background: rgba(0, 0, 0, 0.85);
|
|
233
|
+
z-index: 1000;
|
|
234
|
+
align-items: center;
|
|
235
|
+
justify-content: center;
|
|
236
|
+
padding: 48px;
|
|
237
|
+
}
|
|
238
|
+
.lightbox-overlay.open { display: flex; }
|
|
239
|
+
.lightbox-inner { position: relative; max-width: 100%; max-height: 100%; }
|
|
240
|
+
.lightbox-inner img { display: block; max-width: 100%; max-height: 85vh; border-radius: 8px; }
|
|
241
|
+
.lightbox-download {
|
|
242
|
+
position: absolute;
|
|
243
|
+
top: 10px;
|
|
244
|
+
right: 10px;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: 6px;
|
|
248
|
+
padding: 8px 14px;
|
|
249
|
+
background: rgba(0, 0, 0, 0.7);
|
|
250
|
+
color: #fff;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
text-decoration: none;
|
|
253
|
+
font-size: 13px;
|
|
254
|
+
}
|
|
255
|
+
.lightbox-download:hover { background: rgba(0, 0, 0, 0.9); }
|
|
256
|
+
.lightbox-close {
|
|
257
|
+
position: absolute;
|
|
258
|
+
top: -40px;
|
|
259
|
+
right: 0;
|
|
260
|
+
background: none;
|
|
261
|
+
border: none;
|
|
262
|
+
color: #fff;
|
|
263
|
+
font-size: 28px;
|
|
264
|
+
line-height: 1;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
padding: 4px 8px;
|
|
267
|
+
}
|
|
178
268
|
.dot {
|
|
179
269
|
width: 6px;
|
|
180
270
|
height: 6px;
|
|
@@ -280,6 +370,13 @@ export const playgroundHtml = `<!doctype html>
|
|
|
280
370
|
<div class="pane-body" id="timelinePane"></div>
|
|
281
371
|
</section>
|
|
282
372
|
</main>
|
|
373
|
+
<div class="lightbox-overlay" id="lightboxOverlay">
|
|
374
|
+
<div class="lightbox-inner">
|
|
375
|
+
<button type="button" class="lightbox-close" id="lightboxClose" aria-label="Close">×</button>
|
|
376
|
+
<img id="lightboxImage" src="" alt="">
|
|
377
|
+
<a id="lightboxDownload" class="lightbox-download" href="" target="_blank" rel="noopener noreferrer">⇩ Download</a>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
283
380
|
<script>
|
|
284
381
|
(function () {
|
|
285
382
|
var agentSelect = document.getElementById('agentSelect');
|
|
@@ -338,6 +435,208 @@ export const playgroundHtml = `<!doctype html>
|
|
|
338
435
|
pane.appendChild(p);
|
|
339
436
|
}
|
|
340
437
|
|
|
438
|
+
function escapeHtml(s) {
|
|
439
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
440
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// A small, deliberately non-CommonMark markdown-to-HTML renderer for
|
|
445
|
+
// an assistant message's own reply text (see appendChatMessage below —
|
|
446
|
+
// only ever applied to role: 'assistant', never to a user's own typed
|
|
447
|
+
// message or a raw error string). Same design and same escape-safety
|
|
448
|
+
// reasoning as web/agents-config-page.ts's own renderMarkdownPreview —
|
|
449
|
+
// duplicated here rather than shared since these are two separate
|
|
450
|
+
// self-contained pages with no shared module between them. Covers
|
|
451
|
+
// headings, bold/italic, inline code, fenced code blocks, links,
|
|
452
|
+
// images, and lists — good enough for what an agent's own final reply
|
|
453
|
+
// realistically contains, not a spec-complete parser. Escapes first,
|
|
454
|
+
// so no markdown source can inject raw HTML.
|
|
455
|
+
//
|
|
456
|
+
// The one thing beyond plain CommonMark: an image immediately followed
|
|
457
|
+
// on its own line by a link (see lp-product-ad-images' own SKILL.md,
|
|
458
|
+
// "After generating") merges into one widget — a full-bubble-width
|
|
459
|
+
// preview with its own corner download icon, the image itself opening
|
|
460
|
+
// openImageLightbox on click instead of navigating anywhere. The
|
|
461
|
+
// download icon (inline or in the lightbox) is a plain <a href> to a
|
|
462
|
+
// separately-signed URL carrying its own Content-Disposition:
|
|
463
|
+
// attachment — a real HTTP response header, so a plain click already
|
|
464
|
+
// downloads correctly regardless of origin, no JS fetch/blob trick or
|
|
465
|
+
// same-origin requirement needed.
|
|
466
|
+
function renderMessageMarkdown(md) {
|
|
467
|
+
var lines = escapeHtml(md).split('\\n');
|
|
468
|
+
var html = '';
|
|
469
|
+
var inCode = false;
|
|
470
|
+
var codeBuffer = '';
|
|
471
|
+
var listType = null;
|
|
472
|
+
var paragraph = [];
|
|
473
|
+
|
|
474
|
+
// Same "build patterns from a runtime backslash character, never a
|
|
475
|
+
// literal backslash-letter sequence in this file's own source" gotcha
|
|
476
|
+
// web/agents-config-page.ts's own renderMarkdownPreview already
|
|
477
|
+
// documents in full — this page is just as much an outer template
|
|
478
|
+
// literal whose own output is itself parsed as JS a second time.
|
|
479
|
+
var bs = String.fromCharCode(92);
|
|
480
|
+
var inlineCodePattern = new RegExp('\\u0060([^\\u0060]+)\\u0060', 'g');
|
|
481
|
+
var fencePattern = new RegExp('^\\u0060\\u0060\\u0060');
|
|
482
|
+
var boldPattern = new RegExp(bs + '*' + bs + '*([^*]+)' + bs + '*' + bs + '*', 'g');
|
|
483
|
+
var italicPattern = new RegExp(bs + '*([^*]+)' + bs + '*', 'g');
|
|
484
|
+
var imagePattern = new RegExp('!' + bs + '[([^' + bs + ']]*)' + bs + ']' + bs + '(([^)]+)' + bs + ')', 'g');
|
|
485
|
+
var linkPattern = new RegExp(bs + '[([^' + bs + ']]+)' + bs + ']' + bs + '(([^)]+)' + bs + ')', 'g');
|
|
486
|
+
var headingPattern = new RegExp('^(#{1,6})' + bs + 's+(.*)$');
|
|
487
|
+
var hrPattern = new RegExp('^(---|' + bs + '*' + bs + '*' + bs + '*)' + bs + 's*$');
|
|
488
|
+
var ulPattern = new RegExp('^[-*]' + bs + 's+(.*)$');
|
|
489
|
+
var olPattern = new RegExp('^' + bs + 'd+' + bs + '.' + bs + 's+(.*)$');
|
|
490
|
+
// Runs before linkPattern — an already-replaced image's own <img> tag
|
|
491
|
+
// has no literal "[...](...)" text left in it for linkPattern to
|
|
492
|
+
// also (mis)match.
|
|
493
|
+
function inlineFormat(text) {
|
|
494
|
+
return text
|
|
495
|
+
.replace(inlineCodePattern, '<code>$1</code>')
|
|
496
|
+
.replace(boldPattern, '<strong>$1</strong>')
|
|
497
|
+
.replace(italicPattern, '<em>$1</em>')
|
|
498
|
+
.replace(imagePattern, '<img src="$2" alt="$1" class="msg-plain-img" data-full-src="$2">')
|
|
499
|
+
.replace(linkPattern, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
500
|
+
}
|
|
501
|
+
function flushParagraph() {
|
|
502
|
+
if (paragraph.length) {
|
|
503
|
+
html += '<p>' + inlineFormat(paragraph.join(' ')) + '</p>';
|
|
504
|
+
paragraph = [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function closeList() {
|
|
508
|
+
if (listType) {
|
|
509
|
+
html += '</' + listType + '>';
|
|
510
|
+
listType = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (var i = 0; i < lines.length; i++) {
|
|
515
|
+
var line = lines[i];
|
|
516
|
+
if (fencePattern.test(line)) {
|
|
517
|
+
if (!inCode) {
|
|
518
|
+
flushParagraph();
|
|
519
|
+
closeList();
|
|
520
|
+
inCode = true;
|
|
521
|
+
codeBuffer = '';
|
|
522
|
+
} else {
|
|
523
|
+
html += '<pre><code>' + codeBuffer + '</code></pre>';
|
|
524
|
+
inCode = false;
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (inCode) {
|
|
529
|
+
codeBuffer += line + '\\n';
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
var heading = line.match(headingPattern);
|
|
534
|
+
if (heading) {
|
|
535
|
+
flushParagraph();
|
|
536
|
+
closeList();
|
|
537
|
+
var level = heading[1].length;
|
|
538
|
+
html += '<h' + level + '>' + inlineFormat(heading[2]) + '</h' + level + '>';
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (hrPattern.test(line)) {
|
|
542
|
+
flushParagraph();
|
|
543
|
+
closeList();
|
|
544
|
+
html += '<hr>';
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
var ul = line.match(ulPattern);
|
|
548
|
+
var ol = line.match(olPattern);
|
|
549
|
+
if (ul || ol) {
|
|
550
|
+
flushParagraph();
|
|
551
|
+
var wantType = ul ? 'ul' : 'ol';
|
|
552
|
+
if (listType !== wantType) {
|
|
553
|
+
closeList();
|
|
554
|
+
html += '<' + wantType + '>';
|
|
555
|
+
listType = wantType;
|
|
556
|
+
}
|
|
557
|
+
html += '<li>' + inlineFormat((ul || ol)[1]) + '</li>';
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
closeList();
|
|
561
|
+
|
|
562
|
+
if (line.trim() === '') {
|
|
563
|
+
flushParagraph();
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
paragraph.push(line.trim());
|
|
567
|
+
}
|
|
568
|
+
flushParagraph();
|
|
569
|
+
closeList();
|
|
570
|
+
|
|
571
|
+
// The merge step: an <img class="msg-plain-img"> directly followed
|
|
572
|
+
// by (only whitespace between) an <a href>...</a> becomes one
|
|
573
|
+
// preview widget instead of two unrelated-looking elements. Built
|
|
574
|
+
// with new RegExp(string) rather than a /pattern/ literal — a
|
|
575
|
+
// literal's own closing "/" inside "</a>" would need escaping, and
|
|
576
|
+
// "\/" hits the exact same silently-dropped-backslash problem as
|
|
577
|
+
// "\d"/"\s" do (see this function's own header comment) since "/"
|
|
578
|
+
// isn't a recognized string escape character either. A plain string
|
|
579
|
+
// needs no such escaping for "/" at all.
|
|
580
|
+
var previewMergePattern = new RegExp(
|
|
581
|
+
'<img src="([^"]*)" alt="([^"]*)" class="msg-plain-img" data-full-src="[^"]*">' + bs + 's*<a href="([^"]*)"[^>]*>[^<]*</a>',
|
|
582
|
+
'g',
|
|
583
|
+
);
|
|
584
|
+
html = html.replace(previewMergePattern, function (match, src, alt, href) {
|
|
585
|
+
return '<span class="msg-preview-wrap">' +
|
|
586
|
+
'<img src="' + src + '" alt="' + alt + '" class="msg-preview-img" data-full-src="' + src + '" data-download-href="' + href + '">' +
|
|
587
|
+
'<a href="' + href + '" class="msg-download-icon" target="_blank" rel="noopener noreferrer" title="Download" aria-label="Download">⇩</a>' +
|
|
588
|
+
'</span>';
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return html;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
var lightboxOverlay = document.getElementById('lightboxOverlay');
|
|
595
|
+
var lightboxImage = document.getElementById('lightboxImage');
|
|
596
|
+
var lightboxDownload = document.getElementById('lightboxDownload');
|
|
597
|
+
var lightboxClose = document.getElementById('lightboxClose');
|
|
598
|
+
|
|
599
|
+
function openImageLightbox(src, downloadHref) {
|
|
600
|
+
lightboxImage.src = src;
|
|
601
|
+
// A plain image (no download link — see renderMessageMarkdown's own
|
|
602
|
+
// doc comment on .msg-plain-img) has no downloadHref at all; hiding
|
|
603
|
+
// the button beats pointing it at '' (a dead link that would just
|
|
604
|
+
// reload the page) or at src (opens inline, not an actual download).
|
|
605
|
+
if (downloadHref) {
|
|
606
|
+
lightboxDownload.href = downloadHref;
|
|
607
|
+
lightboxDownload.style.display = '';
|
|
608
|
+
} else {
|
|
609
|
+
lightboxDownload.style.display = 'none';
|
|
610
|
+
}
|
|
611
|
+
lightboxOverlay.classList.add('open');
|
|
612
|
+
}
|
|
613
|
+
function closeImageLightbox() {
|
|
614
|
+
lightboxOverlay.classList.remove('open');
|
|
615
|
+
lightboxImage.src = '';
|
|
616
|
+
}
|
|
617
|
+
lightboxClose.addEventListener('click', closeImageLightbox);
|
|
618
|
+
// Clicking the darkened backdrop closes it; clicking the image/download
|
|
619
|
+
// icon/close button (all inside .lightbox-inner) must not, since a
|
|
620
|
+
// click anywhere inside that inner box would otherwise bubble up to
|
|
621
|
+
// this same listener on the overlay and immediately close what was
|
|
622
|
+
// just opened.
|
|
623
|
+
lightboxOverlay.addEventListener('click', function (ev) {
|
|
624
|
+
if (ev.target === lightboxOverlay) closeImageLightbox();
|
|
625
|
+
});
|
|
626
|
+
document.addEventListener('keydown', function (ev) {
|
|
627
|
+
if (ev.key === 'Escape' && lightboxOverlay.classList.contains('open')) closeImageLightbox();
|
|
628
|
+
});
|
|
629
|
+
// Delegated on chatPane, not per-image — messages (and their preview
|
|
630
|
+
// widgets) get created continuously as the conversation goes on, and a
|
|
631
|
+
// single listener here covers every one of them without needing to be
|
|
632
|
+
// rewired after each new message.
|
|
633
|
+
chatPane.addEventListener('click', function (ev) {
|
|
634
|
+
var target = ev.target;
|
|
635
|
+
if (target && target.classList && (target.classList.contains('msg-preview-img') || target.classList.contains('msg-plain-img'))) {
|
|
636
|
+
openImageLightbox(target.getAttribute('data-full-src'), target.getAttribute('data-download-href'));
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
341
640
|
function appendChatMessage(role, text) {
|
|
342
641
|
clearEmptyHint(chatPane);
|
|
343
642
|
var div = document.createElement('div');
|
|
@@ -347,7 +646,15 @@ export const playgroundHtml = `<!doctype html>
|
|
|
347
646
|
label.textContent = role;
|
|
348
647
|
var body = document.createElement('div');
|
|
349
648
|
body.className = 'msg-body';
|
|
350
|
-
|
|
649
|
+
// Only an assistant's own reply gets rendered as markdown — a user's
|
|
650
|
+
// typed message is shown exactly as typed (no reinterpreting
|
|
651
|
+
// whatever they literally typed as formatting), and an error string
|
|
652
|
+
// is plain diagnostic text, not content worth rendering.
|
|
653
|
+
if (role === 'assistant') {
|
|
654
|
+
body.innerHTML = renderMessageMarkdown(text);
|
|
655
|
+
} else {
|
|
656
|
+
body.textContent = text;
|
|
657
|
+
}
|
|
351
658
|
div.appendChild(label);
|
|
352
659
|
div.appendChild(body);
|
|
353
660
|
chatPane.appendChild(div);
|