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

837
LINES

< > BotCompany Repo | #1029541 // Web Chat Bot as module Spike [OK, shows the bot]

JavaX source code (Dynamic Module) [tags: use-pretranspiled] - run with: Stefan's OS

Uses 911K of libraries. Click here for Pure Java version (17099L/100K).

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

Author comment

Began life as a copy of #1028922

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: #1029541
Snippet name: Web Chat Bot as module Spike [OK, shows the bot]
Eternal ID of this version: #1029541/18
Text MD5: 7b9888c36aea3f85bdfa6763a3f2f26e
Transpilation MD5: 3d8788ff514e766e5aaa69adc3fd75c9
Author: stefan
Category: javax / web chat bots
Type: JavaX source code (Dynamic Module)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-08-24 10:11:38
Source code size: 25237 bytes / 837 lines
Pitched / IR pitched: No / No
Views / Downloads: 171 / 972
Version history: 17 change(s)
Referenced in: [show references]