diff --git a/app/Search/Queries/QueryController.php b/app/Search/Queries/QueryController.php index cfaf2e920..4d8c71b61 100644 --- a/app/Search/Queries/QueryController.php +++ b/app/Search/Queries/QueryController.php @@ -3,7 +3,6 @@ namespace BookStack\Search\Queries; use BookStack\Http\Controller; -use BookStack\Search\SearchOptions; use BookStack\Search\SearchRunner; use Illuminate\Http\Request; @@ -12,6 +11,13 @@ class QueryController extends Controller public function __construct( protected SearchRunner $searchRunner, ) { + // TODO - Check via testing + $this->middleware(function ($request, $next) { + if (!VectorQueryServiceProvider::isEnabled()) { + $this->showPermissionError('/'); + } + return $next($request); + }); } /** @@ -19,17 +25,12 @@ class QueryController extends Controller */ public function show(Request $request) { - // TODO - Validate if query system is active $query = $request->get('ask', ''); - // TODO - Placeholder - $entities = $this->searchRunner->searchEntities(SearchOptions::fromString("cat"), 'all', 1, 20)['results']; - // TODO - Set page title return view('search.query', [ 'query' => $query, - 'entities' => $entities, ]); } @@ -38,16 +39,23 @@ class QueryController extends Controller */ public function run(Request $request, VectorSearchRunner $searchRunner, LlmQueryRunner $llmRunner) { - // TODO - Validate if query system is active + // TODO - Rate limiting $query = $request->get('query', ''); 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}"; + $entities = []; + foreach ($results as $result) { + $entityKey = $result->entity->getMorphClass() . ':' . $result->entity->id; + if (!isset($entities[$entityKey])) { + $entities[$entityKey] = $result->entity; + } + } + + yield ['view' => view('entities.list', ['entities' => $entities])->render()]; + + yield ['result' => $llmRunner->run($query, $results)]; }); } } diff --git a/resources/js/components/query-manager.ts b/resources/js/components/query-manager.ts index 40a71489b..91bd63a22 100644 --- a/resources/js/components/query-manager.ts +++ b/resources/js/components/query-manager.ts @@ -1,5 +1,4 @@ import {Component} from "./component"; -import {createEventSource} from "eventsource-client"; export class QueryManager extends Component { protected input!: HTMLTextAreaElement; @@ -8,33 +7,52 @@ export class QueryManager extends Component { protected contentLoading!: HTMLElement; protected contentDisplay!: HTMLElement; protected form!: HTMLFormElement; + protected fieldset!: HTMLFieldSetElement; setup() { this.input = this.$refs.input as HTMLTextAreaElement; this.form = this.$refs.form as HTMLFormElement; + this.fieldset = this.$refs.fieldset as HTMLFieldSetElement; this.generatedLoading = this.$refs.generatedLoading; this.generatedDisplay = this.$refs.generatedDisplay; this.contentLoading = this.$refs.contentLoading; this.contentDisplay = this.$refs.contentDisplay; - // TODO - Start lookup if query set + this.setupListeners(); - // TODO - Update URL on query change + // Start lookup if a query is set + if (this.input.value.trim() !== '') { + this.runQuery(); + } + } - // TODO - Handle query form submission + protected setupListeners(): void { + // Handle form submission this.form.addEventListener('submit', event => { event.preventDefault(); this.runQuery(); }); + + // Allow Ctrl+Enter to run a query + this.input.addEventListener('keydown', event => { + if (event.key === 'Enter' && event.ctrlKey && this.input.value.trim() !== '') { + this.runQuery(); + } + }); } - async runQuery() { + protected async runQuery(): Promise { this.contentLoading.hidden = false; this.generatedLoading.hidden = false; this.contentDisplay.innerHTML = ''; this.generatedDisplay.innerHTML = ''; + this.fieldset.disabled = true; + + const query = this.input.value.trim(); + const url = new URL(window.location.href); + url.searchParams.set('ask', query); + window.history.pushState({}, '', url.toString()); - const query = this.input.value; const es = window.$http.eventSource('/query', 'POST', {query}); let messageCount = 0; @@ -42,16 +60,18 @@ export class QueryManager extends Component { messageCount++; if (messageCount === 1) { // Entity results - this.contentDisplay.innerText = data; // TODO - Update to HTML + this.contentDisplay.innerHTML = JSON.parse(data).view; this.contentLoading.hidden = true; } else if (messageCount === 2) { // LLM Output - this.generatedDisplay.innerText = data; // TODO - Update to HTML + this.generatedDisplay.innerText = JSON.parse(data).result; this.generatedLoading.hidden = true; } else { - es.close() + es.close(); break; } } + + this.fieldset.disabled = false; } } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 61f46201c..ff17cf527 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -614,4 +614,12 @@ input.shortcut-input { margin: 0; font-size: 1.6rem; } + button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + textarea:disabled { + opacity: 0.5; + cursor: not-allowed; + } } \ No newline at end of file diff --git a/resources/views/search/query.blade.php b/resources/views/search/query.blade.php index 48cb1eeaf..3293c0ddc 100644 --- a/resources/views/search/query.blade.php +++ b/resources/views/search/query.blade.php @@ -8,55 +8,45 @@
- - + method="post"> +
+ + +

Generated Response

-
+ -

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad adipisci aliquid architecto cupiditate dolor doloribus eligendi et expedita facilis fugiat fugit illo, ipsa laboriosam maiores, molestias mollitia non obcaecati porro quasi quis quos reprehenderit rerum sunt tenetur ullam unde voluptate voluptates! Distinctio et eum id molestiae nisi quisquam sed ut.

+

+ + When you run a query, the relevant content found & shown below will be used to help generate a smart machine generated response. + +

Relevant Content

-
+
- @include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true]) +

+ Start a query to find relevant matching content. + The items shown here reflect those used to help provide the above response. +

- -{{-- @if($results)--}} -{{--

Results

--}} - -{{--

LLM Output

--}} -{{--

{{ $results['llm_result'] }}

--}} - -{{--

Entity Matches

--}} -{{-- @foreach($results['entity_matches'] as $match)--}} -{{--
--}} -{{--
{{ $match['entity_type'] }}:{{ $match['entity_id'] }}; Distance: {{ $match['distance'] }}
--}} -{{--
--}} -{{-- match text--}} -{{--
{{ $match['text'] }}
--}} -{{--
--}} -{{--
--}} -{{-- @endforeach--}} -{{-- @endif--}}
@stop