Chat Interface (html)

Implementing a Custom html Chat Interface

Issue Description

I’ve developed a custom chat interface using HTML, displayed in a WebView in my VIKTOR app. However, I’m facing challenges with:

  1. Calling Python functions from the HTML/JavaScript to handle chat logic and visualize graphs.
  2. Maintaining state in the chat interface, given VIKTOR’s stateless nature.

I prefer not to use a TextField in the VIKTOR parametrization class for user messages due to the poor user experience of constantly pressing the update button and the separation between user input and chat history.

I considered using the VIKTOR JavaScript SDK for WebView interaction, but the resulting pop-up that users need to dismiss each time is not ideal for a smooth chat experience.

Support Needed

I’m seeking guidance on:

  • Integrating Python functions with the HTML interface without full page reloads
  • Maintaining state in a stateless environment
  • Using the JavaScript SDK without triggering pop-ups?

Any insights or examples would be greatly appreciated. Thank you!

Hi Anande,

Good that you reach out, sounds like an interesting use case!

I understand your point. We are currently working on product improvements to facilitate chat functionality better.

In the meantime:

  • Integrating Python functions with the HTML interface without full page reloads
    Could you explain this in a bit more detail? Maybe with some example code?

  • Maintaining state in a stateless environment
    The easiest way to do this is to use storage.

  • Using the JavaScript SDK without triggering pop-ups?
    This will be released on short notice (somewhere in the coming 2 weeks)! It offers the possibility to disable the confimation modal.

Hopefully this helps already a bit, we can also schedule a call to dive in further!

Cheers,
Stijn

Hi Stijn,

So, this is an image of the interface. Now the chat just give a random answer from a list. But instead of that, I would like to get the agent’s answer from a function called: get_response()

The question is, how do I get this line to call get_response without loosing the conversation in the chat interface. A job will reload the webview.

const SAMPLE_RESPONSES = """ + str(SAMPLE_RESPONSES) + """ <==== Call get_response instead;

Here the complete code. I reduced it so much as I could:

import viktor as vkt
import random
import os

# Sample responses for testing
SAMPLE_RESPONSES = [
    "I understand you're asking about that. Let me help you with your request.",
    "That's an interesting question! Here's what I think about it.",
    "I'd be happy to assist you with that. Here's what I can tell you.",
    "Let me process that request for you.",
    "I've analyzed your question and here's my response."
]

history = []

def get_response(self, message, history):
    history.append({"role": "user", "content": message})
    response = random.choice(SAMPLE_RESPONSES)
    history.append({"role": "assistant", "content": response})
    return response

class Parametrization(vkt.Parametrization):
    pass

    
class Controller(vkt.Controller):
    parametrization = Parametrization


    
    @vkt.WebView("Chat", duration_guess=1)
    def show_chat(self, params, **kwargs):
        # Get the current directory
        current_dir = os.path.dirname(os.path.abspath(__file__))
        
        # Read the CSS file
        with open(os.path.join(current_dir, 'static', 'css', 'styles.css'), 'r') as f:
            css_content = f.read()
            
        # Read the JS file
        with open(os.path.join(current_dir, 'static', 'js', 'chat.js'), 'r') as f:
            js_content = f.read()

        html = """
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                """ + css_content + """
            </style>
        </head>
        <body>
            <div id="chat-container">
                <div id="messages">
                    <div class="message bot-message">Hello! How can I help you today?</div>
                </div>
                <div id="input-container">
                    <input type="text" id="message-input" placeholder="Type your message...">
                    <button id="send-button">Send</button>
                </div>
            </div>

            <script>
                const SAMPLE_RESPONSES = """ + str(SAMPLE_RESPONSES) + """;
            </script>
            <script>
                """ + js_content + """
            </script>
        </body>
        </html>
        """
        return vkt.WebResult(html=html)
document.getElementById('message-input').addEventListener('keypress', function(e) {
    if (e.key === 'Enter') {
        sendMessage();
    }
});

function sendMessage() {
    const input = document.getElementById('message-input');
    const message = input.value.trim();
    
    if (message) {
        addMessage(message, 'user');
        input.value = '';
        
        // Simulate bot response after a short delay
        setTimeout(() => {
            const randomResponse = SAMPLE_RESPONSES[Math.floor(Math.random() * SAMPLE_RESPONSES.length)];
            addMessage(randomResponse, 'bot');
        }, 1000);
    }
}

Maybe hackers @khameeteman or @mslootweg know how?

Hi Anande,

To answer your first question:

Integrating Python functions with the HTML interface without full page reloads

You could consider looking into using Pyodide? Although I do not have much experience with using Pyodide, it does seem to help with simple scripts running in the browser. Here are some code snippets with the results:

page.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python Graph in Browser</title>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js"></script>
</head>
<body>
    <h1>Python Graph in Browser</h1>
    <canvas id="plot"></canvas>

    <script type="text/javascript">
        async function main() {
            let pyodide = await loadPyodide();
            await pyodide.loadPackage("matplotlib");

            let result = await pyodide.runPythonAsync(`
                import matplotlib.pyplot as plt
                import io
                import base64

                # Create a simple plot
                x = [1, 2, 3, 4, 5]
                y = [2, 4, 6, 8, 10]

                plt.figure(figsize=(8, 6))
                plt.plot(x, y)
                plt.title('Simple Line Graph')
                plt.xlabel('X-axis')
                plt.ylabel('Y-axis')

                # Save the plot to a bytes buffer
                buf = io.BytesIO()
                plt.savefig(buf, format='png')
                buf.seek(0)

                # Encode the bytes buffer to base64
                img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')

                # Return the base64 encoded image
                img_base64
            `);

            // Display the plot on the canvas
            let img = new Image();
            img.src = 'data:image/png;base64,' + result;
            img.onload = function() {
                let canvas = document.getElementById('plot');
                canvas.width = img.width;
                canvas.height = img.height;
                let ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
            };
        }

        main();
    </script>
</body>
</html>

app.py

from pathlib import Path

import viktor as vkt


class Parametrization(vkt.Parametrization):
    pass


class Controller(vkt.Controller):
    parametrization = Parametrization

    @vkt.WebView('Hello page')
    def get_web_view(self, params, **kwargs):
        static_html_path = Path(__file__).parent / 'page.html'
        return vkt.WebResult.from_path(static_html_path)

Results

Curious to see how your investigation goes!

Thanks. This seems to work. I will keep you updated. Here the first draft using Pyodide:

marcel

3 Likes

I see the issue was solved. But here a little warning:

  • Yes. You can use Pyodide to run Python in the browser.
  • Yes. It also works with your VIKTOR app.
  • Yes. You can use libraries, like numpy and plotly with it. However, it makes the initial loading of the page a bit slow, as it needs to install them in the browser first.
  • No. You cannot use it with Claude. It does not accept API calls from the browser for security reasons.

Hacky solution: I made a small server to handle the requests for Claude and generate the graphs, which is called by the HTML view.