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",
"\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 == ""