Creating a Website Chat Widget with Gradio

I’m pretty new to Python, and I’ve been playing with the gradio chat windows. I can run them locally no problem.

But I want to use the chat widget for gradio as detailed here:

https://www.gradio.app/guides/creating-a-website-widget-from-a-gradio-chatbot

Here is a very basic version of code that I’m using (whatever the users says it just replies with “hello”), I’m just trying to test functionality -

import gradio as gr

def chat(message, history):

*return* f"Hello"

gr.ChatInterface(fn=chat, type=“messages”).launch()

It works fine locally, and if I upload the code to spaces it also works fine there. The problem is when I try to call it from the widget it doesn’t work and in the devtools pane it says:

Hello

contact:74 Error: TypeError: result.data[0].join is not a function
at sendMessage (contact:71:51)

It receives “hello” so that shows it is correctly communicating with the gradio chat app on spaces, but the error suggests that this response it has received is not in the correct format (it is getting a string but seems to expect an array of strings or some kind of structured data)

I keep going round in circles trying to fix this, does any one have some basic python code that actually works with this widget?

1 Like

Gradio is a GUI that runs on a combination of Python and JavaScript, so if you’re building the JavaScript part yourself, you need to be careful to ensure it communicates properly with the Python side. I don’t use site embedding, so I don’t have much to do with it…


Below is a small, self-contained Gradio app that actually works with the official “Creating a Website Widget from a Gradio Chatbot” widget JavaScript. I will first give the code, then walk through it slowly and clearly so you can see why it works and how it connects to the widget.


1. The basic working app.py

This version is written to match exactly what the Gradio widget guide expects:

  • It uses gr.ChatInterface.
  • It uses multimodal=True, so the first argument is a {"text": ..., "files": [...]} dict, just like in the docs. (Gradio)
  • It returns a list of strings, so the widget JavaScript can safely do result.data[0].join('\n'), as shown in the guide. (Gradio)
# app.py
# Minimal Gradio chatbot that works with the official website widget example.

import gradio as gr


def chat(message, history):
    """
    This function is called every time the user sends a message
    from the widget (or the Gradio UI).

    Parameters
    ----------
    message : dict
        Because we set `multimodal=True` on ChatInterface, Gradio passes
        a dictionary here with the following shape:
          {
              "text":  "<user message as a string>",
              "files": ["/path/to/file1", "/path/to/file2", ...]
          }
        The widget example sends exactly this shape.

    history : list
        A list representing the chat history.
        We don't need it for this simple example, but it must be accepted.

    Returns
    -------
    list[str]
        A list of strings. The JavaScript widget receives this list
        as `result.data[0]` and then calls `.join('\\n')` on it.
        So returning a list is critical.
    """
    # Safely get the text the user typed
    if isinstance(message, dict):
        user_text = message.get("text", "")
    else:
        # Fallback, in case message is not a dict for some reason
        user_text = str(message)

    # Build a list of strings that will become the bot's reply.
    # Each item in this list will become one "line" in the widget.
    response_lines = [
        "Hello from your Gradio Space!",
        f"You said: {user_text}",
        "",
        "This response is a *list of strings*,",
        "so the website widget's `result.data[0].join('\\n')` works.",
    ]

    return response_lines


# Create the chatbot interface.
# - type="messages"   → history is handled in messages format (as in docs)
# - multimodal=True   → input is a dict {text, files}
# - api_name="chat"   → HTTP endpoint is /chat (the widget calls this)
demo = gr.ChatInterface(
    fn=chat,
    type="messages",
    multimodal=True,
    title="Widget Demo Bot",
    api_name="chat",   # this matches the "/chat" path used in the JS guide
)


if __name__ == "__main__":
    # Run this locally with:  python app.py
    demo.launch()

This file alone is enough to:

  • run locally in a Gradio browser window, and
  • work correctly when called from the official widget JavaScript in the guide. (Gradio)

2. How this ties into the official widget guide

The Gradio guide “Creating a Website Widget from a Gradio Chatbot” shows JavaScript like this (simplified): (Gradio)

const result = await client.predict("/chat", {
  message: { text: userMessage, files: [] },
});

const botMessage = result.data[0].join('\n');
appendMessage(botMessage, 'bot');

There are three very important expectations here:

  1. Endpoint name ("/chat")

    • The widget calls client.predict("/chat", ...).
    • Our Python ChatInterface sets api_name="chat", which exposes that function at /chat.
    • That matches exactly what the widget uses.
  2. Input format (multimodal dict)

    • The widget sends message: { text: userMessage, files: [] }.
    • In Gradio 5, when you use multimodal=True on ChatInterface, the first parameter to your function is a dict like that:
      {"text": <str>, "files": <list-of-paths>}. (Gradio)
    • That is why the function parameter is message, and inside we do message.get("text", "").
  3. Output format (list of strings)

    • The widget does result.data[0].join('\n'). That only works if result.data[0] is an array/list, not a single string.
    • Whatever your Python function returns becomes result.data[0] in JS (for the first output). (Gradio)
    • So we return response_lines, a list[str] in Python, which becomes an array of strings in JavaScript.
    • Now .join('\n') succeeds, and you do not get TypeError: result.data[0].join is not a function.

