@crestapps/ai-chat-ui 2.0.0-preview.193 → 2.0.0-preview.199

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
  }
@@ -811,8 +851,10 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
811
851
  mediaRecorder: null,
812
852
  preRecordingPrompt: '',
813
853
  micButton: null,
814
- speechToTextEnabled: config.chatMode === 'AudioInput' || config.chatMode === 'Conversation',
815
- textToSpeechEnabled: config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
854
+ // Conversation mode is carried either by a realtime session or by the speech-to-text plus
855
+ // text-to-speech cascade, never both. Dictation and spoken playback belong to the cascade.
856
+ speechToTextEnabled: !config.realtimeEnabled && (config.chatMode === 'AudioInput' || config.chatMode === 'Conversation'),
857
+ textToSpeechEnabled: !config.realtimeEnabled && config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
816
858
  ttsVoiceName: config.ttsVoiceName || null,
817
859
  audioChunks: [],
818
860
  audioPlayQueue: [],
@@ -835,6 +877,7 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
835
877
  realtimeWebRtcIceServers: Array.isArray(config.realtimeWebRtcIceServers) && config.realtimeWebRtcIceServers.length ? config.realtimeWebRtcIceServers : null,
836
878
  realtimeVoiceName: config.realtimeVoiceName || null,
837
879
  conversationButton: null,
880
+ realtimeHintElement: config.realtimeHintElementSelector ? document.querySelector(config.realtimeHintElementSelector) : null,
838
881
  isConversationMode: false,
839
882
  notificationDismissTimers: {},
840
883
  pendingSessionPromise: null,
@@ -1827,6 +1870,11 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
1827
1870
  return;
1828
1871
  }
1829
1872
 
1873
+ // A live voice session and a typed turn are two writers into one chat session, and they
1874
+ // would interleave. End the session first; the turns it already produced are persisted, so
1875
+ // the typed message simply continues the same thread.
1876
+ this.forceStopActiveVoice();
1877
+
1830
1878
  // Stop any active recording before sending.
1831
1879
  if (this.isRecording) {
1832
1880
  this.stopRecording();
@@ -2305,18 +2353,18 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2305
2353
  }, 0);
2306
2354
  var combined = new Uint8Array(totalLength);
2307
2355
  var offset = 0;
2308
- var _iterator8 = _createForOfIteratorHelper(this.audioChunks),
2309
- _step8;
2356
+ var _iterator9 = _createForOfIteratorHelper(this.audioChunks),
2357
+ _step9;
2310
2358
  try {
2311
- for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
2312
- var chunk = _step8.value;
2359
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
2360
+ var chunk = _step9.value;
2313
2361
  combined.set(chunk, offset);
2314
2362
  offset += chunk.length;
2315
2363
  }
2316
2364
  } catch (err) {
2317
- _iterator8.e(err);
2365
+ _iterator9.e(err);
2318
2366
  } finally {
2319
- _iterator8.f();
2367
+ _iterator9.f();
2320
2368
  }
2321
2369
  this.audioChunks = [];
2322
2370
  var blob = new Blob([combined], {
@@ -2399,6 +2447,45 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2399
2447
  return _this16.updateTtsPlaybackButtons();
2400
2448
  });
2401
2449
  },
