Skip to content

Commit

Permalink
Put streaming visual cue at the end of the content (#1659)
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert authored Aug 30, 2024
1 parent aab145d commit d53e0b7
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 56 deletions.
21 changes: 0 additions & 21 deletions js/chat/chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,6 @@ shiny-chat-container {
.message-content {
align-self: center;
}
.message-streaming-icon {
display: none;
opacity: 0;
}
&[streaming] .message-streaming-icon {
display: block;
animation-delay: 2s;
animation-name: fade-in;
animation-duration: 10ms;
animation-fill-mode: forwards;
}
}

/* Align the user message to the right */
Expand Down Expand Up @@ -154,13 +143,3 @@ pre:has(.code-copy-button) {
background-color: var(--bs-success, #198754);
}
}

/* Keyframes for the fading spinner */
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
28 changes: 23 additions & 5 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@ const ICONS = {
// https://github.com/n3r4zzurr0/svg-spinners/blob/main/svg-css/3-dots-fade.svg
dots_fade:
'<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_S1WN{animation:spinner_MGfb .8s linear infinite;animation-delay:-.8s}.spinner_Km9P{animation-delay:-.65s}.spinner_JApP{animation-delay:-.5s}@keyframes spinner_MGfb{93.75%,100%{opacity:.2}}</style><circle class="spinner_S1WN" cx="4" cy="12" r="3"/><circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3"/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3"/></svg>',
// https://github.com/n3r4zzurr0/svg-spinners/blob/main/svg-css/bouncing-ball.svg
ball_bounce:
'<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_rXNP{animation:spinner_YeBj .8s infinite; opacity:.8}@keyframes spinner_YeBj{0%{animation-timing-function:cubic-bezier(0.33,0,.66,.33);cy:5px}46.875%{cy:20px;rx:4px;ry:4px}50%{animation-timing-function:cubic-bezier(0.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px}53.125%{rx:4px;ry:4px}100%{cy:5px}}</style><ellipse class="spinner_rXNP" cx="12" cy="5" rx="4" ry="4"/></svg>',
dot: '<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" class="chat-streaming-dot" style="margin-left:.25em;margin-top:-.25em"><circle cx="6" cy="6" r="6"/></svg>',
};

function createSVGIcon(icon: string): HTMLElement {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(icon, "image/svg+xml");
return svgDoc.documentElement;
}

const SVG_DOT = createSVGIcon(ICONS.dot);

const requestScroll = (el: HTMLElement, cancelIfScrolledUp = false) => {
el.dispatchEvent(
new CustomEvent("shiny-chat-request-scroll", {
Expand Down Expand Up @@ -127,17 +133,29 @@ class ChatMessage extends LightElement {
return html`
<div class="message-icon">${unsafeHTML(icon)}</div>
<div class="message-content">${content}</div>
<div class="message-streaming-icon">${unsafeHTML(ICONS.ball_bounce)}</div>
`;
}

updated(changedProperties: Map<string, unknown>): void {
if (changedProperties.has("content")) {
this.#highlightAndCodeCopy();
if (this.streaming) this.#appendStreamingDot();
// It's important that the scroll request happens at this point in time, since
// otherwise, the content may not be fully rendered yet
requestScroll(this, this.streaming);
}
if (changedProperties.has("streaming")) {
this.streaming ? this.#appendStreamingDot() : this.#removeStreamingDot();
}
}

#appendStreamingDot(): void {
const content = this.querySelector(".message-content") as HTMLElement;
content.lastElementChild?.appendChild(SVG_DOT);
}

#removeStreamingDot(): void {
this.querySelector(".message-content svg.chat-streaming-dot")?.remove();
}

// Highlight code blocks after the element is rendered
Expand Down Expand Up @@ -416,6 +434,7 @@ class ChatContainer extends LightElement {
lastMessage.setAttribute("content", message.content);

if (message.chunk_type === "message_end") {
this.lastMessage?.removeAttribute("streaming");
this.#finalizeMessage();
}
}
Expand All @@ -441,7 +460,6 @@ class ChatContainer extends LightElement {

#finalizeMessage(): void {
this.input.disabled = false;
this.lastMessage?.removeAttribute("streaming");
}

#onRequestScroll(event: CustomEvent<requestScrollEvent>): void {
Expand Down
2 changes: 1 addition & 1 deletion shiny/www/py-shiny/chat/chat.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d53e0b7

Please sign in to comment.