diff --git a/app/Search/Queries/QueryController.php b/app/Search/Queries/QueryController.php index 95888a88f..cfaf2e920 100644 --- a/app/Search/Queries/QueryController.php +++ b/app/Search/Queries/QueryController.php @@ -41,8 +41,13 @@ class QueryController extends Controller // TODO - Validate if query system is active $query = $request->get('query', ''); - $results = $query ? $searchRunner->run($query) : []; - $llmResult = $llmRunner->run($query, $results); - dd($results, $llmResult); + return response()->eventStream(function () use ($query, $searchRunner, $llmRunner) { + $results = $query ? $searchRunner->run($query) : []; + + $count = count($results); + yield "Found {$count} results for query: {$query}!"; + $llmResult = $llmRunner->run($query, $results); + yield "LLM result: {$llmResult}"; + }); } } diff --git a/package-lock.json b/package-lock.json index 079e39770..86bdc05e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "@types/jest": "^29.5.14", "codemirror": "^6.0.1", + "eventsource-client": "^1.1.4", "idb-keyval": "^6.2.1", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", @@ -4336,6 +4337,27 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/eventsource-client/-/eventsource-client-1.1.4.tgz", + "integrity": "sha512-CKnqZTwXCnHN2EqrEB9eLSjMMRqHum09VOsikkgSPoa2Jr2XgQnX7P1Fxhnnj/UHxi3GQ2xVsXDKIktEes07bg==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz", + "integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", diff --git a/package.json b/package.json index 151338d8c..637457a93 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "@types/jest": "^29.5.14", "codemirror": "^6.0.1", + "eventsource-client": "^1.1.4", "idb-keyval": "^6.2.1", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", diff --git a/resources/js/components/query-manager.ts b/resources/js/components/query-manager.ts index 9252c543d..40a71489b 100644 --- a/resources/js/components/query-manager.ts +++ b/resources/js/components/query-manager.ts @@ -1,4 +1,5 @@ import {Component} from "./component"; +import {createEventSource} from "eventsource-client"; export class QueryManager extends Component { protected input!: HTMLTextAreaElement; @@ -21,5 +22,36 @@ export class QueryManager extends Component { // TODO - Update URL on query change // TODO - Handle query form submission + this.form.addEventListener('submit', event => { + event.preventDefault(); + this.runQuery(); + }); + } + + async runQuery() { + this.contentLoading.hidden = false; + this.generatedLoading.hidden = false; + this.contentDisplay.innerHTML = ''; + this.generatedDisplay.innerHTML = ''; + + const query = this.input.value; + const es = window.$http.eventSource('/query', 'POST', {query}); + + let messageCount = 0; + for await (const {data, event, id} of es) { + messageCount++; + if (messageCount === 1) { + // Entity results + this.contentDisplay.innerText = data; // TODO - Update to HTML + this.contentLoading.hidden = true; + } else if (messageCount === 2) { + // LLM Output + this.generatedDisplay.innerText = data; // TODO - Update to HTML + this.generatedLoading.hidden = true; + } else { + es.close() + break; + } + } } } \ No newline at end of file diff --git a/resources/js/services/http.ts b/resources/js/services/http.ts index f9eaafc39..07f150220 100644 --- a/resources/js/services/http.ts +++ b/resources/js/services/http.ts @@ -1,3 +1,5 @@ +import {createEventSource, EventSourceClient} from "eventsource-client"; + type ResponseData = Record|string; type RequestOptions = { @@ -59,7 +61,6 @@ export class HttpManager { } createXMLHttpRequest(method: string, url: string, events: Record void> = {}): XMLHttpRequest { - const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content'); const req = new XMLHttpRequest(); for (const [eventName, callback] of Object.entries(events)) { @@ -68,7 +69,7 @@ export class HttpManager { req.open(method, url); req.withCredentials = true; - req.setRequestHeader('X-CSRF-TOKEN', csrfToken || ''); + req.setRequestHeader('X-CSRF-TOKEN', this.getCSRFToken()); return req; } @@ -95,12 +96,11 @@ export class HttpManager { requestUrl = urlObj.toString(); } - const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content') || ''; const requestOptions: RequestInit = {...options, credentials: 'same-origin'}; requestOptions.headers = { ...requestOptions.headers || {}, baseURL: window.baseUrl(''), - 'X-CSRF-TOKEN': csrfToken, + 'X-CSRF-TOKEN': this.getCSRFToken(), }; const response = await fetch(requestUrl, requestOptions); @@ -191,6 +191,27 @@ export class HttpManager { return this.dataRequest('DELETE', url, data); } + eventSource(url: string, method: string = 'GET', body: object = {}): EventSourceClient { + if (!url.startsWith('http')) { + url = window.baseUrl(url); + } + + return createEventSource({ + url, + method, + body: JSON.stringify(body), + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': this.getCSRFToken(), + } + }); + } + + protected getCSRFToken(): string { + return document.querySelector('meta[name=token]')?.getAttribute('content') || ''; + } + /** * Parse the response text for an error response to a user * presentable string. Handles a range of errors responses including