Skip to content
Merged
11 changes: 9 additions & 2 deletions js/chat/chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ shiny-chat-container {
text-decoration-style: solid;
}

&.submit,
&[data-suggestion-submit=""],
&[data-suggestion-submit="true"] {
&::after {
content: " \21B5"; // return key symbol
}
}

&::after {
content: "\21B5";
margin-inline-start: 0.25rem;
content: " \2726"; // diamond/star
}
}
}
Expand Down
69 changes: 58 additions & 11 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,38 @@ class ChatMessages extends LightElement {
}

class ChatInput extends LightElement {
private _disabled = false;

@property() placeholder = "Enter a message...";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean })
get disabled() {
return this._disabled;
}

set disabled(value: boolean) {
const oldValue = this._disabled;
if (value === oldValue) {
return;
}

this._disabled = value;
value
? this.setAttribute("disabled", "")
: this.removeAttribute("disabled");

this.requestUpdate("disabled", oldValue);
this.#onInput();
}

attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
) {
if (name === "disabled") {
this.disabled = newValue !== null;
}
}

private get textarea(): HTMLTextAreaElement {
return this.querySelector("textarea") as HTMLTextAreaElement;
Expand Down Expand Up @@ -193,17 +223,21 @@ class ChatInput extends LightElement {
this.dispatchEvent(sentEvent);

this.setInputValue("");
this.disabled = true;

this.textarea.focus();
}

setInputValue(value: string): void {
setInputValue(value: string, submit = false): void {
this.textarea.value = value;
this.disabled = value.trim().length === 0;

// Simulate an input event (to trigger the textarea autoresize)
const inputEvent = new Event("input", { bubbles: true, cancelable: true });
this.textarea.dispatchEvent(inputEvent);

if (submit) {
this.#sendInput();
}
}
}

Expand Down Expand Up @@ -285,6 +319,9 @@ class ChatContainer extends LightElement {

#appendMessage(message: Message, finalize = true): void {
this.#removeLoadingMessage();
if (!this.input.disabled) {
this.input.disabled = true;
}

const TAG_NAME =
message.role === "user" ? CHAT_USER_MESSAGE_TAG : CHAT_MESSAGE_TAG;
Expand Down Expand Up @@ -356,31 +393,41 @@ class ChatContainer extends LightElement {
}

#onInputSuggestionClick(e: Event): void {
const suggestion = this.#getSuggestion(e.target);
const { suggestion, submit } = this.#getSuggestion(e.target);
if (!suggestion) return;

e.preventDefault();
this.input.setInputValue(suggestion);
this.input.setInputValue(suggestion, submit);
}

#onInputSuggestionKeydown(e: KeyboardEvent): void {
const isEnter = e.key === "Enter" || e.key === " ";
if (!isEnter) return;
const suggestion = this.#getSuggestion(e.target);
const { suggestion, submit } = this.#getSuggestion(e.target);
if (!suggestion) return;

e.preventDefault();
this.input.setInputValue(suggestion);
this.input.setInputValue(suggestion, submit);
}

#getSuggestion(x: EventTarget | null): string | null {
if (!(x instanceof HTMLElement)) return null;
#getSuggestion(x: EventTarget | null): {
suggestion?: string;
submit?: boolean;
} {
if (!(x instanceof HTMLElement)) return {};

const isSuggestion =
x.classList.contains("suggestion") || x.dataset.suggestion !== undefined;
if (!isSuggestion) return null;
if (!isSuggestion) return {};

return x.dataset.suggestion || x.textContent;
const suggestion = x.dataset.suggestion || x.textContent;

return {
suggestion: suggestion || undefined,
submit:
x.classList.contains("submit") ||
["", "true"].includes(x.dataset.suggestionSubmit || "false"),
};
}

#onRemoveLoadingMessage(): 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.

4 changes: 2 additions & 2 deletions shiny/www/py-shiny/chat/chat.css.map

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

Loading
Loading