Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
avorozheev opened this issue Dec 11, 2018 · 17 comments
Open

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

avorozheev opened this issue Dec 11, 2018 · 17 comments

Comments

@avorozheev
Copy link

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
Copy link

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:

  • The request must be POST
  • The driver parameter must be specified (web)
  • The attachment parameter could be any of the four possible types (audio | video | location | file)
  • The file can be uploaded with any parameter name, here I used file

@menno-dev
Copy link

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

@claretnnamocha
Copy link

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

@menno-dev
Copy link

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

@claretnnamocha
Copy link

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
Copy link

diosdado93 commented Nov 23, 2020

@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
Copy link

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
Copy link

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
Copy link

teevon commented Apr 27, 2022

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
Copy link

teevon commented Apr 27, 2022

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
Copy link

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
Copy link

teevon commented Aug 8, 2022

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
Copy link

teevon commented Aug 8, 2022

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
Copy link

vaamoz commented Nov 21, 2023

shdi

@vaamoz
Copy link

vaamoz commented Nov 21, 2023

the code u have refere is not working

@csavelief
Copy link

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests