Skip to content

Commit afaf415

Browse files
committed
Merge branch 'chat-suggestions' into chat-suggestions-submit
2 parents f8e61a4 + 250ff99 commit afaf415

File tree

6 files changed

+107
-48
lines changed

6 files changed

+107
-48
lines changed

js/chat/chat.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,28 @@ class ChatMessage extends LightElement {
7070
content-type=${this.content_type}
7171
?streaming=${this.streaming}
7272
auto-scroll
73+
.onContentChange=${this.#onContentChange}
74+
.onStreamEnd=${this.#makeSuggestionsAccessible}
7375
></shiny-markdown-stream>
7476
`;
7577
}
78+
79+
#onContentChange(): void {
80+
if (!this.streaming) this.#makeSuggestionsAccessible();
81+
}
82+
83+
#makeSuggestionsAccessible(): void {
84+
this.querySelectorAll(".suggestion,[data-suggestion]").forEach((el) => {
85+
if (!(el instanceof HTMLElement)) return;
86+
if (el.hasAttribute("tabindex")) return;
87+
88+
el.setAttribute("tabindex", "0");
89+
el.setAttribute("role", "button");
90+
91+
const suggestion = el.dataset.suggestion || el.textContent;
92+
el.setAttribute("aria-label", `Use chat suggestion: ${suggestion}`);
93+
});
94+
}
7695
}
7796

7897
class ChatUserMessage extends LightElement {
@@ -262,6 +281,7 @@ class ChatContainer extends LightElement {
262281
this.#onRemoveLoadingMessage
263282
);
264283
this.addEventListener("click", this.#onInputSuggestionClick);
284+
this.addEventListener("keydown", this.#onInputSuggestionKeydown);
265285
}
266286

267287
disconnectedCallback(): void {
@@ -283,6 +303,7 @@ class ChatContainer extends LightElement {
283303
this.#onRemoveLoadingMessage
284304
);
285305
this.removeEventListener("click", this.#onInputSuggestionClick);
306+
this.removeEventListener("keydown", this.#onInputSuggestionKeydown);
286307
}
287308

288309
// When user submits input, append it to the chat, and add a loading message
@@ -372,26 +393,41 @@ class ChatContainer extends LightElement {
372393
}
373394

374395
#onInputSuggestionClick(e: Event): void {
375-
const target = e.target;
376-
if (!(target instanceof HTMLElement)) return;
396+
const { suggestion, submit } = this.#getSuggestion(e.target);
397+
if (!suggestion) return;
377398

378-
const isSuggestion =
379-
target.classList.contains("suggestion") ||
380-
target.dataset.suggestion !== undefined;
399+
e.preventDefault();
400+
this.input.setInputValue(suggestion, submit);
401+
}
381402

382-
if (!isSuggestion) return;
403+
#onInputSuggestionKeydown(e: KeyboardEvent): void {
404+
const isEnter = e.key === "Enter" || e.key === " ";
405+
if (!isEnter) return;
406+
const { suggestion, submit } = this.#getSuggestion(e.target);
407+
if (!suggestion) return;
383408

384409
e.preventDefault();
410+
this.input.setInputValue(suggestion, submit);
411+
}
385412

386-
const suggestion = target.dataset.suggestion || target.textContent;
413+
#getSuggestion(x: EventTarget | null): {
414+
suggestion?: string;
415+
submit?: boolean;
416+
} {
417+
if (!(x instanceof HTMLElement)) return {};
387418

388-
if (suggestion) {
389-
const doSubmit =
390-
target.classList.contains("submit") ||
391-
["", "true"].includes(target.dataset.suggestionSubmit || "false");
419+
const isSuggestion =
420+
x.classList.contains("suggestion") || x.dataset.suggestion !== undefined;
421+
if (!isSuggestion) return {};
392422

393-
this.input.setInputValue(suggestion, doSubmit);
394-
}
423+
const suggestion = x.dataset.suggestion || x.textContent;
424+
425+
return {
426+
suggestion: suggestion || undefined,
427+
submit:
428+
x.classList.contains("submit") ||
429+
["", "true"].includes(x.dataset.suggestionSubmit || "false"),
430+
};
395431
}
396432

397433
#onRemoveLoadingMessage(): void {

js/markdown-stream/markdown-stream.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class MarkdownElement extends LightElement {
7979
streaming = false;
8080
@property({ type: Boolean, reflect: true, attribute: "auto-scroll" })
8181
auto_scroll = false;
82+
@property({ type: Function }) onContentChange?: () => void;
83+
@property({ type: Function }) onStreamEnd?: () => void;
8284

8385
render() {
8486
return html`${contentToHTML(this.content, this.content_type)}`;
@@ -112,10 +114,29 @@ class MarkdownElement extends LightElement {
112114
// Possibly scroll to bottom after content has been added
113115
this.#isContentBeingAdded = false;
114116
this.#maybeScrollToBottom();
117+
118+
if (this.onContentChange) {
119+
try {
120+
this.onContentChange();
121+
} catch (error) {
122+
console.warn("Failed to call onContentUpdate callback:", error);
123+
}
124+
}
115125
}
116126

117127
if (changedProperties.has("streaming")) {
118-
this.streaming ? this.#appendStreamingDot() : this.#removeStreamingDot();
128+
if (this.streaming) {
129+
this.#appendStreamingDot();
130+
} else {
131+
this.#removeStreamingDot();
132+
if (this.onStreamEnd) {
133+
try {
134+
this.onStreamEnd();
135+
} catch (error) {
136+
console.warn("Failed to call onStreamEnd callback:", error);
137+
}
138+
}
139+
}
119140
}
120141
}
121142

shiny/www/py-shiny/chat/chat.js

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/py-shiny/chat/chat.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/py-shiny/markdown-stream/markdown-stream.js

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/py-shiny/markdown-stream/markdown-stream.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)