@crestapps/ai-chat-ui 2.0.0-preview.197 → 2.0.0-preview.203

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/ai-chat.css CHANGED
@@ -448,6 +448,8 @@
448
448
  display: inline-block;
449
449
  vertical-align: top;
450
450
  margin-top: 0.5rem;
451
+ /* The positioning context for the download that sits inside it. */
452
+ position: relative;
451
453
  }
452
454
 
453
455
  .generated-image-container img {
@@ -455,6 +457,66 @@
455
457
  max-height: 60vh;
456
458
  height: auto;
457
459
  object-fit: contain;
460
+ /* The cap above is what makes a figure a thumbnail: an ingested diagram lands at roughly half size,
461
+ legible as a picture but not as a drawing. Pointing at it says it can be opened. */
462
+ cursor: zoom-in;
463
+ }
464
+
465
+ /* The opened picture is a copy the library appends outside the figure, so it is matched on its own class
466
+ rather than through the container -- a rule written as a descendant of the figure never reaches it. The
467
+ copy carries the thumbnail's classes, so the frame is taken off here: its padding and white ground are
468
+ scaled up with the drawing and would otherwise ring the picture with a broad white border. The cap is
469
+ lifted for the same reason, and only here, so the thumbnail sitting in the conversation is left exactly
470
+ as it was. */
471
+ img.medium-zoom-image--opened {
472
+ max-width: none !important;
473
+ max-height: none !important;
474
+ padding: 0;
475
+ border: 0;
476
+ background: none;
477
+ }
478
+
479
+ /* Over the picture's lower corner rather than below it, so a figure costs only the height of the picture.
480
+ It is drawn on a ground of its own because it sits on whatever the drawing happens to put in that
481
+ corner. */
482
+ .generated-image-container .ai-download-image {
483
+ position: absolute;
484
+ right: 0.5rem;
485
+ bottom: 0.5rem;
486
+ z-index: 2;
487
+ opacity: 0;
488
+ color: #1f2933;
489
+ background-color: rgba(255, 255, 255, 0.92);
490
+ border-color: rgba(0, 0, 0, 0.15);
491
+ transition: opacity 0.15s ease-in-out;
492
+ }
493
+
494
+ /* The outline button this is built from inverts on hover: it fills itself with its own colour and turns
495
+ its label white. Against the pale ground kept here that leaves a white icon on white -- the button is
496
+ still there and still works, but there is nothing to see. Both colours are therefore stated for every
497
+ state the button can be in rather than only the resting one. */
498
+ .generated-image-container .ai-download-image:hover,
499
+ .generated-image-container .ai-download-image:focus,
500
+ .generated-image-container .ai-download-image:focus-visible,
501
+ .generated-image-container .ai-download-image:active {
502
+ color: #1f2933;
503
+ background-color: #ffffff;
504
+ border-color: rgba(0, 0, 0, 0.35);
505
+ }
506
+
507
+ /* Revealed by pointing at the figure. focus-within is what keeps it reachable by keyboard: the button is
508
+ invisible until it takes focus, and an invisible control that cannot be brought into view cannot be
509
+ used at all. */
510
+ .generated-image-container:hover .ai-download-image,
511
+ .generated-image-container:focus-within .ai-download-image {
512
+ opacity: 1;
513
+ }
514
+
515
+ /* A touch screen has no hover to reveal it with, so there it is simply always shown. */
516
+ @media (hover: none) {
517
+ .generated-image-container .ai-download-image {
518
+ opacity: 1;
519
+ }
458
520
  }
459
521
 
460
522
  /* ── Tables ── */