In your original code, you did:

def chat(message, history):
    return "Hello"  # a plain string

That is perfectly valid for the Gradio UI, but then:

  • result.data[0] is "Hello" (a string),
  • result.data[0].join('\n') throws a TypeError,
  • and you see the exact error you described.

The only real change you need is: return a list of strings instead of a plain string.


3. How to run this locally (step-by-step)

  1. Create a virtual environment (optional but recommended):

    python -m venv venv
    source venv/bin/activate  # Linux/macOS
    # or:
    venv\Scripts\activate     # Windows
    
  2. Install Gradio:

    pip install "gradio>=5.9,<6.0"
    

    The >=5.9,<6.0 range keeps you on Gradio 5, which is what the current website widget docs describe. (Gradio)

  3. Save the app.py from section 1 in the same directory.

  4. Run:

    python app.py
    
  5. Gradio prints a local URL like http://127.0.0.1:7860. Open it in your browser.

    • You should see the chatbot UI.
    • When you type a message, it replies with multiple lines (the list we returned).

At this point, you know that:

  • The Python code runs.
  • The Gradio UI works.
  • The function receives a dict with "text" and "files", and returns a list of strings.

4. How to deploy it as a Hugging Face Space

The widget guide is written around using a public Space, like abidlabs/gradio-playground-bot. (Gradio)
You can use your own Space instead.

4.1 Required files

Create two files:

  1. app.py – the code above.

  2. requirements.txt – to tell Spaces which packages to install:

    gradio>=5.9,<6.0
    

Nothing else is strictly required for this minimal example.

4.2 Steps on Hugging Face

  1. Go to Hugging Face → “New Space” → choose Gradio template.
  2. Name it something like your-username/website-widget-test.
  3. Upload app.py and requirements.txt.
  4. Wait for the build to finish.
  5. Once it is running, visit the Space and confirm the chatbot works.

The Space URL will look like:

https://your-username-website-widget-test.hf.space

You will use this URL inside your website’s JavaScript.


5. How to hook it up to your website widget

The official guide gives you HTML/CSS/JS for a small floating chat window. The key part is the JavaScript that connects to your Space. (Gradio)

A minimal version looks like this:

<script type="module">
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";

  async function initChatWidget() {
    // 1. Connect to YOUR Space
    const client = await Client.connect("https://your-username-website-widget-test.hf.space");

    // 2. Grab DOM elements (buttons, input, container, etc.)
    const chatToggle = document.getElementById("chat-toggle");
    const chatContainer = document.getElementById("chat-container");
    const closeChat = document.getElementById("close-chat");
    const chatInput = document.getElementById("chat-input");
    const sendButton = document.getElementById("send-message");
    const messagesContainer = document.getElementById("chat-messages");

    chatToggle.addEventListener("click", () => {
      chatContainer.classList.remove("hidden");
    });

    closeChat.addEventListener("click", () => {
      chatContainer.classList.add("hidden");
    });

    function appendMessage(text, sender) {
      const div = document.createElement("div");
      div.className = `message ${sender}-message`;
      div.textContent = text;
      messagesContainer.appendChild(div);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    async function sendMessage() {
      const userMessage = chatInput.value.trim();
      if (!userMessage) return;

      appendMessage(userMessage, "user");
      chatInput.value = "";

      try {
        // 3. Call your /chat endpoint with a multimodal message
        const result = await client.predict("/chat", {
          message: { text: userMessage, files: [] },
        });

        // 4. Get the bot reply: this is an array of strings
        console.log("raw data[0]:", result.data[0]);
        const botMessage = result.data[0].join("\n");
        appendMessage(botMessage, "bot");
      } catch (error) {
        console.error("Error from widget:", error);
        appendMessage("Sorry, there was an error.", "bot");
      }
    }

    sendButton.addEventListener("click", sendMessage);
    chatInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter") sendMessage();
    });
  }

  initChatWidget();
</script>

Because your Python code:

  • uses multimodal=True
  • returns a list[str]

the call result.data[0].join("\n") now works exactly as in the official Gradio guide.


6. Mental model: data shapes (beginner-safe summary)

To keep it conceptually clear:

  • Think of the Gradio app as a function you expose over HTTP.

  • For each prediction, Gradio sends your function:

    • the new user message (message),
    • the existing chat history (history). (Gradio)
  • Your function returns something (string, list of strings, message dicts, etc.).

When you call that app from JavaScript using @gradio/client:

  • result.data[0] is exactly whatever you returned from Python (first output), but in JavaScript form. (Gradio)

So:

  • If you return "Hello"result.data[0] is a string → .join() fails.
  • If you return ["Hello"]result.data[0] is an array → .join() works.

