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

865
LINES

< > BotCompany Repo | #1028422 // BookBetter Chat Bot

JavaX source code (desktop) [tags: butter use-pretranspiled] - run with: x30.jar - homepage

Download Jar. Uses 5016K of libraries. Click here for Pure Java version (47085L/261K).

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

Author comment

Began life as a copy of #1028300

download  show line numbers  debug dex  old transpilations   

Travelled to 5 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, tvejysmllsmz, xrpafgyirdlv

No comments. add comment

Snippet ID: #1028422
Snippet name: BookBetter Chat Bot
Eternal ID of this version: #1028422/98
Text MD5: 042e2d540b0f9b0a13adb25032b5a816
Transpilation MD5: d915ba7ea4136bba63ad6e6d449671d6
Author: stefan
Category: javax / web chat bots
Type: JavaX source code (desktop)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2022-05-21 17:43:45
Source code size: 26273 bytes / 865 lines
Pitched / IR pitched: No / No
Views / Downloads: 511 / 2192
Version history: 97 change(s)
Referenced in: [show references]