DEV Community

M
M

Posted on

1

realtime chat with bot using data-star

Image description

Hello everyone,

in part 1 i made simple server side clock https://dev.to/blinkinglight/golang-data-star-1o53/

and now decided to write more complex things using https://nats.io and https://data-star.dev -

Chat bot which returns what you wrote to it:

some golang code:

package handlers

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/blinkinglight/chat-data-star/web/views/chatview"
    "github.com/delaneyj/datastar"
    "github.com/delaneyj/toolbelt"
    "github.com/delaneyj/toolbelt/embeddednats"
    "github.com/go-chi/chi/v5"
    "github.com/gorilla/sessions"
    "github.com/nats-io/nats.go"
)

func SetupChat(router chi.Router, session sessions.Store, ns *embeddednats.Server) error {

    type Message struct {
        Message string `json:"message"`
        Sender  string `json:"sender"`
    }

    nc, err := ns.Client()
    if err != nil {
        return err
    }

    nc.Subscribe("chat.>", func(msg *nats.Msg) {
        var message = Message{}
        err := json.Unmarshal(msg.Data, &message)
        if err != nil {
            log.Printf("%v", err)
            return
        }

        if message.Sender != "bot" {
            // do something with user message and send back response
            nc.Publish("chat."+message.Sender, []byte(`{"message":"hello, i am bot. You send me: `+message.Message+`", "sender":"bot"}`))
        }
    })

    _ = nc
    chatIndex := func(w http.ResponseWriter, r *http.Request) {
        _, err := upsertSessionID(session, r, w)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        chatview.Index().Render(r.Context(), w)
    }

    chatMessage := func(w http.ResponseWriter, r *http.Request) {
        id, err := upsertSessionID(session, r, w)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        var state = Message{}

        err = datastar.BodyUnmarshal(r, &state)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        state.Sender = id
        b, err := json.Marshal(state)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        nc.Publish("chat."+id, b)
        sse := datastar.NewSSE(w, r)
        _ = sse
    }

    chatMessages := func(w http.ResponseWriter, r *http.Request) {

        id, err := upsertSessionID(session, r, w)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        var ch = make(chan *nats.Msg)
        sub, err := nc.Subscribe("chat."+id, func(msg *nats.Msg) {
            ch <- msg
        })

        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer close(ch)
        defer sub.Unsubscribe()

        sse := datastar.NewSSE(w, r)

        for {
            select {
            case <-r.Context().Done():
                return

            case msg := <-ch:
                var message = Message{}
                err := json.Unmarshal(msg.Data, &message)
                if err != nil {
                    datastar.Error(sse, err)
                    return
                }
                if message.Sender == "bot" {
                    datastar.RenderFragmentTempl(sse, chatview.Bot(message.Message), datastar.WithMergeAppend(), datastar.WithQuerySelector("#chatbox"))
                } else {
                    datastar.RenderFragmentTempl(sse, chatview.Me(message.Message), datastar.WithMergeAppend(), datastar.WithQuerySelector("#chatbox"))
                }
            }
        }
    }

    router.Get("/chat", chatIndex)
    router.Post("/chat", chatMessage)
    router.Get("/messages", chatMessages)

    return nil
}

func upsertSessionID(store sessions.Store, r *http.Request, w http.ResponseWriter) (string, error) {

    sess, err := store.Get(r, "chatters")
    if err != nil {
        return "", fmt.Errorf("failed to get session: %w", err)
    }
    id, ok := sess.Values["id"].(string)
    if !ok {
        id = toolbelt.NextEncodedID()
        sess.Values["id"] = id
        if err := sess.Save(r, w); err != nil {
            return "", fmt.Errorf("failed to save session: %w", err)
        }
    }
    return id, nil
}

Enter fullscreen mode Exit fullscreen mode

and template

package chatview

import "github.com/blinkinglight/chat-data-star/web/views/layoutview"

templ Index() {
    @layoutview.Main() {
        <!-- component -->
        <div class="fixed bottom-0 right-0 mb-4 mr-4">
            <button id="open-chat" class="bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition duration-300 flex items-center">
                <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
                </svg>
                Chat with Admin Bot
            </button>
        </div>
        <div id="chat-container" class="hidden fixed bottom-16 right-4 w-96">
            <div class="bg-white shadow-md rounded-lg max-w-lg w-full">
                <div class="p-4 border-b bg-blue-500 text-white rounded-t-lg flex justify-between items-center">
                    <p class="text-lg font-semibold">Admin Bot</p>
                    <button id="close-chat" class="text-gray-300 hover:text-gray-400 focus:outline-none focus:text-gray-400">
                        <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                        </svg>
                    </button>
                </div>
                <div id="chatbox" class="p-4 h-80 overflow-y-auto" data-on-load="$$get('/messages')" data-store="{ message: '' }">
                    <!-- Chat messages will be displayed here -->
                </div>
                <div class="p-4 border-t flex">
                    <input data-model="message" id="user-input" type="text" placeholder="Type a message" class="w-full px-3 py-2 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500"/>
                    <button data-on-keydown.window.key_enter="$$post('/chat'); $message=''" data-on-click="$$post('/chat'); $message=''" id="send-button" class="bg-blue-500 text-white px-4 py-2 rounded-r-md hover:bg-blue-600 transition duration-300">Send</button>
                </div>
            </div>
        </div>
        <script>
        const chatbox = document.getElementById("chatbox");
                const chatContainer = document.getElementById("chat-container");
                const userInput = document.getElementById("user-input");
                const sendButton = document.getElementById("send-button");
                const openChatButton = document.getElementById("open-chat");
                const closeChatButton = document.getElementById("close-chat");

                let isChatboxOpen = true; // Set the initial state to open

                function toggleChatbox() {
                    chatContainer.classList.toggle("hidden");
                    isChatboxOpen = !isChatboxOpen; // Toggle the state
                }

                openChatButton.addEventListener("click", toggleChatbox);

                closeChatButton.addEventListener("click", toggleChatbox);
                toggleChatbox();

        </script>
    }
}

templ Me(message string) {
    <div class="mb-2 text-right">
        <p class="bg-blue-500 text-white rounded-lg py-2 px-4 inline-block">{ message }</p>
    </div>
}

templ Bot(message string) {
    <div class="mb-2">
        <p class="bg-gray-200 text-gray-700 rounded-lg py-2 px-4 inline-block">{ message }</p>
    </div>
}

Enter fullscreen mode Exit fullscreen mode

you can find working example at https://github.com/blinkinglight/chat-data-star

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay