Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to build a Smart Compose like Gmail? Possible in a textarea?

The new predictive type feature Smart Compose of Gmail is quite interesting.

Let's say we want to implement such a functionality ourselves:

  1. User enters beginning of text, e.g. How and in gray behind it appears are you?.
  2. User hits TAB and the word tomorrow is set.

Example:

example of smart compose

Can a textarea with Javascript be used to achieve this?

And if not, how could this be implemented otherwise?

like image 208
Avatar Avatar asked Nov 19 '18 12:11

Avatar


2 Answers

My previous answer got deleted, so here's a better attempt at explaining how I've somewhat replicated Smart Compose. My answer only focuses on the pertinent aspects. See https://github.com/jkhaui/predictable for the code.

  1. We are using vanilla js and contenteditable in our solution (just like Gmail does). I bootstrap my example with create-react-app and Medium-Editor, but neither React nor Medium-Editor are necessary.

  2. We have a database of "suggestions" which can be an array of words or phrases. For our purposes, in my example, I use a static array containing 50,000+ common English phrases. But you can easily see how this could be substituted for a dynamic data-source - such as how Gmail uses its neural network API to offer suggestions based on the current context of users' emails: https://ai.googleblog.com/2018/05/smart-compose-using-neural-networks-to.html

  3. Smart Compose uses JavaScript to insert a <span></span> element immediately after the word you are writing when it detects a phrase to suggest. The span element contains only the characters of the suggestion that have not been typed.

E.g. Say you've written "Hi, how a" and a suggestion appears. Let's say the entire suggestion is "how are you going today". In this case, the suggestion is rendered as "re you going today" within the span. If you continue typing the characters in the placeholder - such as "Hi, how are you goi" - then the text content of the span changes dynamically - such that "ng today" is now the text within the span.

