@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 +62 -0
- package/dist/ai-chat.css.map +1 -1
- package/dist/ai-chat.js +136 -39
- package/dist/ai-chat.js.map +1 -1
- package/dist/ai-chat.min.css +1 -1
- package/dist/ai-chat.min.js +1 -1
- package/dist/ai-chat.min.js.map +1 -1
- package/dist/chat-interaction.js +174 -42
- package/dist/chat-interaction.js.map +1 -1
- package/dist/chat-interaction.min.js +1 -1
- package/dist/chat-interaction.min.js.map +1 -1
- package/dist/chat-widget.css +61 -0
- package/dist/chat-widget.css.map +1 -1
- package/dist/chat-widget.min.css +1 -1
- package/dist/realtime-audio.js +16 -2
- package/dist/realtime-audio.js.map +1 -1
- package/dist/realtime-audio.min.js +1 -1
- package/dist/realtime-audio.min.js.map +1 -1
- package/package.json +1 -1
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 ── */
|
package/dist/ai-chat.css.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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 (
|
|
659
|
+
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
620
660
|
_loop();
|
|
621
661
|
}
|
|
622
662
|
} catch (err) {
|
|
623
|
-
|
|
663
|
+
_iterator7.e(err);
|
|
624
664
|
} finally {
|
|
625
|
-
|
|
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
|
|
642
|
-
|
|
681
|
+
var _iterator8 = _createForOfIteratorHelper(charts),
|
|
682
|
+
_step8;
|
|
643
683
|
try {
|
|
644
|
-
for (
|
|
645
|
-
var c =
|
|
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
|
-
|
|
689
|
+
_iterator8.e(err);
|
|
650
690
|
} finally {
|
|
651
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
|
2309
|
-
|
|
2356
|
+
var _iterator9 = _createForOfIteratorHelper(this.audioChunks),
|
|
2357
|
+
_step9;
|
|
2310
2358
|
try {
|
|
2311
|
-
for (
|
|
2312
|
-
var chunk =
|
|
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
|
-
|
|
2365
|
+
_iterator9.e(err);
|
|
2318
2366
|
} finally {
|
|
2319
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
//
|
|
3336
|
-
if (this.
|
|
3337
|
-
this.
|
|
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'),
|