Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
different ways (see #1845 for more details):
* By adding a `.suggestion` CSS class to an HTML element (e.g., `<span class="suggestion">A suggestion</span>`)
* Add a `data-suggestion` attribute to an HTML element, and set the value to the input suggestion text (e.g., `<span data-suggestion="Suggestion value">Suggestion link</span>`)
* To auto-submit the suggestion when clicked by the user, include the `.submit` class or the `data-suggestion-submit="true"` attribute on the HTML element.

* Added a new `.add_sass_layer_file()` method to `ui.Theme` that supports reading a Sass file with layer boundary comments, e.g. `/*-- scss:defaults --*/`. This format [is supported by Quarto](https://quarto.org/docs/output-formats/html-themes-more.html#bootstrap-bootswatch-layering) and makes it easier to store Sass rules and declarations that need to be woven into Shiny's Sass Bootstrap files. (#1790)

Expand All @@ -26,6 +27,8 @@ different ways (see #1845 for more details):

* Available `input` ids can now be listed via `dir(input)`. This also works on the new `session.clientdata` object. (#1832)

* The `ui.Chat()` component's `.update_user_input()` method gains `submit` and `focus` options that allow you to submit the input on behalf of the user and to choose whether the input receives focus after the update. (#1851)

### Bug fixes

* `ui.Chat()` now correctly handles new `ollama.chat()` return value introduced in `ollama` v0.4. (#1787)
Expand Down
50 changes: 34 additions & 16 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type ShinyChatMessage = {
type UpdateUserInput = {
value?: string;
placeholder?: string;
submit?: false;
focus?: false;
};

// https://github.com/microsoft/TypeScript/issues/28357#issuecomment-748550734
Expand Down Expand Up @@ -113,6 +115,11 @@ class ChatMessages extends LightElement {
}
}

interface ChatInputSetInputOptions {
submit?: boolean;
focus?: boolean;
}

class ChatInput extends LightElement {
private _disabled = false;

Expand Down Expand Up @@ -208,7 +215,7 @@ class ChatInput extends LightElement {
this.#onInput();
}

#sendInput(): void {
#sendInput(focus = true): void {
if (this.valueIsEmpty) return;
if (this.disabled) return;

Expand All @@ -225,18 +232,23 @@ class ChatInput extends LightElement {
this.setInputValue("");
this.disabled = true;

this.textarea.focus();
if (focus) this.textarea.focus();
}

setInputValue(value: string, submit = false): void {
setInputValue(
value: string,
{ submit = false, focus = false }: ChatInputSetInputOptions = {}
): void {
this.textarea.value = value;

// 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();
this.#sendInput(focus);
} else if (focus) {
this.textarea.focus();
}
}
}
Expand Down Expand Up @@ -383,31 +395,32 @@ class ChatContainer extends LightElement {
}

#onUpdateUserInput(event: CustomEvent<UpdateUserInput>): void {
const { value, placeholder } = event.detail;
const { value, placeholder, submit, focus } = event.detail;
if (value !== undefined) {
this.input.setInputValue(value);
this.input.setInputValue(value, { submit, focus });
}
if (placeholder !== undefined) {
this.input.placeholder = placeholder;
}
}

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

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

#onInputSuggestionKeydown(e: KeyboardEvent): void {
const isEnter = e.key === "Enter" || e.key === " ";
if (!isEnter) return;

this.#applySuggestion(e);
}

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

e.preventDefault();
this.input.setInputValue(suggestion, submit);
this.input.setInputValue(suggestion, { submit, focus: !submit });
}

#getSuggestion(x: EventTarget | null): {
Expand All @@ -416,17 +429,22 @@ class ChatContainer extends LightElement {
} {
if (!(x instanceof HTMLElement)) return {};

const el = x.closest(".suggestion, [data-suggestion]");
if (!(el instanceof HTMLElement)) return {};

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

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

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

Expand Down
24 changes: 18 additions & 6 deletions shiny/ui/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,6 @@ async def _transform_message(
chunk: ChunkOption = False,
chunk_content: str | None = None,
) -> TransformedMessage | None:

res = as_transformed_message(message)
key = res["transform_key"]

Expand Down Expand Up @@ -791,7 +790,6 @@ def _store_message(
chunk: ChunkOption = False,
index: int | None = None,
) -> None:

# Don't actually store chunks until the end
if chunk is True or chunk == "start":
return None
Expand All @@ -817,7 +815,6 @@ def _trim_messages(
token_limits: tuple[int, int],
format: MISSING_TYPE | ProviderMessageFormat,
) -> tuple[TransformedMessage, ...]:

n_total, n_reserve = token_limits
if n_total <= n_reserve:
raise ValueError(
Expand Down Expand Up @@ -878,7 +875,6 @@ def _trim_anthropic_messages(
self,
messages: tuple[TransformedMessage, ...],
) -> tuple[TransformedMessage, ...]:

if any(m["role"] == "system" for m in messages):
raise ValueError(
"Anthropic requires a system prompt to be specified in it's `.create()` method "
Expand Down Expand Up @@ -938,7 +934,12 @@ def _user_input(self) -> str:
return cast(str, self._session.input[id]())

def update_user_input(
self, *, value: str | None = None, placeholder: str | None = None
self,
*,
value: str | None = None,
placeholder: str | None = None,
submit: bool = False,
focus: bool = False,
):
"""
Update the user input.
Expand All @@ -949,9 +950,20 @@ def update_user_input(
The value to set the user input to.
placeholder
The placeholder text for the user input.
submit
Whether to automatically submit the text for the user.
focus
Whether to move focus to the input element.
"""

obj = _utils.drop_none({"value": value, "placeholder": placeholder})
obj = _utils.drop_none(
{
"value": value,
"placeholder": placeholder,
"submit": submit,
"focus": focus,
}
)

_utils.run_coro_sync(
self._session.send_custom_message(
Expand Down
Loading
Loading