botman / web-widget

MIT License
39 stars 69 forks source link

Is there a way to send attachments from web widget? #23

Open avorozheev opened 5 years ago

avorozheev commented 5 years ago

image

I'm using Botman WebWidget implementation from tutorial here: https://botman.io/2.0/driver-web

But it doesn't seem to have an attachment functionality: neither button, nor drag-n-drop into the chat window. Also surfed in documentation and related issues - no even mention of attachment functionality in web widget.

Am I doing something wrong, or it is really not implemented?

claretnnamocha commented 5 years ago

If you wish to upload files you may need to redesign your interface and implement the upload function your self using javascript.
Basically, the driver supports it but the original interface does not provide for it.
The implementation in javascript looks like this:

var form = new FormData();
form.append("driver", "web");
form.append("attachment", "file");
form.append("file", "");

var settings = {
  "url": "http://localhost:500",
  "method": "POST",
  "timeout": 0,
  "processData": false,
  "mimeType": "multipart/form-data",
  "contentType": false,
  "data": form
};

$.ajax(settings).done(function (response) {
  console.log(response);
});

Explanation:

menno-dev commented 4 years ago

@claretnnamocha where do I have to add this code? Into which file?

claretnnamocha commented 4 years ago

It could be in a file or script that handles the upload event on your interface @menno-dev

menno-dev commented 4 years ago

@claretnnamocha mhm.. unfortunately I have no clue. Where could it be within Botman Studio? Thank you very much!

claretnnamocha commented 4 years ago

It could be in a file or script that handles the upload event on your interface @menno-dev

This upload code is written on the front-end not in the back-end

diosdado93 commented 4 years ago

@claretnnamocha could you help me with an example of implementation? do I have to copy paste your code in my front end and that all? which parameter do I have to change? do you have any working example that will help me please?

@avorozheev have you finaly found a solution? @menno-dev have you found a solution? Thank you very much for your help

nachoampu commented 3 years ago

Hi, any news on this issue? I am currently trying to implement this solution but don´t know where should I put this code or if something is missing. Thanks!

arch2603 commented 3 years ago

Just wondering if there is any more help with this issue as I am having the same dilemma uploading images via the web widget.

teevon commented 2 years ago

There is a way. It's entirely a front-end issue, on the back-end you can listen for images, video, and attachments in general however you want to. but from the front-end here's how things work (at least for me) I use the botman widget by using two js script files on the front-end, A widget.js file on the main page, and a chat.js on a second page. The second page is the page the the frameEndpoint property of the botmanWidget object (The botmanWidget object remember is the object used to configure certain properties of botman from the main page) now in that page that the frameEndpoint property targets, That is where changes to the interface can be implemented here's how the page looks from my point of view

<html>
<head>
    <title>Widget Body</title>
    <!-- <link rel="stylesheet" type="text/css" href="static/css/chat.min.css"> -->
    <link href="static/bootstrap-4.1.3/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="static/css/font-awesome-4.7.0/css/font-awesome.css">
    <link rel="stylesheet" type="text/css" href="static/css/chat.css">
    <link rel="stylesheet" type="text/css" href="static/css/styles_attachment.css">
</head>
<body>
    <script src="static/js/jquery-1.10.2.min.js"></script>
    <script src="static/bootstrap-4.1.3/js/bootstrap.min.js"></script>
    <script id="botmanWidget" src='static/js/chat.js?v=1'></script>
    <script src="static/js/chat_changes.js?v=1"></script>
    <div id="fileApp"></div>
    <div class="div-attachments-container">
        <div class="div-attachments">
            <span id="view-audio" class="view-attachment-left fa fa-file-audio-o"></span>
            <span id="view-video" class="view-attachment-left fa fa-file-video-o"></span>
            <span id="view-file-name" class="view-attachment-left"></span>
            <span id="send" class="view-attachment-right fa fa-paper-plane" en="true"></span>
        </div>
    </div>
    <script src='static/js/bot_attachment.js?v=1'></script>
</body>
</html>

Two script files do the most of my magic for me, the chat_changes.js (This file helps me adjust the appearance of changes I make to the interface) and bot_attachment.js (Here I upload the selected file to the server) files

Here's what my bot_attachment.js file looks like

