Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

828
LINES

< > BotCompany Repo | #1029565 // WebChatBot2 [dev.]

JavaX fragment (include) [tags: use-pretranspiled]

Libraryless. Compilation Failed (19458L/144K).

1  
sclass WebChatBot2 {
2  
  bool newDesign = true; // use new chat bot design
3  
  bool ariaLiveTrick = false;
4  
  bool ariaLiveTrick2 = true;
5  
  
6  
  S dbBotID = "";
7  
  S templateID = #1028952/*#1028282*/;
8  
  S cssID = #1028951;
9  
  
10  
  S thoughtBotID = null; // thought bot is this program
11  
  S botName = "Demo Bot";
12  
  S heading = "Demo Bot";
13  
  S adminName = "Demo Chat Bot Admin";
14  
  S botImageID = #1102935;
15  
  S userImageID = #1102803;
16  
  S chatHeaderImageID = #1102802;
17  
  S timeZone = ukTimeZone_string(); 
18  
  
19  
  S baseLink = "";
20  
  bool botOnRight = true;
21  
  
22  
  !include once #1028434 // WorkerChat 
23  
   new WorkerChat workerChat;
24  
  
25  
  void start {
26  
    standardTimeZone();
27  
    standardTimeZone_name = timeZone;
28  
    thoughtBot = this;
29  
    baseLink = "";
30  
    realPW(); // make password
31  
    pWebChatBot();
32  
  }
33  
  
34  
    O html(virtual Request request) {
35  
      ret main html(request);
36  
    }  
37  
    
38  
    !include #1029545 // API for Eleu
39  
  }
40  
  
41  
  void processParams(SS map) {}
42  
  
43  
   new ThreadLocal<Out> out;
44  
   new ThreadLocal<Conversation> conv;
45  
  
46  
   transient long lastConversationChange = now();
47  
  
48  
  bool testFunctions = false;
49  
  
50  
  class FormStep {
51  
    S key;
52  
    S displayText, desc;
53  
    S defaultValue;
54  
    S placeholder; // null for same as displayText, "" for none
55  
    LS buttons;
56  
    bool allowFreeText; // only matters when there are buttons
57  
    S value;
58  
    
59  
    // called before data entry
60  
    void update(Runnable onChange) {}
61  
  
62  
    // called after data entry
63  
    // return error message or null
64  
    // call conv->cancelForm(); to cancel the form (and make sure to return a text)
65  
    S verifyData(S s) { null; }
66  
  }
67  
  
68  
  class FormInFlight {
69  
    Conversation conversation;
70  
    S hashTag;
71  
    new L<FormStep> steps;
72  
    int stepIndex; // in steps list
73  
    
74  
    S handleInput(S s) { null; }
75  
    
76  
    FormStep currentStep() {
77  
      ret get(steps, stepIndex);
78  
    }
79  
    
80  
    void update(Runnable onChange) {
81  
      if (currentStep() != null) currentStep().update(onChange);
82  
    }
83  
    
84  
    S complete() { ret "Form complete"; }
85  
    S cancel() { ret "Request cancelled"; }
86  
  
87  
    FormStep byKey(S key) {
88  
      ret objectWhere(steps, +key);
89  
    }
90  
    
91  
    S getValue(S key) {
92  
      FormStep step = byKey(key);
93  
      ret step?.value;
94  
    }
95  
    
96  
    SS allValues() {
97  
      SS map = litorderedmap();
98  
      for (FormStep step : steps)
99  
        map.put(step.key, step.value);
100  
      ret map;
101  
    }
102  
    
103  
    bool allowGeneralOverride() { false; }
104  
  
105  
    Conversation cancelMe() {
106  
      Conversation conv = conversation;
107  
      conversation.cancelForm();
108  
      ret conv;
109  
    }
110  
    
111  
    void change {
112  
      if (conversation != null) conversation.change();
113  
    }
114  
  }
115  
  
116  
  bool debug;
117  
  
118  
  S answer(S s) {
119  
    out.set(new Out);
120  
    if (creator() == null) if "debug" set debug;
121  
    
122  
    S a = rawAnswer(s);
123  
    
124  
    // handle hashtags
125  
    LS tokHashtags = regexpICMatchesAsCNC(regexpNegativeLookbehind("\\w") + "#\\w+\\b", a);
126  
    for (int i = 1; i < l(tokHashtags); i += 2) {
127  
      print("Found hashtag " + tokHashtags.get(i));
128  
      /*S a2 = Handover.handleHashtag(conv!, tokHashtags.get(i));
129  
      if (a2 != null) {
130  
        print("Replaced with " + quote(a2));
131  
        tokHashtags.set(i, a2);
132  
      }*/
133  
    }
134  
    
135  
    a = join(tokHashtags);
136  
    ret deliverAnswerAndFormStep(a);
137  
  }
