1 | sclass LiveInputWeb {
|
2 | concept Screen {
|
3 | S screenID; |
4 | long lastSeen = now(); |
5 | S query; |
6 | transient S status; |
7 | transient Lock lock = lock(); |
8 | transient O search; |
9 | |
10 | toString { ret "Screen " + screenID; }
|
11 | } |
12 | |
13 | int statusUpdateInterval = 100; |
14 | |
15 | transient Map<virtual WebSocket, Screen> webSockets = syncWeakHashMap(); |
16 | |
17 | transient Set<virtual WebSocket> liveSearches = weakHashSet(); |
18 | |
19 | void start {
|
20 | //dbIndexing(Screen, 'screenID); |
21 | dm_doEvery(statusUpdateInterval, r updateStati); |
22 | } |
23 | |
24 | set flag NoNanoHTTPD. |
25 | O html(IWebRequest request) { try {
|
26 | try object redirectToHTTPS(request); |
27 | |
28 | S uri = request.uri(); |
29 | SS params = request.params(); |
30 | print(+params); |
31 | |
32 | S cookie = request.cookie(); |
33 | print("cookie", cookie);
|
34 | |
35 | new Matches m; |
36 | |
37 | if (eqic(uri, "/about")) |
38 | ret serveText(unindent_mls([[ |
39 | What is the BEA Live Input? |
40 | |
41 | Bla bla bla. Also, bla. |
42 | ]])); |
43 | |
44 | if (eqic(uri, "/websockettest")) {
|
45 | ret htitle("Web Socket Test")
|
46 | + hreconnectingWebSockets() |
47 | + hloadjquery() |
48 | + hdiv("bla<br>", id := "dynamic", style := "font-size: 40px")
|
49 | + hjavascript([[ |
50 | var ws = new ReconnectingWebSocket("wss://wikify.live/test");
|
51 | ws.onopen = function(event) {
|
52 | ws.send("here i am");
|
53 | $("#dynamic").append("Streaming...<br>");
|
54 | }; |
55 | ws.onmessage = function(event) {
|
56 | $("#dynamic").html(event.data);
|
57 | document.title = event.data; |
58 | }; |
59 | ]]); |
60 | } |
61 | |
62 | // home page |
63 | S query = ""; |
64 | if (swic(uri, "/search/", m)) |
65 | query = takeFirst(1000, urldecode(m.rest())); |
66 | else // now we actually accept wikify.live/{any search term}
|
67 | query = urldecode(dropSlashPrefix(uri)); |
68 | |
69 | ret hhtml(hmobilefix() + hhead( |
70 | htitle("BEA Live Input [dev.]")
|
71 | + hsansserif() |
72 | + hstyle([[ |
73 | .queryContainer {
|
74 | height: 100%; |
75 | width: 100%; |
76 | |
77 | display: flex; |
78 | flex-flow: column; |
79 | } |
80 | |
81 | input.query {
|
82 | font-size: 40px; |
83 | padding: 20px; |
84 | width: 400px; |
85 | height: 2em; |
86 | |
87 | position: relative; |
88 | left: 50%; |
89 | transform: translate(-50%, 0); |
90 | flex: .01 1 auto; |
91 | } |
92 | |
93 | .filler {
|
94 | flex: .5 1 auto; |
95 | } |
96 | |
97 | .filler.ontop {
|
98 | flex: .01 1 auto; |
99 | } |
100 | |
101 | input.query.ontop {
|
102 | transform: translate(-50%, 0); |
103 | margin-top: 20px; |
104 | font-size: 20px; |
105 | } |
106 | |
107 | .searching {
|
108 | display: none; |
109 | } |
110 | |
111 | .searching.show {
|
112 | display: block; |
113 | position: relative; |
114 | left: 50%; |
115 | margin-left: -45px; |
116 | } |
117 | |
118 | .results {
|
119 | font-family: monospace; |
120 | white-space: pre; /* no word-wrapping */ |
121 | overflow: auto; /* scrollbars */ |
122 | margin-top: 20px; |
123 | border: 2px solid black; |
124 | padding: 0px; |
125 | display: block; |
126 | flex: .01 1 auto; |
127 | opacity: 0%; |
128 | width: 1px; |
129 | margin-left: auto; |
130 | margin-right: auto; |
131 | } |
132 | |
133 | .results.ontop {
|
134 | padding: 20px; |
135 | flex: 1 0 0; |
136 | opacity: 100%; |
137 | width: calc(100% - 50px); |
138 | left: 0; |
139 | transform: none; |
140 | } |
141 | |
142 | .about {
|
143 | margin-bottom: 10px; |
144 | font-size: 12px; |
145 | text-align: center; |
146 | margin-left: auto; |
147 | margin-right: auto; |
148 | } |
149 | |
150 | .status {
|
151 | display: none; |
152 | margin-top: 10px; |
153 | font-size: 12px; |
154 | } |
155 | |
156 | .status.ontop {
|
157 | display: block; |
158 | margin-left: auto; |
159 | margin-right: auto; |
160 | } |
161 | |
162 | .query, .results, .filler {
|
163 | transition: all 1s; |
164 | } |
165 | ]]) |
166 | + hLoadJQuery2() |
167 | + hreconnectingWebSockets()) |
168 | + hbody(hdiv( |
169 | hdiv("", class := "filler")
|
170 | + hdiv("AI bla bla<br>"
|
171 | + "By " + ahref("https://BotCompany.de", "BotCompany") + "."
|
172 | + targetBlank("/about", "More info."), class := "about")
|
173 | + hinput("", value := query, autofocus := true, class := "query", placeholder := "TYPE ANYTHING")
|
174 | + hdiv("", class := "status")
|
175 | + hdiv( |
176 | hdiv("", class := "actualResults")
|
177 | + hdiv(himgsnippet(#1102949, title := "Searching..."), class := "searching"), |
178 | class := "results") |
179 | + hdiv("", class := "filler"),
|
180 | class := "queryContainer") |
181 | + hjs_urlencode() |
182 | + hjs([[ |
183 | function queryUpdate() {
|
184 | var sel = $("input.query, .queryContainer, .results, .filler, .status");
|
185 | var q = $("input.query").val();
|
186 | if (q == "") {
|
187 | $(".actualResults").html("");
|
188 | sel.removeClass("ontop");
|
189 | } else {
|
190 | sel.addClass("ontop");
|
191 | } |
192 | if (history.replaceState) |
193 | history.replaceState(null, "Wikify.LIVE", q == "" ? "/" : "/search/" + urlencode(q)); |
194 | sendQuery(); |
195 | } |
196 | |
197 | $("input.query").on('change keydown paste input', queryUpdate);
|
198 | var screenID = Math.random().toString(36).substr(2, 9); |
199 | var wsURL = "wss://wikify.live/search?screen=" + urlencode(screenID); |
200 | console.log("WS: " + wsURL);
|
201 | var ws = new ReconnectingWebSocket(wsURL); |
202 | var wsReady = false; |
203 | var querySent = ""; |
204 | var maxQueryLength = 1000; |
205 | |
206 | function wsSend(text) {
|
207 | if (wsReady) {
|
208 | console.log("wsSend: " + text);
|
209 | ws.send(text); |
210 | } else |
211 | console.log("wsNotReady: " + text);
|
212 | } |
213 | |
214 | function sendQuery() {
|
215 | var q = $("input.query").val().substring(0, maxQueryLength);
|
216 | if (querySent != q) {
|
217 | querySent = q; |
218 | wsSend(JSON.stringify({ q: q }));
|
219 | } |
220 | } |
221 | |
222 | ws.onopen = function(event) {
|
223 | wsReady = true; |
224 | ws.onmessage = function(event) {
|
225 | var text = event.data; |
226 | console.log("Got text: " + text);
|
227 | var data = JSON.parse(text); |
228 | if ('clearResults' in data)
|
229 | $(".actualResults").html("");
|
230 | if ('searching' in data)
|
231 | $(".searching").addClass("show");
|
232 | if ('result' in data)
|
233 | $(".actualResults").append(data.result + "<br>");
|
234 | if ('searchCancelled' in data)
|
235 | $(".searching").removeClass("show");
|
236 | if ('searchDone' in data) {
|
237 | $(".actualResults").append("<br>[SEARCH DONE]<br>");
|
238 | $(".searching").removeClass("show");
|
239 | } |
240 | if ('status' in data)
|
241 | $(".status").html(data.status);
|
242 | }; |
243 | queryUpdate(); |
244 | }; |
245 | ]]) |
246 | )); |
247 | } catch e { printStackTrace(e); throw rethrow(e); }
|
248 | } |
249 | |
250 | class Request {
|
251 | S cookie; |
252 | } |
253 | |
254 | void cleanMeUp_webSockets {
|
255 | for (virtual WebSocket ws : cloneAndClearKeys(webSockets)) {
|
256 | print("Closing WebSocket");
|
257 | close((AutoCloseable) ws); |
258 | } |
259 | } |
260 | |
261 | void cleanMeUp_searches {
|
262 | for (virtual WebSocket ws : cloneAndClearList(liveSearches)) |
263 | cancelSearch_noLock(webSockets.get(ws)); |
264 | } |
265 | |
266 | // API for Eleu |
267 | |
268 | void handleWebSocket(virtual WebSocket ws) {
|
269 | print("Got new websocket!");
|
270 | set(ws, onClose := r { webSockets.remove(ws) });
|
271 | |
272 | set(ws, onOpen := rEnter {
|
273 | S uri = cast rcall getUri(ws); |
274 | |
275 | print("WebSocket opened! uri: " + uri);
|
276 | |
277 | SS params = cast rcall getParms(ws); |
278 | S screenID = takeFirst(params.get("screen"), 100);
|
279 | Screen screen = uniq Screen(+screenID); |
280 | screen.status = null; |
281 | webSockets.put(ws, screen); |
282 | cset(screen, lastSeen := now()); |
283 | |
284 | print("Screen ID: " + screenID);
|
285 | |
286 | setFieldToIVF1Proxy(ws, onMessage := msg -> {
|
287 | temp enter(); |
288 | pcall {
|
289 | S text = cast rcall getTextPayload(msg); |
290 | print("Got msg: " + text);
|
291 | |
292 | Map json = jsonDecodeMap(text); |
293 | S q = upper((S) json.get("q"));
|
294 | if (q != null && neq(screen.query, q)) {
|
295 | temp tempLock("start search", screen.lock);
|
296 | |
297 | cancelSearch_noLock(screen); |
298 | cset(screen, query := q); |
299 | if (nempty(q)) |
300 | startSearch_noLock(ws, screen, q); |
301 | } |
302 | } |
303 | }); |
304 | |
305 | if (eq(uri, "/test")) thread {
|
306 | repeat 100 {
|
307 | call(ws, 'send, "It is " + localTimeWithMilliseconds()); |
308 | sleep(100); |
309 | } |
310 | } |
311 | |
312 | if (eq(uri, "/search")) {
|
313 | } |
314 | }); |
315 | } |
316 | |
317 | void cancelSearch_noLock(Screen screen) {
|
318 | if (screen == null) ret; |
319 | close(screen.search); |
320 | screen.search = null; |
321 | } |
322 | |
323 | void startSearch_noLock(virtual WebSocket ws, Screen screen, S query) {
|
324 | cset(screen, +query); |
325 | call(ws, "send", jsonEncode(litorderedmap("clearResults", true)));
|
326 | |
327 | if (empty(query)) ret; |
328 | |
329 | call(ws, "send", jsonEncode(litorderedmap("searching", true)));
|
330 | |
331 | print("Starting search for " + quote(query) + " for " + screen);
|
332 | /*LC_ParallelSearch search = new(searchers, query); |
333 | screen.search = search; |
334 | liveSearches.add(ws); |
335 | search.enter = lambda0 enter; |
336 | search.maxResults = defaultResults; |
337 | search.onResult = (iSearcher, iFile, position) -> {
|
338 | temp tempLock("onResult", screen.lock);
|
339 | if (screen.search != search) ret; |
340 | try {
|
341 | //print("Sending result");
|
342 | S status = search.status_html(); |
343 | call(ws, "send", jsonEncode(litorderedmap( |
344 | result := renderResult(iSearcher, iFile, position, query, screen.search.startTime), |
345 | +status))); |
346 | //print("Sent result");
|
347 | } catch print e {
|
348 | cancelSearch_noLock(screen); |
349 | } |
350 | }; |
351 | search.onSearchDone = r {
|
352 | liveSearches.remove(ws); |
353 | print("Search done. " + screen.search + " / " + search);
|
354 | temp tempLock("onSearchDone", screen.lock);
|
355 | if (screen.search != search) ret; |
356 | updateStatus(ws, screen); |
357 | print("Cancelling search");
|
358 | cancelSearch_noLock(screen); |
359 | print("Sending searchDone");
|
360 | call(ws, "send", jsonEncode(litorderedmap( |
361 | search.completed() ? "searchDone" : "searchCancelled", true))); |
362 | print("Sent searchDone");
|
363 | }; |
364 | search.start();*/ |
365 | } |
366 | |
367 | void updateStati enter {
|
368 | for (virtual WebSocket ws : cloneList(liveSearches)) pcall {
|
369 | Screen screen = webSockets.get(ws); |
370 | updateStatus(ws, screen); |
371 | } |
372 | } |
373 | |
374 | void updateStatus(virtual WebSocket ws, Screen screen) {
|
375 | if (screen == null) ret; |
376 | /*LC_ParallelSearch search = screen.search; |
377 | if (search == null) ret; |
378 | S status = search.status_html(); |
379 | if (neq(status, screen.status)) {
|
380 | screen.status = status; |
381 | call(ws, "send", jsonEncode(litorderedmap(status := screen.status))); |
382 | }*/ |
383 | } |
384 | } |
Began life as a copy of #1030819
download show line numbers debug dex old transpilations
Travelled to 4 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, vouqrxazstgt
No comments. add comment
| Snippet ID: | #1030842 |
| Snippet name: | LiveInputWeb [embeddable WebSocket-based live search, dev.] |
| Eternal ID of this version: | #1030842/3 |
| Text MD5: | cde6489243c2d8212868508db06df82e |
| Author: | stefan |
| Category: | javax |
| Type: | JavaX fragment (include) |
| Public (visible to everyone): | Yes |
| Archived (hidden from active list): | No |
| Created/modified: | 2021-04-02 15:06:28 |
| Source code size: | 11332 bytes / 384 lines |
| Pitched / IR pitched: | No / No |
| Views / Downloads: | 394 / 416 |
| Version history: | 2 change(s) |
| Referenced in: | [show references] |