{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "import panel as pn\n",
    "from panel.chat import ChatInterface\n",
    "\n",
    "pn.extension(\"perspective\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `ChatInterface` is a high-level layout, providing a user-friendly front-end interface for inputting different kinds of messages: text, images, PDFs, etc.\n",
    "\n",
    "This layout provides front-end methods to:\n",
    "\n",
    "- Input (append) messages to the chat log.\n",
    "- Re-run (resend) the most recent `user` input [`ChatMessage`](ChatMessage.ipynb).\n",
    "- Remove messages until the previous `user` input [`ChatMessage`](ChatMessage.ipynb).\n",
    "- Clear the chat log, erasing all [`ChatMessage`](ChatMessage.ipynb) objects.\n",
    "\n",
    "**Since `ChatInterface` inherits from [`ChatFeed`](ChatFeed.ipynb), it features all the capabilities of [`ChatFeed`](ChatFeed.ipynb); please see [ChatFeed.ipynb](ChatFeed.ipynb) for its backend capabilities.**\n",
    "\n",
    "Check out the [panel-chat-examples](https://holoviz-topics.github.io/panel-chat-examples/) docs to see applicable examples related to [LangChain](https://python.langchain.com/docs/get_started/introduction), [OpenAI](https://openai.com/blog/chatgpt), [Mistral](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwjZtP35yvSBAxU00wIHHerUDZAQFnoECBEQAQ&url=https%3A%2F%2Fdocs.mistral.ai%2F&usg=AOvVaw2qpx09O_zOzSksgjBKiJY_&opi=89978449), [Llama](https://ai.meta.com/llama/), etc. If you have an example to demo, we'd love to add it to the panel-chat-examples gallery!\n",
    "\n",
    "<img alt=\"Chat Design Specification\" src=\"../../assets/ChatDesignSpecification.png\"></img>\n",
    "\n",
    "#### Parameters:\n",
    "\n",
    "##### Core\n",
    "\n",
    "* **`widgets`** (`Widget | List[Widget]`): Widgets to use for the input. If not provided, defaults to `[TextInput]`.\n",
    "* **`user`** (`str`): Name of the ChatInterface user.\n",
    "* **`avatar`** (`str | bytes | BytesIO | pn.pane.Image`): The avatar to use for the user. Can be a single character text, an emoji, or anything supported by `pn.pane.Image`. If not set, uses the first character of the name.\n",
    "* **`adaptive`** (`bool`): Whether to allow interrupting and restarting the callback when new messages are sent while a callback is already running. When True, sending a new message will cancel the current callback and start a new one with the latest message. Default is False.\n",
    "* **`reset_on_send`** (`bool`): Whether to reset the widget's value after sending a message; has no effect for `TextInput`.\n",
    "* **`auto_send_types`** (`tuple`): The widget types to automatically send when the user presses enter or clicks away from the widget. If not provided, defaults to `[TextInput]`.\n",
    "* **`button_properties`** (`Dict[Dict[str, Any]]`): Allows addition of functionality or customization of buttons by supplying a mapping from the button name to a dictionary containing the `icon`, `callback`, `post_callback`, and/or `js_on_click` keys. \n",
    "  * If the button names correspond to default buttons\n",
    "(send, rerun, undo, clear), the default icon can be\n",
    "updated and if a `callback` key value pair is provided,\n",
    "the specified callback functionality runs before the existing one.\n",
    "  * For button names that don't match existing ones,\n",
    "new buttons are created and must include a\n",
    "`callback`, `post_callback`, and/or `js_on_click` key.\n",
    "  * The provided callbacks should have a signature that accepts\n",
    "two positional arguments: instance (the ChatInterface instance)\n",
    "and event (the button click event).\n",
    "  * The `js_on_click` key should be a str or dict. If str,\n",
    "provide the JavaScript code; else if dict, it must have a\n",
    "`code` key, containing the JavaScript code\n",
    "to execute when the button is clicked, and optionally an `args` key,\n",
    "containing dictionary of arguments to pass to the JavaScript\n",
    "code.\n",
    "\n",
    "##### Styling\n",
    "\n",
    "* **`show_send`** (`bool`): Whether to show the send button. Default is True.\n",
    "* **`show_stop`** (`bool`): Whether to show the stop button, temporarily replacing the send button during callback; has no effect if `callback` is not async.\n",
    "* **`show_rerun`** (`bool`): Whether to show the rerun button. Default is True.\n",
    "* **`show_undo`** (`bool`): Whether to show the undo button. Default is True.\n",
    "* **`show_clear`** (`bool`): Whether to show the clear button. Default is True.\n",
    "* **`show_button_name`** (`bool`): Whether to show the button name. Default is True.\n",
    "\n",
    "#### Properties:\n",
    "\n",
    "* **`active_widget`** (`Widget`): The currently active widget.\n",
    "* **`active`** (`int`): The currently active input widget tab index; -1 if there is only one widget available which is not in a tab.\n",
    "\n",
    "___"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Basics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ChatInterface()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although `ChatInterface` can be initialized without any arguments, it becomes much more useful, and interesting, with a `callback`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def even_or_odd(contents):\n",
    "    if len(contents) % 2 == 0:\n",
    "        return \"Even number of characters.\"\n",
    "    return \"Odd number of characters.\"\n",
    "\n",
    "ChatInterface(callback=even_or_odd)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You may also provide a more relevant, default `user` name and `avatar`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ChatInterface(\n",
    "    callback=even_or_odd,\n",
    "    user=\"Asker\",\n",
    "    avatar=\"?\",\n",
    "    callback_user=\"Counter\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Input Widgets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can also use a different type of widget for input, like `TextInput` instead of the default `ChatAreaInput`, by setting `widgets`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_chars(contents):\n",
    "    return f\"Found {len(contents)} characters.\"\n",
    "\n",
    "\n",
    "ChatInterface(\n",
    "    callback=count_chars,\n",
    "    widgets=pn.widgets.TextInput(\n",
    "        placeholder=\"Enter some text to get a count!\"\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Multiple `widgets` can be set, which will be nested under a `Tabs` layout."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_num(contents):\n",
    "    if isinstance(contents, str):\n",
    "        num = len(contents)\n",
    "    else:\n",
    "        num = contents\n",
    "    return f\"Got {num}.\"\n",
    "\n",
    "ChatInterface(\n",
    "    callback=get_num,\n",
    "    widgets=[\n",
    "        pn.chat.ChatAreaInput(placeholder=\"Enter some text to get a count!\"),\n",
    "        pn.widgets.IntSlider(name=\"Number Input\", end=10)\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Widgets other than `TextInput` and `ChatAreaInput` will require the user to manually click the `Send` button, unless the type is specified in `auto_send_types`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ChatInterface(\n",
    "    callback=get_num,\n",
    "    widgets=[\n",
    "        pn.chat.ChatAreaInput(placeholder=\"Enter some text to get a count!\"),\n",
    "        pn.widgets.IntSlider(name=\"Number Input\", end=10)\n",
    "    ],\n",
    "    auto_send_types=[pn.widgets.IntSlider],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you include a `FileInput` in the list of widgets you can enable the user to upload files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ChatInterface(widgets=pn.widgets.FileInput(name=\"CSV File\", accept=\".csv\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Try uploading a dataset! If you don't have a dataset in hand, download this sample dataset, [`penguins.csv`](https://datasets.holoviz.org/penguins/v1/penguins.csv)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note, if you don't like the default renderer, `pn.pane.DataFrame` for CSVs, you can specify `renderers` to use `pn.pane.Perspective`; just be sure you have the `\"perspective\"` extension added to `pn.extension(...)` at the top of your file!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ChatInterface(\n",
    "    widgets=pn.widgets.FileInput(name=\"CSV File\", accept=\".csv\"),\n",
    "    renderers=pn.pane.Perspective\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If a list is provided to `renderers`, will attempt to use the first renderer that does not raise an exception.\n",
    "\n",
    "In addition, you may render the input however you'd like with a custom renderer as long as the signature accepts one argument, namely `value`!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bad_renderer(value):\n",
    "    raise Exception(\"Won't render using this...\")\n",
    "\n",
    "def custom_renderer(value):\n",
    "    return pn.Column(\n",
    "        f\"Found {len(value)} rows in the CSV.\",\n",
    "        pn.pane.Perspective(value, height=600)\n",
    "    )\n",
    "\n",
    "ChatInterface(\n",
    "    widgets=pn.widgets.FileInput(name=\"CSV File\", accept=\".csv\"),\n",
    "    renderers=[bad_renderer, custom_renderer]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you'd like to guide the user into using one widget after another, you can set `active` in the callback."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def guided_get_num(contents, user, instance):\n",
    "    if isinstance(contents, str):\n",
    "        num = len(contents)\n",
    "        instance.active = 1  # change to IntSlider tab\n",
    "    else:\n",
    "        num = contents\n",
    "        instance.active = 0  # Change to TextAreaInput tab\n",
    "    return f\"Got {num}.\"\n",
    "\n",
    "pn.chat.ChatInterface(\n",
    "    callback=guided_get_num,\n",
    "    widgets=[\n",
    "        pn.chat.ChatAreaInput(placeholder=\"Enter some text to get a count!\"),\n",
    "        pn.widgets.IntSlider(name=\"Number Input\", end=10)\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or, simply initialize with a single widget first, then replace with another widget in the callback."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_num_guided(contents, user, instance):\n",
    "    if isinstance(contents, str):\n",
    "        num = len(contents)\n",
    "        instance.widgets = [widgets[1]]  # change to IntSlider\n",
    "    else:\n",
    "        num = contents\n",
    "        instance.widgets = [widgets[0]]  # Change to ChatAreaInput\n",
    "    return f\"Got {num}.\"\n",
    "\n",
    "\n",
    "widgets = [\n",
    "    pn.chat.ChatAreaInput(placeholder=\"Enter some text to get a count!\"),\n",
    "    pn.widgets.IntSlider(name=\"Number Input\", end=10)\n",
    "]\n",
    "pn.chat.ChatInterface(\n",
    "    callback=get_num_guided,\n",
    "    widgets=widgets[0],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The currently active widget can be accessed with the `active_widget` property."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "widgets = [\n",
    "    pn.chat.ChatAreaInput(placeholder=\"Enter some text to get a count!\"),\n",
    "    pn.widgets.IntSlider(name=\"Number Input\", end=10)\n",
    "]\n",
    "chat_interface = pn.chat.ChatInterface(\n",
    "    widgets=widgets,\n",
    ")\n",
    "print(chat_interface.active_widget)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sometimes, you may not want the widget to be reset after its contents has been sent.\n",
    "\n",
    "To have the widgets' `value` persist, set `reset_on_send=False`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.chat.ChatInterface(\n",
    "    widgets=pn.chat.ChatAreaInput(),\n",
    "    reset_on_send=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Buttons"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you're not using an LLM to respond, the `Rerun` button may not be practical so it can be hidden by setting `show_rerun=False`.\n",
    "\n",
    "The same can be done for other buttons as well with `show_send`, `show_undo`, and `show_clear`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.chat.ChatInterface(callback=get_num, show_rerun=False, show_undo=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you want a slimmer `ChatInterface`, use `show_button_name=False` to hide the labels of the buttons and/ or `width` to set the total width of the component."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.chat.ChatInterface(callback=get_num, show_button_name=False, width=400)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "New buttons with custom functionality can be added to the input row through `button_properties`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_notice(instance, event):\n",
    "    instance.send(\"This is how you add buttons!\", respond=False, user=\"System\")\n",
    "\n",
    "\n",
    "pn.chat.ChatInterface(\n",
    "    button_properties={\"help\": {\"callback\": show_notice, \"icon\": \"help\"}}\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Default buttons can also be updated with custom behaviors, before using `callback` and after using `post_callback`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_before(instance, event):\n",
    "    instance.send(\n",
    "        \"This will be cleared so it won't show after clear!\",\n",
    "        respond=False,\n",
    "        user=\"System\",\n",
    "    )\n",
    "\n",
    "\n",
    "def run_after(instance, event):\n",
    "    instance.send(\"This will show after clear!\", respond=False, user=\"System\")\n",
    "\n",
    "\n",
    "pn.chat.ChatInterface(\n",
    "    button_properties={\n",
    "        \"clear\": {\"callback\": run_before, \"post_callback\": run_after, \"icon\": \"help\"}\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You may also use custom Javascript code with `js_on_click` containing `code` and `args` keys for the buttons, and also set the `button_properties` after definition.\n",
    "\n",
    "Try typing something in the chat input, and then click the new `Help` button on the bottom right."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chat_interface = pn.chat.ChatInterface()\n",
    "chat_interface.button_properties = {\n",
    "    \"help\": {\n",
    "        \"icon\": \"help\",\n",
    "        \"js_on_click\": {\n",
    "            \"code\": \"alert(`Typed: '${chat_input.value}'`)\",\n",
    "            \"args\": {\"chat_input\": chat_interface.active_widget},\n",
    "        },\n",
    "    },\n",
    "}\n",
    "chat_interface"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Adaptive Mode\n",
    "\n",
    "When `adaptive=True` is set, the chat interface allows interrupting ongoing callback responses by sending new messages. This enables real-time adaptation and redirection of conversations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def adaptive_callback(message, user, instance):\n",
    "    # Simulate a long-running response\n",
    "    for i in range(10):\n",
    "        await asyncio.sleep(1)\n",
    "        instance.send(f\"Step {i+1}: Processing '{message}'...\", respond=False)\n",
    "    return f\"Completed processing: {message}\"\n",
    "\n",
    "ChatInterface(\n",
    "    callback=adaptive_callback,\n",
    "    adaptive=True,  # Allow interrupting responses\n",
    "    placeholder_text=\"Send follow-ups anytime to interrupt and redirect!\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With `adaptive=True`, users can:\n",
    "- Send clarifications while responses are being generated\n",
    "- Interrupt and redirect conversations in real-time  \n",
    "- Provide new requirements without waiting for completion\n",
    "\n",
    "Try sending a message, then immediately send another to see the first response get interrupted!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Check out the [panel-chat-examples](https://holoviz-topics.github.io/panel-chat-examples/) docs for more examples related to [LangChain](https://python.langchain.com/docs/get_started/introduction), [OpenAI](https://openai.com/blog/chatgpt), [Mistral](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwjZtP35yvSBAxU00wIHHerUDZAQFnoECBEQAQ&url=https%3A%2F%2Fdocs.mistral.ai%2F&usg=AOvVaw2qpx09O_zOzSksgjBKiJY_&opi=89978449), [Llama](https://ai.meta.com/llama/), etc.\n",
    "\n",
    "\n",
    "Also, since `ChatInterface` inherits from [`ChatFeed`](ChatFeed.ipynb), be sure to also read [ChatFeed.ipynb](ChatFeed.ipynb) to understand `ChatInterface`'s full potential!"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
