Libraryless. Click here for Pure Java version (32571L/193K).
1 | // Note: use module, not cmodule - we want our own structure() because of DynamicObject |
2 | |
3 | set flag DynModule. |
4 | |
5 | abstract sclass DynGazelleWebServer > DynPrintLogAndEnabled { |
6 | new Set<S> authedCookies; |
7 | bool allowVisitors = true; |
8 | int httpPort = 80; |
9 | transient int absoluteMaxResults = 1000, defaultMaxResults = 100; |
10 | |
11 | transient MyHTTPD server; |
12 | transient new GazelleContextCache_v2 contextCache; |
13 | |
14 | transient LS searchTypes = ll("", "Rule", "Line"); |
15 | |
16 | S switchableFields() { ret "allowVisitors httpPort"; } |
17 | |
18 | transient new HTTPSpamBlocker1 spamBlocker; |
19 | transient double spammerSleep = 60.0; |
20 | |
21 | void start() ctex { |
22 | super.start(); |
23 | if (!enabled) ret; |
24 | |
25 | vm_cleanPrints(); |
26 | dm_useLocalMechListCopies(); |
27 | |
28 | ownResource(contextCache.listenToMessages()); |
29 | contextCache.alwaysFullRefresh(); |
30 | |
31 | server = new MyHTTPD(httpPort); |
32 | server.serveFunction = func(S uri, SS parms) { |
33 | serve(uri, parms) |
34 | }; |
35 | server.start(); |
36 | print("HTTP server started on port " + server.getPort()); |
37 | } |
38 | |
39 | void cleanMeUp { |
40 | server.stop(); |
41 | server = null; |
42 | } |
43 | |
44 | O serve(S uri, SS params) enter { |
45 | print exceptions { |
46 | ret newRequest().serve(uri, params); |
47 | } |
48 | } |
49 | |
50 | Request newRequest() { |
51 | ret new Request; |
52 | } |
53 | |
54 | S lineURL(long msgID) { ret msgID == 0 ? null : "/line/" + msgID; } |
55 | S lineURL(S context) { ret empty(context) ? null : "/line/" + urlencode(context); } |
56 | S ruleURL(S ruleID) { ret "/rule/" + urlencode(ruleID); } |
57 | |
58 | long contextToMsgID(S s) { |
59 | new Matches m; |
60 | if "discord msg *" if (isInteger($1)) ret parseLong($1); |
61 | ret 0; |
62 | } |
63 | |
64 | class Request { |
65 | bool authed; |
66 | S uri; |
67 | SS params; |
68 | O[] bodyParams; |
69 | Pair<Int, Bool> spamCheck; |
70 | |
71 | O spamTest() { |
72 | spamCheck = spamBlocker.checkRequest(uri, clientIP()); |
73 | if (spamCheck.b) { |
74 | sleepSeconds(spammerSleep); |
75 | ret print("go away"); |
76 | } |
77 | |
78 | null; |
79 | } |
80 | |
81 | O serve(S uri, SS params) { |
82 | this.uri = uri; |
83 | this.params = params; |
84 | new Matches m; |
85 | |
86 | try object spamTest(); |
87 | |
88 | S domain = serveHttp_domainName(); |
89 | for (S a, S b : parseDoubleArrowLinkedHashMap(loadTextFile(javaxDataDir("gazelle-redirects.txt")))) { |
90 | if (swic(domain, a)) |
91 | ret hrefresh(doubleToSingleSlashesInURL(b + uri)); |
92 | } |
93 | |
94 | S cookie = serveHttp_cookieHandling(); |
95 | authed = nempty(cookie) && syncContains(authedCookies, cookie); |
96 | print("Cookie: " + cookie + ", authed: " + authed + ", spam count: " + pairA(spamCheck)); |
97 | S master = trim(loadTextFile(javaxSecretDir("gazelle-master-pw"))); |
98 | S attempt = params.get('_pass); |
99 | if (nempty(attempt) && nempty(cookie) && nempty(master)) { |
100 | if (eq(attempt, master)) { |
101 | print("Login SUCCEEDED"); |
102 | syncAdd(authedCookies, cookie); |
103 | change(); |
104 | authed = true; |
105 | } else |
106 | print("Login FAILED"); |
107 | } |
108 | |
109 | bodyParams = litobjectarray(style := "font-family: Roboto Mono; " + (authed ? "background-image: url(\"" + imageServerLink(#1101575) + "\")" : "background-color: #FFFF88")); |
110 | |
111 | if (eq(uri, "/logout")) { |
112 | authedCookies.remove(cookie); |
113 | ret "OK, logged out"; |
114 | } |
115 | |
116 | if (eq(uri, "/login")) |
117 | ret hframe("Login", |
118 | hfullcenter(h3(htmlEncode2(gazelle_name()) + " Login") |
119 | + (nempty(master) ? hpostform(hpasswordfield('_pass), action := or(params.get('uri), "/")) : "No master PW"))); |
120 | |
121 | if (eq(uri, "/favicon.ico")) |
122 | ret serveFile(loadLibrary(gazelle_faviconID()), faviconMimeType()); |
123 | |
124 | if (!allowVisitors && !authed) |
125 | ret hrefresh("/login" + hquery(+uri)); |
126 | |
127 | ret serve2(); |
128 | } |
129 | |
130 | O serve2() { |
131 | new Matches m; |
132 | |
133 | if (eq(uri, "/tests")) |
134 | ret serveTests(); |
135 | |
136 | if (swic(uri, "/texts/", m)) { |
137 | S textID = m.rest(); |
138 | if (!possibleMD5(textID)) ret serve404(); |
139 | ret serveTextFileAsUTF8(javaxDataDir("Gazelle Texts/" + textID)); |
140 | } |
141 | |
142 | if (eq(uri, "/testInput")) |
143 | ret serveTestInput(); |
144 | |
145 | if (eq(uri, "/lastOutput")) { |
146 | L<GazelleLine> lines = reversed(takeLast(10, dm_discord_allBotLines())); |
147 | ret hframe("Last Output", hcenter(hheading("Last Output") |
148 | + htmlTable2( |
149 | map(lines, line -> { |
150 | L applications = dm_gazelle_applicationsForMsgID(line.msgID); |
151 | //print("Got " + n2(applications, "application") + " for " + line.msgID + " from " + dm_gazelle_longFeedbackCRUD()); |
152 | ret litorderedmap( |
153 | "Date" := spanTitle("Message ID: " + line.msgID, htmlencode2(localDateWithMinutes(line.timestamp))), |
154 | "Gazelle said:" := ahref(lineURL(line.msgID), htmlencode2(line.text)), |
155 | "Applied rules" := joinWithSpace(map(f<S, S> ruleLink, collect ruleID(applications))) |
156 | ); |
157 | }), htmlEncode := false, tableParams := litparams(cellpadding := 5)) |
158 | )); |
159 | } |
160 | |
161 | if (eq(uri, "/json/rawInputLines")) |
162 | ret serveText(jsonEncode(collect text(dm_discord_allLines()))); |
163 | |
164 | if (eq(uri, "/channels")) |
165 | ret serveJSON(collectMulti_notNulls(ll('channelID, 'name), dm_discord_channels())); |
166 | |
167 | if (eq(uri, "/allDistinctInputs")) |
168 | ret serveJSON(uniquify(dm_discord_allTexts())); |
169 | |
170 | if (eq(uri, "/allLines")) { |
171 | LS fields = tok_splitAtComma_emptyOnEmpty(params.get('fields)); |
172 | if (empty(fields)) ret serve500("Need fields param"); |
173 | // TODO: restrict fields? seems OK though, it's just fields of GazelleLine |
174 | ret serveJSON(collectMulti_notNulls(fields, selectLines(params))); |
175 | } |
176 | |
177 | if (eq(uri, "/lastInput")) { |
178 | L<GazelleLine> lines = reversed(takeLast(30, dm_discord_allLines())); |
179 | ret hframe("Last Input", hcenter(hheading("Last Input") |
180 | + htmlTable2( |
181 | map(lines, line -> { |
182 | //L applications = dm_gazelle_applicationsForMsgID(line.msgID); |
183 | ret litorderedmap( |
184 | "Date" := spanTitle("Message ID: " + line.msgID, htmlencode2(localDateWithMinutes(line.timestamp))), |
185 | "Author" := htmlencode2(line.user), |
186 | "Bot?" := htmlencode2(line.bot ? "yes" : ""), |
187 | "Line" := ahref(lineURL(line.msgID), htmlencode2(escapeNewLines(line.text))), |
188 | //"Applied rules" := joinWithSpace(map(f<S, S> ruleLink, collect ruleID(applications))), |
189 | "More" := moreForMsgID(line.msgID) |
190 | ); |
191 | }), htmlEncode := false, tableParams := litparams(cellpadding := 5)) |
192 | )); |
193 | } |
194 | |
195 | if (eq(uri, "/commentPatterns")) |
196 | ret hframeAndHeading("Comment patterns that mean something", |
197 | htmlTable2(listToMapsWithSingleKey("Pattern", gazelle_commentPatterns()))); |
198 | |
199 | if (eq(uri, "/ruleComments")) |
200 | ret serveRuleComments(); |
201 | |
202 | if (swic(uri, "/revisit/", m)) { |
203 | long msgID = assertNotZero(parseLong(m.rest())); |
204 | GazelleLine line = dm_discord_lineForMsgID(msgID); |
205 | if (line == null) ret "Message not found: " + msgID; |
206 | long startTime = sysNow(); |
207 | L<virtual GazelleTree> tree = evalWithTimeoutOrNull(30.0, func -> L<virtual GazelleTree> { |
208 | /*dm_gazelle_revisitChatLine(msgID, contextMaker := contextCache, skipBad := false)*/ |
209 | (L) dm_call('gazelleLineRevisiter, 'revisitChatLine, msgID, skipBad := false) |
210 | }); |
211 | long endTime = sysNow(); |
212 | |
213 | S tv = lines(map(f<O, S> gazelleTreeToHTML, tree)); |
214 | |
215 | ret hframe("Revisiting msg " + msgID, |
216 | hheading("Revisiting message " + msgID + ": " + htmlEncode2(line.text)) |
217 | + nlToBr_withIndents(tv) |
218 | + p((endTime-startTime) + " ms")); |
219 | } |
220 | |
221 | if (swic(uri, "/rule/", m)) { |
222 | S ruleID = assertGlobalID(m.rest()); |
223 | ret serveRule(ruleID); |
224 | } |
225 | |
226 | if (swic(uri, "/ruleApplication/", m)) { |
227 | S globalID = assertGlobalID(m.rest()); |
228 | ret serveRuleApplication(globalID); |
229 | } |
230 | |
231 | if (swic(uri, "/rawReasoning/", m)) { |
232 | S context = urldecode(m.rest()); |
233 | File f = gazelle_discord_reasoningFileForContext(context); |
234 | ret serveText(loadGZippedTextFile(f)); |
235 | } |
236 | |
237 | if (eq(uri, "/createRule")) ret createRule(); |
238 | |
239 | if (swic(uri, "/line/", m)) { |
240 | ret serveLine(urldecode(m.rest())); |
241 | } |
242 | |
243 | if (eq(uri, "/search") || eq(uri, "/") && nemptyAfterTrim(params.get('q))) |
244 | ret search(params); |
245 | |
246 | if (eq(uri, "/wikipedia")) |
247 | ret serveWikipedia(); |
248 | |
249 | if (eq(uri, "/commands")) |
250 | ret hframe("Commands", |
251 | linesLL(hheading("Commands"), |
252 | h3("!eval"), |
253 | |
254 | p([[You can evaluate Java code directly through Discord. |
255 | Unless you are specifically authorized, only a ]] + ahref(rawSelfLink("safeIdentifiers"), "safe subset of identifiers") + " is allowed."), |
256 | |
257 | p("Example: " + tt("!eval 1+2")), |
258 | p("In rare cases " + tt("!eval") + " may fail and you need to type " + tt("!real-eval") + " instead (which invokes an actual Java compiler)."))); |
259 | |
260 | if (eq(uri, "/safeIdentifiers")) |
261 | ret hframe("Safe Java Identifiers | Gazelle", |
262 | linesLL( |
263 | hheading("Safe Java(X) identifiers for !eval"), |
264 | hpre(lines(sortedIC(codeAnalysis_allSafeIdentifiers()))))); |
265 | |
266 | if (!eq(uri, "/")) |
267 | ret serve404(); |
268 | |
269 | //final Map<S, Int> feedbackStats = dm_gazelle_feedbackStats(); |
270 | final Map<S, Int> feedbackStats2 = unnull(dm_gazelle_feedbackStatsByJudgement()); |
271 | final Map<S, Int> feedbackStats3 = unnull(dm_gazelle_feedbackStatsForNonJudged()); |
272 | |
273 | // Main Page / Rules List |
274 | |
275 | S filterByPurpose = params.get('purpose); |
276 | L<T3<S>> rules = empty(filterByPurpose) |
277 | ? dm_allRulesFromRulesModuleWithCommentsAndIDs() |
278 | : dm_gazelle_allRulesWithPurpose(filterByPurpose); |
279 | // dm_gazelle_allRulesWithComment("discord"); |
280 | |
281 | bool showAll = eq("1", params.get('showAll)); |
282 | |
283 | L<Map> mapped = mapReversed(rules, |
284 | func(T3<S> t) -> SS { |
285 | S ruleID = t.c; |
286 | ret litorderedmap( |
287 | "Rule ID" := !showAll ? null : htmlEncode2(ruleID), |
288 | "Rule Text" := ahref(ruleURL(ruleID), htmlEncode_nlToBr(t.a)), |
289 | "Comments" := !showAll ? null : nlToBr( |
290 | mapEachLine(/*withoutLine("discord",*/ t.b/*)*/, func(S s) -> S { |
291 | new Matches m; |
292 | if "use helper table mech list *" |
293 | ret "use helper table mech list " + |
294 | ahref(neatMechListURL($1), m.get(0)); |
295 | ret htmlEncode2(s); |
296 | })), |
297 | "# of applications" := strOr(feedbackStats3.get(ruleID), "-"), |
298 | "Feedback" := ahref(rawSelfLink("rule/" + ruleID), |
299 | "+" + toInt(feedbackStats2.get(ruleID + "/good")) |
300 | /*+ " / " + |
301 | toInt(feedbackStats3.get(ruleID))*/ |
302 | + " / " + |
303 | "-" + toInt(feedbackStats2.get(ruleID + "/bad")))); |
304 | }); |
305 | |
306 | S sortBy = params.get('sortBy); |
307 | bool desc = eq("1", params.get('desc)); |
308 | if (nempty(sortBy)) |
309 | if (eq(sortBy, "Feedback")) |
310 | mapped = sortByCalculatedField(mapped, func(Map map) -> Int { |
311 | S s = cast map.get("Feedback"); |
312 | ret parseFirstInt(s) - parseSecondInt(s); |
313 | }); |
314 | else if (eq(sortBy, "# of applications")) |
315 | mapped = sortByMapKeyAlphaNum(mapped, sortBy); |
316 | else |
317 | mapped = sortByMapKey(mapped, sortBy); |
318 | if (desc) reverseInPlace(mapped); |
319 | |
320 | int start = parseInt(params.get("start")), step = 50; |
321 | S nav = pageNav2(hquery(mapMinus(params, 'start)), l(mapped), start, step, 'start, |
322 | /*leftArrow := span(htmlencode2(unicode_leftPointingTriangle()), style := "font-family: Lucida Sans"), |
323 | rightArrow := span(htmlencode2(unicode_rightPointingTriangle()), style := "font-family: Lucida Sans")*/); |
324 | mapped = subList(mapped, start, start+step); |
325 | |
326 | ret hhtml_head_title_body(gazelle_name() + " - Next-Gen Chat Bot", |
327 | hprelude() + |
328 | hopeningTag link(rel :="icon", href := "/favicon.ico?v=2") + |
329 | hcenter( |
330 | p(b(ahref_unstyled("http://gazelle.botcompany.de", htmlEncode2(gazelle_fullName())), style := "font-size: 5em")) + |
331 | p(hsnippetimg(gazelle_imageID(), height := 150, title := gazelle_name())) |
332 | + greeting() |
333 | + navLine() |
334 | |
335 | + h3("Rules " + (nempty(filterByPurpose) ? " for purpose " + quote(filterByPurpose) : "") + " (" + l(rules) + ")") |
336 | + p("By purpose: " + joinWithVBar(map(listPlus_inFront(gazelle_allRulePurposes_cached(), 'default), purpose -> |
337 | ahref(hquery(mapPlus(params, +purpose)), htmlEncode2(purpose))))) |
338 | + nav |
339 | + htmlTable2(mapped, |
340 | htmlEncode := false, |
341 | paramsByColName := litmap( |
342 | "Feedback" := litobjectarray(align := 'center), |
343 | "# of applications" := litobjectarray(align := 'center)), |
344 | replaceHeaders := litmap( |
345 | "Rule Text" := "Rule (input + more input => output)", |
346 | "# of applications" := sortLink(params, "# of applications"), |
347 | "Feedback" := sortLink(params, "Feedback")), |
348 | tdParams := litobjectarray(valign := 'top) |
349 | ) |
350 | + p(ahref(hquery(mapPlus(params, showAll := 1)), "Show technical stuff")) |
351 | ), bodyParams); |
352 | } |
353 | |
354 | S ruleLink(S ruleID) { |
355 | if (!isGlobalID(ruleID)) ret htmlEncode2(ruleID); |
356 | ret ahref("/rule/" + ruleID, ruleID); |
357 | } |
358 | |
359 | S commentToHTML(S s) { |
360 | new Matches m; |
361 | LS tok = javaTok(s); |
362 | S t = nextToLast(tok); |
363 | S html = null; |
364 | if (dm_gazelle_ruleExists(t)) |
365 | html = htmlEncode2(joinSubList(tok, 0, l(tok)-2)) + ruleLink(t); |
366 | else if (matchX("... list *", s, m) && isQuoted(m.get(0))) |
367 | html = htmlEncode2(joinSubList(tok, 0, l(tok)-2)) + ahref(neatMechListURL($1), t); |
368 | if (html == null) html = htmlEncode2(s); |
369 | ret html; |
370 | } |
371 | |
372 | S gazelleTreeToHTML(virtual GazelleTree tree) { |
373 | if (tree == null) ret "*"; |
374 | ret (getBool isSplitNode(tree) ? "[split] " : "") |
375 | + htmlEncode2(getString line(tree)) + appendSquareBracketed( |
376 | joinWithComma(listPlus( |
377 | map htmlEncode2((LS) call(tree, "renderQualityElements")), |
378 | ruleLink((S) call(tree, "ruleID"))))); |
379 | } |
380 | |
381 | S moreForMsgID(long msgID) { |
382 | ret ahref(rawSelfLink("revisit/" + msgID), "revisit", rel := "nofollow"); |
383 | } |
384 | |
385 | S search(SS params) { |
386 | S q = trim(params.get('q)); |
387 | Set<S> types = singletonCISetIfNempty(params.get('type)); |
388 | int maxResults = min(absoluteMaxResults, toIntOr(params.get('max), defaultMaxResults)); |
389 | L results = empty(q) ? null : dm_gazelle_fullSearch(q, +types, maxResults := maxResults+1); |
390 | S title = empty(q) ? "Search" : "Search results for: " + q; |
391 | ret hframe(title, |
392 | hheading(htmlEncode2(title)) + |
393 | hform("Terms: " + hinputfield('q, value := q, autofocus := true) |
394 | + " Type: " + hselect_list(searchTypes, params.get('type), name := 'type) |
395 | + " " + hsubmit("Search"), action := "/search") + |
396 | (empty(results) ? |
397 | (empty(q) ? "" : "No results") : p( |
398 | l(results) > maxResults ? maxResults + "+ results" : n2(results, "result")) |
399 | + htmlTable2(map(takeFirst_lazy(maxResults, results), o -> { |
400 | S type = shortClassName(o); |
401 | bool isLine = eq(type, "Line"); |
402 | long msgID = isLine ? getLong msgID(o) : 0; |
403 | Map map = litorderedmap("Type" := |
404 | isLine ? spanTitle("Message ID: " + msgID, type) : type); |
405 | S text = htmlEncode2OrNull(getStringOpt text(o)); |
406 | if (isLine) |
407 | { |
408 | map.put("Date" := localDateWithMinutes(getLong created(o))); |
409 | map.put("By" := (S) call(o, 'userName)); |
410 | } |
411 | if (text != null) { |
412 | if (eq(type, "Rule")) |
413 | text = ahref("/rule/" + getString globalID(o), text); |
414 | else if (isLine) |
415 | text = ahref(lineURL(msgID), text); |
416 | mapPut(map, "Text", text); |
417 | } |
418 | if (isLine) |
419 | map.put("More" := moreForMsgID(msgID)); |
420 | ret map; |
421 | }), htmlEncode := false))); |
422 | } |
423 | |
424 | S serveLine(S contextOrMsgID) { |
425 | GazelleLine l = isInteger(contextOrMsgID) |
426 | ? dm_discord_lineForMsgID(parseLong(contextOrMsgID)) |
427 | : gazelle_lineForContext(contextOrMsgID); |
428 | |
429 | if (l == null) ret "Line " + contextOrMsgID + " not found"; |
430 | L context = dm_discord_contextAroundLine(l.msgID, -3, 3); |
431 | L applications = |
432 | nempty(l.context) |
433 | ? gazelle_applicationsForContext(l.context) |
434 | : dm_gazelle_applicationsForMsgID(l.msgID); |
435 | saveFeedback(applications); |
436 | L<GazelleFulfillment> fulfillments = dm_gazelle_fulfillmentsForMsg(l.msgID); |
437 | |
438 | File reasoningFile = gazelle_discord_reasoningFileForContext(l.context); |
439 | bool hasRawReasoning = fileExists(reasoningFile); |
440 | |
441 | ret hframe("Line " + l.context, |
442 | hheading("Line @ " + localDateWithMinutes(l.timestamp) + ": " + htmlEncode2("<" + l.user + "> " + l.text)) |
443 | + p(moreForMsgID(l.msgID)) |
444 | + (empty(context) ? "" : |
445 | h3("Context") |
446 | + nlToBr(lines(map(context, line2 -> { |
447 | GazelleLine line = dm_discord_importLine(line2); |
448 | S s = localDateWithMinutes(line.timestamp) + ": " + ahref(line.msgID == l.msgID ? null : lineURL(line.msgID), |
449 | htmlEncode2("<" + line.user + "> " + line.text)); |
450 | ret line.msgID == l.msgID ? b(s) : s; |
451 | })))) |
452 | + (hasRawReasoning ? p(ahref("/rawReasoning/" + urlencode(l.context), "Raw reasoning.")) : "") |
453 | + (empty(applications) ? "" : h3("Reasoning") |
454 | + htmlTable2(map(applications, app -> { |
455 | S ruleID = getString ruleID(app); |
456 | Map map = new LinkedHashMap; |
457 | if (nempty(ruleID)) { |
458 | S ruleText = dm_textForRule(ruleID); |
459 | map.put("Rule" := ahref("/rule/" + urlencode(ruleID), |
460 | empty(ruleText) ? "?" : htmlEncode2_nlToBr(ruleText))); |
461 | } |
462 | map.putAll(applicationToMap(app, null, null)); |
463 | map.remove("Context"); |
464 | map.remove("Generated Output"); |
465 | ret map; |
466 | }), |
467 | tdParams := litobjectarray(align := 'center, valign := 'top), htmlEncode := false)) |
468 | + (empty(fulfillments) ? "" : h3("Fulfillments") |
469 | + htmlTable2(map(fulfillments, f -> { |
470 | new LinkedHashMap map; |
471 | if (neq(f.line, l.text)) map.put("Originally seen line" := f.line); |
472 | mapPut(map, "Rule" := f.rule); |
473 | mapPut(map, "Judgement" := f.judgement); |
474 | mapPut(map, "Mapping" := dropPrefix("cimap", f.varMap)); |
475 | ret map; |
476 | }), tdParams := litobjectarray(align := 'center, valign := 'top), htmlEncode := false)) |
477 | ); |
478 | } |
479 | |
480 | S createRule() { |
481 | S text = trim(params.get('text)), comments = params.get('comments); |
482 | if (nempty(text)) { |
483 | LS _comments = tlft(comments); |
484 | _comments = map unSquareBracket(_comments); |
485 | if (!authed) |
486 | _comments = map(_comments, s -> "[anon] " + s); |
487 | text = gazelle_processSquareBracketAnnotations(text, _comments); |
488 | S ruleID = pairA(dm_gazelle_addRuleWithComment(text, lines_rtrim(_comments))); |
489 | ret hredirect(ruleURL(ruleID)); |
490 | } |
491 | |
492 | S copyFrom = params.get('copyFrom); |
493 | PairS p = empty(copyFrom) ? pair("in => out", "") |
494 | : dm_textAndCommentForRule(copyFrom); |
495 | if (nempty(p.b)) |
496 | p.b = appendWithNewLine(withoutLinesStartingWithIC("copied from rule ", p.b), |
497 | "copied from rule " + copyFrom); |
498 | |
499 | ret hframeAndHeading("Create Rule", |
500 | hpostform( |
501 | h3("Rule") |
502 | + htextarea(p.a, |
503 | name := 'text, cols := 80, rows := 10) + "<br><br>" + |
504 | h3("Comments") |
505 | + htextarea(p.b, name := 'comments, cols := 80, rows := 10) + |
506 | "<br><br>" + |
507 | hsubmit("Create Rule") |
508 | ) |
509 | + p(targetBlank("/commentPatterns", "Comment patterns."))); |
510 | } |
511 | |
512 | O serveRuleApplication(S globalID) { |
513 | O app = gazelle_applicationForID(globalID); |
514 | if (app == null) ret serve404(); |
515 | S title = "Rule Application " + globalID; |
516 | S mrStruct = getString matchedRuleStruct(app); |
517 | O mr = safeUnstruct(mrStruct); |
518 | ret hframe(title, |
519 | hheading(title) + |
520 | htmlTable2(map(objectToMap(mr), func(S field, O value) -> Map { |
521 | litorderedmap("Field" := field, "Value" := sfu(value)) |
522 | }))); |
523 | } |
524 | |
525 | S serveRule(S ruleID) { |
526 | // add rule comment |
527 | |
528 | S comment = trim(params.get("comment")); |
529 | if (nempty(comment)) { |
530 | if (!authed) comment = "[anon] " + comment; |
531 | dm_gazelle_addRuleComments_verbose(ruleID, ll(comment)); |
532 | } |
533 | |
534 | // delete rule comment |
535 | |
536 | S del = params.get("deleteComment"); |
537 | if (authed && nempty(del)) |
538 | gazelle_removeCommentFromRule(ruleID, del); |
539 | |
540 | PairS textAndComment = unnull(dm_textAndCommentForRule(ruleID)); |
541 | L feedback = dm_gazelle_feedbackForRule(ruleID); |
542 | L applications = dm_gazelle_applicationsForRule(ruleID); |
543 | Map<S, O> feedbackByContext = indexByFieldNotNull context(feedback); |
544 | L<GazelleFulfillment> fulfillments = dm_gazelle_fulfillmentsForRule_imported(ruleID); |
545 | L list = cloneList(feedback); |
546 | for (O o : applications) |
547 | if (!containsKey(feedbackByContext, getString context(o))) |
548 | list.add(o); |
549 | |
550 | // add feedback comments + judgement |
551 | |
552 | for (S applicationID, S text : subMapStartingWith_dropPrefix(params, "comment_")) { |
553 | continue if empty(text = trim(text)); |
554 | if (!authed) text = "[anon] " + text; |
555 | O app = findByField(list, globalID := applicationID); |
556 | print("Processing comment for " + applicationID + " - " + app); |
557 | if (app == null) continue; |
558 | Set<S> comments = asSet(tlft(getString comments(app))); |
559 | if (!contains(comments, text)) |
560 | call(app, '_setField, comments := appendWithNewLine(getString comments(app), text)); |
561 | } |
562 | |
563 | saveFeedback(list); |
564 | |
565 | // change rule text |
566 | |
567 | S newRuleText = params.get('ruleText); |
568 | if (authed && nempty(newRuleText)) { |
569 | LS _comments = tlft(textAndComment.b); |
570 | newRuleText = gazelle_processSquareBracketAnnotations(newRuleText, _comments); |
571 | gazelle_setRuleText(ruleID, newRuleText); |
572 | gazelle_setRuleComment(ruleID, lines_rtrim(_comments)); |
573 | textAndComment = unnull(dm_textAndCommentForRule(ruleID)); |
574 | } |
575 | |
576 | if (authed && nempty(params.get('expandMultiRule))) { |
577 | gazelle_splitMultiRule(ruleID); |
578 | textAndComment = unnull(dm_textAndCommentForRule(ruleID)); |
579 | } |
580 | |
581 | // Serve Rule & Applications & Feedback |
582 | |
583 | GazelleEvalContext ctx = dm_gazelle_evalContextForSingleRule(ruleID); |
584 | GazellePredictor predictor = dm_gazelle_standardPredictor(); |
585 | |
586 | L<Map> mapped = map(list, app -> applicationToMap(app, ctx, predictor)); |
587 | |
588 | S sortBy = params.get('sortBy); |
589 | if (nempty(sortBy)) |
590 | mapped = sortByMapKey(mapped, sortBy); |
591 | |
592 | MultiSet<S> statements = dm_gazelle_statementsForRule_multiSet(ruleID); |
593 | |
594 | LS comments = tlft(textAndComment.b); |
595 | |
596 | // warnings |
597 | Collection<S> warnings = null; |
598 | pcall { |
599 | warnings = (Collection<S>) dm_call('gazelleThoughtHelper, 'autoTestRule, ruleID); |
600 | } |
601 | |
602 | // comment suggestions |
603 | Collection<S> suggestedComments = null; |
604 | pcall { |
605 | //suggestedComments = gazelle_suggestRuleComments(TextCommentsID(textAndComment.a, textAndComment.b, ruleID)); |
606 | suggestedComments = (Collection<S>) dm_findAndCall("#1022585/GRuleCommentSuggester", 'suggestComments, textAndComment.a, textAndComment.b, ruleID); |
607 | suggestedComments = listMinusSet(suggestedComments, comments); |
608 | } |
609 | |
610 | S title = "Rule " + ruleLink(ruleID) + ": " + htmlEncode2(newLinesToSpaces_trim(textAndComment.a)); |
611 | ret hframe(htmldecode_dropAllTags(title), |
612 | form(p(searchBox2(), style := "text-align: right"), action := "/search") |
613 | + hheading(title) |
614 | + hpostform( |
615 | h3("Rule") |
616 | + div(hblock(textAndComment.a), id := 'ruleText) |
617 | + (authed ? "<br>" + hbuttonOnClick_returnFalse("Edit", |
618 | [[ |
619 | $(this).text('Save'); |
620 | $(this).attr('onclick', ''); |
621 | $('#ruleText').html(]] + jsQuote(htextarea(textAndComment.a, name := 'ruleText, rows := 5, cols := 80)) + [[); |
622 | $('#ruleText textarea').focus();]]) : "") |
623 | |
624 | // Render comments |
625 | |
626 | + (nempty(textAndComment.b) ? h3("Comments") + nlToBr(mapEachLine(textAndComment.b, s -> { |
627 | S html = commentToHTML(s); |
628 | if (authed) |
629 | html += " " + ahref(ruleURL(ruleID) + hquery(deleteComment := s), "[x]"); |
630 | ret html; |
631 | })) : "") |
632 | + "<br><br>" |
633 | + "Add comment: " + htextfield("comment") + " " + hsubmit()) |
634 | |
635 | // Render warnings |
636 | |
637 | + (empty(warnings) ? "" |
638 | : h3("Warnings") + |
639 | nlToBr(lines(map(f<S, S> commentToHTML, warnings)))) |
640 | |
641 | // Render suggested comments |
642 | |
643 | + (empty(suggestedComments) ? "" |
644 | : h3("Suggested comments") + |
645 | nlToBr(lines(map(suggestedComments, c -> |
646 | htmlEncode2(c) + " " + ahref(ruleURL(ruleID) + hquery(comment := c), "[accept]"))))) |
647 | |
648 | + p(ahref("/createRule" + hquery(copyFrom := ruleID), "Duplicate rule.") |
649 | + " " + targetBlank("/commentPatterns", "Comment patterns.")) |
650 | |
651 | + (authed && cic(comments, "multi-rule") |
652 | ? hpostform(hsubmit("Deploy Multi-Rule") + hhidden(expandMultiRule := 1)) |
653 | : "") |
654 | |
655 | // Serve Feedback |
656 | |
657 | + (empty(mapped) ? "" : h3("Feedback") |
658 | + htmlTable2(mapped, |
659 | tdParams := litobjectarray(align := 'center, valign := 'top), |
660 | htmlEncode := false) |
661 | ) |
662 | // Serve Fulfillments |
663 | |
664 | + (empty(fulfillments) ? "" : h3("Fulfillments") |
665 | + htmlTable2(map(fulfillments, f -> { |
666 | new LinkedHashMap map; |
667 | mapPut(map, "Line" := ahref(lineURL(contextToMsgID(f.context)), htmlEncode2_nlToBr(f.line))); |
668 | mapPut(map, "Judgement" := f.judgement); |
669 | mapPut(map, "Mapping" := dropPrefix("cimap", f.varMap)); |
670 | mapPut(map, "Context" := f.context); |
671 | ret map; |
672 | }), |
673 | tdParams := litobjectarray(align := 'center, valign := 'top), |
674 | paramsByColName := litmap("Line" := litparams(align := 'left)), |
675 | htmlEncode := false)) |
676 | + (empty(statements) ? "" : h3("Analysis 1") |
677 | + hblock(multiSetToLines(statements)) |
678 | ) |
679 | + (empty(statements) ? "" : h3("Analysis 2") |
680 | + hblock(sfuLines(mapWithout(objectToMap( |
681 | ai_gazelle_analyzeStatementsForRule_multiSet(statements)), 'statements))))); |
682 | } |
683 | |
684 | S serveWikipedia() { |
685 | /*hheading("All Simple English Wikipedia Topics") |
686 | + htmlTable2(map(simpleWikipediaTopics_cached(), topic -> litorderedmap("Topic" := topic))));*/ |
687 | ret hframe("Wikipedia", |
688 | hheading("Retrieved Simple Wikipedia Pages") |
689 | + htmlTable2(map(simpleWikipedia_allCachedTopics(), topic -> litorderedmap("Topic" := topic)))); |
690 | |
691 | } |
692 | |
693 | Map applicationToMap(O o, GazelleEvalContext ctx, GazellePredictor predictor) { |
694 | bool showStruct = eq("1", params.get('struct)); |
695 | S prediction = null; |
696 | if (ctx != null && predictor != null) { |
697 | prediction = "?"; |
698 | pcall { |
699 | RuleEngine2_MatchedRule mr = cast unstruct(getString matchedRuleStruct(o)); |
700 | if (mr != null) |
701 | prediction = or(predictor.get(ctx, mr), prediction); |
702 | } |
703 | } |
704 | |
705 | S judgement = getString judgement(o); |
706 | S globalID = getString globalID(o); |
707 | S context = getString context(o); |
708 | |
709 | ret litorderedmap( |
710 | "Judgement" := nempty(judgement) || !authed ? htmlEncode2(judgement) : |
711 | ahref(uri + hqueryWithoutNanoHTTPDStuff(mapPlus(params, "judgeFeedback_" + globalID, 'good)), "mark good") + " " + |
712 | ahref(uri + hqueryWithoutNanoHTTPDStuff(mapPlus(params, "judgeFeedback_" + globalID, 'bad)), "mark bad"), |
713 | "Prediction" := prediction, |
714 | "Generated Output" := getString_htmlEncode outText(o), |
715 | "Rewritten Rule" := ahref("/ruleApplication/" + globalID, getString_htmlEncode modifiedRule(o)), |
716 | "Mapping" := dropPrefix("cimap", getString_htmlEncode varMap(o)), |
717 | "Context" := ahref(lineURL(context), htmlEncode2(context)), |
718 | "Comments" := hform( |
719 | appendIfNempty(nlToBr(rtrim(getString_htmlEncode comments(o))), "<br>") |
720 | + htextfield("comment_" + globalID)), |
721 | "Struct" := showStruct ? getString matchedRuleStruct(o) : null); |
722 | } |
723 | |
724 | S serveRuleComments() { |
725 | ret hframe("All Rule Comments", |
726 | hheading("All Rule Comments") |
727 | + htmlTable2(map(dm_gazelle_allRuleComments(), comment -> litorderedmap("Comment" := comment)))); |
728 | } |
729 | |
730 | S hframeAndHeading(S title, O body) { |
731 | ret hframe(title, hheading(htmlEncode2(title)) + body); |
732 | } |
733 | |
734 | // html-encoded title |
735 | S hframeAndHeading2(S title, O body) { |
736 | ret hframe(htmldecode_dropAllTags(title), |
737 | hheading(title) + body); |
738 | } |
739 | |
740 | S hframe(S title, O body) { |
741 | //print("Params: " + sfu(bodyParams)); |
742 | ret hhtml_head_title_body(title + " | " + htmlEncode2(gazelle_name()), |
743 | loadJQuery() + |
744 | hprelude() + |
745 | body, |
746 | bodyParams); |
747 | } |
748 | |
749 | S searchBox() { |
750 | ret hinputfield('q, style := "width: 120px", autofocus := true) |
751 | + " " + hselect_list(searchTypes, "Rule", name := 'type) |
752 | + " " + hsubmit("Search"); |
753 | } |
754 | |
755 | S searchBox2() { |
756 | ret "[search] " + hinputfield('q, style := "width: 120px") |
757 | + " " + hselect_list(searchTypes, "Rule", name := 'type); |
758 | } |
759 | |
760 | void saveFeedback(L list) { |
761 | if (authed) for (S applicationID, S judgement : subMapStartingWith_dropPrefix(params, "judgeFeedback_")) { |
762 | // TODO: copy to feedback module |
763 | continue if !eqOneOf(judgement, 'good, 'bad); |
764 | O app = findByField(list, globalID := applicationID); |
765 | if (dm_isConceptOfModule(app, dm_gazelle_longFeedbackCRUD())) { |
766 | print("Migrating to feedback CRUD"); |
767 | app = dm_call(dm_gazelle_feedbackCRUD(), 'uniqConcept, (O) getFieldsAsParams(app, conceptFields_gen(app))); |
768 | } |
769 | print("Judging feedback for " + applicationID + " - " + app); |
770 | call(app, '_setField, +judgement); |
771 | } |
772 | } |
773 | |
774 | S serveTestInput() { |
775 | S input = params.get('input); |
776 | L<virtual GazelleTree> out = null; |
777 | new L<virtual GInterpretable> interpretables; |
778 | |
779 | if (nempty(input)) { |
780 | // Hopefully safe! |
781 | |
782 | O inputTester = vmBus_query('gazelleInputTester); |
783 | if (inputTester != null) |
784 | out = (L) dm_call(inputTester, 'reason, input, |
785 | interpretables_out := listCollector(interpretables), |
786 | noEvals := true); |
787 | |
788 | /*out = gazelle_reason_repeat(input, |
789 | interpretables_out := listCollector(interpretables), |
790 | noEvals := true, |
791 | contextMaker := contextCache);*/ |
792 | } |
793 | |
794 | ret hframeAndHeading("Test Input", |
795 | hform("Input: " + htextfield('input) |
796 | + " " + hsubmit("Test")) |
797 | + (empty(input) ? "" : p("Processed input: " + b(htmlEncode2(input))) |
798 | + (empty(interpretables) ? "" : |
799 | h3("Results of preprocessing") |
800 | + htmlTable2( |
801 | map(interpretables, func(virtual GInterpretable intp) -> Map { |
802 | litorderedmap( |
803 | "Text" := htmlEncode2(getString text(intp)), |
804 | "Rule" := ruleLink(getString ruleID(intp))) |
805 | }), htmlEncode := false)) |
806 | + h3("Output") |
807 | + htmlTable2( |
808 | map(out, func(virtual GazelleTree t) -> Map { |
809 | litorderedmap( |
810 | "Output" := htmlEncode2(getString line(t)), |
811 | "Rule" := ruleLink(getString ruleID(t)), |
812 | "Quality" := htmlEncode2(joinWithComma((LS) call(t, 'renderQualityElements)))) |
813 | } |
814 | ), htmlEncode := false))); |
815 | } |
816 | |
817 | S greeting() { |
818 | ret p(span("<b>Hello! I am a next-generation chat bot in training.</b>", style := "font-size: 1.4em") |
819 | + (!gazelle_isOriginal() ? "" : "<br>" |
820 | + span(targetBlank("https://BotCompany.de", "Maker.") + " " |
821 | + targetBlank("https://slides.com/stefanreich/how-about-thinking-machines/", "Slides.") |
822 | + " " |
823 | + targetBlank("https://discordapp.com/invite/SEAjPqk", b("Join my Discord server!")), style := "font-size: 1.2em"))); |
824 | } |
825 | |
826 | S navLine(O... _) { |
827 | bool showAll = eq("1", params.get('showAll)); |
828 | |
829 | ret hform(p(ahref(rawSelfLink("commands"), "Commands.") |
830 | + " " + ahref(rawSelfLink("lastInput"), "Last input.") |
831 | + " " + ahref(rawSelfLink("lastOutput"), "Last output.") |
832 | + " " + ahref(rawSelfLink("testInput"), "Test input.") |
833 | + " " + ahref("/tests", "Tests.") |
834 | + stringParOrEmpty more(_) |
835 | + (authed ? " " + ahref(rawSelfLink("createRule"), "Create rule.") : "") |
836 | + (!showAll ? "" : " " + ahref(rawSelfLink("ruleComments"), "Rule comments.")) |
837 | + " | " + searchBox()), action := "/search"); |
838 | } |
839 | |
840 | S testsModule() { ret gazelle_testsModule(); } |
841 | |
842 | O serveTests() { |
843 | L tests = cast dm_call(testsModule(), 'concepts); |
844 | |
845 | S show = params.get('test); |
846 | S runTest = params.get('runTest); |
847 | if (nempty(runTest)) { |
848 | show = runTest; |
849 | dm_call(testsModule(), 'runTestAndWait, runTest); |
850 | } |
851 | |
852 | O showing = empty(show) ? null : firstWhere(tests, name := show); |
853 | S showingName = getString name(showing); |
854 | |
855 | Pair<Int> stats = cast quickImport(dm_callOpt(testsModule(), 'countAndSuccesses)); |
856 | |
857 | bool running = isTrue(dm_getOpt(testsModule(), 'running)); |
858 | ret hframeAndHeading2(ahref("/tests", "Tests") |
859 | + (empty(showingName) ? "" : htmlEncode2(" => ") + ahref("/tests" + hquery(test := showingName), htmlEncode2(showingName))), |
860 | (running ? p(tag('blink, "TESTS RUNNING")) : "") |
861 | + (stats == null ? "" : p(n2(stats.a, "test") + ", " + stats.b + " OK")) |
862 | + hSingleRowTable_withSpacing(50, |
863 | htmlTable2(map(tests, test -> litorderedmap( |
864 | "Test" := ahref(hquery(test := getString name(test)), htmlEncode2(getString name(test))), |
865 | "Last Run" := localDateWithMinutes(getLong lastRun(test)), |
866 | //"Duration" := getLong runtime(test) + " ms", |
867 | "Success" := yesNo2(getBool success(test)), |
868 | "Any Success Ever" := yesNo2(getBool anySuccess(test))) |
869 | ), htmlEncode := false), |
870 | |
871 | (showing == null ? null : |
872 | h3(htmlEncode2("Test " + quote(showingName) + " [" + (getBool success(showing) ? "OK" : "FAILED") + "]")) |
873 | + hpostform(hsubmit("Run Test"), action := hquery(runTest := showingName)) |
874 | + htmlEncode_nlToBr(getString contents(showing))) |
875 | )); |
876 | } |
877 | |
878 | // end of Request |
879 | } |
880 | } // end of class |
881 | |
882 | sS rawSelfLink(S uri) { ret addSlashPrefix(uri); } |
883 | |
884 | sS hheading(O contents) { |
885 | ret h2(ahref(rawSelfLink(""), htmlEncode2(gazelle_name())) + " | " + contents); |
886 | } |
887 | |
888 | sS hprelude() { |
889 | ret [[<link href="https://fonts.googleapis.com/css?family=Roboto+Mono" rel="stylesheet">]] + |
890 | hmobilefix(); |
891 | } |
892 | |
893 | sS hblock(S s) { |
894 | ret htmlEncode_nlToBr(s); |
895 | } |
896 | |
897 | sS sortLink(SS params, S key) { |
898 | params = cloneMap(params); |
899 | mapPutOrRemove(params, 'desc, |
900 | eq(params.get('sortBy), key) && !eq("1", params.get('desc)) ? "1" : null); |
901 | params.put(sortBy := key); |
902 | ret ahref(hquery(params), htmlEncode2(key)); |
903 | } |
904 | |
905 | static L<GazelleLine> selectLines(SS params) { |
906 | long modifiedAfter = getLong modifiedAfter(params); |
907 | if (modifiedAfter != 0) ret dm_discord_linesModifiedAfter(modifiedAfter); |
908 | ret dm_discord_allLines(); |
909 | } |
Began life as a copy of #1021679
download show line numbers debug dex old transpilations
Travelled to 10 computer(s): bhatertpkbcr, cfunsshuasjs, gwrvuhgaqvyk, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, whxojlpjdney, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1022531 |
Snippet name: | DynGazelleWebServer |
Eternal ID of this version: | #1022531/71 |
Text MD5: | 89da73adad5c94b5016b81f07d57a3b0 |
Transpilation MD5: | a7b233a676dfa9bebea22fe836cd2d46 |
Author: | stefan |
Category: | javax / stefan's os / a.i. / web |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2020-05-19 17:33:46 |
Source code size: | 37312 bytes / 909 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 462 / 1423 |
Version history: | 70 change(s) |
Referenced in: | [show references] |