From 30860812bdf727e72df65cfc001f95349881cd71 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:01:42 -0500 Subject: [PATCH 01/10] polish commons chat states --- pkg-r/inst/www/commons-chat/commons-chat.css | 624 +++++++++++++++++-- pkg-r/tests/testthat/apps/chat-pending/app.R | 18 + pkg-r/tests/testthat/test-chat-browser.R | 67 ++ 3 files changed, 663 insertions(+), 46 deletions(-) create mode 100644 pkg-r/tests/testthat/apps/chat-pending/app.R create mode 100644 pkg-r/tests/testthat/test-chat-browser.R diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 5b734354..7eac1cb9 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -5,39 +5,410 @@ /* ---- Chat-level polish ---------------------------------------------- */ shiny-chat-container { + --commons-chat-radius-control: 0.5rem; + --commons-chat-radius-surface: 0.75rem; + --commons-chat-radius-message: 1.25rem; + --commons-chat-radius-round: 999px; + --commons-chat-motion-fast: 120ms; + --commons-chat-motion-base: 180ms; + --commons-chat-ease-out: cubic-bezier(0.2, 0, 0, 1); + --commons-chat-state-hover: color-mix( + in srgb, + var(--bs-emphasis-color, #000) 6%, + transparent + ); + --commons-chat-state-active: color-mix( + in srgb, + var(--bs-emphasis-color, #000) 10%, + transparent + ); + --commons-chat-border-strong: color-mix( + in srgb, + var(--bs-body-color, #212529) 24%, + transparent + ); + --commons-chat-focus-color: var(--bs-primary, #007bc2); + --commons-chat-focus-ring: color-mix( + in srgb, + var(--bs-primary, #007bc2) 16%, + transparent + ); + --commons-chat-shadow-control: + 0 1px 2px color-mix(in srgb, var(--bs-body-color, #212529) 8%, transparent), + 0 8px 24px -20px + color-mix(in srgb, var(--bs-body-color, #212529) 30%, transparent); --shiny-chat-user-message-bg: var(--bs-tertiary-bg, #f4f5f7); - --shiny-tool-card-spinner-color: var(--bs-secondary-color, #6c757d); + --shiny-chat-streaming-color: var(--bs-primary, #007bc2); + --shiny-tool-card-spinner-color: var(--bs-primary, #007bc2); } -.shiny-chat-user-message { - border-radius: 1.25rem; - padding: 0.65rem 1.1rem; +shiny-chat-container .shiny-chat-messages-content { + --shiny-chat-messages-padding-bottom: 4.5rem; } -.shiny-chat-input .tiptap { +shiny-chat-container .shiny-chat-user-message:not(:has(.shiny-chat-edit-wrap)) { + border-radius: var(--commons-chat-radius-message); + max-width: min(85%, 42rem); + padding: 0.625rem 1rem; +} + +shiny-chat-container .shiny-chat-input .tiptap { + --bs-border-radius: var(--commons-chat-radius-message); + background-color: var(--bs-body-bg, #fff); border-color: var(--bs-border-color, #dee2e6); - box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + box-shadow: var(--commons-chat-shadow-control); + line-height: 1.45; + min-height: 3.25rem; + padding-block: 0.8rem !important; + padding-right: 3rem !important; + transition: + border-color var(--commons-chat-motion-fast) ease, + box-shadow var(--commons-chat-motion-fast) ease, + background-color var(--commons-chat-motion-fast) ease; } -/* Send/stop keep shinychat's primary/danger hues, softened toward the - * background so they don't dominate the chat. */ -.shiny-chat-input .shiny-chat-btn-send { - color: color-mix(in srgb, var(--bs-primary, #007bc2) 60%, var(--bs-body-bg, #fff)); +shiny-chat-container .shiny-chat-input:has(.shiny-chat-btn-attach) .tiptap { + padding-left: 3rem !important; } -.shiny-chat-input .shiny-chat-btn-send.shiny-chat-btn-cancel { - color: color-mix(in srgb, var(--bs-danger, #dc3545) 60%, var(--bs-body-bg, #fff)); +shiny-chat-container .shiny-chat-input .tiptap:hover:not(:focus) { + border-color: var(--commons-chat-border-strong); } -.shiny-chat-input .shiny-chat-btn-send:disabled { - color: var(--bs-gray-400, #ced4da); +shiny-chat-container .shiny-chat-input .tiptap:focus { + border-color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 50%, + var(--bs-border-color, #dee2e6) + ); + box-shadow: + 0 0 0 0.2rem var(--commons-chat-focus-ring), + var(--commons-chat-shadow-control); +} + +shiny-chat-container .shiny-chat-input:has(.shiny-chat-input-attachments) { + background-color: var(--bs-body-bg, #fff); + border-color: var(--bs-border-color, #dee2e6); + border-radius: var(--commons-chat-radius-message); + box-shadow: var(--commons-chat-shadow-control); + transition: + border-color var(--commons-chat-motion-fast) ease, + box-shadow var(--commons-chat-motion-fast) ease; +} + +shiny-chat-container + .shiny-chat-input:has(.shiny-chat-input-attachments):focus-within { + border-color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 50%, + var(--bs-border-color, #dee2e6) + ); + box-shadow: + 0 0 0 0.2rem var(--commons-chat-focus-ring), + var(--commons-chat-shadow-control); +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-send, +shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { + align-items: center; + bottom: calc(0.625rem + var(--_input-padding-bottom, 0px)); + display: inline-flex; + height: 2rem; + justify-content: center; + transition: + color var(--commons-chat-motion-fast) ease, + background-color var(--commons-chat-motion-fast) ease, + opacity var(--commons-chat-motion-fast) ease, + transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); + width: 2rem; +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-send { + color: var(--bs-primary, #007bc2); + right: 0.625rem; +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:focus-visible, +shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color); + outline-offset: 0.1rem; +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send.shiny-chat-btn-cancel { + color: var(--bs-body-color, #212529); +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send.shiny-chat-btn-spinner { + color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 72%, + var(--bs-body-color, #212529) + ); +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send.shiny-chat-btn-spinner.shiny-chat-btn-cancel { + color: var(--bs-secondary-color, #6c757d); +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:disabled { + color: color-mix(in srgb, var(--bs-body-color, #212529) 28%, transparent); +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { + border-radius: var(--commons-chat-radius-round); + color: var(--bs-secondary-color, #6c757d); + left: 0.625rem; +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach svg { + height: 1.25rem; + width: 1.25rem; +} + +shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:disabled { + opacity: 0.38; +} + +/* Commons removes the assistant icon, so its empty row owns pending feedback. */ +shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::before { + animation: commons-chat-working 1.6s ease-in-out infinite; + background: linear-gradient( + 90deg, + var(--bs-secondary-color, #6c757d) 25%, + var(--bs-body-color, #212529) 50%, + var(--bs-secondary-color, #6c757d) 75% + ); + background-clip: text; + background-size: 200% 100%; + color: var(--bs-secondary-color, #6c757d); + content: "Working…"; + display: inline-block; + font-size: 0.8em; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +@keyframes commons-chat-working { + from { + background-position: 150% 0; + } + + to { + background-position: -50% 0; + } +} + +shiny-chat-container + .shiny-chat-suggestion-list + .shiny-chat-suggestion-list-item { + border-radius: var(--commons-chat-radius-surface); + transition: + border-color var(--commons-chat-motion-fast) ease, + background-color var(--commons-chat-motion-fast) ease, + box-shadow var(--commons-chat-motion-fast) ease, + transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); +} + +shiny-chat-container + .shiny-chat-suggestion-list + .shiny-chat-suggestion-list-item:hover { + background-color: var(--bs-tertiary-bg, #f8f9fa); + border-color: var(--commons-chat-border-strong); + box-shadow: 0 6px 16px -12px + color-mix(in srgb, var(--bs-body-color, #212529) 38%, transparent); +} + +shiny-chat-container + .shiny-chat-suggestion-list + .shiny-chat-suggestion-list-item:active { + background-color: var(--commons-chat-state-active); + transform: translateY(0) scale(0.99); +} + +shiny-chat-container + .shiny-chat-suggestion-list + .shiny-chat-suggestion-list-item:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color); + outline-offset: 0.1rem; +} + +shiny-chat-container + .shiny-chat-suggestion-list + .shiny-chat-suggestion-list-item[data-last-clicked] { + background-color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 8%, + var(--bs-body-bg, #fff) + ); + border-color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 42%, + var(--bs-border-color, #dee2e6) + ); +} + +shiny-chat-container + :is( + .shiny-chat-tool-group__row, + .shiny-chat-tool-call-row__summary, + .shiny-chat-thinking-header + ) { + border-radius: var(--commons-chat-radius-control); + transition: + background-color var(--commons-chat-motion-fast) ease, + color var(--commons-chat-motion-fast) ease; +} + +shiny-chat-container + :is( + .shiny-chat-tool-group__row, + .shiny-chat-tool-call-row__summary, + .shiny-chat-thinking-header + ):is(:hover, :focus-visible) { + background-color: var(--commons-chat-state-hover); +} + +shiny-chat-container + :is( + .shiny-chat-tool-group__row, + .shiny-chat-tool-call-row__summary, + .shiny-chat-thinking-header + ):active { + background-color: var(--commons-chat-state-active); +} + +shiny-chat-container + :is( + .shiny-chat-tool-group__row, + .shiny-chat-tool-call-row__summary, + .shiny-chat-thinking-header + ):focus-visible { + outline-color: var(--commons-chat-focus-color); + outline-offset: 0.1rem; +} + +shiny-chat-container + :is( + .shiny-chat-tool-group__chevron, + .shiny-chat-tool-call-row__chevron, + .shiny-chat-thinking-disclosure + ) { + transition: transform var(--commons-chat-motion-base) + var(--commons-chat-ease-out); +} + +shiny-chat-container .shiny-chat-scroll-to-bottom { + border-radius: var(--commons-chat-radius-round); + box-shadow: var(--commons-chat-shadow-control); + transition: + background-color var(--commons-chat-motion-fast) ease, + box-shadow var(--commons-chat-motion-fast) ease, + transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); +} + +shiny-chat-container .shiny-chat-scroll-to-bottom:hover { + background-color: var(--bs-tertiary-bg, #f8f9fa); + box-shadow: 0 4px 12px + color-mix(in srgb, var(--bs-body-color, #212529) 14%, transparent); +} + +shiny-chat-container .shiny-chat-scroll-to-bottom:active { + background-color: var(--commons-chat-state-active); + transform: translateX(-50%) scale(0.94); +} + +shiny-chat-container .shiny-chat-scroll-to-bottom:focus-visible { + outline-color: var(--commons-chat-focus-color); + outline-offset: 0.1rem; +} + +shiny-chat-container .code-copy-button { + align-items: center; + border-radius: var(--commons-chat-radius-control); + color: var(--bs-secondary-color, #6c757d); + cursor: pointer; + display: inline-flex; + justify-content: center; + min-height: 2rem; + min-width: 2rem; + padding: 0.35rem; + transition: + background-color var(--commons-chat-motion-fast) ease, + color var(--commons-chat-motion-fast) ease, + transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); +} + +shiny-chat-container .code-copy-button > .bi::after { + background-color: currentcolor; +} + +shiny-chat-container .code-copy-button:hover { + background-color: var(--commons-chat-state-hover); + color: var(--bs-body-color, #212529); +} + +shiny-chat-container .code-copy-button:active { + background-color: var(--commons-chat-state-active); + transform: scale(0.96); +} + +shiny-chat-container .code-copy-button:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color); + outline-offset: 0.1rem; +} + +shiny-chat-container .code-copy-button-checked { + color: var(--bs-success, #198754); +} + +@media (hover: hover) { + shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not(:disabled, .shiny-chat-btn-spinner):hover { + color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 82%, + var(--bs-body-color, #212529) + ); + transform: scale(1.04); + } + + shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send.shiny-chat-btn-cancel:hover { + color: var(--bs-emphasis-color, #000); + } + + shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-attach:hover:not(:disabled) { + background-color: var(--commons-chat-state-hover); + color: var(--bs-body-color, #212529); + } +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not(:disabled, .shiny-chat-btn-spinner):active, +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-attach:not(:disabled):active { + transform: scale(0.95); } /* ---- Answer provenance ----------------------------------------------- */ .commons-answer-pill { align-items: center; - border-radius: 999px; + border-radius: var(--commons-chat-radius-round, 999px); display: inline-flex; font-size: 0.74rem; font-weight: 400; @@ -46,6 +417,10 @@ shiny-chat-container { max-width: 100%; padding: 0.18rem 0.45rem; position: relative; + transition: + background-color var(--commons-chat-motion-fast, 120ms) ease, + border-color var(--commons-chat-motion-fast, 120ms) ease, + color var(--commons-chat-motion-fast, 120ms) ease; vertical-align: text-bottom; } @@ -56,21 +431,62 @@ shiny-chat-container { } .commons-answer-pill-trusted { - background: #f2fbf5; - border: 1px solid #cfeedd; - color: #286144; + background: color-mix( + in srgb, + var(--bs-success, #198754) 9%, + var(--bs-body-bg, #fff) + ); + border: 1px solid var(--bs-success-border-subtle, #a3cfbb); + color: var(--bs-success-text-emphasis, #0a3622); } .commons-answer-pill-cited { - background: #eef7fa; - border: 1px solid #c9e2e9; - color: #285867; + background: color-mix( + in srgb, + var(--bs-info, #17a2b8) 9%, + var(--bs-body-bg, #fff) + ); + border: 1px solid var(--bs-info-border-subtle, #9eeaf9); + color: var(--bs-info-text-emphasis, #055160); } .commons-answer-pill-caution { - background: #fff8ec; - border: 1px solid #f2ddbb; - color: #6b4b1b; + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 12%, + var(--bs-body-bg, #fff) + ); + border: 1px solid var(--bs-warning-border-subtle, #ffe69c); + color: var(--bs-warning-text-emphasis, #664d03); +} + +.commons-answer-pill-trusted:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-success, #198754) 15%, + var(--bs-body-bg, #fff) + ); +} + +.commons-answer-pill-cited:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-info, #17a2b8) 15%, + var(--bs-body-bg, #fff) + ); +} + +.commons-answer-pill-caution:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 20%, + var(--bs-body-bg, #fff) + ); +} + +.commons-answer-pill:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color, currentcolor); + outline-offset: 0.1rem; } /* ---- Provenance tooltips ---------------------------------------------- */ @@ -79,9 +495,10 @@ shiny-chat-container { .commons-tooltip { background: var(--bs-body-bg, #fff); border: 1px solid var(--bs-border-color, #dee2e6); - border-radius: 6px; + border-radius: var(--commons-chat-radius-control, 0.5rem); bottom: calc(100% + 0.45rem); - box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12); + box-shadow: 0 6px 18px + color-mix(in srgb, var(--bs-body-color, #212529) 14%, transparent); color: var(--bs-body-color, #212529); display: none; font-size: 0.74rem; @@ -144,38 +561,55 @@ shiny-chat-container [data-shinychat-aside-display="compact"] { } /* Temporary bridge until Shinychat provides a public per-aside tone API. */ -shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { - background: #f2fbf5; - box-shadow: inset 0 0 0 1px #cfeedd; - color: #286144; +shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { + background: color-mix( + in srgb, + var(--bs-success, #198754) 9%, + var(--bs-body-bg, #fff) + ); + box-shadow: inset 0 0 0 1px var(--bs-success-border-subtle, #a3cfbb); + color: var(--bs-success-text-emphasis, #0a3622); } -shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):hover, +shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):hover, shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):focus-visible, shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"])[aria-expanded="true"] { - background: #e4f6ea; - color: #204f38; + background: color-mix( + in srgb, + var(--bs-success, #198754) 15%, + var(--bs-body-bg, #fff) + ); + color: var(--bs-success-text-emphasis, #0a3622); } -shiny-chat-container - .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { - background: #fff8ec; - box-shadow: inset 0 0 0 1px #f2ddbb; - color: #6b4b1b; +shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 12%, + var(--bs-body-bg, #fff) + ); + box-shadow: inset 0 0 0 1px var(--bs-warning-border-subtle, #ffe69c); + color: var(--bs-warning-text-emphasis, #664d03); } -shiny-chat-container - .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):hover, +shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):hover, shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):focus-visible, shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"])[aria-expanded="true"] { - background: #fcefd8; - color: #583b14; + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 20%, + var(--bs-body-bg, #fff) + ); + color: var(--bs-warning-text-emphasis, #664d03); +} + +shiny-chat-container .shiny-aside-pill:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color); + outline-offset: 0.1rem; } shiny-chat-container @@ -269,7 +703,7 @@ shiny-chat-container .commons-measure-plot { border: 1px solid var(--bs-border-color, #dee2e6); - border-radius: 0.5rem; + border-radius: var(--commons-chat-radius-surface, 0.75rem); display: block; height: auto; max-width: min(100%, 34rem); @@ -284,12 +718,33 @@ shiny-chat-container } .commons-run-r-details > summary { + border-radius: var(--commons-chat-radius-control, 0.5rem); color: var(--bs-secondary-color, #6c757d); cursor: pointer; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.07em; + margin-left: -0.4rem; + padding: 0.25rem 0.4rem; text-transform: uppercase; + transition: + background-color var(--commons-chat-motion-fast, 120ms) ease, + color var(--commons-chat-motion-fast, 120ms) ease; + width: fit-content; +} + +.commons-run-r-details > summary:hover { + background-color: var(--commons-chat-state-hover, rgba(0, 0, 0, 0.06)); + color: var(--bs-body-color, #212529); +} + +.commons-run-r-details > summary:active { + background-color: var(--commons-chat-state-active, rgba(0, 0, 0, 0.1)); +} + +.commons-run-r-details > summary:focus-visible { + outline: 0.125rem solid var(--commons-chat-focus-color, currentcolor); + outline-offset: 0.1rem; } .commons-run-r-code { @@ -304,7 +759,84 @@ shiny-chat-container .commons-run-r-plot { border: 1px solid var(--bs-border-color, #dee2e6); - border-radius: 0.5rem; + border-radius: var(--commons-chat-radius-surface, 0.75rem); height: auto; max-width: min(100%, 34rem); } + +@media (prefers-reduced-motion: reduce) { + shiny-chat-container *, + shiny-chat-container *::before, + shiny-chat-container *::after { + scroll-behavior: auto !important; + } + + shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::before { + animation: none; + background: none; + -webkit-text-fill-color: currentcolor; + } + + shiny-chat-container + :is( + .shiny-chat-btn-send, + .shiny-chat-btn-attach, + .shiny-chat-suggestion-list-item, + .shiny-chat-scroll-to-bottom, + .code-copy-button, + .shiny-chat-tool-group__chevron, + .shiny-chat-tool-call-row__chevron, + .shiny-chat-thinking-disclosure + ) { + transition: none; + } + + shiny-chat-container + :is( + .shiny-chat-btn-send, + .shiny-chat-btn-attach, + .shiny-chat-suggestion-list-item, + .code-copy-button + ) { + transform: none !important; + } + + shiny-chat-container .shiny-chat-scroll-to-bottom { + transform: translateX(-50%) !important; + } +} + +@media (prefers-contrast: more), (forced-colors: active) { + shiny-chat-container { + --commons-chat-border-strong: currentcolor; + --commons-chat-state-active: transparent; + --commons-chat-state-hover: transparent; + } + + shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::before { + animation: none; + background: none; + -webkit-text-fill-color: currentcolor; + } + + shiny-chat-container + :is( + .shiny-chat-btn-send, + .shiny-chat-btn-attach, + .shiny-chat-suggestion-list-item, + .shiny-chat-scroll-to-bottom, + .code-copy-button, + .shiny-chat-tool-group__row, + .shiny-chat-tool-call-row__summary, + .shiny-chat-thinking-header, + .shiny-aside-pill + ):focus-visible, + .commons-answer-pill:focus-visible, + .commons-run-r-details > summary:focus-visible { + outline-color: Highlight; + } +} diff --git a/pkg-r/tests/testthat/apps/chat-pending/app.R b/pkg-r/tests/testthat/apps/chat-pending/app.R new file mode 100644 index 00000000..3bab3289 --- /dev/null +++ b/pkg-r/tests/testthat/apps/chat-pending/app.R @@ -0,0 +1,18 @@ +library(commons) + +ui <- bslib::page_fillable( + commons_ui( + "chat", + greeting = "Ask a question", + enable_cancel = TRUE + ) +) + +server <- function(input, output, session) { + shiny::observeEvent(input$chat_user_input, { + Sys.sleep(3) + shinychat::chat_append("chat", "The response is ready.") + }) +} + +shiny::shinyApp(ui, server) diff --git a/pkg-r/tests/testthat/test-chat-browser.R b/pkg-r/tests/testthat/test-chat-browser.R new file mode 100644 index 00000000..e75be52d --- /dev/null +++ b/pkg-r/tests/testthat/test-chat-browser.R @@ -0,0 +1,67 @@ +test_that("Commons shows feedback while a chat response is pending", { + skip_on_cran() + skip_if_not_installed("shinytest2") + skip_if_not_installed("chromote") + skip_if_browser_tests_disabled() + + app <- shinytest2::AppDriver$new( + browser_test_app("chat-pending"), + name = "chat-pending", + timeout = 30 * 1000, + load_timeout = 30 * 1000 + ) + withr::defer(app$stop()) + + app$wait_for_js( + "document.querySelector('.suggestion');", + timeout = 30 * 1000 + ) + app$get_js( + "document.querySelector('.suggestion').click();" + ) + app$wait_for_js( + paste0( + "document.querySelector(", + "'.shiny-chat-message > .shiny-chat-message-content:empty'", + ");" + ), + timeout = 30 * 1000 + ) + + expect_identical( + app$get_js( + paste0( + "getComputedStyle(document.querySelector(", + "'.shiny-chat-message > .shiny-chat-message-content:empty'", + "), '::before').content;" + ) + ), + '"Working…"' + ) + expect_identical( + app$get_js( + paste0( + "document.querySelector('", + ".shiny-chat-btn-send[aria-label=\"Loading\"], ", + ".shiny-chat-btn-cancel[aria-label=\"Stop generating\"]", + "') !== null;" + ) + ), + TRUE + ) + + app$wait_for_js( + "document.body.innerText.includes('The response is ready.');", + timeout = 30 * 1000 + ) + expect_identical( + app$get_js( + paste0( + "document.querySelector(", + "'.shiny-chat-message > .shiny-chat-message-content:empty'", + ") === null;" + ) + ), + TRUE + ) +}) From 0efe2593917b5572fc96aada6d03d63cf6814c70 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:24:22 -0500 Subject: [PATCH 02/10] soften composer and provenance tones --- pkg-r/inst/www/commons-chat/commons-chat.css | 179 +++++++++++-------- 1 file changed, 106 insertions(+), 73 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 7eac1cb9..23fbad4a 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -30,7 +30,7 @@ shiny-chat-container { --commons-chat-focus-color: var(--bs-primary, #007bc2); --commons-chat-focus-ring: color-mix( in srgb, - var(--bs-primary, #007bc2) 16%, + var(--bs-body-color, #212529) 10%, transparent ); --commons-chat-shadow-control: @@ -76,11 +76,7 @@ shiny-chat-container .shiny-chat-input .tiptap:hover:not(:focus) { } shiny-chat-container .shiny-chat-input .tiptap:focus { - border-color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 50%, - var(--bs-border-color, #dee2e6) - ); + border-color: var(--commons-chat-border-strong); box-shadow: 0 0 0 0.2rem var(--commons-chat-focus-ring), var(--commons-chat-shadow-control); @@ -98,11 +94,7 @@ shiny-chat-container .shiny-chat-input:has(.shiny-chat-input-attachments) { shiny-chat-container .shiny-chat-input:has(.shiny-chat-input-attachments):focus-within { - border-color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 50%, - var(--bs-border-color, #dee2e6) - ); + border-color: var(--commons-chat-border-strong); box-shadow: 0 0 0 0.2rem var(--commons-chat-focus-ring), var(--commons-chat-shadow-control); @@ -431,57 +423,33 @@ shiny-chat-container } .commons-answer-pill-trusted { - background: color-mix( - in srgb, - var(--bs-success, #198754) 9%, - var(--bs-body-bg, #fff) - ); - border: 1px solid var(--bs-success-border-subtle, #a3cfbb); - color: var(--bs-success-text-emphasis, #0a3622); + background: #f2fbf5; + border: 1px solid #cfeedd; + color: #286144; } .commons-answer-pill-cited { - background: color-mix( - in srgb, - var(--bs-info, #17a2b8) 9%, - var(--bs-body-bg, #fff) - ); - border: 1px solid var(--bs-info-border-subtle, #9eeaf9); - color: var(--bs-info-text-emphasis, #055160); + background: #eef7fa; + border: 1px solid #c9e2e9; + color: #285867; } .commons-answer-pill-caution { - background: color-mix( - in srgb, - var(--bs-warning, #ffc107) 12%, - var(--bs-body-bg, #fff) - ); - border: 1px solid var(--bs-warning-border-subtle, #ffe69c); - color: var(--bs-warning-text-emphasis, #664d03); + background: #fff8ec; + border: 1px solid #f2ddbb; + color: #6b4b1b; } .commons-answer-pill-trusted:is(:hover, :focus-visible, :focus-within) { - background: color-mix( - in srgb, - var(--bs-success, #198754) 15%, - var(--bs-body-bg, #fff) - ); + background: #e4f6ea; } .commons-answer-pill-cited:is(:hover, :focus-visible, :focus-within) { - background: color-mix( - in srgb, - var(--bs-info, #17a2b8) 15%, - var(--bs-body-bg, #fff) - ); + background: #e3f1f5; } .commons-answer-pill-caution:is(:hover, :focus-visible, :focus-within) { - background: color-mix( - in srgb, - var(--bs-warning, #ffc107) 20%, - var(--bs-body-bg, #fff) - ); + background: #fcefd8; } .commons-answer-pill:focus-visible { @@ -562,13 +530,9 @@ shiny-chat-container [data-shinychat-aside-display="compact"] { /* Temporary bridge until Shinychat provides a public per-aside tone API. */ shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { - background: color-mix( - in srgb, - var(--bs-success, #198754) 9%, - var(--bs-body-bg, #fff) - ); - box-shadow: inset 0 0 0 1px var(--bs-success-border-subtle, #a3cfbb); - color: var(--bs-success-text-emphasis, #0a3622); + background: #f2fbf5; + box-shadow: inset 0 0 0 1px #cfeedd; + color: #286144; } shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):hover, @@ -576,22 +540,14 @@ shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):focus-visible, shiny-chat-container .shiny-aside-pill:has(img[src$="/trusted-icon.svg"])[aria-expanded="true"] { - background: color-mix( - in srgb, - var(--bs-success, #198754) 15%, - var(--bs-body-bg, #fff) - ); - color: var(--bs-success-text-emphasis, #0a3622); + background: #e4f6ea; + color: #204f38; } shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { - background: color-mix( - in srgb, - var(--bs-warning, #ffc107) 12%, - var(--bs-body-bg, #fff) - ); - box-shadow: inset 0 0 0 1px var(--bs-warning-border-subtle, #ffe69c); - color: var(--bs-warning-text-emphasis, #664d03); + background: #fff8ec; + box-shadow: inset 0 0 0 1px #f2ddbb; + color: #6b4b1b; } shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):hover, @@ -599,12 +555,8 @@ shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):focus-visible, shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"])[aria-expanded="true"] { - background: color-mix( - in srgb, - var(--bs-warning, #ffc107) 20%, - var(--bs-body-bg, #fff) - ); - color: var(--bs-warning-text-emphasis, #664d03); + background: #fcefd8; + color: #583b14; } shiny-chat-container .shiny-aside-pill:focus-visible { @@ -629,6 +581,87 @@ shiny-chat-container opacity: 1; } +[data-bs-theme="dark"] .commons-answer-pill-trusted, +[data-bs-theme="dark"] + shiny-chat-container + .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { + background: color-mix( + in srgb, + var(--bs-success, #198754) 9%, + var(--bs-body-bg, #212529) + ); + color: var(--bs-success-text-emphasis, #75b798); +} + +[data-bs-theme="dark"] .commons-answer-pill-trusted { + border-color: var(--bs-success-border-subtle, #0f5132); +} + +[data-bs-theme="dark"] + .commons-answer-pill-trusted:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-success, #198754) 15%, + var(--bs-body-bg, #212529) + ); +} + +[data-bs-theme="dark"] + shiny-chat-container + .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { + box-shadow: inset 0 0 0 1px var(--bs-success-border-subtle, #0f5132); +} + +[data-bs-theme="dark"] .commons-answer-pill-cited { + background: color-mix( + in srgb, + var(--bs-info, #17a2b8) 9%, + var(--bs-body-bg, #212529) + ); + border-color: var(--bs-info-border-subtle, #087990); + color: var(--bs-info-text-emphasis, #6edff6); +} + +[data-bs-theme="dark"] + .commons-answer-pill-cited:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-info, #17a2b8) 15%, + var(--bs-body-bg, #212529) + ); +} + +[data-bs-theme="dark"] .commons-answer-pill-caution, +[data-bs-theme="dark"] + shiny-chat-container + .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 12%, + var(--bs-body-bg, #212529) + ); + color: var(--bs-warning-text-emphasis, #ffda6a); +} + +[data-bs-theme="dark"] .commons-answer-pill-caution { + border-color: var(--bs-warning-border-subtle, #997404); +} + +[data-bs-theme="dark"] + .commons-answer-pill-caution:is(:hover, :focus-visible, :focus-within) { + background: color-mix( + in srgb, + var(--bs-warning, #ffc107) 20%, + var(--bs-body-bg, #212529) + ); +} + +[data-bs-theme="dark"] + shiny-chat-container + .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { + box-shadow: inset 0 0 0 1px var(--bs-warning-border-subtle, #997404); +} + /* ---- Measure display ------------------------------------------------- */ .commons-measure-display { From 498af4d982e539470afd0f0a50939a3bbab96819 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:35:08 -0500 Subject: [PATCH 03/10] lighten active send control --- pkg-r/inst/www/commons-chat/commons-chat.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 23fbad4a..b6e33310 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -116,7 +116,11 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { } shiny-chat-container .shiny-chat-input .shiny-chat-btn-send { - color: var(--bs-primary, #007bc2); + color: color-mix( + in srgb, + var(--bs-primary, #007bc2) 68%, + var(--bs-body-bg, #fff) + ); right: 0.625rem; } @@ -367,8 +371,8 @@ shiny-chat-container .code-copy-button-checked { .shiny-chat-btn-send:not(:disabled, .shiny-chat-btn-spinner):hover { color: color-mix( in srgb, - var(--bs-primary, #007bc2) 82%, - var(--bs-body-color, #212529) + var(--bs-primary, #007bc2) 78%, + var(--bs-body-bg, #fff) ); transform: scale(1.04); } From 1df89848adbd1b316df6617a5bd793dcc963c042 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:40:01 -0500 Subject: [PATCH 04/10] debounce transient working state --- pkg-r/inst/www/commons-chat/commons-chat.css | 17 +++++++++++++---- pkg-r/tests/testthat/test-chat-browser.R | 8 ++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index b6e33310..2a699c8d 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -171,11 +171,13 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:disabled { opacity: 0.38; } -/* Commons removes the assistant icon, so its empty row owns pending feedback. */ +/* Commons has no assistant icon; debounce empty rows past block handoffs. */ shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: commons-chat-working 1.6s ease-in-out infinite; + animation: + commons-chat-working-reveal 0s linear 200ms forwards, + commons-chat-working 1.6s ease-in-out 200ms infinite; background: linear-gradient( 90deg, var(--bs-secondary-color, #6c757d) 25%, @@ -188,10 +190,17 @@ shiny-chat-container content: "Working…"; display: inline-block; font-size: 0.8em; + opacity: 0; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } +@keyframes commons-chat-working-reveal { + to { + opacity: 1; + } +} + @keyframes commons-chat-working { from { background-position: 150% 0; @@ -811,7 +820,7 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: none; + animation: commons-chat-working-reveal 0s linear 200ms forwards; background: none; -webkit-text-fill-color: currentcolor; } @@ -855,7 +864,7 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: none; + animation: commons-chat-working-reveal 0s linear 200ms forwards; background: none; -webkit-text-fill-color: currentcolor; } diff --git a/pkg-r/tests/testthat/test-chat-browser.R b/pkg-r/tests/testthat/test-chat-browser.R index e75be52d..817d3712 100644 --- a/pkg-r/tests/testthat/test-chat-browser.R +++ b/pkg-r/tests/testthat/test-chat-browser.R @@ -27,6 +27,14 @@ test_that("Commons shows feedback while a chat response is pending", { ), timeout = 30 * 1000 ) + app$wait_for_js( + paste0( + "getComputedStyle(document.querySelector(", + "'.shiny-chat-message > .shiny-chat-message-content:empty'", + "), '::before').opacity === '1';" + ), + timeout = 30 * 1000 + ) expect_identical( app$get_js( From 93c96d9497b34e3cee8541dd2fc88f862191988c Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:40:58 -0500 Subject: [PATCH 05/10] document working state delay --- pkg-r/inst/www/commons-chat/commons-chat.css | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 2a699c8d..9db8e98a 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -11,6 +11,8 @@ shiny-chat-container { --commons-chat-radius-round: 999px; --commons-chat-motion-fast: 120ms; --commons-chat-motion-base: 180ms; + /* shinychat main can leave an empty row for ~120ms between blocks. */ + --commons-chat-working-delay: 200ms; --commons-chat-ease-out: cubic-bezier(0.2, 0, 0, 1); --commons-chat-state-hover: color-mix( in srgb, @@ -171,13 +173,15 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:disabled { opacity: 0.38; } -/* Commons has no assistant icon; debounce empty rows past block handoffs. */ +/* Commons has no assistant icon, so its empty row owns pending feedback. */ shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { animation: - commons-chat-working-reveal 0s linear 200ms forwards, - commons-chat-working 1.6s ease-in-out 200ms infinite; + commons-chat-working-reveal 0s linear var(--commons-chat-working-delay) + forwards, + commons-chat-working 1.6s ease-in-out var(--commons-chat-working-delay) + infinite; background: linear-gradient( 90deg, var(--bs-secondary-color, #6c757d) 25%, @@ -820,7 +824,8 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: commons-chat-working-reveal 0s linear 200ms forwards; + animation: commons-chat-working-reveal 0s linear + var(--commons-chat-working-delay) forwards; background: none; -webkit-text-fill-color: currentcolor; } @@ -864,7 +869,8 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: commons-chat-working-reveal 0s linear 200ms forwards; + animation: commons-chat-working-reveal 0s linear + var(--commons-chat-working-delay) forwards; background: none; -webkit-text-fill-color: currentcolor; } From 0ce4057cca71d2fcc97af7e07d9815a0c480d869 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 09:47:40 -0500 Subject: [PATCH 06/10] align working and thinking states --- pkg-r/inst/www/commons-chat/commons-chat.css | 57 ++++++++++++++++---- pkg-r/tests/testthat/test-chat-browser.R | 4 +- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 9db8e98a..b37e7330 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -173,15 +173,42 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:disabled { opacity: 0.38; } -/* Commons has no assistant icon, so its empty row owns pending feedback. */ +/* Match shinychat's activity-row geometry so pending and thinking stay aligned. */ +shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty { + --_row-pad: 0.4rem; + --_row-glyph-width: 0.9em; + + align-items: center; + animation: commons-chat-working-reveal 0s linear + var(--commons-chat-working-delay) forwards; + display: flex; + font-size: 0.8em; + gap: 0.5rem; + min-height: 2em; + opacity: 0; + padding: 0.25rem var(--_row-pad); + width: 100%; +} + shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) > .shiny-chat-message-content:empty::before { - animation: - commons-chat-working-reveal 0s linear var(--commons-chat-working-delay) - forwards, - commons-chat-working 1.6s ease-in-out var(--commons-chat-working-delay) - infinite; + background-color: var(--bs-secondary-color, #6c757d); + content: ""; + flex: none; + height: 0.4em; + margin-inline: calc((var(--_row-glyph-width) - 0.4em) / 2); + transform: rotate(45deg); + width: 0.4em; +} + +shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::after { + animation: commons-chat-working 1.6s ease-in-out + var(--commons-chat-working-delay) infinite; background: linear-gradient( 90deg, var(--bs-secondary-color, #6c757d) 25%, @@ -193,15 +220,13 @@ shiny-chat-container color: var(--bs-secondary-color, #6c757d); content: "Working…"; display: inline-block; - font-size: 0.8em; - opacity: 0; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } @keyframes commons-chat-working-reveal { to { - opacity: 1; + opacity: var(--_activity-opacity, 0.7); } } @@ -823,9 +848,14 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::before { + > .shiny-chat-message-content:empty { animation: commons-chat-working-reveal 0s linear var(--commons-chat-working-delay) forwards; + } + + shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::after { background: none; -webkit-text-fill-color: currentcolor; } @@ -868,9 +898,14 @@ shiny-chat-container shiny-chat-container .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::before { + > .shiny-chat-message-content:empty { animation: commons-chat-working-reveal 0s linear var(--commons-chat-working-delay) forwards; + } + + shiny-chat-container + .shiny-chat-message:not(:has(> .message-icon)) + > .shiny-chat-message-content:empty::after { background: none; -webkit-text-fill-color: currentcolor; } diff --git a/pkg-r/tests/testthat/test-chat-browser.R b/pkg-r/tests/testthat/test-chat-browser.R index 817d3712..61011a41 100644 --- a/pkg-r/tests/testthat/test-chat-browser.R +++ b/pkg-r/tests/testthat/test-chat-browser.R @@ -31,7 +31,7 @@ test_that("Commons shows feedback while a chat response is pending", { paste0( "getComputedStyle(document.querySelector(", "'.shiny-chat-message > .shiny-chat-message-content:empty'", - "), '::before').opacity === '1';" + ")).opacity !== '0';" ), timeout = 30 * 1000 ) @@ -41,7 +41,7 @@ test_that("Commons shows feedback while a chat response is pending", { paste0( "getComputedStyle(document.querySelector(", "'.shiny-chat-message > .shiny-chat-message-content:empty'", - "), '::before').content;" + "), '::after').content;" ) ), '"Working…"' From 5a0a7884c92d53947248fab75a80b166193118ec Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 13:32:39 -0500 Subject: [PATCH 07/10] align chat geometry with design review --- pkg-r/inst/www/commons-chat/commons-chat.css | 101 ++++++++++++++----- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index b37e7330..e1f4e6e5 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -7,7 +7,7 @@ shiny-chat-container { --commons-chat-radius-control: 0.5rem; --commons-chat-radius-surface: 0.75rem; - --commons-chat-radius-message: 1.25rem; + --commons-chat-radius-message: 0.75rem; --commons-chat-radius-round: 999px; --commons-chat-motion-fast: 120ms; --commons-chat-motion-base: 180ms; @@ -39,7 +39,15 @@ shiny-chat-container { 0 1px 2px color-mix(in srgb, var(--bs-body-color, #212529) 8%, transparent), 0 8px 24px -20px color-mix(in srgb, var(--bs-body-color, #212529) 30%, transparent); - --shiny-chat-user-message-bg: var(--bs-tertiary-bg, #f4f5f7); + --shiny-chat-user-message-bg: color-mix( + in srgb, + var(--bs-tertiary-bg, #f4f5f7) 88%, + var(--bs-primary, #007bc2) + ); + --shiny-chat-user-message-border-radius: var( + --commons-chat-radius-message + ); + --shiny-chat-user-message-padding: 0.5rem 1.5rem; --shiny-chat-streaming-color: var(--bs-primary, #007bc2); --shiny-tool-card-spinner-color: var(--bs-primary, #007bc2); } @@ -49,9 +57,10 @@ shiny-chat-container .shiny-chat-messages-content { } shiny-chat-container .shiny-chat-user-message:not(:has(.shiny-chat-edit-wrap)) { - border-radius: var(--commons-chat-radius-message); - max-width: min(85%, 42rem); - padding: 0.625rem 1rem; + border-radius: var(--shiny-chat-user-message-border-radius); + max-width: 100%; + padding: var(--shiny-chat-user-message-padding); + width: min(85%, 42rem); } shiny-chat-container .shiny-chat-input .tiptap { @@ -60,8 +69,8 @@ shiny-chat-container .shiny-chat-input .tiptap { border-color: var(--bs-border-color, #dee2e6); box-shadow: var(--commons-chat-shadow-control); line-height: 1.45; - min-height: 3.25rem; - padding-block: 0.8rem !important; + min-height: 3rem; + padding-block: 0.65rem !important; padding-right: 3rem !important; transition: border-color var(--commons-chat-motion-fast) ease, @@ -105,7 +114,7 @@ shiny-chat-container shiny-chat-container .shiny-chat-input .shiny-chat-btn-send, shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { align-items: center; - bottom: calc(0.625rem + var(--_input-padding-bottom, 0px)); + bottom: calc(0.5rem + var(--_input-padding-bottom, 0px)); display: inline-flex; height: 2rem; justify-content: center; @@ -118,12 +127,54 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { } shiny-chat-container .shiny-chat-input .shiny-chat-btn-send { - color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 68%, - var(--bs-body-bg, #fff) - ); - right: 0.625rem; + color: var(--bs-secondary-color, #6c757d); + right: 0.5rem; +} + +/* Preserve shinychat's button states while presenting an outlined send icon. */ +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not(.shiny-chat-btn-cancel, .shiny-chat-btn-spinner) { + background-color: transparent; + border: var(--bs-border-width, 1px) solid + var(--bs-border-color, #dee2e6); +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not( + :disabled, + .shiny-chat-btn-cancel, + .shiny-chat-btn-spinner + ) { + border-color: var(--commons-chat-border-strong); + color: var(--bs-body-color, #212529); +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not( + .shiny-chat-btn-cancel, + .shiny-chat-btn-spinner + ) + svg { + display: none; +} + +shiny-chat-container + .shiny-chat-input + .shiny-chat-btn-send:not( + .shiny-chat-btn-cancel, + .shiny-chat-btn-spinner + )::before { + --_send-icon: url('data:image/svg+xml,'); + + background-color: currentcolor; + content: ""; + height: 1rem; + mask: var(--_send-icon) center / contain no-repeat; + -webkit-mask: var(--_send-icon) center / contain no-repeat; + width: 1rem; } shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:focus-visible, @@ -141,11 +192,7 @@ shiny-chat-container shiny-chat-container .shiny-chat-input .shiny-chat-btn-send.shiny-chat-btn-spinner { - color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 72%, - var(--bs-body-color, #212529) - ); + color: var(--bs-secondary-color, #6c757d); } shiny-chat-container @@ -161,7 +208,7 @@ shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:disabled { shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { border-radius: var(--commons-chat-radius-round); color: var(--bs-secondary-color, #6c757d); - left: 0.625rem; + left: 0.5rem; } shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach svg { @@ -406,12 +453,14 @@ shiny-chat-container .code-copy-button-checked { @media (hover: hover) { shiny-chat-container .shiny-chat-input - .shiny-chat-btn-send:not(:disabled, .shiny-chat-btn-spinner):hover { - color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 78%, - var(--bs-body-bg, #fff) - ); + .shiny-chat-btn-send:not( + :disabled, + .shiny-chat-btn-spinner, + .shiny-chat-btn-cancel + ):hover { + background-color: var(--commons-chat-state-hover); + border-color: var(--commons-chat-border-strong); + color: var(--bs-body-color, #212529); transform: scale(1.04); } From ff2211c15a35c6d738dd1fed1283063ab87cec93 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 13:34:46 -0500 Subject: [PATCH 08/10] restore neutral user messages --- pkg-r/inst/www/commons-chat/commons-chat.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index e1f4e6e5..6f17fb79 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -39,11 +39,7 @@ shiny-chat-container { 0 1px 2px color-mix(in srgb, var(--bs-body-color, #212529) 8%, transparent), 0 8px 24px -20px color-mix(in srgb, var(--bs-body-color, #212529) 30%, transparent); - --shiny-chat-user-message-bg: color-mix( - in srgb, - var(--bs-tertiary-bg, #f4f5f7) 88%, - var(--bs-primary, #007bc2) - ); + --shiny-chat-user-message-bg: var(--bs-tertiary-bg, #f4f5f7); --shiny-chat-user-message-border-radius: var( --commons-chat-radius-message ); From 6245ae9561ac408cb8d6a2026f0c6e44b367316a Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 21 Aug 2026 13:36:32 -0500 Subject: [PATCH 09/10] inherit user message color --- pkg-r/inst/www/commons-chat/commons-chat.css | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 6f17fb79..04f8b2b9 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -39,7 +39,6 @@ shiny-chat-container { 0 1px 2px color-mix(in srgb, var(--bs-body-color, #212529) 8%, transparent), 0 8px 24px -20px color-mix(in srgb, var(--bs-body-color, #212529) 30%, transparent); - --shiny-chat-user-message-bg: var(--bs-tertiary-bg, #f4f5f7); --shiny-chat-user-message-border-radius: var( --commons-chat-radius-message ); From 055be2f6a3ebe57fcf910df791496045f78a4c13 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Tue, 25 Aug 2026 14:30:28 -0700 Subject: [PATCH 10/10] adopt shinychat page and pending hooks --- pkg-r/R/chat.R | 40 +- pkg-r/inst/www/commons-chat/commons-chat.css | 436 +------------------ pkg-r/man/commons_app.Rd | 2 +- pkg-r/tests/testthat/test-chat-browser.R | 28 +- pkg-r/tests/testthat/test-chat.R | 5 +- 5 files changed, 61 insertions(+), 450 deletions(-) diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index ead8b877..1040d6f6 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -1,6 +1,6 @@ #' Shiny chat UI and server for commons agents #' -#' These functions wrap [shinychat::chat_app()], [shinychat::chat_ui()], and +#' These functions wrap [shinychat::page_chat()], [shinychat::chat_ui()], and #' [shinychat::chat_server()] for commons agents. The server verifies each #' `` the model writes against its own context, measure #' definitions, and data documentation as the answer streams, and rewrites @@ -54,21 +54,24 @@ commons_app <- function(client, ...) { check_commons_client(client) ui <- function(req) { - bslib::page_fillable( - commons_ui( - "chat", - height = "100%", + with_commons_chat_assets( + shinychat::page_chat( + "commons", + id = "chat", enable_cancel = TRUE, - allow_attachments = TRUE - ), - if (rlang::is_interactive()) { - shiny::actionButton( - "close_btn", - label = "", - class = "btn-close", - style = "position: fixed; top: 6px; right: 6px;" + allow_attachments = TRUE, + toolbar_global = bslib::toolbar( + bslib::input_dark_mode(), + if (rlang::is_interactive()) { + shiny::actionButton( + "close_btn", + label = "", + class = "btn-close", + `aria-label` = "Close" + ) + } ) - } + ) ) } @@ -89,9 +92,7 @@ commons_app <- function(client, ...) { #' @export commons_ui <- function(id, ...) { check_chat_packages() - register_commons_icon_resources() - ui <- shinychat::chat_ui(id, icon_assistant = htmltools::HTML(""), ...) - htmltools::attachDependencies(ui, commons_chat_dependency(), append = TRUE) + with_commons_chat_assets(shinychat::chat_ui(id, ...)) } #' @rdname commons_app @@ -172,6 +173,11 @@ register_commons_icon_resources <- function() { ) } +with_commons_chat_assets <- function(ui) { + register_commons_icon_resources() + htmltools::attachDependencies(ui, commons_chat_dependency(), append = TRUE) +} + # Asset mtimes ride in the version so the dependency URL changes whenever # the files do; browsers otherwise cache edited assets under the stable # version's URL indefinitely. diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 04f8b2b9..e63e53dc 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -10,10 +10,6 @@ shiny-chat-container { --commons-chat-radius-message: 0.75rem; --commons-chat-radius-round: 999px; --commons-chat-motion-fast: 120ms; - --commons-chat-motion-base: 180ms; - /* shinychat main can leave an empty row for ~120ms between blocks. */ - --commons-chat-working-delay: 200ms; - --commons-chat-ease-out: cubic-bezier(0.2, 0, 0, 1); --commons-chat-state-hover: color-mix( in srgb, var(--bs-emphasis-color, #000) 6%, @@ -43,19 +39,14 @@ shiny-chat-container { --commons-chat-radius-message ); --shiny-chat-user-message-padding: 0.5rem 1.5rem; + --shiny-chat-suggestion-card-border-radius: var( + --commons-chat-radius-surface + ); --shiny-chat-streaming-color: var(--bs-primary, #007bc2); - --shiny-tool-card-spinner-color: var(--bs-primary, #007bc2); -} - -shiny-chat-container .shiny-chat-messages-content { - --shiny-chat-messages-padding-bottom: 4.5rem; } shiny-chat-container .shiny-chat-user-message:not(:has(.shiny-chat-edit-wrap)) { - border-radius: var(--shiny-chat-user-message-border-radius); - max-width: 100%; - padding: var(--shiny-chat-user-message-padding); - width: min(85%, 42rem); + max-width: min(85%, 42rem); } shiny-chat-container .shiny-chat-input .tiptap { @@ -106,151 +97,27 @@ shiny-chat-container var(--commons-chat-shadow-control); } -shiny-chat-container .shiny-chat-input .shiny-chat-btn-send, -shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { - align-items: center; - bottom: calc(0.5rem + var(--_input-padding-bottom, 0px)); - display: inline-flex; - height: 2rem; - justify-content: center; - transition: - color var(--commons-chat-motion-fast) ease, - background-color var(--commons-chat-motion-fast) ease, - opacity var(--commons-chat-motion-fast) ease, - transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); - width: 2rem; -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-send { - color: var(--bs-secondary-color, #6c757d); - right: 0.5rem; -} - -/* Preserve shinychat's button states while presenting an outlined send icon. */ -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not(.shiny-chat-btn-cancel, .shiny-chat-btn-spinner) { - background-color: transparent; - border: var(--bs-border-width, 1px) solid - var(--bs-border-color, #dee2e6); -} - -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not( - :disabled, - .shiny-chat-btn-cancel, - .shiny-chat-btn-spinner - ) { - border-color: var(--commons-chat-border-strong); - color: var(--bs-body-color, #212529); +/* Shinychat owns the pending lifecycle; Commons replaces only its visuals. */ +shiny-chat-container .shiny-chat-pending-indicator { + gap: 0.5rem; } -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not( - .shiny-chat-btn-cancel, - .shiny-chat-btn-spinner - ) - svg { +shiny-chat-container .shiny-chat-pending-indicator > svg { display: none; } -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not( - .shiny-chat-btn-cancel, - .shiny-chat-btn-spinner - )::before { - --_send-icon: url('data:image/svg+xml,'); - - background-color: currentcolor; - content: ""; - height: 1rem; - mask: var(--_send-icon) center / contain no-repeat; - -webkit-mask: var(--_send-icon) center / contain no-repeat; - width: 1rem; -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:focus-visible, -shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:focus-visible { - outline: 0.125rem solid var(--commons-chat-focus-color); - outline-offset: 0.1rem; -} - -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send.shiny-chat-btn-cancel { - color: var(--bs-body-color, #212529); -} - -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send.shiny-chat-btn-spinner { - color: var(--bs-secondary-color, #6c757d); -} - -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send.shiny-chat-btn-spinner.shiny-chat-btn-cancel { - color: var(--bs-secondary-color, #6c757d); -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-send:disabled { - color: color-mix(in srgb, var(--bs-body-color, #212529) 28%, transparent); -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach { - border-radius: var(--commons-chat-radius-round); - color: var(--bs-secondary-color, #6c757d); - left: 0.5rem; -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach svg { - height: 1.25rem; - width: 1.25rem; -} - -shiny-chat-container .shiny-chat-input .shiny-chat-btn-attach:disabled { - opacity: 0.38; -} - -/* Match shinychat's activity-row geometry so pending and thinking stay aligned. */ -shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty { - --_row-pad: 0.4rem; - --_row-glyph-width: 0.9em; - - align-items: center; - animation: commons-chat-working-reveal 0s linear - var(--commons-chat-working-delay) forwards; - display: flex; - font-size: 0.8em; - gap: 0.5rem; - min-height: 2em; - opacity: 0; - padding: 0.25rem var(--_row-pad); - width: 100%; -} - -shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::before { +shiny-chat-container .shiny-chat-pending-indicator::before { background-color: var(--bs-secondary-color, #6c757d); content: ""; flex: none; height: 0.4em; - margin-inline: calc((var(--_row-glyph-width) - 0.4em) / 2); + margin-inline: 0.25em; transform: rotate(45deg); width: 0.4em; } -shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::after { - animation: commons-chat-working 1.6s ease-in-out - var(--commons-chat-working-delay) infinite; +shiny-chat-container .shiny-chat-pending-indicator::after { + animation: commons-chat-working 1.6s ease-in-out infinite; background: linear-gradient( 90deg, var(--bs-secondary-color, #6c757d) 25%, @@ -261,17 +128,11 @@ shiny-chat-container background-size: 200% 100%; color: var(--bs-secondary-color, #6c757d); content: "Working…"; - display: inline-block; + font-size: 0.8em; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } -@keyframes commons-chat-working-reveal { - to { - opacity: var(--_activity-opacity, 0.7); - } -} - @keyframes commons-chat-working { from { background-position: 150% 0; @@ -282,206 +143,6 @@ shiny-chat-container } } -shiny-chat-container - .shiny-chat-suggestion-list - .shiny-chat-suggestion-list-item { - border-radius: var(--commons-chat-radius-surface); - transition: - border-color var(--commons-chat-motion-fast) ease, - background-color var(--commons-chat-motion-fast) ease, - box-shadow var(--commons-chat-motion-fast) ease, - transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); -} - -shiny-chat-container - .shiny-chat-suggestion-list - .shiny-chat-suggestion-list-item:hover { - background-color: var(--bs-tertiary-bg, #f8f9fa); - border-color: var(--commons-chat-border-strong); - box-shadow: 0 6px 16px -12px - color-mix(in srgb, var(--bs-body-color, #212529) 38%, transparent); -} - -shiny-chat-container - .shiny-chat-suggestion-list - .shiny-chat-suggestion-list-item:active { - background-color: var(--commons-chat-state-active); - transform: translateY(0) scale(0.99); -} - -shiny-chat-container - .shiny-chat-suggestion-list - .shiny-chat-suggestion-list-item:focus-visible { - outline: 0.125rem solid var(--commons-chat-focus-color); - outline-offset: 0.1rem; -} - -shiny-chat-container - .shiny-chat-suggestion-list - .shiny-chat-suggestion-list-item[data-last-clicked] { - background-color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 8%, - var(--bs-body-bg, #fff) - ); - border-color: color-mix( - in srgb, - var(--bs-primary, #007bc2) 42%, - var(--bs-border-color, #dee2e6) - ); -} - -shiny-chat-container - :is( - .shiny-chat-tool-group__row, - .shiny-chat-tool-call-row__summary, - .shiny-chat-thinking-header - ) { - border-radius: var(--commons-chat-radius-control); - transition: - background-color var(--commons-chat-motion-fast) ease, - color var(--commons-chat-motion-fast) ease; -} - -shiny-chat-container - :is( - .shiny-chat-tool-group__row, - .shiny-chat-tool-call-row__summary, - .shiny-chat-thinking-header - ):is(:hover, :focus-visible) { - background-color: var(--commons-chat-state-hover); -} - -shiny-chat-container - :is( - .shiny-chat-tool-group__row, - .shiny-chat-tool-call-row__summary, - .shiny-chat-thinking-header - ):active { - background-color: var(--commons-chat-state-active); -} - -shiny-chat-container - :is( - .shiny-chat-tool-group__row, - .shiny-chat-tool-call-row__summary, - .shiny-chat-thinking-header - ):focus-visible { - outline-color: var(--commons-chat-focus-color); - outline-offset: 0.1rem; -} - -shiny-chat-container - :is( - .shiny-chat-tool-group__chevron, - .shiny-chat-tool-call-row__chevron, - .shiny-chat-thinking-disclosure - ) { - transition: transform var(--commons-chat-motion-base) - var(--commons-chat-ease-out); -} - -shiny-chat-container .shiny-chat-scroll-to-bottom { - border-radius: var(--commons-chat-radius-round); - box-shadow: var(--commons-chat-shadow-control); - transition: - background-color var(--commons-chat-motion-fast) ease, - box-shadow var(--commons-chat-motion-fast) ease, - transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); -} - -shiny-chat-container .shiny-chat-scroll-to-bottom:hover { - background-color: var(--bs-tertiary-bg, #f8f9fa); - box-shadow: 0 4px 12px - color-mix(in srgb, var(--bs-body-color, #212529) 14%, transparent); -} - -shiny-chat-container .shiny-chat-scroll-to-bottom:active { - background-color: var(--commons-chat-state-active); - transform: translateX(-50%) scale(0.94); -} - -shiny-chat-container .shiny-chat-scroll-to-bottom:focus-visible { - outline-color: var(--commons-chat-focus-color); - outline-offset: 0.1rem; -} - -shiny-chat-container .code-copy-button { - align-items: center; - border-radius: var(--commons-chat-radius-control); - color: var(--bs-secondary-color, #6c757d); - cursor: pointer; - display: inline-flex; - justify-content: center; - min-height: 2rem; - min-width: 2rem; - padding: 0.35rem; - transition: - background-color var(--commons-chat-motion-fast) ease, - color var(--commons-chat-motion-fast) ease, - transform var(--commons-chat-motion-fast) var(--commons-chat-ease-out); -} - -shiny-chat-container .code-copy-button > .bi::after { - background-color: currentcolor; -} - -shiny-chat-container .code-copy-button:hover { - background-color: var(--commons-chat-state-hover); - color: var(--bs-body-color, #212529); -} - -shiny-chat-container .code-copy-button:active { - background-color: var(--commons-chat-state-active); - transform: scale(0.96); -} - -shiny-chat-container .code-copy-button:focus-visible { - outline: 0.125rem solid var(--commons-chat-focus-color); - outline-offset: 0.1rem; -} - -shiny-chat-container .code-copy-button-checked { - color: var(--bs-success, #198754); -} - -@media (hover: hover) { - shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not( - :disabled, - .shiny-chat-btn-spinner, - .shiny-chat-btn-cancel - ):hover { - background-color: var(--commons-chat-state-hover); - border-color: var(--commons-chat-border-strong); - color: var(--bs-body-color, #212529); - transform: scale(1.04); - } - - shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send.shiny-chat-btn-cancel:hover { - color: var(--bs-emphasis-color, #000); - } - - shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-attach:hover:not(:disabled) { - background-color: var(--commons-chat-state-hover); - color: var(--bs-body-color, #212529); - } -} - -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-send:not(:disabled, .shiny-chat-btn-spinner):active, -shiny-chat-container - .shiny-chat-input - .shiny-chat-btn-attach:not(:disabled):active { - transform: scale(0.95); -} - /* ---- Answer provenance ----------------------------------------------- */ .commons-answer-pill { @@ -884,53 +545,11 @@ shiny-chat-container } @media (prefers-reduced-motion: reduce) { - shiny-chat-container *, - shiny-chat-container *::before, - shiny-chat-container *::after { - scroll-behavior: auto !important; - } - - shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty { - animation: commons-chat-working-reveal 0s linear - var(--commons-chat-working-delay) forwards; - } - - shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::after { + shiny-chat-container .shiny-chat-pending-indicator::after { + animation: none; background: none; -webkit-text-fill-color: currentcolor; } - - shiny-chat-container - :is( - .shiny-chat-btn-send, - .shiny-chat-btn-attach, - .shiny-chat-suggestion-list-item, - .shiny-chat-scroll-to-bottom, - .code-copy-button, - .shiny-chat-tool-group__chevron, - .shiny-chat-tool-call-row__chevron, - .shiny-chat-thinking-disclosure - ) { - transition: none; - } - - shiny-chat-container - :is( - .shiny-chat-btn-send, - .shiny-chat-btn-attach, - .shiny-chat-suggestion-list-item, - .code-copy-button - ) { - transform: none !important; - } - - shiny-chat-container .shiny-chat-scroll-to-bottom { - transform: translateX(-50%) !important; - } } @media (prefers-contrast: more), (forced-colors: active) { @@ -940,32 +559,13 @@ shiny-chat-container --commons-chat-state-hover: transparent; } - shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty { - animation: commons-chat-working-reveal 0s linear - var(--commons-chat-working-delay) forwards; - } - - shiny-chat-container - .shiny-chat-message:not(:has(> .message-icon)) - > .shiny-chat-message-content:empty::after { + shiny-chat-container .shiny-chat-pending-indicator::after { + animation: none; background: none; -webkit-text-fill-color: currentcolor; } - shiny-chat-container - :is( - .shiny-chat-btn-send, - .shiny-chat-btn-attach, - .shiny-chat-suggestion-list-item, - .shiny-chat-scroll-to-bottom, - .code-copy-button, - .shiny-chat-tool-group__row, - .shiny-chat-tool-call-row__summary, - .shiny-chat-thinking-header, - .shiny-aside-pill - ):focus-visible, + shiny-chat-container .shiny-aside-pill:focus-visible, .commons-answer-pill:focus-visible, .commons-run-r-details > summary:focus-visible { outline-color: Highlight; diff --git a/pkg-r/man/commons_app.Rd b/pkg-r/man/commons_app.Rd index 92b86979..b34ea474 100644 --- a/pkg-r/man/commons_app.Rd +++ b/pkg-r/man/commons_app.Rd @@ -29,7 +29,7 @@ returns UI. \code{commons_server()} returns the \code{\link[shinychat:chat_serve result. } \description{ -These functions wrap \code{\link[shinychat:chat_app]{shinychat::chat_app()}}, \code{\link[shinychat:chat_ui]{shinychat::chat_ui()}}, and +These functions wrap \code{\link[shinychat:page_chat]{shinychat::page_chat()}}, \code{\link[shinychat:chat_ui]{shinychat::chat_ui()}}, and \code{\link[shinychat:chat_server]{shinychat::chat_server()}} for commons agents. The server verifies each \verb{} the model writes against its own context, measure definitions, and data documentation as the answer streams, and rewrites diff --git a/pkg-r/tests/testthat/test-chat-browser.R b/pkg-r/tests/testthat/test-chat-browser.R index 61011a41..54ab38ec 100644 --- a/pkg-r/tests/testthat/test-chat-browser.R +++ b/pkg-r/tests/testthat/test-chat-browser.R @@ -1,4 +1,4 @@ -test_that("Commons shows feedback while a chat response is pending", { +test_that("Commons customizes shinychat's pending feedback", { skip_on_cran() skip_if_not_installed("shinytest2") skip_if_not_installed("chromote") @@ -20,17 +20,13 @@ test_that("Commons shows feedback while a chat response is pending", { "document.querySelector('.suggestion').click();" ) app$wait_for_js( - paste0( - "document.querySelector(", - "'.shiny-chat-message > .shiny-chat-message-content:empty'", - ");" - ), + "document.querySelector('.shiny-chat-pending-indicator');", timeout = 30 * 1000 ) app$wait_for_js( paste0( "getComputedStyle(document.querySelector(", - "'.shiny-chat-message > .shiny-chat-message-content:empty'", + "'.shiny-chat-pending-indicator'", ")).opacity !== '0';" ), timeout = 30 * 1000 @@ -40,12 +36,22 @@ test_that("Commons shows feedback while a chat response is pending", { app$get_js( paste0( "getComputedStyle(document.querySelector(", - "'.shiny-chat-message > .shiny-chat-message-content:empty'", + "'.shiny-chat-pending-indicator'", "), '::after').content;" ) ), '"Working…"' ) + expect_identical( + app$get_js( + paste0( + "getComputedStyle(document.querySelector(", + "'.shiny-chat-pending-indicator > svg'", + ")).display;" + ) + ), + "none" + ) expect_identical( app$get_js( paste0( @@ -64,11 +70,7 @@ test_that("Commons shows feedback while a chat response is pending", { ) expect_identical( app$get_js( - paste0( - "document.querySelector(", - "'.shiny-chat-message > .shiny-chat-message-content:empty'", - ") === null;" - ) + "document.querySelector('.shiny-chat-pending-indicator') === null;" ), TRUE ) diff --git a/pkg-r/tests/testthat/test-chat.R b/pkg-r/tests/testthat/test-chat.R index 74335afe..8922a899 100644 --- a/pkg-r/tests/testthat/test-chat.R +++ b/pkg-r/tests/testthat/test-chat.R @@ -1,4 +1,4 @@ -test_that("commons_app builds a single-user app from commons chat wrappers", { +test_that("commons_app builds a single-user page chat", { skip_if_not_installed("shiny") skip_if_not_installed("shinychat") @@ -9,9 +9,12 @@ test_that("commons_app builds a single-user app from commons chat wrappers", { app_env <- environment(app$serverFuncSource) ui <- app_env$ui(NULL) + page <- htmltools::tagQuery(ui)$find("shiny-chat-page")$selectedTags() chat <- htmltools::tagQuery(ui)$find("shiny-chat-container")$selectedTags() deps <- htmltools::findDependencies(ui) + expect_length(page, 1) + expect_identical(page[[1]]$attribs$id, "chat_page") expect_length(chat, 1) expect_true(is.na(chat[[1]]$attribs[["allow-attachments"]])) expect_true(is.na(chat[[1]]$attribs[["enable-cancel"]]))