@@ -1 +1 @@
1
- {"version":3,"sources":["ai-chat.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"ai-chat.css","sourcesContent":[".ai-chat-message-heading {\n font-size: 0.75rem;\n margin-bottom: 0.35rem;\n}\n\n.ai-chat-message-heading .ai-chat-msg-role {\n margin-bottom: 0;\n letter-spacing: 0.03em;\n}\n\n.ai-chat-message-heading-assistant .ai-chat-msg-role {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.ai-chat-message-role-label {\n display: inline-block;\n text-transform: inherit;\n}\n\n.ai-chat-msg-role {\n font-weight: 600;\n margin-bottom: 0.35rem;\n text-transform: uppercase;\n}\n\n.ai-chat-msg-role-user {\n color: var(--bs-primary, #0d6efd);\n}\n\n.ai-chat-msg-role-assistant {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n}\n\n.ai-chat-messages .ai-chat-message-item {\n position: relative;\n padding: 0.25rem 0;\n}\n\n.ai-chat-messages .ai-chat-message-item + .ai-chat-message-item::before {\n content: '';\n display: block;\n width: 100%;\n margin: 0.5rem auto 0.75rem;\n border-top: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-messages .ai-chat-message-item p:last-child {\n margin-bottom: 0;\n}\n\n.ai-chat-messages .ai-chat-message-body {\n position: relative;\n padding: 0;\n overflow-wrap: break-word;\n word-break: break-word;\n min-width: 0;\n}\n\n.ai-chat-messages .ai-chat-message-body sup {\n font-size: 0.75em;\n line-height: 0;\n}\n\n/* A markdown table is rendered with the host framework's table classes, which do the bulk of the\n styling. These rules only add what those classes leave out, and are scoped to the message list\n rather than to a wrapper element, because a table is emitted as a direct child of the message. */\n.ai-chat-messages table {\n margin: 0.75rem 0;\n font-size: 0.9em;\n}\n\n.ai-chat-messages table th,\n.ai-chat-messages table td {\n vertical-align: top;\n white-space: nowrap;\n}\n\n/* A right-aligned column in the source markdown is almost always numeric, so the alignment the author\n asked for is honored rather than flattened to the default. */\n.ai-chat-messages table th[align=\"right\"],\n.ai-chat-messages table td[align=\"right\"] {\n text-align: right;\n}\n\n.ai-chat-messages table th[align=\"center\"],\n.ai-chat-messages table td[align=\"center\"] {\n text-align: center;\n}\n\n.ai-chat-messages .ai-chat-citation-list {\n margin: 0.75rem 0 0;\n padding-left: 1.25rem;\n}\n\n.ai-chat-messages .ai-chat-citation-item + .ai-chat-citation-item {\n margin-top: 0.25rem;\n}\n\n.ai-chat-messages .ai-chat-citation-item a {\n word-break: break-word;\n}\n\n.ai-partial-transcript {\n font-style: italic;\n opacity: 0.7;\n}\n\n.ai-chat-messages .message-buttons-container {\n position: absolute;\n right: 0.1rem;\n bottom: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n width: auto;\n max-width: max-content;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease-in-out;\n background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.96);\n padding: 0.2rem 0.35rem;\n border-radius: 999px;\n box-shadow: 0 1px 3px rgba(var(--bs-body-color-rgb, 33, 37, 41), 0.08);\n margin-bottom: 1rem;\n transform: translateY(50%);\n z-index: 1;\n}\n\n.ai-chat-messages .ai-chat-message-item:hover .message-buttons-container,\n.ai-chat-messages .ai-chat-message-item:focus-within .message-buttons-container {\n opacity: 1;\n pointer-events: auto;\n}\n\n.ai-chat-messages .message-buttons-container:has(.tts-playing) {\n opacity: 1;\n pointer-events: auto;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 1.75rem;\n height: 1.75rem;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 50%;\n color: var(--bs-secondary-color, #6c757d) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container .ai-chat-message-assistant-feedback {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox:hover,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox:focus {\n color: var(--bs-body-color, #212529) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success:hover,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success:focus {\n color: var(--bs-success, #198754) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success svg {\n color: inherit !important;\n fill: currentColor;\n}\n\n.ai-chat-messages .message-buttons-container .tts-playing {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n}\n\n@keyframes spark-burst {\n 0% {\n transform: scale(1);\n opacity: 1;\n }\n 20% {\n transform: scale(1.8);\n opacity: 0.7;\n }\n 40% {\n transform: scale(0.9);\n opacity: 1;\n }\n 60% {\n transform: scale(1.3);\n }\n 100% {\n transform: scale(1);\n }\n}\n\n.spark-effect i,\n.spark-effect svg {\n display: inline-block;\n animation: spark-burst 0.5s ease-out;\n}\n\n.ai-chat-messages .ai-bot-icon {\n color: currentColor;\n display: inline-block;\n animation: none;\n}\n\n@keyframes ai-chat-streaming-effect {\n 0% { opacity: 1; transform: rotate(0deg); }\n 10% { opacity: 0.3; transform: rotate(0deg); }\n 20% { opacity: 1; transform: rotate(0deg); }\n 30% { opacity: 0.3; transform: rotate(0deg); }\n 40% { opacity: 1; transform: rotate(0deg); }\n 50% { opacity: 0.3; transform: rotate(0deg); }\n 60% { opacity: 1; transform: rotate(0deg); }\n 80% { opacity: 1; transform: rotate(360deg); }\n 100% { opacity: 1; transform: rotate(360deg); }\n}\n\n.ai-chat-messages .ai-streaming-icon {\n color: currentColor;\n display: inline-block;\n animation: ai-chat-streaming-effect 4s ease-in-out infinite;\n}\n\n.ai-chat-messages pre {\n position: relative;\n margin: 0;\n}\n\n.ai-chat-messages .ai-code-block {\n border: 1px solid var(--bs-border-color, #dee2e6);\n border-radius: 0.5rem;\n overflow: hidden;\n margin: 0.75rem 0;\n background: var(--bs-body-bg, #fff);\n}\n\n.ai-chat-messages .ai-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n font-size: 0.8rem;\n min-height: 2rem;\n}\n\n.ai-chat-messages .ai-code-lang {\n font-weight: 600;\n color: var(--bs-body-color, #212529);\n text-transform: capitalize;\n}\n\n.ai-chat-messages .ai-code-lang i {\n color: var(--bs-secondary-color, #6c757d);\n margin-right: 0.25rem;\n}\n\n.ai-chat-messages .ai-code-copy-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.2rem;\n font-size: 0.8rem;\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--bs-secondary-color, #6c757d);\n transition: color 0.15s;\n white-space: nowrap;\n}\n\n.ai-chat-messages .ai-code-copy-btn:hover {\n color: var(--bs-body-color, #212529);\n}\n\n.ai-chat-messages .ai-code-block pre {\n margin: 0;\n background: transparent !important;\n}\n\n.ai-chat-messages .ai-code-block pre code.hljs {\n background: transparent !important;\n padding: 0.75rem 1rem 1rem;\n}\n\n.ai-chat-messages pre code.hljs {\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n border-radius: 0.375rem;\n}\n\n.ai-chat-doc-bar {\n border-bottom: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-drag-over {\n outline: 2px dashed var(--bs-primary, #0d6efd) !important;\n outline-offset: -2px;\n background-color: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.05) !important;\n}\n\n.ai-chat-notification {\n margin: 0.75rem 0 0.25rem;\n padding: 0.625rem 0.875rem;\n border-radius: 0.5rem;\n border: 1px solid var(--bs-border-color, #dee2e6);\n background-color: var(--bs-tertiary-bg, #f8f9fa);\n font-size: 0.875rem;\n line-height: 1.4;\n}\n\n.ai-chat-notification-content {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.ai-chat-notification-icon {\n flex-shrink: 0;\n font-size: 1rem;\n opacity: 0.8;\n}\n\n.ai-chat-notification-text {\n flex: 1;\n}\n\n.ai-chat-notification-dismiss {\n flex-shrink: 0;\n opacity: 0.5;\n transition: opacity 0.15s;\n}\n\n.ai-chat-notification-dismiss:hover {\n opacity: 1;\n}\n\n.ai-chat-notification-actions {\n display: flex;\n gap: 0.375rem;\n margin-top: 0.5rem;\n padding-top: 0.5rem;\n border-top: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-notification-actions .btn {\n font-size: 0.8125rem;\n padding: 0.1875rem 0.625rem;\n}\n\n.ai-chat-notification-typing {\n border-color: var(--ai-chat-primary, var(--bs-success, #198754));\n background-color: rgba(var(--ai-chat-primary-rgb, var(--bs-success-rgb, 25, 135, 84)), 0.05);\n}\n\n.ai-chat-notification-typing .ai-chat-notification-icon {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n animation: ai-chat-typing-pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes ai-chat-typing-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.ai-chat-notification-transfer {\n border-color: var(--bs-warning, #ffc107);\n background-color: rgba(var(--bs-warning-rgb, 255, 193, 7), 0.06);\n}\n\n.ai-chat-notification-transfer .ai-chat-notification-icon {\n color: var(--bs-warning, #ffc107);\n animation: ai-chat-transfer-pulse 2s ease-in-out infinite;\n}\n\n@keyframes ai-chat-transfer-pulse {\n 0%, 100% { transform: scale(1); opacity: 0.7; }\n 50% { transform: scale(1.1); opacity: 1; }\n}\n\n.ai-chat-notification-ended,\n.ai-chat-notification-session-ended,\n.ai-chat-notification-conversation-ended {\n border-color: var(--bs-secondary, #6c757d);\n background-color: rgba(var(--bs-secondary-rgb, 108, 117, 125), 0.05);\n}\n\n.ai-chat-notification-ended .ai-chat-notification-icon,\n.ai-chat-notification-session-ended .ai-chat-notification-icon,\n.ai-chat-notification-conversation-ended .ai-chat-notification-icon {\n color: var(--bs-secondary, #6c757d);\n}\n\n.ai-chat-notification-info,\n.ai-chat-notification-agent-connected {\n border-color: var(--bs-info, #0dcaf0);\n background-color: rgba(var(--bs-info-rgb, 13, 202, 240), 0.05);\n}\n\n.ai-chat-notification-info .ai-chat-notification-icon,\n.ai-chat-notification-agent-connected .ai-chat-notification-icon {\n color: var(--bs-info, #0dcaf0);\n}\n\n.ai-chat-notification-warning,\n.ai-chat-notification-agent-reconnecting {\n border-color: var(--bs-warning, #ffc107);\n background-color: rgba(var(--bs-warning-rgb, 255, 193, 7), 0.06);\n}\n\n.ai-chat-notification-warning .ai-chat-notification-icon,\n.ai-chat-notification-agent-reconnecting .ai-chat-notification-icon {\n color: var(--bs-warning, #ffc107);\n}\n\n.ai-chat-notification-error,\n.ai-chat-notification-connection-lost {\n border-color: var(--bs-danger, #dc3545);\n background-color: rgba(var(--bs-danger-rgb, 220, 53, 69), 0.05);\n}\n\n.ai-chat-notification-error .ai-chat-notification-icon,\n.ai-chat-notification-connection-lost .ai-chat-notification-icon {\n color: var(--bs-danger, #dc3545);\n}\n\n/* ── Embedded and generated images ── */\n/* The renderer writes an inline max-width in pixels, which bounds a picture across but not down: an\n ingested page scan stays inside that width and still runs thousands of pixels tall, pushing the rest\n of the conversation off screen. A pixel width also overflows any panel narrower than it. Both axes are\n capped relative to the container instead, so a picture is bounded whatever its proportions and\n wherever it is shown. !important is needed because the width it overrides is written inline. */\n.generated-image-container {\n /* The wrapper is a span so a figure marker written mid-sentence does not close the paragraph it\n sits in. Inline-block restores the box behaviour the layout below assumes, and a top margin\n keeps a figure off the line of text it follows. */\n display: inline-block;\n vertical-align: top;\n margin-top: 0.5rem;\n}\n\n.generated-image-container img {\n max-width: 100% !important;\n max-height: 60vh;\n height: auto;\n object-fit: contain;\n}\n\n/* ── Tables ── */\n/* Scoped to .ai-chat-message-item rather than the message body, because the surfaces disagree on\n the body: the shared script renders .ai-chat-message-body while the MVC chat-interaction page\n renders a bare .lh-base. The item wrapper is the one element all of them share. */\n/* A markdown table arrives as a bare <table> with no classes, so by default it has no rules, no padding\n and a width that shrinks to its content -- an ingested table renders as run-together text. The width\n trio is the responsive-table idiom rather than a plain width: max-content lets a wide table keep its\n columns legible, min-width fills the message when it is narrow, and max-width with overflow-x means a\n table wider than the panel scrolls itself instead of stretching the conversation sideways. Colours come\n from Bootstrap's variables so the table follows the host's theme, with literal fallbacks for a host that\n does not define them. */\n.ai-chat-message-item table {\n display: block;\n width: max-content;\n min-width: 100%;\n max-width: 100%;\n overflow-x: auto;\n margin-bottom: 1rem;\n border-collapse: collapse;\n font-size: 0.9375rem;\n}\n\n.ai-chat-message-item table th,\n.ai-chat-message-item table td {\n padding: 0.4rem 0.75rem;\n border: 1px solid var(--bs-border-color, #dee2e6);\n text-align: left;\n vertical-align: top;\n}\n\n.ai-chat-message-item table thead th {\n background-color: var(--bs-secondary-bg, #e9ecef);\n font-weight: 600;\n border-bottom: 2px solid var(--bs-border-color, #dee2e6);\n white-space: nowrap;\n}\n\n/* Banding earns its place on an ingested table, which is often twenty-odd rows of similar-looking numbers\n where losing your place costs the reader the row. */\n.ai-chat-message-item table tbody tr:nth-child(even) {\n background-color: var(--bs-tertiary-bg, #f8f9fa);\n}\n"]}
1
+ {"version":3,"sources":["ai-chat.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"ai-chat.css","sourcesContent":[".ai-chat-message-heading {\n font-size: 0.75rem;\n margin-bottom: 0.35rem;\n}\n\n.ai-chat-message-heading .ai-chat-msg-role {\n margin-bottom: 0;\n letter-spacing: 0.03em;\n}\n\n.ai-chat-message-heading-assistant .ai-chat-msg-role {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.ai-chat-message-role-label {\n display: inline-block;\n text-transform: inherit;\n}\n\n.ai-chat-msg-role {\n font-weight: 600;\n margin-bottom: 0.35rem;\n text-transform: uppercase;\n}\n\n.ai-chat-msg-role-user {\n color: var(--bs-primary, #0d6efd);\n}\n\n.ai-chat-msg-role-assistant {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n}\n\n.ai-chat-messages .ai-chat-message-item {\n position: relative;\n padding: 0.25rem 0;\n}\n\n.ai-chat-messages .ai-chat-message-item + .ai-chat-message-item::before {\n content: '';\n display: block;\n width: 100%;\n margin: 0.5rem auto 0.75rem;\n border-top: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-messages .ai-chat-message-item p:last-child {\n margin-bottom: 0;\n}\n\n.ai-chat-messages .ai-chat-message-body {\n position: relative;\n padding: 0;\n overflow-wrap: break-word;\n word-break: break-word;\n min-width: 0;\n}\n\n.ai-chat-messages .ai-chat-message-body sup {\n font-size: 0.75em;\n line-height: 0;\n}\n\n/* A markdown table is rendered with the host framework's table classes, which do the bulk of the\n styling. These rules only add what those classes leave out, and are scoped to the message list\n rather than to a wrapper element, because a table is emitted as a direct child of the message. */\n.ai-chat-messages table {\n margin: 0.75rem 0;\n font-size: 0.9em;\n}\n\n.ai-chat-messages table th,\n.ai-chat-messages table td {\n vertical-align: top;\n white-space: nowrap;\n}\n\n/* A right-aligned column in the source markdown is almost always numeric, so the alignment the author\n asked for is honored rather than flattened to the default. */\n.ai-chat-messages table th[align=\"right\"],\n.ai-chat-messages table td[align=\"right\"] {\n text-align: right;\n}\n\n.ai-chat-messages table th[align=\"center\"],\n.ai-chat-messages table td[align=\"center\"] {\n text-align: center;\n}\n\n.ai-chat-messages .ai-chat-citation-list {\n margin: 0.75rem 0 0;\n padding-left: 1.25rem;\n}\n\n.ai-chat-messages .ai-chat-citation-item + .ai-chat-citation-item {\n margin-top: 0.25rem;\n}\n\n.ai-chat-messages .ai-chat-citation-item a {\n word-break: break-word;\n}\n\n.ai-partial-transcript {\n font-style: italic;\n opacity: 0.7;\n}\n\n.ai-chat-messages .message-buttons-container {\n position: absolute;\n right: 0.1rem;\n bottom: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n width: auto;\n max-width: max-content;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease-in-out;\n background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.96);\n padding: 0.2rem 0.35rem;\n border-radius: 999px;\n box-shadow: 0 1px 3px rgba(var(--bs-body-color-rgb, 33, 37, 41), 0.08);\n margin-bottom: 1rem;\n transform: translateY(50%);\n z-index: 1;\n}\n\n.ai-chat-messages .ai-chat-message-item:hover .message-buttons-container,\n.ai-chat-messages .ai-chat-message-item:focus-within .message-buttons-container {\n opacity: 1;\n pointer-events: auto;\n}\n\n.ai-chat-messages .message-buttons-container:has(.tts-playing) {\n opacity: 1;\n pointer-events: auto;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 1.75rem;\n height: 1.75rem;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 50%;\n color: var(--bs-secondary-color, #6c757d) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container .ai-chat-message-assistant-feedback {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox:hover,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox:focus {\n color: var(--bs-body-color, #212529) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success:hover,\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success:focus {\n color: var(--bs-success, #198754) !important;\n text-decoration: none;\n}\n\n.ai-chat-messages .message-buttons-container > .button-message-toolbox.text-success svg {\n color: inherit !important;\n fill: currentColor;\n}\n\n.ai-chat-messages .message-buttons-container .tts-playing {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n}\n\n@keyframes spark-burst {\n 0% {\n transform: scale(1);\n opacity: 1;\n }\n 20% {\n transform: scale(1.8);\n opacity: 0.7;\n }\n 40% {\n transform: scale(0.9);\n opacity: 1;\n }\n 60% {\n transform: scale(1.3);\n }\n 100% {\n transform: scale(1);\n }\n}\n\n.spark-effect i,\n.spark-effect svg {\n display: inline-block;\n animation: spark-burst 0.5s ease-out;\n}\n\n.ai-chat-messages .ai-bot-icon {\n color: currentColor;\n display: inline-block;\n animation: none;\n}\n\n@keyframes ai-chat-streaming-effect {\n 0% { opacity: 1; transform: rotate(0deg); }\n 10% { opacity: 0.3; transform: rotate(0deg); }\n 20% { opacity: 1; transform: rotate(0deg); }\n 30% { opacity: 0.3; transform: rotate(0deg); }\n 40% { opacity: 1; transform: rotate(0deg); }\n 50% { opacity: 0.3; transform: rotate(0deg); }\n 60% { opacity: 1; transform: rotate(0deg); }\n 80% { opacity: 1; transform: rotate(360deg); }\n 100% { opacity: 1; transform: rotate(360deg); }\n}\n\n.ai-chat-messages .ai-streaming-icon {\n color: currentColor;\n display: inline-block;\n animation: ai-chat-streaming-effect 4s ease-in-out infinite;\n}\n\n.ai-chat-messages pre {\n position: relative;\n margin: 0;\n}\n\n.ai-chat-messages .ai-code-block {\n border: 1px solid var(--bs-border-color, #dee2e6);\n border-radius: 0.5rem;\n overflow: hidden;\n margin: 0.75rem 0;\n background: var(--bs-body-bg, #fff);\n}\n\n.ai-chat-messages .ai-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n font-size: 0.8rem;\n min-height: 2rem;\n}\n\n.ai-chat-messages .ai-code-lang {\n font-weight: 600;\n color: var(--bs-body-color, #212529);\n text-transform: capitalize;\n}\n\n.ai-chat-messages .ai-code-lang i {\n color: var(--bs-secondary-color, #6c757d);\n margin-right: 0.25rem;\n}\n\n.ai-chat-messages .ai-code-copy-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.2rem;\n font-size: 0.8rem;\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--bs-secondary-color, #6c757d);\n transition: color 0.15s;\n white-space: nowrap;\n}\n\n.ai-chat-messages .ai-code-copy-btn:hover {\n color: var(--bs-body-color, #212529);\n}\n\n.ai-chat-messages .ai-code-block pre {\n margin: 0;\n background: transparent !important;\n}\n\n.ai-chat-messages .ai-code-block pre code.hljs {\n background: transparent !important;\n padding: 0.75rem 1rem 1rem;\n}\n\n.ai-chat-messages pre code.hljs {\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n border-radius: 0.375rem;\n}\n\n.ai-chat-doc-bar {\n border-bottom: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-drag-over {\n outline: 2px dashed var(--bs-primary, #0d6efd) !important;\n outline-offset: -2px;\n background-color: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.05) !important;\n}\n\n.ai-chat-notification {\n margin: 0.75rem 0 0.25rem;\n padding: 0.625rem 0.875rem;\n border-radius: 0.5rem;\n border: 1px solid var(--bs-border-color, #dee2e6);\n background-color: var(--bs-tertiary-bg, #f8f9fa);\n font-size: 0.875rem;\n line-height: 1.4;\n}\n\n.ai-chat-notification-content {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.ai-chat-notification-icon {\n flex-shrink: 0;\n font-size: 1rem;\n opacity: 0.8;\n}\n\n.ai-chat-notification-text {\n flex: 1;\n}\n\n.ai-chat-notification-dismiss {\n flex-shrink: 0;\n opacity: 0.5;\n transition: opacity 0.15s;\n}\n\n.ai-chat-notification-dismiss:hover {\n opacity: 1;\n}\n\n.ai-chat-notification-actions {\n display: flex;\n gap: 0.375rem;\n margin-top: 0.5rem;\n padding-top: 0.5rem;\n border-top: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.ai-chat-notification-actions .btn {\n font-size: 0.8125rem;\n padding: 0.1875rem 0.625rem;\n}\n\n.ai-chat-notification-typing {\n border-color: var(--ai-chat-primary, var(--bs-success, #198754));\n background-color: rgba(var(--ai-chat-primary-rgb, var(--bs-success-rgb, 25, 135, 84)), 0.05);\n}\n\n.ai-chat-notification-typing .ai-chat-notification-icon {\n color: var(--ai-chat-primary, var(--bs-success, #198754));\n animation: ai-chat-typing-pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes ai-chat-typing-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.ai-chat-notification-transfer {\n border-color: var(--bs-warning, #ffc107);\n background-color: rgba(var(--bs-warning-rgb, 255, 193, 7), 0.06);\n}\n\n.ai-chat-notification-transfer .ai-chat-notification-icon {\n color: var(--bs-warning, #ffc107);\n animation: ai-chat-transfer-pulse 2s ease-in-out infinite;\n}\n\n@keyframes ai-chat-transfer-pulse {\n 0%, 100% { transform: scale(1); opacity: 0.7; }\n 50% { transform: scale(1.1); opacity: 1; }\n}\n\n.ai-chat-notification-ended,\n.ai-chat-notification-session-ended,\n.ai-chat-notification-conversation-ended {\n border-color: var(--bs-secondary, #6c757d);\n background-color: rgba(var(--bs-secondary-rgb, 108, 117, 125), 0.05);\n}\n\n.ai-chat-notification-ended .ai-chat-notification-icon,\n.ai-chat-notification-session-ended .ai-chat-notification-icon,\n.ai-chat-notification-conversation-ended .ai-chat-notification-icon {\n color: var(--bs-secondary, #6c757d);\n}\n\n.ai-chat-notification-info,\n.ai-chat-notification-agent-connected {\n border-color: var(--bs-info, #0dcaf0);\n background-color: rgba(var(--bs-info-rgb, 13, 202, 240), 0.05);\n}\n\n.ai-chat-notification-info .ai-chat-notification-icon,\n.ai-chat-notification-agent-connected .ai-chat-notification-icon {\n color: var(--bs-info, #0dcaf0);\n}\n\n.ai-chat-notification-warning,\n.ai-chat-notification-agent-reconnecting {\n border-color: var(--bs-warning, #ffc107);\n background-color: rgba(var(--bs-warning-rgb, 255, 193, 7), 0.06);\n}\n\n.ai-chat-notification-warning .ai-chat-notification-icon,\n.ai-chat-notification-agent-reconnecting .ai-chat-notification-icon {\n color: var(--bs-warning, #ffc107);\n}\n\n.ai-chat-notification-error,\n.ai-chat-notification-connection-lost {\n border-color: var(--bs-danger, #dc3545);\n background-color: rgba(var(--bs-danger-rgb, 220, 53, 69), 0.05);\n}\n\n.ai-chat-notification-error .ai-chat-notification-icon,\n.ai-chat-notification-connection-lost .ai-chat-notification-icon {\n color: var(--bs-danger, #dc3545);\n}\n\n/* ── Embedded and generated images ── */\n/* The renderer writes an inline max-width in pixels, which bounds a picture across but not down: an\n ingested page scan stays inside that width and still runs thousands of pixels tall, pushing the rest\n of the conversation off screen. A pixel width also overflows any panel narrower than it. Both axes are\n capped relative to the container instead, so a picture is bounded whatever its proportions and\n wherever it is shown. !important is needed because the width it overrides is written inline. */\n.generated-image-container {\n /* The wrapper is a span so a figure marker written mid-sentence does not close the paragraph it\n sits in. Inline-block restores the box behaviour the layout below assumes, and a top margin\n keeps a figure off the line of text it follows. */\n display: inline-block;\n vertical-align: top;\n margin-top: 0.5rem;\n /* The positioning context for the download that sits inside it. */\n position: relative;\n}\n\n.generated-image-container img {\n max-width: 100% !important;\n max-height: 60vh;\n height: auto;\n object-fit: contain;\n /* The cap above is what makes a figure a thumbnail: an ingested diagram lands at roughly half size,\n legible as a picture but not as a drawing. Pointing at it says it can be opened. */\n cursor: zoom-in;\n}\n\n/* The opened picture is a copy the library appends outside the figure, so it is matched on its own class\n rather than through the container -- a rule written as a descendant of the figure never reaches it. The\n copy carries the thumbnail's classes, so the frame is taken off here: its padding and white ground are\n scaled up with the drawing and would otherwise ring the picture with a broad white border. The cap is\n lifted for the same reason, and only here, so the thumbnail sitting in the conversation is left exactly\n as it was. */\nimg.medium-zoom-image--opened {\n max-width: none !important;\n max-height: none !important;\n padding: 0;\n border: 0;\n background: none;\n}\n\n/* Over the picture's lower corner rather than below it, so a figure costs only the height of the picture.\n It is drawn on a ground of its own because it sits on whatever the drawing happens to put in that\n corner. */\n.generated-image-container .ai-download-image {\n position: absolute;\n right: 0.5rem;\n bottom: 0.5rem;\n z-index: 2;\n opacity: 0;\n color: #1f2933;\n background-color: rgba(255, 255, 255, 0.92);\n border-color: rgba(0, 0, 0, 0.15);\n transition: opacity 0.15s ease-in-out;\n}\n\n/* The outline button this is built from inverts on hover: it fills itself with its own colour and turns\n its label white. Against the pale ground kept here that leaves a white icon on white -- the button is\n still there and still works, but there is nothing to see. Both colours are therefore stated for every\n state the button can be in rather than only the resting one. */\n.generated-image-container .ai-download-image:hover,\n.generated-image-container .ai-download-image:focus,\n.generated-image-container .ai-download-image:focus-visible,\n.generated-image-container .ai-download-image:active {\n color: #1f2933;\n background-color: #ffffff;\n border-color: rgba(0, 0, 0, 0.35);\n}\n\n/* Revealed by pointing at the figure. focus-within is what keeps it reachable by keyboard: the button is\n invisible until it takes focus, and an invisible control that cannot be brought into view cannot be\n used at all. */\n.generated-image-container:hover .ai-download-image,\n.generated-image-container:focus-within .ai-download-image {\n opacity: 1;\n}\n\n/* A touch screen has no hover to reveal it with, so there it is simply always shown. */\n@media (hover: none) {\n .generated-image-container .ai-download-image {\n opacity: 1;\n }\n}\n\n/* ── Tables ── */\n/* Scoped to .ai-chat-message-item rather than the message body, because the surfaces disagree on\n the body: the shared script renders .ai-chat-message-body while the MVC chat-interaction page\n renders a bare .lh-base. The item wrapper is the one element all of them share. */\n/* A markdown table arrives as a bare <table> with no classes, so by default it has no rules, no padding\n and a width that shrinks to its content -- an ingested table renders as run-together text. The width\n trio is the responsive-table idiom rather than a plain width: max-content lets a wide table keep its\n columns legible, min-width fills the message when it is narrow, and max-width with overflow-x means a\n table wider than the panel scrolls itself instead of stretching the conversation sideways. Colours come\n from Bootstrap's variables so the table follows the host's theme, with literal fallbacks for a host that\n does not define them. */\n.ai-chat-message-item table {\n display: block;\n width: max-content;\n min-width: 100%;\n max-width: 100%;\n overflow-x: auto;\n margin-bottom: 1rem;\n border-collapse: collapse;\n font-size: 0.9375rem;\n}\n\n.ai-chat-message-item table th,\n.ai-chat-message-item table td {\n padding: 0.4rem 0.75rem;\n border: 1px solid var(--bs-border-color, #dee2e6);\n text-align: left;\n vertical-align: top;\n}\n\n.ai-chat-message-item table thead th {\n background-color: var(--bs-secondary-bg, #e9ecef);\n font-weight: 600;\n border-bottom: 2px solid var(--bs-border-color, #dee2e6);\n white-space: nowrap;\n}\n\n/* Banding earns its place on an ingested table, which is often twenty-odd rows of similar-looking numbers\n where losing your place costs the reader the row. */\n.ai-chat-message-item table tbody tr:nth-child(even) {\n background-color: var(--bs-tertiary-bg, #f8f9fa);\n}\n"]}
package/dist/ai-chat.js CHANGED
@@ -517,7 +517,11 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
517
517
  // at the first one, so the sentence is torn into fragments and the commas between the markers are
518
518
  // left stranded on their own lines. Inline-block keeps the figure looking exactly the same while
519
519
  // remaining something a paragraph can legally contain.
520
- return "<span class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <span class=\"mt-2 d-block\">\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </span>\n </span>");
520
+ // The download sits over the picture's lower corner rather than on a line of its own beneath it.
521
+ // A row of its own cost a band of vertical space under every figure, which a conversation full of
522
+ // them pays for once per figure. The link itself is unchanged -- same address, same download
523
+ // attribute, same behaviour -- only where it is drawn.
524
+ return "<span class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </span>");
521
525
  };
522
526
 
523
527
  // Chart counter for unique IDs
@@ -592,14 +596,50 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
592
596
  // Deferred for the same reason the charts below are: the render that produced these elements has
593
597
  // not necessarily been flushed to the DOM when this runs.
594
598
  requestAnimationFrame(wireBrokenImageHandlers);
599
+ requestAnimationFrame(wireImageZoom);
595
600
  }
596
- function wireBrokenImageHandlers() {
597
- var images = document.querySelectorAll('.generated-image-container img:not([data-broken-wired])');
601
+
602
+ // A picture arrives after the markdown is handed back -- the view writes it, and a streamed answer
603
+ // rewrites it on every chunk -- so zooming cannot be attached once at load. It is attached on the same
604
+ // pass as the broken-image handler above, and the attribute marks a picture already attached so a
605
+ // re-render does not bind it a second time.
606
+ function wireImageZoom() {
607
+ // The library is the host's to load. Without it the picture is still shown and still downloadable;
608
+ // only the enlarging is missing, so this is not worth failing a render over.
609
+ if (typeof mediumZoom !== 'function') {
610
+ return;
611
+ }
612
+ var images = document.querySelectorAll('.generated-image-container img:not([data-zoom-wired])');
613
+ if (images.length === 0) {
614
+ return;
615
+ }
598
616
  var _iterator6 = _createForOfIteratorHelper(images),
599
617
  _step6;
600
618
  try {
601
- var _loop = function _loop() {
619
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
602
620
  var image = _step6.value;
621
+ image.setAttribute('data-zoom-wired', 'true');
622
+ }
623
+ } catch (err) {
624
+ _iterator6.e(err);
625
+ } finally {
626
+ _iterator6.f();
627
+ }
628
+ mediumZoom(images, {
629
+ // The drawing is bounded by the window rather than by this margin, so the margin is kept
630
+ // small: an ingested figure is read detail by detail, and every pixel given away here comes
631
+ // off the size it is read at.
632
+ margin: 16,
633
+ background: 'rgba(15, 15, 15, 0.92)'
634
+ });
635
+ }
636
+ function wireBrokenImageHandlers() {
637
+ var images = document.querySelectorAll('.generated-image-container img:not([data-broken-wired])');
638
+ var _iterator7 = _createForOfIteratorHelper(images),
639
+ _step7;
640
+ try {
641
+ var _loop = function _loop() {
642
+ var image = _step7.value;
603
643
  image.setAttribute('data-broken-wired', 'true');
604
644
  image.addEventListener('error', function () {
605
645
  var container = image.closest('.generated-image-container');
@@ -616,13 +656,13 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
616
656
  once: true
617
657
  });
618
658
  };
619
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
659
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
620
660
  _loop();
621
661
  }
622
662
  } catch (err) {
623
- _iterator6.e(err);
663
+ _iterator7.e(err);
624
664
  } finally {
625
- _iterator6.f();
665
+ _iterator7.f();
626
666
  }
627
667
  }
628
668
  function renderMessageMedia(message) {
@@ -638,17 +678,17 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
638
678
  // Defer to requestAnimationFrame so the browser has fully laid out the
639
679
  // canvas elements before Chart.js reads their dimensions.
640
680
  requestAnimationFrame(function () {
641
- var _iterator7 = _createForOfIteratorHelper(charts),
642
- _step7;
681
+ var _iterator8 = _createForOfIteratorHelper(charts),
682
+ _step8;
643
683
  try {
644
- for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
645
- var c = _step7.value;
684
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
685
+ var c = _step8.value;
646
686
  renderChartOnCanvas(c.chartId, c.config);
647
687
  }
648
688
  } catch (err) {
649
- _iterator7.e(err);
689
+ _iterator8.e(err);
650
690
  } finally {
651
- _iterator7.f();
691
+ _iterator8.f();
652
692
  }
653
693
  });
654
694
  }
@@ -2313,18 +2353,18 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2313
2353
  }, 0);
2314
2354
  var combined = new Uint8Array(totalLength);
2315
2355
  var offset = 0;
2316
- var _iterator8 = _createForOfIteratorHelper(this.audioChunks),
2317
- _step8;
2356
+ var _iterator9 = _createForOfIteratorHelper(this.audioChunks),
2357
+ _step9;
2318
2358
  try {
2319
- for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
2320
- var chunk = _step8.value;
2359
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
2360
+ var chunk = _step9.value;
2321
2361
  combined.set(chunk, offset);
2322
2362
  offset += chunk.length;
2323
2363
  }
2324
2364
  } catch (err) {
2325
- _iterator8.e(err);
2365
+ _iterator9.e(err);
2326
2366
  } finally {
2327
- _iterator8.f();
2367
+ _iterator9.f();
2328
2368
  }
2329
2369
  this.audioChunks = [];
2330
2370
  var blob = new Blob([combined], {