The widget JS in the guide was written for the playground bot Space, whose backend returns a list of strings. (Hugging Face)
Your original code returned just a string, which is why you saw join is not a function.

The app.py given at the top simply makes your Space behave like the example Space regarding input and output shapes, so the widget code “just works”.

1 Like

Hi, thanks for that very comprehensive answer, your explanation made it all make a lot more sense.

I used the code you provided, and it works fine as an app on HF space, and it has corrected the initial error with the widget. But then the widget gave a new error:

It seems the java code in the widget was referencing “marked” but had not called the library. I inserted a call to the library in the widget code, and now it all seems to work as expected (now I just need to write the AI part of the chat bot, but that should be the easy bit!)

Bit weird that the published widget code had an error in it though, unless I’m missing something.

1 Like

Oh, maybe you’re right.


You didn’t miss anything – your diagnosis is exactly right.

1. What that new error means

The error in your screenshot:

ReferenceError: marked is not defined

means:

  • JavaScript sees marked being used somewhere,
  • but there is no variable named marked in the current page (no import, no <script> that defines it). (Stack Overflow)

In the Gradio widget guide, the relevant lines are:

function appendMessage(text, sender) {
    const messageDiv = document.createElement('div');
    messageDiv.className = `message ${sender}-message`;

    if (sender === 'bot') {
        messageDiv.innerHTML = marked.parse(text);   // <-- uses `marked`
    } else {
        messageDiv.textContent = text;
    }
    messagesContainer.appendChild(messageDiv);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
}

from the official page. (gradio.app)

So the docs’ JS assumes a global marked object exists, but the snippet never shows how to load it. On the Gradio docs site they already load marked (they use it for their own Markdown rendering in chat components (Zenn)), so the example works there. When you copy it into your own page, that global is missing, so you get the ReferenceError.

Your fix – adding the marked library yourself – is exactly what’s needed.


2. What marked is and why they’re using it

marked is a small JavaScript library that converts Markdown text → HTML. It’s very common in chat/markdown widgets. (GitHub)

  • Your bot’s reply is plain text with Markdown (e.g. **bold**, code blocks).
  • marked.parse(text) converts that Markdown string into an HTML string.
  • They then set messageDiv.innerHTML = ... so the chat bubble renders bold, code blocks, links, etc.

So the intention is:

  • User messagestextContent (shown literally, no HTML parsing).
  • Bot messages → run through marked.parse and then put in innerHTML so Markdown is rendered nicely.

That’s why marked is referenced in the snippet at all.


3. How to include marked cleanly (the simple way)

For the exact code in the docs (which expects a global marked), the easiest fix is:

<!-- Load marked first (non-module script, defines global `marked`) -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

<!-- Then your widget script from the guide -->
<script type="module">
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";

  async function initChatWidget() {
    // ... everything from the guide, including:
    // messageDiv.innerHTML = marked.parse(text);
  }

  initChatWidget();
</script>

Key points:

  • Use the non-module <script src="...marked.min.js"></script> form so it adds a global marked variable, as shown in the official marked docs. (marked.js.org)
  • Load it before the <script type="module"> that uses marked, so it exists by the time your widget code runs.

That’s essentially what you did – and that’s why it now works.


4. Alternative: drop Markdown support for now (even simpler)

If you don’t care about fancy Markdown rendering yet, you can also just remove marked from the equation entirely.

Change the appendMessage function to:

function appendMessage(text, sender) {
  const messageDiv = document.createElement('div');
  messageDiv.className = `message ${sender}-message`;

  // Just always treat as plain text
  messageDiv.textContent = text;

  messagesContainer.appendChild(messageDiv);
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
}

Then you don’t need marked at all; both user and bot messages are displayed as straight text.

Later, when your AI starts returning Markdown (e.g. lists, code blocks), you can reintroduce marked to make it look nicer.


5. Why the published widget code “has an error”

Short version: the docs snippet is environment-dependent and they forgot to show that dependency.

  • On gradio.app, they already load marked globally because gr.Chatbot and gr.Markdown components now use it in the frontend for Markdown parsing. (Zenn)
  • The widget guide reuses that global marked without re-declaring it, so it appears to work fine on their own site.
  • When you copy the snippet into a clean page that doesn’t load marked first, JavaScript quite reasonably says: “ReferenceError: marked is not defined.”

So you are not missing anything; it’s just a small docs oversight. If you feel like it, you could even open a quick issue on the Gradio GitHub repo suggesting they add the marked <script> tag to the guide. (GitHub)


6. Where you are now

You’ve already:

  1. Fixed the return type from Python (list[str] instead of str), so .join('\n') works.
  2. Added the marked library, so Markdown rendering works and the ReferenceError is gone.

At this point, the plumbing – browser ⇄ widget ⇄ Gradio Space – is in good shape, and you can focus on the “actual AI” part: plugging in a model and gradually upgrading that simple chat function.

1 Like

This topic was automatically closed 12 hours after the last reply. New replies are no longer allowed.