Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chat area input and integrate #6379

Merged
merged 14 commits into from
Mar 2, 2024
130 changes: 130 additions & 0 deletions examples/reference/chat/ChatAreaInput.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"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",
"\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",
"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",
"\n",
"<img alt=\"Chat Design Specification\" src=\"../../assets/ChatDesignSpecification.png\"></img>\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"##### Core\n",
"\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",
"* **``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",
"* **`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",
"* **`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",
"* **`resizable`** (boolean | str, default='both'): Whether the layout is interactively resizable, and if so in which dimensions: `width`, `height`, or `both`.\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Basics\n",
"\n",
"To submit a message, press the Enter key."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.chat.ChatAreaInput(placeholder=\"Type something, and press Enter to clear!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `ChatAreaInput` is useful alongside `pn.bind` or `param.depends`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def output(value):\n",
" return f\"Submitted: {value}\"\n",
"\n",
"chat_area_input = pn.chat.ChatAreaInput(placeholder=\"Type something, and press Enter to submit!\")\n",
"output_markdown = pn.bind(output, chat_area_input.param.value)\n",
"pn.Row(chat_area_input, output_markdown)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To see what's currently typed in, use `value_input` instead because `value` will be `\"\"` besides during submission."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"chat_area_input = pn.chat.ChatAreaInput(placeholder=\"Type something, leave it, and run the next cell\")\n",
"chat_area_input"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(chat_area_input.value_input, chat_area_input.value)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
3 changes: 2 additions & 1 deletion panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"""
from param import rx

from . import chat # noqa
from . import layout # noqa
from . import links # noqa
from . import pane # noqa
Expand All @@ -72,6 +71,8 @@
from .template import Template # noqa
from .widgets import indicators, widget # noqa

from . import chat # isort:skip noqa has to be after widgets

__all__ = (
"__version__",
"Accordion",
Expand Down
2 changes: 2 additions & 0 deletions panel/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from .feed import ChatFeed # noqa
from .icon import ChatReactionIcons # noqa
from .input import ChatAreaInput # noqa
from .interface import ChatInterface # noqa
from .message import ChatMessage # noqa

Expand All @@ -45,6 +46,7 @@ def __getattr__(name):
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

__all__ = (
"ChatAreaInput",
"ChatFeed",
"ChatInterface",
"ChatMessage",
Expand Down
90 changes: 90 additions & 0 deletions panel/chat/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

from typing import (
TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional, Type,
)

import param

from ..models.chatarea_input import (
ChatAreaInput as _bkChatAreaInput, ChatMessageEvent,
)
from ..widgets import TextAreaInput as _PnTextAreaInput

if TYPE_CHECKING:
from bokeh.document import Document
from pyviz_comms import Comm

from bokeh.model import Model


class ChatAreaInput(_PnTextAreaInput):
"""
The `ChatAreaInput` allows entering any multiline string using a text input
box, with the ability to click enter to submit the message.

Unlike TextAreaInput, the `ChatAreaInput` defaults to auto_grow=True and
max_rows=10, and the value is not synced to the server until the enter key
is pressed so key on `value_input` if you need to access the existing value.

Lines are joined with the newline character `\n`.

Reference: https://panel.holoviz.org/reference/widgets/ChatAreaInput.html
:Example:

>>> ChatAreaInput(max_rows=10)
"""

auto_grow = param.Boolean(
default=True,
doc="""
Whether the text area should automatically grow vertically to
accommodate the current text.""",
)

max_rows = param.Integer(
default=10,
doc="""
When combined with auto_grow this determines the maximum number
of rows the input area can grow.""",
)

resizable = param.ObjectSelector(
default="height",
objects=["both", "width", "height", False],
doc="""
Whether the layout is interactively resizable,
and if so in which dimensions: `width`, `height`, or `both`.
Can only be set during initialization.""",
)

_widget_type: ClassVar[Type[Model]] = _bkChatAreaInput

_rename: ClassVar[Mapping[str, str | None]] = {
"value": None,
**_PnTextAreaInput._rename,
}

def _get_properties(self, doc: Document) -> Dict[str, Any]:
props = super()._get_properties(doc)
props.update({"value_input": self.value, "value": self.value})
return props

def _get_model(
self,
doc: Document,
root: Optional[Model] = None,
parent: Optional[Model] = None,
comm: Optional[Comm] = None,
) -> Model:
model = super()._get_model(doc, root, parent, comm)
self._register_events("chat_message_event", model=model, doc=doc, comm=comm)
return model

def _process_event(self, event: ChatMessageEvent) -> None:
"""
Clear value on shift enter key down.
"""
self.value = event.value
with param.discard_events(self):
self.value = ""
12 changes: 8 additions & 4 deletions panel/chat/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ..widgets.button import Button
from ..widgets.input import FileInput, TextInput
from .feed import CallbackState, ChatFeed
from .input import ChatAreaInput
from .message import ChatMessage, _FileInputMessage


Expand Down Expand Up @@ -147,7 +148,7 @@ class ChatInterface(ChatFeed):
def __init__(self, *objects, **params):
widgets = params.get("widgets")
if widgets is None:
params["widgets"] = [TextInput(placeholder="Send a message")]
params["widgets"] = [ChatAreaInput(placeholder="Send a message")]
elif not isinstance(widgets, list):
params["widgets"] = [widgets]
active = params.pop("active", None)
Expand Down Expand Up @@ -267,7 +268,7 @@ def _init_widgets(self):
# TextAreaInput will trigger auto send!
auto_send = (
isinstance(widget, tuple(self.auto_send_types)) or
type(widget) is TextInput
type(widget) in (TextInput, ChatAreaInput)
)
if auto_send and widget in new_widgets:
callback = partial(self._button_data["send"].callback, self)
Expand Down Expand Up @@ -366,7 +367,9 @@ def _click_send(
return

active_widget = self.active_widget
value = active_widget.value
# value_input for ChatAreaInput because value is unsynced until "Enter",
# value for TextInput and others
value = active_widget.value or active_widget.value_input
if value:
if isinstance(active_widget, FileInput):
value = _FileInputMessage(
Expand All @@ -380,7 +383,8 @@ def _click_send(
if hasattr(active_widget, "value_input"):
updates["value_input"] = ""
try:
active_widget.param.update(updates)
with param.discard_events(self):
active_widget.param.update(updates)
except ValueError:
pass
else:
Expand Down
16 changes: 16 additions & 0 deletions panel/models/chatarea_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from bokeh.events import ModelEvent

from .widgets import TextAreaInput


class ChatMessageEvent(ModelEvent):

event_name = 'chat_message_event'

def __init__(self, model, value=None):
self.value = value
super().__init__(model=model)


class ChatAreaInput(TextAreaInput):
...
60 changes: 60 additions & 0 deletions panel/models/chatarea_input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { TextAreaInput as PnTextAreaInput, TextAreaInputView as PnTextAreaInputView } from "./textarea_input";
import * as p from "@bokehjs/core/properties";
import { ModelEvent } from "@bokehjs/core/bokeh_events"
import type {Attrs} from "@bokehjs/core/types"


export class ChatMessageEvent extends ModelEvent {
constructor(readonly value: string) {
super()
}

protected get event_values(): Attrs {
return {model: this.origin, value: this.value}
}

static {
this.prototype.event_name = "chat_message_event"
}
}

export class ChatAreaInputView extends PnTextAreaInputView {
model: ChatAreaInput;

render(): void {
super.render()

this.el.addEventListener("keydown", (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
this.model.trigger_event(new ChatMessageEvent(this.model.value_input))
this.model.value_input = ""
event.preventDefault();
}
});
}
}

export namespace ChatAreaInput {
export type Attrs = p.AttrsOf<Props>;
export type Props = PnTextAreaInput.Props & {
};
}

export interface ChatAreaInput extends ChatAreaInput.Attrs { }

export class ChatAreaInput extends PnTextAreaInput {
properties: ChatAreaInput.Props;

constructor(attrs?: Partial<ChatAreaInput.Attrs>) {
super(attrs);
}

static __module__ = "panel.models.chatarea_input";

static {
this.prototype.default_view = ChatAreaInputView;

this.define<ChatAreaInput.Props>(({ }) => ({
}));
}
}
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {ButtonIcon} from "./button_icon"
export {ClickableIcon} from "./icon"
export {Card} from "./card"
export {CheckboxButtonGroup} from "./checkbox_button_group"
export {ChatAreaInput} from "./chatarea_input"
export {Column} from "./column"
export {CommManager} from "./comm_manager"
export {CustomSelect} from "./customselect"
Expand Down
10 changes: 10 additions & 0 deletions panel/tests/chat/test_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from panel.chat.input import ChatAreaInput


class TestChatAreaInput:

def test_chat_area_input(self):
chat_area_input = ChatAreaInput()
assert chat_area_input.auto_grow
assert chat_area_input.max_rows == 10
assert chat_area_input.resizable == "height"
Loading
Loading