My solution works slightly differently but achieves the same visual effect. The difference is I can't figure out how to insert an inline span adjacent to the user's current text and dynamically mutate the span's content in response to the user's input. So, Instead, I've opted for an overlay element containing the suggestion. The trick is now to position the overlay container exactly over the last word being typed (where the suggestion will be rendered). This provides the same visual effect of an inline typeahead suggestion.

  1. We achieve correct positioning of the overlay by calculating the top + left coordinates for the last word being typed. Then, using JavaScript, we couple the top + left CSS attributes of the overlay container so that they always match the coordinates of the last word. The tricky part is getting these coordinates in the first place. The general steps are:

    • Call window.getSelection().anchorNode.data.length which retrieves the current text node the user is writing in and returns its length, which is necessary to calculate the offset of the last word within its parent element (explained in the following steps).
    • For simplicity's sake, only continue if the caret is at the end of the text.
    • Get the parent node of the current text node we're in. Then get the length of the parent node's text content.
    • The parent node's text length - the current text node's (i.e the last word's) text length = the offset position of the last text node within its contenteditable parent.

    • Now we have the offset of the last word, we can use the various range methods to insert a span element immediately preceding the last word: https://developer.mozilla.org/en-US/docs/Web/API/Range

    • Let's call this span element a shadowNode. Mentally, you can now picture the DOM as follows: we have the user's text content, and we have a shadowNode placed at the position of the last word.
    • Finally, we call getBoundingClientRect on the shadowNode which returns specific metadata, including the top + left coordinates we're after.
    • Apply the top + left coordinates to the suggestions overlay container and add the appropriate event handlers/listeners to render the suggestion when Tab is pressed.
like image 155
jkhaui Avatar answered Oct 27 '22 13:10

jkhaui


Visit this link for documentation https://linkkaro.com/autocomplete.html .

May be you need to make few adjustment in CSS ( padding and width ).

I hope it will help.[![

$(document).ready(function(){
    //dummy random output. You can use api
    var example = {
        1:"dummy text 1",
        2:"dummy text 2"
    };
function randomobj(obj) {
    var objkeys = Object.keys(obj)
    return objkeys[Math.floor(Math.random() * objkeys.length)]
}

var autocomplete = document.querySelectorAll("#autocomplete");
var mainInput = document.querySelectorAll("#mainInput");
var foundName = '';
var predicted = '';
var apibusy= false;
var mlresponsebusy = false;

$('#mainInput').keyup(function(e) {
//check if null value send
    if (mainInput[0].value == '') {
        autocomplete[0].textContent = '';
        return;
}
//check if space key press
    if (e.keyCode == 32) {
        CallMLDataSetAPI(e);
        scrolltobototm();
        return;
}
//check if Backspace key press
    if (e.key == 'Backspace'){
        autocomplete[0].textContent = '';
        predicted = '';
        apibusy = true;
        return;
}
//check if ArrowRight or Tab key press
    if(e.key != 'ArrowRight'){
        if (autocomplete[0].textContent != '' && predicted){
            var first_character = predicted.charAt(0);
            if(e.key == first_character){
                var s1 = predicted;
                var s2 = s1.substr(1);
                predicted = s2;
                apibusy = true;
            }else{
                autocomplete[0].textContent = '';
                apibusy= false;
            }
        }else{
            autocomplete[0].textContent = '';
            apibusy= false;
        }
        return;
        }else{
            if(predicted){
                if (apibusy == true){
                    apibusy= false;
                }
                if (apibusy== false){
                    mainInput[0].value = foundName;
                    autocomplete[0].textContent = '';
                }
            }else{
            return;
        }
    }

    function CallMLDataSetAPI(event) {
        //call api and get response
        var response = {
        "predicted": example[randomobj(example)]
        };
        if(response.predicted != ''){
            predicted = response.predicted;
            var new_text = event.target.value + response.predicted;
            autocomplete[0].textContent = new_text;
            foundName = new_text
        }else{
            predicted = '';
            var new_text1 = event.target.value + predicted;
            autocomplete[0].textContent = new_text1;
            foundName = new_text1
        }
    };
});
$('#mainInput').keypress(function(e) {
    var sc = 0;
    $('#mainInput').each(function () {
        this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
        this.setAttribute('style', 'height:' + (this.scrollHeight+3) + 'px;overflow-y:hidden;');
        sc = this.scrollHeight;
    });
    $('#autocomplete').each(function () {
        if (sc <=400){
            this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
            this.setAttribute('style', 'height:' + (sc+2) + 'px;overflow-y:hidden;');
        }
    }).on('input', function () {
        this.style.height = 0;
        this.style.height = (sc+2) + 'px';
    });
});
    function scrolltobototm() {
        var target = document.getElementById('autocomplete');
        var target1 = document.getElementById('mainInput');
        setInterval(function(){
            target.scrollTop = target1.scrollHeight;
        }, 1000);
    };
    $( "#mainInput" ).keydown(function(e) {
        if (e.keyCode === 9) {
            e.preventDefault();
            presstabkey();
        }
    });
    function presstabkey() {
        if(predicted){
            if (apibusy == true){
                apibusy= false;
            }
            if (apibusy== false){
                mainInput[0].value = foundName;
                autocomplete[0].textContent = '';
            }
        }else{
        return;
        }
    };
});
#autocomplete { opacity: 0.6; background: transparent; position: absolute; box-sizing: border-box; cursor: text; pointer-events: none; color: black; width: 421px;border:none;} .vc_textarea{ padding: 10px; min-height: 100px; resize: none; } #mainInput{ background: transparent; color: black; opacity: 1; width: 400px; } #autocomplete{ opacity: 0.6; background: transparent;padding: 11px 11px 11px 11px; }
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<textarea id="autocomplete" type="text" class="vc_textarea"></textarea>
<textarea id="mainInput" type="text" name="comments" placeholder="Write some text" class="vc_textarea"></textarea>

]1]1

like image 35
Golap Hazarika Avatar answered Oct 27 '22 12:10

Golap Hazarika