138  
  
139  
  S rawAnswer(S s) {
140  
    FormInFlight form = conv->form;
141  
    S a = null;
142  
    
143  
    // enter propose mode, get general answer
144  
    
145  
    O bot = dbBot();
146  
    out->proposeMode = true;
147  
    S generalAnswer;
148  
    {
149  
      // call without #default
150  
      temp tempSetTL((ThreadLocal) getOpt(bot, 'opt_noDefault), true);
151  
      generalAnswer = (S) call(bot, 'answer, s, conv->language());
152  
    }
153  
    
154  
    out->proposeMode = false;
155  
  
156  
    if (form != null && generalAnswer != null && form.allowGeneralOverride())
157  
      conv->cancelForm();
158  
    else if (form != null) {
159  
      if ((a = form.handleInput(s)) != null) ret a;
160  
  
161  
      if (eqicOneOf(s, "cancel", "Abbrechen", unicode_crossProduct())) {
162  
        S answer = form.cancel();
163  
        conv->cancelForm();
164  
        ret answer;
165  
      } else if (eqicOneOf(s, "back", "zurück", unicode_undoArrow()) && form.stepIndex > 0) {
166  
        --form.stepIndex;
167  
        conv->change();
168  
        ret "";
169  
      } else if (form.currentStep() != null) {
170  
        FormStep step = form.currentStep();
171  
        if (!step.allowFreeText && nempty(step.buttons) && !cic(step.buttons, s))
172  
          ret de() ? "Bitte wählen Sie eine Option!" : "Please choose an option.";
173  
        print("Verifying data " + quote(s) + " in step " + step);
174  
        S error = step.verifyData(s);
175  
        if (error != null)
176  
          ret error;
177  
  
178  
        step.value = s;
179  
        ++form.stepIndex;
180  
        conv->change();
181  
        if (form.currentStep() == null) {
182  
          S answer = form.complete();
183  
          if (conv->form == form)
184  
            conv->cancelForm(); // if complete() hasn't put us on a new form
185  
          ret answer;
186  
        }
187  
        ret "";
188  
      }
189  
    }
190  
  
191  
    // process general answer, switch language
192  
    
193  
    a = generalAnswer;
194  
    if (a == null)
195  
      a = (S) call(bot, 'answer, "#default", conv->language());
196  
      
197  
    if (out->proposedForm != null)
198  
      conv->setForm(out->proposedForm);
199  
      
200  
    ret a;
201  
  }
202  
  
203  
  S deliverAnswerAndFormStep(S answer) {
204  
    FormInFlight form = conv->form; // form may have cancelled itself in update
205  
    if (form == null || form.currentStep() == null)  ret answer;
206  
    
207  
    FormStep step = form.currentStep();
208  
    printVars_str(+answer, displayText := step.displayText);
209  
    answer = joinNemptiesWithSpace(answer, step.displayText);
210  
    out->placeholder = or(step.placeholder, step.displayText);
211  
    print("Step " + form.stepIndex + ": " + sfu(step));
212  
    out->defaultInput = or2(step.value, step.defaultValue);
213  
    out->buttons = cloneList(step.buttons);
214  
    if (form.stepIndex > 0) out->buttons.add(de() ? "Zurück" : "Back");
215  
    out->buttons.add(de() ? "Abbrechen" : "Cancel");
216  
    ret answer;
217  
  }
218  
  
219  
  S initialMessage() {
220  
    ret template("#greeting");
221  
  }
222  
  
223  
  sO dbBot() {
224  
    null; // TODO ret getBot(dbBotID);
225  
  }
226  
  
227  
  bool de() {
228  
    ret eqic(conv->language(), "de");
229  
  }
230  
  
231  
  // Web Chat Bot Include
232  
  
233  
  sO thoughtBot;
234  
  
235  
   int longPollTick = 200;
236  
   int longPollMaxWait = 1000*30; // lowered to 30 seconds
237  
   int activeConversationSafetyMargin = 15000; // allow client 15 seconds to reload
238  
  
239  
   Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück");
240  
  
241  
  class Out extends DynamicObject {
242  
    LS buttons;
243  
    bool multipleChoice;
244  
    S multipleChoiceSeparator;
245  
    S placeholder;
246  
    S defaultInput;
247  
    Int progressBarValue;
248  
    bool glow;
249  
    
250  
    transient bool proposeMode;
251  
    transient FormInFlight proposedForm;
252  
  }
253  
  
254  
  class Msg extends DynamicObject {
255  
    long time;
256  
    bool fromUser;
257  
    Worker fromWorker;
258  
    S text;
259  
    Out out;
260  
    
261  
    *() {}
262  
    *(bool *fromUser, S *text) { time = now(); }
263  
    *(S *text, bool *fromUser) { time = now(); }
264  
  }
265  
  
266  
  concept AuthedDialogID {
267  
    S dialogID;
268  
    Worker loggedIn; // who is logged in with this cookie
269  
  }
270  
  
271  
  //concept Session {} // LEGACY
272  
  
273  
  // our base concept - a conversation between a user and a bot or sales representative
274  
  concept Conversation {
275  
    S cookie, ip, country;
276  
    new LL<Msg> oldDialogs;
277  
    new L<Msg> msgs;
278  
    long lastPing;
279  
    bool botOn = true;
280  
    Worker worker; // who are we talking to?
281  
    transient long userTyping, botTyping; // sysNow timestamps
282  
    bool testMode;
283  
    transient bool dryRun;
284  
    transient FormInFlight proposedForm;
285  
    
286  
    FormInFlight form;
287  
    S language;
288  
    Long lastProposedDate;
289  
  
290  
    void add(Msg m) {
291  
      m.text = trim(m.text);
292  
      if (!m.fromUser && empty(m.text)) ret; // don't store empty msgs from bot
293  
      syncAdd(msgs, m);
294  
      noteConversationChange();
295  
      change();
296  
      vmBus_send chatBot_messageAdded(mc(), this, m);
297  
    }
298  
    
299  
    int allCount() { ret lengthLevel2(oldDialogs) + l(msgs); }
300  
    int archiveSize() { ret lengthLevel2(oldDialogs); }
301  
    
302  
    long lastMsgTime() { Msg m = last(msgs); ret m == null ? 0 : m.time; }
303  
    
304  
    void cancelForm {
305  
      if (form != null) {
306  
        print("Cancelling form " + form);
307  
        form.conversation = null;
308  
        cset(this, form := null);
309  
      }
310  
    }
311  
    
312  
    <A extends FormInFlight> A setForm(A form) {
313  
      form.conversation = this;
314  
      cset(this, +form);
315  
      ret form;
316  
    }
317  
    
318  
    S language() { ret or2(language, 'en); }
319  
    
320  
    void updateForm {
321  
      if (form != null) form.update(r change);
322  
    }
323  
    
324  
    void turnBotOff {
325  
      cset(this, botOn := false);
326  
      noteConversationChange();
327  
      updateForm();
328  
    }
329  
  
330  
    void turnBotOn {  
331  
      cset(this, botOn := true, worker := null);
332  
      S backMsg = getCannedAnswer("#botBack", this);
333  
      if (empty(msgs) || lastMessageIsFromUser() || !eq(last(msgs).text, backMsg))
334  
        add(new Msg(backMsg, false));
335  
      noteConversationChange();
336  
    }
337  
    
338  
    bool lastMessageIsFromUser() {
339  
      ret nempty(msgs) && last(msgs).fromUser;
340  
    }
341  
    
342  
    void newDialog {
343  
      cancelForm();
344  
      lastProposedDate = null;
345  
      oldDialogs.add(msgs);
346  
      cset(this, msgs := new L);
347  
      change();
348  
      vmBus_send chatBot_clearedSession(mc(), this);
349  
    }
350  
  }
351  
  
352  
  void pWebChatBot {
353  
    dbIndexing(Conversation, 'cookie, Conversation, 'worker,
354  
      Conversation, 'lastPing,
355  
      Worker, 'loginName, AuthedDialogID, 'dialogID);
356  
    Class envType = fieldType(thoughtBot, "env");
357  
    if (envType != null)
358  
      setOpt(thoughtBot, "env", proxy(envType, (O) mc()));
359  
  }
360  
  
361  
  sO html(virtual WebRequest request) {
362  
    temp tempRegisterThread();
363  
    
364  
    S uri = cast get(request, 'uri);
365  
    SS params = cast get(request, 'params);
366  
    S cookie = params.get('cookie);
367  
    if (empty(cookie)) cookie = (S) call(request, 'cookie);
368  
  
369  
    bool workerMode = nempty(params.get("workerMode")) || startsWith(uri, "/worker");
370  
    
371  
    Conversation conv = nempty(cookie) ? getConv(cookie) : null;
372  
    if (conv != null && !workerMode)
373  
      cset(conv, ip := (S) call(request, 'clientIP));
374  
    print("URI: " + uri + ", cookie: " + cookie + ", msgs: " + l(conv.msgs));
375  
    
376  
    S dialogID = getDialogID(); // for authing
377  
    
378  
    S pw = trim(params.get('pw));
379  
    if (nempty(pw)) {
380  
      if (neq(pw, realPW()))
381  
        ret errorMsg("Bad password, please try again");
382  
      uniq AuthedDialogID(+dialogID);
383  
      if (nempty(params.get('redirect)))
384  
        ret hrefresh(params.get('redirect));
385  
    }
386  
    
387  
    new Matches m;
388  
    if (startsWith(uri, "/worker-image/", m)) {
389  
      long id = parseLong(m.rest());
390  
      ret subBot_serveFile(workerImageFile(id), "image/jpeg");
391  
    }
392  
    
393  
    AuthedDialogID auth = authObject();
394  
    bool requestAuthed = auth != null;
395  
    
396  
    if (eq(uri, "/stats")) {
397  
      if (!requestAuthed) ret serveAuthForm(rawLink(uri));
398  
      ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads()));
399  
    }
400  
        
401  
    if (eq(uri, "/logs")) {
402  
      if (!requestAuthed) ret serveAuthForm(rawLink(uri));
403  
      ret webChatBotLogsHTML2(rawLink(uri), params);
404  
    }
405  
    
406  
    if (eq(uri, "/auth-only")) {
407  
      if (eq(params.get('logout), "1"))
408  
        cdelete(AuthedDialogID, dialogID := getDialogID());
409  
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
410  
      ret "";
411  
    }
412  
    
413  
    if (workerChat != null)
414  
      try object workerChat.html(uri, params, conv, auth);
415  
  
416  
    {
417  
      lock dbLock();
418  
      
419  
      S message = trim(params.get("btn"));
420  
      if (empty(message)) message = trim(params.get("message"));
421  
      
422  
      if (match("new dialog", message)) {
423  
        conv.newDialog();
424  
        message = null;
425  
      }
426  
      
427  
      main.conv.set(conv);
428  
      
429  
      if (!workerMode && empty(conv.msgs))
430  
        addReplyToConvo(conv, () -> deliverAnswerAndFormStep(initialMessage()));
431  
  
432  
      if (nempty(message) && !lastUserMessageWas(conv, message)) {
433  
        print("Adding message: " + message);
434  
        if (workerMode) {
435  
          Msg msg = new(false, message);
436  
          msg.fromWorker = auth.loggedIn;
437  
          conv.add(msg);
438  
        } else
439  
          conv.add(new Msg(true, message));
440  
      }
441  
      
442  
      S testMode = params.get("testMode");
443  
      if (nempty(testMode)) {
444  
        print("Setting testMode", testMode);
445  
        cset(conv, testMode := eq("1", testMode));
446  
      }
447  
    
448  
      if (!workerMode && conv.botOn && nempty(conv.msgs) && last(conv.msgs).fromUser)
449  
        addReplyToConvo(conv, () -> makeReply(last(conv.msgs).text));
450  
    } // locked
451  
    
452  
    if (eq(uri, "/msg")) ret withHeader("OK");
453  
    
454  
    if (eq(uri, "/typing")) {
455  
      if (workerMode) {
456  
        conv.botTyping = sysNow();
457  
        print(conv.botTyping + " Bot typing in: " + conv.cookie);
458  
      } else {
459  
        conv.userTyping = sysNow();
460  
        print(conv.userTyping + " User typing in: " + conv.cookie);
461  
      }
462  
      ret withHeader("OK");
463  
    }
464  
    
465  
    if (eq(uri, "/incremental")) {
466  
      vmBus_send chatBot_userPolling(mc(), conv);
467  
      cset(conv, lastPing := now());
468  
      int a = parseInt(params.get("a"));
469  
  
470  
      long start = sysNow();
471  
      L msgs;
472  
      bool first = true;
473  
      while (licensed() && sysNow() < start+longPollMaxWait) {
474  
        int as = conv.archiveSize();
475  
        msgs = cloneSubList(conv.msgs, a-as);
476  
        bool newDialog = a <= as;
477  
        long typing = workerMode ? conv.userTyping : conv.botTyping;
478  
        bool otherPartyTyping = typing > start;
479  
        
480  
        if (empty(msgs) && !otherPartyTyping) {
481  
          if (first) {
482  
            print("Long poll starting on " + cookie + ", " + a + "/" + a);
483  
            first = false;
484  
          }
485  
          sleep(longPollTick);
486  
        } else {
487  
          if (first) print("Long poll ended.");
488  
          new StringBuilder buf;
489  
          if (otherPartyTyping) {
490  
            print("Noticed " + (workerMode ? "user" : "bot") + " typing in " + conv.cookie);
491  
            buf.append(hscript("showTyping();"));
492  
          }
493  
          renderMessages(buf, msgs);
494  
          if (ariaLiveTrick2 && !workerMode) {
495  
            Msg msg = lastBotMsg(msgs);
496  
            if (msg != null) {
497  
              S author = msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : botName;
498  
              buf.append(hscript([[$("#screenreadertrick").html(]] + jsQuote(author + " says: " + msg.text) + ");"));
499  
            }
500  
          }
501  
          if (a != 0 && anyInterestingMessages(msgs, workerMode))
502  
            buf.append(hscript(
503  
              "window.playChatNotification();\n" +
504  
              "window.setTitleStatus(" + jsQuote((workerMode ? "User" : botName) + " says…") + ");"
505  
            ));
506  
          ret withHeader("<!-- " + conv.allCount() + " " + (newDialog ? "NEW DIALOG " : "") + "-->\n"  + buf);
507  
        }
508  
      }
509  
      ret withHeader("");
510  
    }
511  
    
512  
    {
513  
      lock dbLock();
514  
      processParams(params);
515  
      S html = loadSnippet(templateID); // TODO: cache
516  
  
517  
      S workerModeParam = workerMode ? "workerMode=1&" : "";
518  
      S langlinks = "<!-- langlinks here -->";
519  
      if (html.contains(langlinks))
520  
        html = html.replace(langlinks,
521  
          ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German"));
522  
      
523  
      html = html.replace("#BOTIMG#", imageSnippetURLOrEmptyGIF(chatHeaderImageID));
524  
      html = html.replace("#N#", "0");
525  
      html = html.replace("#INCREMENTALURL#", baseLink + "/incremental?" + workerModeParam + "a=");
526  
      html = html.replace("#MSGURL#", baseLink + "/msg?" + workerModeParam + "message=");
527  
      html = html.replace("#TYPINGURL#", baseLink + "/typing?" + workerModeParam);
528  
      html = html.replace("#CSS_ID#", psI_str(cssID));
529  
      if (ariaLiveTrick || ariaLiveTrick2)
530  
        html = html.replace([[aria-live="polite">]], ">");
531  
      html = html.replace("#OTHERSIDE#", workerMode ? "User" : "Representative");
532  
      if (nempty(params.get("debug")))
533  
        html = html.replace("var showActions = false;", "var showActions = true;");
534  
    
535  
      html = html.replace("#AUTOOPEN#", jsBool(workerMode || botAutoOpen()));
536  
      html = html.replace("#BOT_ON#", jsBool(botOn()));
537  
      html = html.replace("$HEADING", heading);
538  
      html = html.replace("#WORKERMODE", jsBool(workerMode));
539  
      html = html.replace("<!-- MSGS HERE -->", "");
540  
      html = hreplaceTitle(html, heading);
541  
      
542  
      if (eqGet(params, "_botDemo", "1"))
543  
        ret hhtml(hhead(
544  
          htitle(heading)
545  
          + loadJQuery()
546  
        ) + hbody(hjavascript(html)));
547  
      else
548  
        ret withHeader(subBot_serveJavaScript(html));
549  
    }
550  
  }
551  
  
552  
  void addReplyToConvo(Conversation conv, IF0<S> think) {
553  
    out.set(new Out);
554  
    S reply = "";
555  
    pcall {
556  
      reply = think!;
557  
    }
558  
    Msg msg = new Msg(false, reply);
559  
    msg.out = out!;
560  
    conv.add(msg);
561  
  }
562  
  
563  
  sO withHeader(S html) {
564  
    ret withHeader(subBot_noCacheHeaders(subBot_serveHTML(html)));
565  
  }
566  
  
567  
  sO withHeader(O response) {
568  
    call(response, 'addHeader, "Access-Control-Allow-Origin", "*");
569  
    ret response;
570  
  }
571  
  
572  
  S renderMessageText(S text, bool htmlEncode) {
573  
    text = trim(text);
574  
    if (htmlEncode) text = htmlEncode2(text);
575  
    text = nlToBr(text);
576  
    ret replace(text, ":wave:", html_wavingHand());
577  
  }
578  
  
579  
  void renderMessages(StringBuilder buf, L<Msg> msgs) {
580  
    if (empty(msgs)) ret;
581  
    new Set<S> buttonsToSkip;
582  
    new LS buttonsHtml;
583  
    for (Msg m : msgs) {
584  
      if (!m.fromUser && eq(m.text, "-")) continue;
585  
      S html = renderMessageText(m.text, shouldHtmlEncodeMsg(m));
586  
      // pull back & cancel buttons to beginning of msg
587  
      if (m == last(msgs) && m.out != null) {
588  
        fOr (S btn : m.out.buttons)
589  
          if (specialButtons.contains(btn)) {
590  
            buttonsToSkip.add(btn);
591  
            buttonsHtml.add(renderButtons(ll(btn)));
592  
          }
593  
      }
594  
      if (nempty(buttonsHtml) && l(m.out.buttons) == l(buttonsToSkip))
595  
        html += " " + hspan("&nbsp;&nbsp;", class := "chat-button-span") + lines(buttonsHtml);
596  
      else
597  
        buttonsToSkip.clear();
598  
      appendMsg(buf, m.fromUser ? defaultUserName() : botName, formatTime(m.time), html, !m.fromUser, m.fromWorker);
599  
    }
600  
        
601  
    appendButtons(buf, last(msgs).out, buttonsToSkip);
602  
  }
603  
  
604  
  void appendMsg(StringBuilder buf, S name, S time, S text, bool bot, Worker fromWorker) {
605  
    bool useTrick = ariaLiveTrick;
606  
    S tag = useTrick ? "div" : "span";
607  
    if (bot) {
608  
      S id = randomID();
609  
      S author = fromWorker != null ? htmlEncode2(fromWorker.displayName ): botName;
610  
      if (fromWorker != null) buf.append([[<div class="chat_botname"><p>]] + author + [[</p>]]);
611  
      buf.append("<" + tag + [[ class="chat_msg_item chat_msg_item_admin"]] + (useTrick ? [[ id="]] + id + [[" aria-live="polite" tabindex="-1"]] : "") + ">");
612  
      S imgURL = snippetImgLink(botImageID);
613  
      if (fromWorker != null && fileExists(workerImageFile(fromWorker.id)))
614  
        imgURL = fullRawLink("worker-image/" + fromWorker.id);
615  
    
616  
      if (nempty(imgURL))
617  
        buf.append([[
618  
          <div class="chat_avatar">
619  
            <img src="$IMG"/>
620  
          </div>]]
621  
          .replace("$IMG", imgURL));
622  
        
623  
      buf.append([[<span class="sr-only">]] + (fromWorker != null ? "" : botName + " ") + [[says</span>]]);
624  
      buf.append(text);
625  
      buf.append([[</]] + tag + [[>]]);
626  
      if (fromWorker != null) buf.append("</div>");
627  
      if (useTrick) buf.append(hscript("$('#" + id + "').focus();"));
628  
    } else
629  
      buf.append(([[
630  
        <span class="sr-only">You say</span>
631  
        <]] + tag + [[ class="chat_msg_item chat_msg_item_user"]] + (useTrick ? [[ aria-live="polite"]] : "") + [[>$TEXT</]] + tag + [[>
632  
      ]]).replace("$TEXT", text));
633  
  }
634  
  
635  
  S replaceButtonText(S s) {
636  
    if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow();
637  
    if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct();
638  
    ret s;
639  
  }
640  
  
641  
  S renderMultipleChoice(LS buttons, Cl<S> selections, S multipleChoiceSeparator) {
642  
    print(+selections);
643  
    Set<S> selectionSet = asCISet(selections);
644  
    S rand = randomID();
645  
    S className = "chat_multiplechoice_" + rand;
646  
    S allCheckboxes = [[$(".]] + className + [[")]];
647  
    ret joinWithBR(map(buttons, name ->
648  
      hcheckbox("", contains(selectionSet, name),
649  
        value := name,
650  
        class := className) + " " + name))
651  
      + "<br>" + hbuttonOnClick_returnFalse("OK", "submitMsg()", style := "float: right")
652  
      + hscript(allCheckboxes
653  
        + ".change(function() {"
654  
        //+ "  console.log('multiple choice change');"
655  
        + " var theList = $('." + className + ":checkbox:checked').map(function() { return this.value; }).get();"
656  
        + "  console.log('theList: ' + theList);"
657  
        + "  $('#chat_message').val(theList.join(" + jsQuote(multipleChoiceSeparator) + "));"
658  
        + "});");
659  
  }
660  
  
661  
  S renderButtons(LS buttons) {
662  
    new LS out;
663  
    for i over buttons: {
664  
      S code = buttons.get(i);
665  
      S text = replaceButtonText(code);
666  
      out.add(hbuttonOnClick_returnFalse(text, "submitAMsg(" + jsQuote(text) + ")", class := "chatbot-choice-button", title := eq(code, text) ? null : code));
667  
      if (!specialButtons.contains(code)
668  
        && i+1 < l(buttons) && specialButtons.contains(buttons.get(i+1)))
669  
        out.add("&nbsp;&nbsp;");
670  
    }
671  
    ret lines(out);
672  
  }
673  
  
674  
  void appendButtons(StringBuilder buf, Out out, Set<S> buttonsToSkip) {
675  
    S placeholder = out == null ? "" : unnull(out.placeholder);
676  
    S defaultInput = out == null ? "" : unnull(out.defaultInput);
677  
    buf.append(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");"));
678  
    if (out == null) ret;
679  
    LS buttons = listMinusSet(out.buttons, buttonsToSkip);
680  
    if (empty(buttons)) ret;
681  
    printVars_str(+buttons, +buttonsToSkip);
682  
    S buttonsHtml;
683  
    if (out.multipleChoice)
684  
      buttonsHtml = renderMultipleChoice(buttons,
685  
        mcSplit(out.defaultInput, out.multipleChoiceSeparator), out.multipleChoiceSeparator);
686  
    else
687  
      buttonsHtml = renderButtons(buttons);
688  
    
689  
    buf.append([[<span class="chat_msg_item chat_msg_item_admin chat_buttons">]]);
690  
    buf.append(buttonsHtml);
691  
    buf.append([[</span>]]);
692  
  }
693  
  
694  
  void appendDate(StringBuilder buf, S date) {
695  
    buf.append([[
696  
    <div class="chat-box-single-line">
697  
      <abbr class="timestamp">DATE</abbr>
698  
    </div>]].replace("DATE", date));
699  
  }
700  
  
701  
   bool lastUserMessageWas(Conversation conv, S message) {
702  
    Msg m = last(conv.msgs);
703  
    ret m != null && m.fromUser && eq(m.text, message);
704  
  }
705  
  
706  
  S makeReply(S message) {
707  
    try {
708  
      ret answer(message);
709  
    } catch print e {
710  
      ret "Internal error";
711  
    }
712  
  }
713  
  
714  
  S formatTime(long time) {
715  
    ret timeInTimeZoneWithOptionalDate_24(timeZone, time);
716  
  }
717  
  
718  
  S formatDialog(S id, L<Msg> msgs) {
719  
    new L<S> lc;
720  
    for (Msg m : msgs)
721  
      lc.add(htmlencode((m.fromUser ? "> " : "< ") + m.text));
722  
    ret id + ul(lc);
723  
  }
724  
  
725  
   Conversation getConv(fS cookie) {
726  
    ret withDBLock(func -> Conversation {
727  
      uniq(Conversation, +cookie)
728  
    });
729  
  }
730  
  
731  
  S serveAuthForm(S redirect) {
732  
    ret hhtml(hhead(htitle("Authorization required")) + hbody(hfullcenter(
733  
      h3_htmlEncode(adminName)
734  
      + hpostform(
735  
        hhidden(+redirect)
736  
      + "Password: " + hpassword('pw) + "<br><br>" + hsubmit()))));
737  
  }
738  
  
739  
  S errorMsg(S msg) {
740  
    ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back"))));
741  
  }
742  
  
743  
  S defaultUserName() {
744  
    ret de() ? "Sie" : "You";
745  
  }
746  
  
747  
  bool botOn() {
748  
    ret isTrue(call(dbBot(), "botOn"));
749  
  }
750  
  
751  
  bool botAutoOpen() {
752  
    ret isTrue(call(dbBot(), "botAutoOpen"));
753  
  }
754  
  
755  
   File workerImageFile(long id) {
756  
    ret id == 0 ? null : javaxDataDir("adaptive-bot/images/" + id + ".jpg");
757  
  }
758  
  
759  
   long activeConversationTimeout() {
760  
    ret longPollMaxWait+activeConversationSafetyMargin;
761  
  }
762  
  
763  
   AuthedDialogID authObject() {
764  
    ret conceptWhere AuthedDialogID(dialogID := getDialogID());
765  
  }
766  
  
767  
  bool anyInterestingMessages(L<Msg> msgs, bool workerMode) {
768  
    ret any(msgs, m -> m.fromUser == workerMode);
769  
  }
770  
  
771  
  bool shouldHtmlEncodeMsg(Msg msg) {
772  
    ret msg.fromUser;
773  
  }
774  
  
775  
  S getCountry(Conversation c) {
776  
    if (empty(c.country) && nempty(c.ip))
777  
      cset(c, country := ipToCountry2020_safe(c.ip));
778  
    ret or2(c.country, "?");
779  
  }
780  
  
781  
  void noteConversationChange {
782  
    lastConversationChange = now();
783  
  }
784  
  
785  
  // TODO: check that action is persistable & persist
786  
  void addTimeout(double seconds, Runnable action) {
787  
    doAfter(seconds, action);
788  
    print("Timeout added: " + seconds + " => " + action);
789  
  }
790  
  
791  
  S template(S hashtag, O... params) {
792  
    ret replaceSquareBracketVars(getCannedAnswer(hashtag), params);
793  
  }
794  
  
795  
  S getCannedAnswer(S hashtag, Conversation conv default null) {
796  
    if (!startsWith(hashtag, "#")) ret hashtag;
797  
    S lang = conv == null ? "en" : conv.language();
798  
    temp tempSetTL((ThreadLocal) getOpt(dbBot(), 'opt_noDefault), true);
799  
    ret or2((S) call(dbBot(), 'answer, hashtag, lang), hashtag); // keep hashtag if no answer found
800  
  }
801  
  
802  
   LS mcSplit(S input, S multipleChoiceSeparator) {
803  
    ret trimAll(splitAt(input, dropSpaces(multipleChoiceSeparator)));
804  
  }
805  
  
806  
   Msg lastBotMsg(L<Msg> l) {
807  
    ret lastThat(l, msg -> !msg.fromUser);
808  
  }
809  
  
810  
  S dbStats() {
811  
    Cl<Conversation> all = list(Conversation);
812  
    int nRealConvos = countPred(all, c -> l(c.msgs) > 1);
813  
    //int nTestConvos = countPred(all, c -> startsWith(c.cookie, "test_"));
814  
    //ret n2(l(all)-nTestConvos, "real conversation") + ", " + n2(nTestConvos, "test conversation");
815  
    ret nConversations(nRealConvos);
816  
  }
817  
  
818  
  void proposeForm(Conversation conversation default conv!, FormInFlight form) {
819  
    if (out! != null && out->proposeMode)
820  
      out->proposedForm = form;
821  
    else if (conversation != null)
822  
      conversation.setForm(form);
823  
  }
824  
  
825  
  S realPW() {
826  
    ret loadSecretTextFileOrCreateWithRandomID("password.txt");
827  
  }
828  
}

Author comment

Began life as a copy of #1029541

download  show line numbers  debug dex  old transpilations   

Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv

No comments. add comment

Snippet ID: #1029565
Snippet name: WebChatBot2 [dev.]
Eternal ID of this version: #1029565/4
Text MD5: d3d40c0953ddc6f19576d398cccee6f9
Transpilation MD5: 0762d8d211f3a9687ef40b30999cc093
Author: stefan
Category: javax / web chat bots
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-08-24 12:05:27
Source code size: 26494 bytes / 828 lines
Pitched / IR pitched: No / No
Views / Downloads: 202 / 259
Version history: 3 change(s)
Referenced in: [show references]