$(document).ready(function () {
    document.getElementById('fileApp').innerHTML = `
         <div>
          <input style="display:none" type="file" id="fileInput" />
         </div>
        `;

    const fileInput = document.querySelector("#fileInput");
    var file_type;
    var files;

    $("#view-video").on("click", function(e){
        file_type = "video";
        fileInput.click();
    });

    $("#view-audio").on("click", function(e){
        file_type = "audio";
        fileInput.click();
    });

    $("#send").on("click", function(e){
        if(($("#view-file-name").text() == "") || (files == null)) return;
        sendFile(files[0], file_type);
    });

    $("#fileInput").on("change", function(e){
        console.log("File here");
        files = e.target.files;
        console.log(files);
        if (files.length > 0) {
            $("#view-file-name").text(files[0]["name"]);
        }

    });

    function sendFile(file, filetype){
        var form = new FormData();
        form.append("driver", "web");
        form.append("attachment", filetype);
        form.append("interactive", 0);
        form.append("file", file);
        form.append("userId", "replace with 0 or your preferred value");
        var settings = {
            "url": "https://chatbot/server",
            "method": "POST",
            "timeout": 0,
            "processData": false,
            "mimeType": "multipart/form-data",
            "contentType": false,
            "data": form
        };
        $.ajax(settings).done(function (response) {
            files = null;
            $("#fileInput").val(null);
            $("#view-file-name").text("");
            window.parent.postMessage(response, '*');
        });
    }
});

here's what my chat_changes.js file looks like

window.addEventListener('load', function () {
    var messageArea = document.getElementById("messageArea");
    var userText =  document.getElementById("userText");
    var chatOl = document.getElementsByClassName("chat")[0];
    var messageAreaHeight = messageArea.clientHeight;
    chatHeight = chatOl.clientHeight;
    messageArea.style.height = (messageAreaHeight - 20) + "px";
    chatOl.style.height = (chatHeight - 20) + "px";
    userText.setAttribute("autocomplete","off");
    userText.style.position = "absolute";
    userText.style.bottom = "40px";
});

now so far, this allows you send files to the server as long as you have the

$botman->receivesVideos(function($bot, $videos){
 //some code to run here for receiving videos
});

or

$botman->receivesAudio(function($bot, $videos){
 //some code to run here for receiving audios
});

similar functions exists for files and images and whatever now all this makes sure the server works with receiving files, but the servers response doesn't get back to your application the way it is wired up by the botman widget by default anymore, here where you get a bit creative. The botman widget on the main page exposes and object window.botmanChatWidget This widget has an api writeToMessages, which allows us write replies from our server.

if you notice in the bot_attachment I use the postMessage method to post a a message from the iframe back to my main page I use that action to trigger a function, now on the main page in a file I called botmanTest.js the function is handleMessage and here's what the script looks like

here's what the code looks like, in a script file I named botmanTest.js (you can use whatever name of course)

$(document).ready(function() {
    const botmanInterval = setInterval(checkBotman, 1000);
    function checkBotman(){
        if(window.botmanChatWidget != "undefined"){
            clearInterval(botmanInterval);
        }
    }

    if(!window.addEventListener){
        // IE8 support
        window.attachEvent('onmessage', handleMessage);
    } else {
        window.addEventListener('message', handleMessage, false);
    }

    function handleMessage(event){
        if(event.origin == ''){
            //just some code to constrain the origin if I need to
        }
        var json_response = JSON.parse(event.data);
        (json_response.messages || []).forEach(function (t) {
            window.botmanChatWidget.writeToMessages(t);
        });
    }
});

i use a tacky method to make sure I'm not using the botmanChatWidet object before it is defined. Documentation for botman web widget isn't all so well set up. Hopefully this would help someone

teevon commented 2 years ago

Here's the stylesheet code for the styles_attachment.css files

.view-attachment-left { z-index: 999999999999999999; padding-left: 20px; cursor: pointer; } .view-attachment-right { z-index: 999999999999999999; padding-right: 10px; display: inline-block; cursor: pointer; position: absolute; right: 0px; } .div-attachments-container { height: 30px; width: 100%; display: inline-block; position: fixed; bottom: 8px; } .div-attachments { margin-top: 10px; position: relative; }

NowakAdmin commented 2 years ago

it is possible to use above code inside conversations? i try to catch an image inside conversation and it seems that image is sended directly to routes/botman.php in my fallback function.

teevon commented 2 years ago

it is possible to use above code inside conversations? i try to catch an image inside conversation and it seems that image is sended directly to routes/botman.php in my fallback function.

The problem is with the userId in the form, make sure the right id is being passed there, I had issues with that too

teevon commented 2 years ago

form.append("userId", make very sure the right userId is appearing here, else the default response gets activated like there is no on going conversation)

Just make sure the right userId is being passed in there, it should work fine afterwards. I Encountered the same problem too, took me a while to figure it out

vaamoz commented 1 year ago

shdi

vaamoz commented 1 year ago

the code u have refere is not working

csavelief commented 4 months ago

Hi!

It took me some time, but I found what I believe is a nice way to do this without having to modify the library. Basically, the idea is to inject the FileUpload HTML/JS code directly into the iframe. Then, use a MutationObserver to react on the bot responses asking for file uploads to display the user with the FileUpload component instead of the textarea.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
    </head>
    <body>
        <!-- YOUR HTML HERE -->
    </body>
    <script>

      const userId = (Math.random() + 1).toString(36).substring(7);
      const botmanInterval = setInterval(checkBotman, 1000);

      function checkBotman() {

        const elChatBotManFrame = document.getElementById('chatBotManFrame');

        if (elChatBotManFrame) {

          const elChatWidget = elChatBotManFrame.contentWindow.document.getElementById('botmanChatRoot');
          const elMessageArea = elChatBotManFrame.contentWindow.document.getElementById('messageArea');
          const elTextInput = elChatBotManFrame.contentWindow.document.getElementById('userText');

          if (!elChatWidget || !elMessageArea || !elTextInput) {
            return;
          }

          clearInterval(botmanInterval);

          // Append the file upload component to the DOM
          const elFileInputContainer = document.createElement('div');
          elFileInputContainer.innerHTML = `
            <style>
               .gg-software-upload {
                 box-sizing: border-box;
                 position: relative;
                 display: block;
                 transform: scale(var(--ggs,1));
                 width: 16px;
                 height: 6px;
                 border: 2px solid;
                 border-top: 0;
                 border-bottom-left-radius: 2px;
                 border-bottom-right-radius: 2px;
                 margin-top: 8px
              }
              .gg-software-upload::after {
                 content: "";
                 display: block;
                 box-sizing: border-box;
                 position: absolute;
                 width: 8px;
                 height: 8px;
                 border-left: 2px solid;
                 border-top: 2px solid;
                 transform: rotate(45deg);
                 left: 2px;
                 bottom: 4px
              }
              .gg-software-upload::before {
                 content: "";
                 display: block;
                 box-sizing: border-box;
                 position: absolute;
                 border-radius: 3px;
                 width: 2px;
                 height: 10px;
                 background: currentColor;
                 left: 5px;
                 bottom: 3px
              }
              .hidden {
                display: none !important;
              }
              .file-input {
                width: 92%;
                position: fixed;
                display: flex;
                bottom: 0;
                padding: 15px;
                background: #ffffff;
                box-shadow: 0 -6px 12px 0 rgba(235,235,235,.95);
              }
              .file-input input {
                width: 90%;
                overflow: hidden;
              }
              .disabled {
                display: none;
              }
              .file-upload-btn {
                align-self: center;
                margin-left: auto;
                cursor: pointer;
              }
            </style>
            <div class="file-input hidden">
              <input type="file" name="file" id="file" />
              <i class="gg-software-upload file-upload-btn disabled"></i>
            </div>
          `;
          elChatWidget.append(elFileInputContainer);

          const elFileUploadBtn = elChatBotManFrame.contentWindow.document.getElementsByClassName('file-upload-btn')[0];
          const elFileInput = elChatBotManFrame.contentWindow.document.getElementsByClassName('file-input')[0];
          const elFile = elChatBotManFrame.contentWindow.document.getElementById('file');
          const selection = [];

          elFile.addEventListener('change', event => {
            const files = event.target.files;
            if (files.length > 0) {
              selection.push(files); // push the whole FileList
              elFileUploadBtn.classList.toggle('disabled'); // show upload button
            }
          });

          elFileUploadBtn.addEventListener('click', event => {

            for (let i=0; i<selection.length; i++) {
              for (let j=0; j<selection[i].length; j++) {

                const file = selection[i][j];
                const filename = file['name'];
                const form = new FormData();

                form.append("driver", "web");
                form.append("attachment", "file"); // audio | video | location | file
                form.append("interactive", "0");
                form.append("file", file);
                form.append("userId", userId);

                const options = {
                  method: 'POST',
                  body: form,
                };

                fetch(window.location.origin + "/botman", options).then(response => {
                    if (response.status === 200) {
                      window.botmanChatWidget.sayAsBot('Your file ' + filename + ' has been sent :-)');
                    } else {
                      window.botmanChatWidget.sayAsBot('Your file ' + filename + ' could not be sent :-(');
                    }
                  });
              }
            }

            selection.length = 0; // remove selected files
            elFile.value = ""; // reset file-input
            elFileInput.classList.toggle('hidden'); // hide file-input
            elFileUploadBtn.classList.toggle('disabled'); // hide upload button
            elTextInput.classList.toggle('hidden'); // display text area
            elTextInput.focus(); // set focus on text area
          });

          // Observe incoming messages and react accordingly
          const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
              mutation.addedNodes.forEach(addedNode => {

                // If the bot asks for a file upload, display the file input instead of the text area
                if (addedNode.classList.contains('chatbot')) {
                  const elMessage = addedNode.getElementsByTagName('p')[0].innerText;
                  if (/.*(upload|téléverser?).*/i.test(elMessage)) {
                    elTextInput.classList.toggle('hidden');
                    elFileInput.classList.toggle('hidden');
                  }
                }
              });
            });
          });

          const elChatArea = elMessageArea.getElementsByClassName('chat')[0];

          observer.observe(elChatArea, { subtree: false, childList: true });
        }
      }
    </script>
    <link rel="stylesheet"
          type="text/css"
          href="https://cdn.jsdelivr.net/npm/botman-web-widget@0/build/assets/css/chat.min.css">
    <script>
      window.botmanWidget = {
        title: 'BotMan',
        aboutText: 'Powered by ComputableFacts',
        aboutLink: 'https://computablefacts.com',
        userId: userId,
      };
    </script>
    <script src='https://cdn.jsdelivr.net/npm/botman-web-widget@0/build/js/widget.js'></script>
</html>

On the server side, I have the following code in AppServiceProvider.php :

<?php

namespace App\Providers;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        VerifyCsrfToken::except(['botman']);
    }
}

I have the following code in routes/web.php :

<?php

use App\Http\Controllers\BotManController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::match(['get', 'post'], '/botman', [BotManController::class, 'handle']);

I have the following code in BotManController.php :

<?php

namespace App\Http\Controllers;

use BotMan\BotMan\BotMan;
use BotMan\BotMan\Messages\Incoming\Answer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class BotManController extends Controller
{
    public function handle(): void
    {
        $botman = app('botman');
        $botman->receivesFiles(function (BotMan $botman, $files) {

        });
        $botman->hears('.*(hi|hello|bonjour).*', function (BotMan $botman, string $message) {
            if (Str::lower($message) === 'hi' || Str::lower($message) === 'hello') {
                $this->askNameEn($botman);
            } else if (Str::lower($message) === 'bonjour') {
                $this->askNameFr($botman);
            }
        });
        $botman->fallback(function (BotMan $botman) {
            $botman->reply('Sorry, I did not understand these commands.');
        });
        $botman->listen();
    }

    private function askNameEn(BotMan $botman): void
    {
        $botman->ask('Hello! What is your name?', function (Answer $answer) use ($botman) {
            $name = $answer->getText();
            $this->say("Nice to meet you {$name}!");
            $this->askForFiles('I am ready now. Upload your file!', function ($files) {
                foreach ($files as $file) {
                    $url = $file->getUrl();
                    $payload = $file->getPayload();
                    Log::debug($url);
                }
            });
        });
    }

    private function askNameFr(BotMan $botman): void
    {
        $botman->ask('Bonjour! Quel est ton nom?', function (Answer $answer) use ($botman) {
            $name = $answer->getText();
            $this->say("Enchanté, {$name}!");
            $this->askForFiles('Je suis prêt maintenant. Téléverse ton fichier!', function ($files) {
                foreach ($files as $file) {
                    $url = $file->getUrl();
                    $payload = $file->getPayload();
                    Log::debug($url);
                }
            });
        });
    }
}
amaliradifan commented 4 months ago

@csavelief, thanks a lot! The code works really well. However, can the code be modified to send multiple files?