Creating a Website Chat Widget with Gradio Part III

Firstly, thanks John for your detailed answer in Part II - I was busy with the chat code all day and the forum auto closed the thread!

Going back to the chat logging - yes, a good idea to write for each prompt/response pair to /data.

So the place to do that would be in the main chat function. The problem is that if we just write it out to one log file, then if multiple people are chatting at the same time the log will be a huge unreadable mess as you won’t know which user is saying what.

So we need some kind of unique identifier per chat session, which is tricky as in the python code because there is only “one code” but the app runs multiple instances (I assume).

So I’ve been trying to use uuid4 to create a unique user_id if none already exists:

if user_id is None:    
   user_id = str(uuid.uuid4()) 

But of course the chat function just goes back to gr.chatinterface, which then passes back to the chat function on user input, so we need some way of maintaining the user_id otherwise it just gets reset to none each time chat is called and creates another unique ID… I’ve tried doing this using gr.state() but I’m not really sure how that works without breaking the widget?

1 Like

gr.State is convenient when everything can be handled solely within the Gradio UI, but when that’s not the case, managing data yourself is ultimately simpler.

Even for functions called directly from ChatInterface, it’s possible to add more arguments. Therefore, the basic approach should be to add arguments and exchange IDs between the frontend and backend.


You’re bumping into a very real, very common backend problem:
“How do I log every message, but still know which lines belong to which user/session, when everything goes through one shared chat() function?”

I’ll walk through the idea carefully and tie it to your current setup:

  1. Why one log file is okay (as long as you add a session id).

  2. Why gr.State feels confusing with your custom widget.

  3. A simple, practical pattern that works well with your widget:

    • Generate a conversation_id in JavaScript
    • Send it to Python on every request
    • Log it with each turn
  4. How this compares to the “Gradio-native” gr.State pattern, so your intuition about it makes sense.

Throughout I’ll relate to what Gradio supports officially (ChatInterface, session state, JS client) so you can see you’re not fighting the framework. (Gradio)


1. One log file is fine if every line has a conversation_id

Background:

  • Your app.py runs inside one Gradio demo (or a small pool), but it serves many users in parallel.
  • Gradio calls your chat(message, history, ...) function once for each incoming message. ((note Subtitles))
  • If you log each turn to /data/chat_logs.jsonl, you’ll indeed get messages from different users interleaved in time.

That interleaving is normal. The way to make it not “a huge unreadable mess” is to include conversation_id on every log line:

{"timestamp": "...", "conversation_id": "abc123", "user_text": "Hi", ...}
{"timestamp": "...", "conversation_id": "def456", "user_text": "Hello", ...}
{"timestamp": "...", "conversation_id": "abc123", "user_text": "Tell me more", ...}

Later, you can:

  • filter or group by conversation_id in any tool (Python, pandas, jq, etc.), and
  • reconstruct each conversation independently.

So the core requirement is not “multiple log files” but “a stable id for each browser/chat session”.


2. Why gr.State feels tricky in your setup

Gradio has session state that persists across submits within one browser tab. (Gradio)

  • With the built-in UI (Gradio page), you can attach a gr.State to a Chatbot and store a UUID there.
  • The official “Chatbot Specific Events” guide shows exactly this: they store a uuid per chat session and reuse it in the handler. (Gradio)

That example looks roughly like:

from uuid import uuid4
import gradio as gr

def clear():
    return uuid4()

def chat_fn(message, history, uuid):
    # use uuid here
    ...

with gr.Blocks() as demo:
    uuid_state = gr.State(uuid4)
    chatbot = gr.Chatbot(type="messages")
    chatbot.clear(clear, outputs=[uuid_state])

    gr.ChatInterface(
        chat_fn,
        chatbot=chatbot,
        additional_inputs=[uuid_state],
        type="messages",
    )

Here:

  • uuid_state is per-session; each browser/tab gets its own UUID.
  • Gradio’s built-in UI handles the wiring: when the user sends a message, it calls chat_fn(message, history, uuid_state_value) automatically. (Gradio)

In your situation:

  • You are not using the built-in UI.

  • Your front-end is your own HTML + JS widget that calls the Space via @gradio/client:

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

When you add gr.State or extra inputs in Python, your function signature changes:

def chat(message, history, uuid):
    ...

and Gradio’s /chat endpoint now expects that extra argument. (Gradio)

For the built-in UI, Gradio injects that for you.
For your widget, you must explicitly send it in the JS payload; otherwise the arguments don’t match and you get errors (this is exactly what people hit in GitHub issues when they see “predict() got an unexpected keyword argument X” or payload length mismatches). (GitHub)

So:

  • gr.State is powerful, but you need to carefully mirror whatever extra inputs your Python function wants on the JS side.
  • Since you already control the JS payload, it’s actually simpler to let JS generate the conversation_id and send it in directly.

That’s why gr.State felt confusing: it’s Gradio’s way to persist things inside the Gradio UI, but you’ve now brought your own UI.


3. Simple, robust pattern for your widget: generate UUID in JS, log it in Python

You were already thinking along these lines with uuid4 in Python. The missing piece is: the stable value should live on the client (browser), and be passed into Python on every request.

3.1. Step 1 – Extend your Python chat to accept conversation_id

Let’s extend your base app minimally.

Python (app.py):

import os
import json
import uuid
from datetime import datetime

import gradio as gr

LOG_PATH = "/data/chat_logs.jsonl"
os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)


