Проблемы со списком предложений в Botframework Webchat React - PullRequest
2 голосов
/ 04 марта 2020

Я только что добавил функцию автозаполнения / автозаполнения в мой веб-чат с бот-фреймворком (v-4) с помощью реакции. js. Но есть некоторые проблемы, которые мне нужно исправить:

1.) Получая предложения, я хочу, чтобы слова, которые я ввожу в веб-чат, были выделены жирным шрифтом в результирующем списке предложений. Я сделал это, но проблема, с которой я сейчас сталкиваюсь, заключается в том, что первая буква должна быть выделена жирным шрифтом (как вы можете видеть на изображении), и я хочу сделать ее жирной, даже если она находится внутри.

2.) Когда я выбираю опцию из списка предложений, она должна быть закрыта. Он закрывает другие параметры, но не выбран. (Как показано на рисунке). Я тоже хочу это закрыть.

3.) Я хочу сделать свою стрелку вверх / вниз для выбора параметров из списка предложений.

Пожалуйста, найдите изображения в ссылках ниже,

import React from 'react';
import { DirectLine, ConnectionStatus } from 'botframework-directlinejs';
import ReactWebChat from 'botframework-webchat';
import './ChatComponent.css';

var val;
var apiParameters = [];
var currentFocus = -1;
export default class extends React.Component {
constructor(props) {
    super(props);

    this.state = {
        token: '',
        conversationId: '',
        directLine: {},
        view: false,
        feedBack: null,
        value: '',
        popupContent: '',
        storeValue: '',
        suggestions: [],
        suggestionCallback: '',
        suggestionTypedText: "",
        typingChecking: "false",

    };
    this.handleTokenGeneration = this.handleTokenGeneration.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleSaveFeedback = this.handleSaveFeedback.bind(this);
    this.handleSuggestion = this.handleSuggestion.bind(this);
    this.handleClose = this.handleClose.bind(this);
    this.handleSuggestionClick = this.handleSuggestionClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);

    this.moveHighlight = this.moveHighlight.bind(this);

    this.getSuggestionHtml = this.getSuggestionHtml.bind(this);
 }


 getSuggestionHtml(suggestion) {
    const lowerCaseSuggestion = suggestion.toLowerCase();
    return {
        __html: lowerCaseSuggestion.includes(this.state.suggestionTypedText) 
 ? lowerCaseSuggestion
            .replace(this.state.suggestionTypedText, 
 `<b>${this.state.suggestionTypedText}</b>`) : lowerCaseSuggestion
    };
}
handleTokenGeneration = async () => {
    console.log("11111");
    const response = await fetch(`api/TokenGenerationService/GetToken`);
    const data = await response.json();
    this.setState({ token: data.categoryObject.token, conversationId: 
    data.categoryObject.conversationId });
    this.state.directLine = new DirectLine({ token: this.state.token });
    this.setState({ view: true });
    this.setState({ typingChecking: "true" });

    console.log("conversationId");
  };
  async handleSuggestion(val, store) {


    if (val === "") {
        this.setState({
            suggestions: []
        });
    }
    else {
        apiParameters = [];
        var valuess = null;
        const response = await fetch(`api/TokenGenerationService/GetAzureSearch?myparam1=${val}`);
        const data = await response.json();
        var values = ["Hello", "yes", "no", "exit", "Welcome", "Thank You", "Approve", "Apply leave", "Reject", "Absence Balance", "Leave Balance", "Upcoming Holidays", "Apply Comp-Off", "Approve Leave", "Raise Incident Tickets", "Project Allocation Info", "Reporting Manager Change", "Reporting Manager Approval", "Approve Isolve Tickets", "My Manager", "My Account Manager", "Generate Salary Certificate", "Isolve Ticket Status", "Internal Job Posting", "My Designation", "My Joining Date", "RM Approval", "RM Change", "Resource Allocation", "ESettlement Approval", "SO Approval", "Cash advance Approval", "Purchase Request Approval", "Referral status", "HR Ticket", "Platinum Support"];
        valuess = values.filter(s =>
            s.toLocaleLowerCase().startsWith(val.toLowerCase())
        );
        valuess = valuess.concat(data.az_search);
        this.setState({
            suggestions: valuess,
            suggestionCallback: store,
            suggestionTypedText: val.toLowerCase()
        });

        var totCount = data.az_search;
        console.log("kkkkkk" + totCount);

     var myNode = document.getElementById('Suggestion1');

    }

}
getSuggestionCss(index) {
    var HIGHLIGHTED_CSS = "HIGHLIGHTED_CSS";
    var SUGGESTION_CSS = "SUGGESTION_CSS";
    console.log("jioouu" + this.state.highlightedIndex);
    return index === this.state.highlightedIndex ? HIGHLIGHTED_CSS : SUGGESTION_CSS;
}