2450
+ // A live voice session takes the message box away and puts the audio settings in its place; ending
2451
+ // the session gives it back. Sending a typed message already force-stopped the session, so the box
2452
+ // was never a way to say something *during* one -- hiding it only makes that honest, and frees the
2453
+ // row for the gear. Both transports land here: realtime through the module's onActivate, the
2454
+ // speech-to-text cascade through startConversationMode.
2455
+ // The realtime module inserts its settings popover directly after the button it was handed, and
2456
+ // hard-codes the element id. A page can host two chat clients at once -- this one and the admin
2457
+ // widget -- so that id is not unique and getElementById would hand the widget the page's gear.
2458
+ // Each client finds its own next to its own button.
2459
+ voiceSettingsElement: function voiceSettingsElement() {
2460
+ var next = this.conversationButton && this.conversationButton.nextElementSibling;
2461
+ return next && next.id === 'realtime-audio-settings' ? next : null;
2462
+ },
2463
+ setVoiceSessionLive: function setVoiceSessionLive(live) {
2464
+ if (this.inputElement) {
2465
+ this.inputElement.hidden = live;
2466
+ }
2467
+ if (this.buttonElement) {
2468
+ this.buttonElement.hidden = live;
2469
+ }
2470
+
2471
+ // With the message box gone the button would otherwise sit at its natural width in an empty
2472
+ // row, so it takes the space the box left behind and reads as the thing now in charge. The
2473
+ // settings popover does not grow, so it stays at the right-hand end.
2474
+ if (this.conversationButton) {
2475
+ this.conversationButton.classList.toggle('flex-grow-1', live);
2476
+ }
2477
+
2478
+ // The gear is built by the realtime module next to its button, and only in realtime mode, so
2479
+ // it is absent on the cascade. It belongs to a live session rather than to the surface.
2480
+ var audioSettings = this.voiceSettingsElement();
2481
+ if (audioSettings) {
2482
+ audioSettings.hidden = !live;
2483
+ }
2484
+ var hint = this.realtimeHintElement;
2485
+ if (hint) {
2486
+ hint.classList.toggle('d-none', !live);
2487
+ }
2488
+ },
2402
2489
  toggleConversationMode: function toggleConversationMode() {
2403
2490
  if (this.realtimeEnabled) {
2404
2491
  // Realtime is driven by the shared module, which owns the button; this only exists for
@@ -2474,19 +2561,25 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2474
2561
  },
2475
2562
  realtimeEnabled: true,
2476
2563
  // The conversation button doubles as the realtime button on this host; the module owns its
2477
- // click, label and state from here on. The text controls are handed over too so the module
2478
- // hides them: realtime is audio-only, and a message box that plays no part in a spoken
2479
- // conversation only invites the user to type into it.
2564
+ // click, label and state from here on.
2480
2565
  //
2481
- // conversationButton is deliberately not passed. It is the same element as the realtime
2566
+ // input and sendButton are deliberately not handed over. The module hides every control it
2567
+ // is given when realtime takes over, and typing has to stay available during a voice
2568
+ // conversation -- that is the whole feature.
2569
+ //
2570
+ // conversationButton is not handed over either. It is the same element as the realtime
2482
2571
  // button here, and the module hides the conversation button before showing the realtime
2483
2572
  // one -- naming it twice would ask it to hide the control it then has to show.
2484
2573
  selectors: {
2485
2574
  realtimeButton: config.conversationButtonElementSelector,
2486
- input: config.inputElementSelector,
2487
- sendButton: config.sendButtonElementSelector,
2488
2575
  micButton: config.micButtonElementSelector
2489
2576
  },
2577
+ // The module owns whether a session is live, so the message box follows its flag rather
2578
+ // than this app's copy of it. Losing one of these events used to leave the box hidden
2579
+ // under a button that had already gone back to "Start speaking".
2580
+ onSessionStateChanged: function onSessionStateChanged(active) {
2581
+ self.setVoiceSessionLive(active);
2582
+ },
2490
2583
  onActivate: function onActivate() {
2491
2584
  self.isConversationMode = true;
2492
2585
  self._conversationAssistantMessage = null;
@@ -3129,6 +3222,11 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3129
3222
  // The shared realtime module owns the button (click, label, state) and adds the
3130
3223
  // voice settings popover next to it.
3131
3224
  this.setupRealtimeController(config);
3225
+
3226
+ // The module reveals the gear as soon as the surface enters realtime mode, but it
3227
+ // belongs to a live session. The watcher that would hide it only runs on a change,
3228
+ // so the idle state has to be stated once here.
3229
+ this.setVoiceSessionLive(false);
3132
3230
  } else {
3133
3231
  this.conversationButton.addEventListener('click', function () {
3134
3232
  _this23.toggleConversationMode();
@@ -3322,22 +3420,20 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3322
3420
  });
3323
3421
  },
3324
3422
  isConversationMode: function isConversationMode(active) {
3325
- // Hide/show mic button.
3423
+ // The dictation microphone is hidden while a voice conversation runs: the conversation
3424
+ // already owns the microphone, and a second one would mean something different.
3326
3425
  if (this.micButton) {
3327
3426
  this.micButton.style.display = active ? 'none' : this.speechToTextEnabled ? '' : 'none';
3328
3427
  }
3329
3428
 
3330
- // Hide/show send button.
3331
- if (this.buttonElement) {
3332
- this.buttonElement.style.display = active ? 'none' : '';
3333
- }
3334
-
3335
- // Disable/enable textarea.
3336
- if (this.inputElement) {
3337
- this.inputElement.disabled = active;
3338
- if (active) {
3339
- this.inputElement.placeholder = '';
3340
- }
3429
+ // The message box and the send button give way to the voice settings while the conversation
3430
+ // runs, and come back when it ends. Both kinds of turn still land in the same thread; what
3431
+ // they cannot do is overlap.
3432
+ //
3433
+ // Only the cascade is driven from here. Realtime follows the module's own session flag
3434
+ // (onSessionStateChanged), so the two never disagree about whether a session is live.
3435
+ if (!this.realtimeEnabled) {
3436
+ this.setVoiceSessionLive(active);
3341
3437
  }
3342
3438
  }
3343
3439
  },
@@ -3450,6 +3546,7 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3450
3546
  chatMode: getAttributeValue(element, 'data-coreai-chat-mode'),
3451
3547
  micButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-mic-button-element-selector'),
3452
3548
  conversationButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-conversation-button-element-selector'),
3549
+ realtimeHintElementSelector: getAttributeValue(element, 'data-coreai-chat-realtime-hint-element-selector'),
3453
3550
  ttsVoiceName: getAttributeValue(element, 'data-coreai-chat-tts-voice-name'),
3454
3551
  realtimeVoiceName: getAttributeValue(element, 'data-coreai-chat-realtime-voice-name'),
3455
3552
  documentBarSelector: getAttributeValue(element, 'data-coreai-chat-document-bar-selector'),