def log_turn(conversation_id, message, history, response_lines):
    # 1) Extract user text (since you're using multimodal=True)
    if isinstance(message, dict):
        user_text = message.get("text", "")
    else:
        user_text = str(message)

    # 2) Build a log record with a conversation_id
    record = {
        "timestamp": datetime.utcnow().isoformat(),
        "conversation_id": conversation_id,
        "user_text": user_text,
        "response": response_lines,
        "history": history,
    }

    # 3) Append as JSONL
    try:
        with open(LOG_PATH, "a", encoding="utf-8") as f:
            f.write(json.dumps(record, ensure_ascii=False) + "\n")
    except Exception as e:
        print(f"[log_turn] failed: {e}")


def chat(message, history, conversation_id):
    # Fallback: if conversation_id somehow missing/empty, generate one
    if not conversation_id:
        conversation_id = str(uuid.uuid4())

    # Your current simple logic
    if isinstance(message, dict):
        user_text = message.get("text", "")
    else:
        user_text = str(message)

    response_lines = [
        "Hello from your Gradio Space!",
        f"You said: {user_text}",
    ]

    # Log this turn
    log_turn(conversation_id, message, history, response_lines)

    return response_lines


# Additional (hidden) input so ChatInterface exposes 'conversation_id'
conversation_id_input = gr.Textbox(
    label="conversation_id",
    visible=False,
    value="",
)

demo = gr.ChatInterface(
    fn=chat,
    type="messages",
    multimodal=True,
    title="Widget Demo Bot",
    api_name="chat",
    additional_inputs=[conversation_id_input],  # extra arg to fn
)

if __name__ == "__main__":
    demo.launch()

What this does:

  • chat() now takes three args: message, history, conversation_id.

    Gradio’s docs say: ChatInterface(fn, ...) will pass standard inputs (message, history) and then any extra additional_inputs you list, in order. (Gradio)

  • conversation_id_input is a hidden textbox; its label becomes the key in the API payload (conversation_id), which matches how @gradio/client expects to receive arguments by name. (Gradio)

  • Every log record includes that conversation_id.

So Python is now ready to receive an id from the front-end.


3.2. Step 2 – Generate a UUID once in JavaScript

On the widget side, you already have something like (simplified):

const client = await Client.connect("https://your-space.hf.space");

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

We extend it only slightly:

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

  async function initChatWidget() {
    const client = await Client.connect("https://your-space.hf.space");

    // 1. Create a conversationId for this browser widget.
    //    Option A: new conversation each page load:
    const conversationId = crypto.randomUUID();

    //    Option B (optional): persist across reloads:
    // let conversationId = localStorage.getItem("my_chat_conversation_id");
    // if (!conversationId) {
    //   conversationId = crypto.randomUUID();
    //   localStorage.setItem("my_chat_conversation_id", conversationId);
    // }

    // ... your existing DOM setup ...

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

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

      try {
        const result = await client.predict("/chat", {
          // required by ChatInterface with multimodal=True
          message: { text: userMessage, files: [] },
          // this extra field must match the label of the extra input
          conversation_id: conversationId,
        });

        const lines = result.data[0]; // list of strings from Python
        const botMessage = Array.isArray(lines) ? lines.join("\n") : String(lines);
        appendMessage(botMessage, "bot");
      } catch (error) {
        console.error("Error:", error);
        appendMessage("Sorry, there was an error.", "bot");
      }
    }

    // ... event listeners, initial greeting, etc. ...
  }

  initChatWidget();
</script>

Key details:

  • conversationId is created once when the widget is initialized.

    • Every call to sendMessage() reuses the same id.
    • Another user on another browser gets a different id.
  • The payload keys (message, conversation_id) match the names of the inputs on the backend:

    • message → the main input (defined by ChatInterface’s type="messages", multimodal=True). (Gradio)
    • conversation_id → the additional hidden Textbox(label="conversation_id").
  • The Gradio JS client just forwards that payload; internally it maps the keys to fn arguments in the same order the inputs are declared. (Gradio)

Now each line in /data/chat_logs.jsonl will look like:

{"timestamp":"2025-11-17T08:00:00Z",
 "conversation_id":"c6e0c7b9-...",
 "user_text":"Hi",
 "response":["Hello from your Gradio Space!","You said: Hi"],
 "history":[...]
}

and for another user:

{"timestamp":"2025-11-17T08:01:00Z",
 "conversation_id":"f1a3e8d2-...",
 "user_text":"Hello",
 "response":["Hello from your Gradio Space!","You said: Hello"],
 "history":[...]
}

Same file, but easy to separate by conversation_id.


4. How this compares to using gr.State

Your instinct to use gr.State() is good; it’s exactly what the official “Chatbot Specific Events” example does to store a UUID per chat session in the Gradio UI. (Gradio)

But because your front-end is not Gradio, Gradio’s usual session-state magic isn’t being used. Instead:

  • The JS client (@gradio/client) sees your app as a plain HTTP API with named inputs. (Gradio)
  • It doesn’t know anything about Gradio’s UI session state unless you explicitly treat that state as just another input or output.

In principle you could:

  • Define a gr.State for conversation_id in Python,
  • Have chat() return the updated conversation_id as an extra output,
  • Have your widget read result.data[1] and send that value back on the next call, etc.

But that’s more wiring and it doesn’t buy you much over simply generating the UUID directly in JS.

So for your specific setup (custom widget + Gradio backend):

  • Standard, simple pattern:

    • Generate conversation_id in the client.
    • Send it to the server on every predict().
    • Use it in logs.
  • Gradio-native gr.State pattern:

    • Great when using the Gradio UI directly, or when you don’t have your own front-end.

Both are valid; the first is simpler for you right now.


Practical bottom line

  1. Yes, you should log for each prompt/response pair inside chat().

  2. Yes, a single log file is fine — as long as you log a conversation_id per line.

  3. The cleanest way for your custom widget is:

    • create a UUID in JavaScript once per widget instance,
    • pass it as conversation_id in every client.predict("/chat", {...}),
    • accept it as an extra conversation_id argument in Python and log it.