moveHighlight(event, direction) {
    event.preventDefault();

    const { suggestions, highlightedIndex=-1 } = this.state;
    if (!suggestions.length) return;
    let newIndex = (highlightedIndex + direction + suggestions.length) % suggestions.length;
    console.log("lokkkk" + direction)
    if (newIndex !== highlightedIndex) {
        console.log("kkkkkkkkkkkkkkkkkkk")
        this.setState({
            highlightedIndex: newIndex,
        });
    }
}
keyDownHandlers = {

    ArrowDown(event) {

        this.moveHighlight(event, 1);
    },
    ArrowUp(event) {

        this.moveHighlight(event, -1);
    },
    Enter(event) {
        const { suggestions } = this.state;
        if (!suggestions.length) {
            // menu is closed so there is no selection to accept -> do nothing
            return
        }
        event.preventDefault()
        this.applySuggestion(suggestions[this.state.highlightedIndex]);
    },
}

handleKeyDown(event) 
    if (this.keyDownHandlers[event.key])
        this.keyDownHandlers[event.key].call(this, event)
}



async handleSuggestionClick(event) {
    await this.applySuggestion(event.currentTarget.textContent);
}

async applySuggestion(newValue) {
    await this.setState({ typingChecking: "false", suggestions: [], highlightedIndex: 0 });
    this.state.suggestionCallback.dispatch({
        type: 'WEB_CHAT/SET_SEND_BOX',
        payload: {
            text: newValue,
        }
    });
    await this.setState({ typingChecking: "true" });
}

handleClose(elmnt) {
    var x = document.getElementsByClassName("autocomplete-items");
    for (var i = 0; i < x.length; i++) {
        if (elmnt !== x[i]) {
            x[i].parentNode.removeChild(x[i]);
        }
    }

}

async componentDidMount() {

    try {

        await this.handleTokenGeneration();


        const store =
            window.WebChat.createStore(
                {},
                ({ getState }) => next => action => {
                    this.state.directLine.connectionStatus$
                        .subscribe(connectionStatus => {

                            //console.log("connect" + connectionStatus);
                            //console.log("LOP" + ConnectionStatus.ExpiredToken);
                            if (connectionStatus === ConnectionStatus.ExpiredToken) {
                                console.log("expired");
                            }


                            if (action.type === 'WEB_CHAT/SET_SEND_BOX') {
                                const val = action.payload.text;
                                if (this.state.typingChecking === "true") {
                                    this.setState({
                                        highlightedIndex: -1,
                                    });
                                    console.log(this.state.typingChecking);
                                    this.handleSuggestion(val, store);
                                }
                            }

                            if (action.type === 'DIRECT_LINE/DISCONNECT_FULFILLED') {
                                console.log("final" + connectionStatus);
                                console.log("finalexpired" + ConnectionStatus.ExpiredToken);
                                console.log("final");
                                this.handleTokenGeneration();
                                // fetch('api/TokenGenerationService/GetToken').then(conversation => this.state.directLine.reconnect(conversation))
                            }
                        });

                    return next(action)
                }
            );

        this.setState({ storeValue: store });


    } catch (error) {
        console.log("error in fetching token");
        console.log(error);
    }


    this.state.directLine.activity$
        .filter(activity => activity.type === 'message')
        .subscribe(function (activity)
        {
            //console.log("oooooooooooooooooooooo");
        }
           // message => console.log("received message ", message.text)
        );
}

