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: | 164 / 182 |
Version history: | 2 change(s) |
Referenced in: | [show references] |