diff --git a/examples/reference/chat/ChatAreaInput.ipynb b/examples/reference/chat/ChatAreaInput.ipynb index 027586e90a..5ea2880622 100644 --- a/examples/reference/chat/ChatAreaInput.ipynb +++ b/examples/reference/chat/ChatAreaInput.ipynb @@ -16,16 +16,17 @@ "metadata": {}, "source": [ "The `ChatAreaInput` inherits from `TextAreaInput`, allowing entering any multiline string using a text input\n", - "box, with the ability to click the \"Enter\" key to submit the message.\n", + "box, with the ability to click the \"Enter\" key or optionally the \"Ctrl-Enter\" key to submit the message.\n", + "\n", + "Whether \"Enter\" or \"Ctrl-Enter\" sends the message depends on whether the `enter_sends` parameter is set to True (the default) or False.\n", "\n", "Unlike TextAreaInput, the `ChatAreaInput` defaults to auto_grow=True and\n", - "max_rows=10, and the `value` is not synced to the server until the \"Enter\" key\n", - "is pressed so watch `value_input` if you need to access what's currently\n", + "max_rows=10, and the `value` is not synced to the server until the message is actually sent, so watch `value_input` if you need to access what's currently\n", "available in the text input box.\n", "\n", "Lines are joined with the newline character `\\n`.\n", "\n", - "It's primary purpose is use within the [`ChatInterface`](ChatInterface.ipynb) for a high-level, *easy to use*, *ChatGPT like* interface.\n", + "The primary purpose of `ChatAreaInput` is its use with [`ChatInterface`](ChatInterface.ipynb) for a high-level, *easy to use*, *ChatGPT like* interface.\n", "\n", "\"Chat\n", "\n", @@ -35,20 +36,21 @@ "\n", "##### Core\n", "\n", - "* **``disabled_enter``** (bool): If True, the enter key will not submit the message (clear the value).\n", - "* **``value``** (str): The value when the \"Enter\" key is pressed. Only to be used with `watch` or `bind` because the `value` resets to `\"\"` after the \"Enter\" key is pressed; use `value_input` instead to access what's currently available in the text input box.\n", + "* **``disabled_enter``** (bool): If True, disables sending the message by pressing the `enter_sends` key.\n", + "* **``enter_sends``** (bool): If True, pressing the Enter key sends the message, if False it is sent by pressing the Ctrl-Enter. Defaults to True.", + "* **``value``** (str): The value when the \"Enter\" or \"Ctrl-Enter\" key is pressed. Only to be used with `watch` or `bind` because the `value` resets to `\"\"` after the message is sent; use `value_input` instead to access what's currently available in the text input box.\n", "* **``value_input``** (str): The current value updated on every key press.\n", "\n", "##### Display\n", "\n", "* **`auto_grow`** (boolean, default=True): Whether the TextArea should automatically grow in height to fit the content.\n", - "* **`cols`** (int, default=2): The number of columns in the text input field. \n", + "* **`cols`** (int, default=2): The number of columns in the text input field.\n", "* **`disabled`** (boolean, default=False): Whether the widget is editable\n", "* **`max_length`** (int, default=5000): Max character length of the input field. Defaults to 5000\n", - "* **`max_rows`** (int, default=10): The maximum number of rows in the text input field when `auto_grow=True`. \n", + "* **`max_rows`** (int, default=10): The maximum number of rows in the text input field when `auto_grow=True`.\n", "* **`name`** (str): The title of the widget\n", "* **`placeholder`** (str): A placeholder string displayed when no value is entered\n", - "* **`rows`** (int, default=2): The number of rows in the text input field. \n", + "* **`rows`** (int, default=2): The number of rows in the text input field.\n", "* **`resizable`** (boolean | str, default='both'): Whether the layout is interactively resizable, and if so in which dimensions: `width`, `height`, or `both`.\n", "\n", "___" @@ -60,7 +62,7 @@ "source": [ "#### Basics\n", "\n", - "To submit a message, press the Enter key." + "To submit a message, press the \"Enter\" key if **``enter_sends``** is True (the default), else press \"Ctrl-Enter\"." ] }, { @@ -97,7 +99,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To see what's currently typed in, use `value_input` instead because `value` will be `\"\"` besides during submission." + "To see what's currently typed in, use `value_input` instead because `value` will only be set during submission and be `\"\"` otherwise." ] }, { @@ -106,8 +108,11 @@ "metadata": {}, "outputs": [], "source": [ - "chat_area_input = pn.chat.ChatAreaInput(placeholder=\"Type something, leave it, and run the next cell\")\n", - "chat_area_input" + "chat_area_input = pn.chat.ChatAreaInput(enter_sends=False, # To submit a message, press Ctrl-Enter\n", + " placeholder=\"Type something, do not submit it, and run the next cell\",)\n", + "output_markdown = pn.bind(output, chat_area_input.param.value)\n", + "\n", + "pn.Row(chat_area_input, output_markdown)" ] }, { @@ -116,7 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(chat_area_input.value_input, chat_area_input.value)" + "print(f'{chat_area_input.value_input=}, {chat_area_input.value=}')" ] } ], diff --git a/panel/chat/input.py b/panel/chat/input.py index 7843a5b9f3..1497125f4d 100644 --- a/panel/chat/input.py +++ b/panel/chat/input.py @@ -45,7 +45,12 @@ class ChatAreaInput(_PnTextAreaInput): disabled_enter = param.Boolean( default=False, - doc="If True, the enter key will not submit the message (clear the value).", + doc="If True, disables sending the message by pressing the `enter_sends` key.", + ) + + enter_sends = param.Boolean( + default=True, + doc="If True, pressing the Enter key sends the message, if False it is sent by pressing the Ctrl+Enter.", ) rows = param.Integer(default=1, doc=""" diff --git a/panel/models/chatarea_input.py b/panel/models/chatarea_input.py index 116741f5d6..c469622c38 100644 --- a/panel/models/chatarea_input.py +++ b/panel/models/chatarea_input.py @@ -16,4 +16,7 @@ def __init__(self, model, value=None): class ChatAreaInput(TextAreaInput): disabled_enter = Bool(default=False, help=""" - If True, the enter key will not submit the message (clear the value).""") + If True, disables sending the message by pressing the `enter_sends` key.""") + + enter_sends = Bool(default=True, help=""" + If True, pressing the Enter key sends the message, if False it is sent by pressing Ctrl+Enter""") diff --git a/panel/models/chatarea_input.ts b/panel/models/chatarea_input.ts index 12d3e0b48f..46fb86870c 100644 --- a/panel/models/chatarea_input.ts +++ b/panel/models/chatarea_input.ts @@ -31,12 +31,17 @@ export class ChatAreaInputView extends PnTextAreaInputView { super.render() this.el.addEventListener("keydown", (event) => { - if (event.key === "Enter" && !event.shiftKey) { - if (!this.model.disabled_enter) { - this.model.trigger_event(new ChatMessageEvent(this.model.value_input)) - this.model.value_input = "" + if (event.key === "Enter") { + if (!event.shiftKey && (event.ctrlKey != this.model.enter_sends)) { + if (!this.model.disabled_enter) { + this.model.trigger_event(new ChatMessageEvent(this.model.value_input)) + this.model.value_input = "" + } + event.preventDefault() + } else if (event.ctrlKey && this.model.enter_sends) { + this.model.value_input += "\n" + event.preventDefault() } - event.preventDefault() } }) } @@ -46,6 +51,7 @@ export namespace ChatAreaInput { export type Attrs = p.AttrsOf export type Props = PnTextAreaInput.Props & { disabled_enter: p.Property + enter_sends: p.Property } } @@ -65,6 +71,7 @@ export class ChatAreaInput extends PnTextAreaInput { this.define(({Bool}) => { return { disabled_enter: [ Bool, false ], + enter_sends: [ Bool, true ], } }) } diff --git a/panel/tests/ui/chat/test_input_ui.py b/panel/tests/ui/chat/test_input_ui.py index 6b3001fdf9..bc570e1c1c 100644 --- a/panel/tests/ui/chat/test_input_ui.py +++ b/panel/tests/ui/chat/test_input_ui.py @@ -54,6 +54,7 @@ def test_chat_area_input_resets_to_row(page): # find chat area input and type a message textbox = page.locator(".bk-input") textbox.fill("Hello World!") + # click shift_enter 3 times textbox.press("Shift+Enter") textbox.press("Shift+Enter") @@ -69,3 +70,41 @@ def test_chat_area_input_resets_to_row(page): assert chat_area_input.value == "" textbox_rows = textbox.evaluate("el => el.rows") assert textbox_rows == 2 + + +def test_chat_area_enter_sends(page): + + chat_area_input = ChatAreaInput() + serve_component(page, chat_area_input) + + # find chat area input + textbox = page.locator(".bk-input") + + # Case enter_sends is True + chat_area_input.enter_sends = True + textbox.fill("enter_sends: True") + wait_until(lambda: chat_area_input.value_input == "enter_sends: True", page) + textbox.press("Shift+Enter") + wait_until(lambda: chat_area_input.value_input == "enter_sends: True\n", page) + textbox.press("Control+Enter") + wait_until(lambda: chat_area_input.value_input == "enter_sends: True\n\n", page) + textbox_rows = textbox.evaluate("el => el.rows") + assert textbox_rows == 3 + + textbox.press("Enter") + wait_until(lambda: chat_area_input.value_input == "", page) + assert chat_area_input.value == "" + + # Case enter_sends is False + chat_area_input.enter_sends = False + textbox.fill("enter_sends: False") + wait_until(lambda: chat_area_input.value_input == "enter_sends: False", page) + textbox.press("Enter") + wait_until(lambda: chat_area_input.value_input == "enter_sends: False\n", page) + textbox.press("Shift+Enter") + wait_until(lambda: chat_area_input.value_input == "enter_sends: False\n\n", page) + textbox_rows = textbox.evaluate("el => el.rows") + assert textbox_rows == 3 + textbox.press("Control+Enter") + wait_until(lambda: chat_area_input.value_input == "", page) + assert chat_area_input.value == ""