handleSaveFeedback(ans) {

    var userID = "U92656";
    var feedbackmsg = this.state.value;
    var feedbacktype = this.state.feedBack;
    var convId = this.state.conversationId;
    fetch('api/Feedback/SaveFeedback',
        {
            method: "POST",
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ Uid: userID, FeedbackMessage: feedbackmsg, Convid: convId, FeedbackType: feedbacktype })
        }).
        then(response => response.text())
        .then(data => {

            console.log(data.getResult);
        });

    this.setState({ value: '' });
}
feedback(ans) {
    this.setState({ feedBack: ans });

    if (ans === "Send") {
        this.handleSaveFeedback(ans);
    }
    else if (ans === "Yes") {
        this.setState({ popupContent: "How was your experience?" });
        // console.log(this.state.value)
    }
    else if (ans === "No") {
        this.setState({ popupContent: "What went wrong?" });
        // console.log(this.state.value)
    }
}

handleChange = (event) => {
    this.setState({ value: event.target.value });
}

render() {

    if (!this.state.view) {
        return <div />
    } else {
        const filteredSuggestions = this.state.suggestions.filter(
            suggestion =>
                suggestion.toLowerCase().indexOf(this.state.suggestionTypedText.toLowerCase()) > -1
        );

        return (


            <div className="react-container webchat" >



                <div  onKeyDown={this.handleKeyDown.bind(this)}>
                    <div >

                <ReactWebChat  directLine={this.state.directLine} webSocket={true} userID='U92656' username='Thomas' store={this.state.storeValue} sendTypingIndicator={true} />


                    </div>
                    </div>

                <div className="SuggestionParent" id="Suggestion1">

                    {this.state.suggestions.map((suggestion, index)=> (
                        <div className={this.getSuggestionCss(index)} key={index}  onClick={this.handleSuggestionClick} >
                            {suggestion
                                .toLowerCase()
                                .startsWith(this.state.suggestionTypedText) ? (
                                    <div>

                                        <b>{this.state.suggestionTypedText}</b>
                                        {suggestion
                                            .toLowerCase()
                                            .replace(this.state.suggestionTypedText, "")}
                                    </div>
                                ) : (
                                    <div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
                                )}
                        </div>
                    ))}
                </div>



                <footer className="chat-footer" >



                    <div className="foot-footer">
                        Was I helpful ?
                        <span className="feedback" onClick={() => this.feedback("Yes")} >Yes</span><span>|</span><span className="feedback" onClick={() => this.feedback("No")}>No</span>


                        {
                            this.state.feedBack === "Yes" || this.state.feedBack === "No" ?
                                (
                                    <div className="dialog" id="myform">
                                        <div id="textfeedback">
                                            <span id="closeFeedback" onClick={() => this.feedback("Close")}>X</span>
                                            <p>{this.state.popupContent}</p>
                                            <input type="text" id="feedbacktxtbox" required name="textfeedback" placeholder="Pleasure to hear from u!"
                                                onChange={this.handleChange}
                                                value={this.state.value} />
                                            <button type="button" id="btnfeedback" onClick={() => this.feedback("Send")}>send</button>
                                        </div>
                                    </div>
                                ) : null
                        }

                    </div>
                </footer>



            </div>

        );
    }
}

}

1 Ответ

1 голос
/ 24 марта 2020

На ваш первый вопрос может быть два способа сделать это. Чтобы сделать это способом React, вы можете использовать indexOf, чтобы найти индекс пользовательского текста в предложении, а затем разбить текст на несколько элементов React, причем один из них выделен жирным шрифтом. Если вы хотите использовать replace так, как вы это делаете в настоящее время, это может быть хорошей возможностью использовать dangerouslySetInnerHTML:

<div className="SuggestionParent" id="Suggestion1">
  {this.state.suggestions.map(suggestion => (
    <div className="Suggestion" onClick={this.handleSuggestionClick} >
      <div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
    </div>
  ))}
</div>

«Опасное» предупреждение заключается в том, что вы должны не позволяют пользователю предоставлять какие-либо потенциальные значения, которые могут go во внутреннем HTML, или они могут вводить теги сценария. Пока ваши предложения берутся из фиксированной базы данных и данные защищены, с вами может быть все в порядке. В противном случае вам придется санировать HTML, и в этом случае, вероятно, будет проще вообще не использовать dangerouslySetInnerHTML. Если мы установим внутренний HTML, то мы можем использовать replace, чтобы просто напрямую применить HTML теги к строке:

getSuggestionHtml(suggestion) {
  const lowerCaseSuggestion = suggestion.toLowerCase();
  return {
    __html: lowerCaseSuggestion.includes(this.state.suggestionTypedText) ? lowerCaseSuggestion
      .replace(this.state.suggestionTypedText, `<b>${this.state.suggestionTypedText}</b>`) : lowerCaseSuggestion
  };
}

