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(" ", 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(" "); |
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 | } |
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] |