Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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