Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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. Alternatively, use Cmd/Ctrl + click to auto-submit any suggestion or Alt/Opt + click to apply any suggestion to the chat input without submitting.

* 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
70 changes: 51 additions & 19 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,29 @@ 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 {
// Store previous value to restore post-submit (if submitting)
const oldValue = this.textarea.value;

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(false);
if (oldValue) this.setInputValue(oldValue);
}

if (focus) {
this.textarea.focus();
}
}
}
Expand Down Expand Up @@ -383,31 +401,40 @@ 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);
#onInputSuggestionClick(e: MouseEvent): void {
this.#onInputSuggestionEvent(e);
}

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

this.#onInputSuggestionEvent(e);
}

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

e.preventDefault();
this.input.setInputValue(suggestion, submit);
// Cmd/Ctrl + (event) = force submitting
// Alt/Opt + (event) = force setting without submitting
const shouldSubmit =
e.metaKey || e.ctrlKey ? true : e.altKey ? false : submit;

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

#getSuggestion(x: EventTarget | null): {
Expand All @@ -416,17 +443,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
29 changes: 23 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,25 @@ 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. Requires `value`.
focus
Whether to move focus to the input element. Requires `value`.
"""

obj = _utils.drop_none({"value": value, "placeholder": placeholder})
if value is None and (submit or focus):
raise ValueError(
"An input `value` must be provided when `submit` or `focus` are `True`."
)

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