По второму вопросу вы сказали, что уже решили Это. Я вижу, что вы используете логический переключатель для временного отключения способа ответа на действие WEB_CHAT / SET_SEND_BOX.

Для вашего третьего вопроса есть много конструктивных соображений, которые вы должны задать себе о том, как выяснить, как будет работать ваш пользовательский интерфейс, например: «Что произойдет, если пользователь наводит подсказки на подсказки, когда они используют клавиши со стрелками?» и "Следует ли предварительно просмотреть выделенное предложение в поле отправки до того, как пользователь нажмет ввод?" Я надеялся найти уже существующий компонент автозаполнения React, который вы могли бы использовать вместо создания своего собственного, потому что он уже устранял бы все эти потенциальные ловушки. К сожалению, два известных пакета автозаполнения React ( здесь и здесь ) имеют одинаковые две проблемы:

  1. В настоящее время они не поддерживаются
  2. Целевой вход включен в компонент, поэтому вы не можете подключить компонент к уже существующему входу.

Тем не менее, они оба с открытым исходным кодом, поэтому мы можем моделировать наше собственное автозаполнение функциональность после них. Я проведу вас через основные функции c, и вы сможете расширить их по своему усмотрению.

События клавиатуры, как правило, обрабатываются в React с использованием свойства onKeyDown. Я поместил его в элемент, содержащий как веб-чат, так и родительский элемент ваших предложений:

<div className={ROOT_CSS} onKeyDown={this.handleKeyDown.bind(this)}>
  <div className={WEB_CHAT_CSS + ''}>
    <ReactWebChat

, который будет обрабатывать все нажатия клавиш, поэтому вам понадобится способ указать правильную клавишу для функции. , Вы можете использовать оператор switch, но исходный код для response-autocomplete использует объект поиска, и я думаю, что это разумно.

keyDownHandlers = {
  ArrowDown(event) {
    this.moveHighlight(event, 1);
  },
  ArrowUp(event) {
    this.moveHighlight(event, -1);
  },
  Enter(event) {
    const {suggestions} = this.state;
    if (!suggestions.length) {
      // menu is closed so there is no selection to accept -> do nothing
      return
    }
    event.preventDefault()
    this.applySuggestion(suggestions[this.state.highlightedIndex]);
  },
}

handleKeyDown(event) {
  if (this.keyDownHandlers[event.key])
  this.keyDownHandlers[event.key].call(this, event)
}

Я централизовал функциональность для и стрелки вниз в одну функцию: moveHighlight. Вам нужно будет определить новое свойство в вашем штате, чтобы отслеживать, какое предложение было выбрано с помощью клавиатуры. Я сохраняю имя highlightedIndex от реагирования-автозаполнения.

moveHighlight(event, direction) {
  event.preventDefault();
  const { highlightedIndex, suggestions } = this.state;
  if (!suggestions.length) return;
  let newIndex = (highlightedIndex + direction + suggestions.length) % suggestions.length;
  if (newIndex !== highlightedIndex) {
    this.setState({
      highlightedIndex: newIndex,
    });
  }
}

Чтобы клавиша ввода применила предложение, вам нужно централизовать свою функциональность, чтобы она работала так же, как мышь щелкните.

async handleSuggestionClick(event) {
  await this.applySuggestion(event.currentTarget.textContent);
}

async applySuggestion(newValue) {
  await this.setState({ typingChecking: "false", suggestions: [], highlightedIndex: 0 });
  this.state.suggestionCallback.dispatch({
    type: 'WEB_CHAT/SET_SEND_BOX',
    payload: {
      text: newValue,
    }
  });
  await this.setState({ typingChecking: "true" });
}

Наконец, убедитесь, что свойство highlightedIndex используется для различного отображения выделенного индекса.

getSuggestionCss(index) {
  return index === this.state.highlightedIndex ? HIGHLIGHTED_CSS : SUGGESTION_CSS;
}

. . .

<div className="SuggestionParent" id="Suggestion1">
  {this.state.suggestions.map((suggestion, index) => (
    <div className={this.getSuggestionCss(index)} key={index} onClick={this.handleSuggestionClick} >
      <div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
    </div>
  ))}
</div>
...