1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-10-20 20:12:39 +03:00

Vectors: Finished core fetch & display functionality

This commit is contained in:
Dan Brown
2025-08-22 12:59:32 +01:00
parent 8eef5a1ee7
commit bb08f62327
4 changed files with 77 additions and 51 deletions

View File

@@ -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)];
});
}
}

View File

@@ -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<void> {
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;
}
}

View File

@@ -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;
}
}

View File

@@ -8,55 +8,45 @@
<form action="{{ url('/query') }}"
refs="query-manager@form"
title="Run Query"
method="post"
class="query-form">
<textarea name="query"
refs="query-manager@input"
class="input-fill-width"
rows="5"
placeholder="Enter a query"
autocomplete="off">{{ $query }}</textarea>
<button class="button icon">@icon('search')</button>
method="post">
<fieldset class="query-form" refs="query-manager@fieldset">
<textarea name="query"
refs="query-manager@input"
class="input-fill-width"
rows="5"
placeholder="Enter a query"
autocomplete="off">{{ $query }}</textarea>
<button class="button icon">@icon('search')</button>
</fieldset>
</form>
</div>
<div class="card content-wrap auto-height pb-xl">
<h2 class="list-heading">Generated Response</h2>
<div refs="query-manager@generated-loading">
<div refs="query-manager@generated-loading" hidden>
@include('common.loading-icon')
</div>
<p refs="query-manager@generated-display">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.</p>
<p refs="query-manager@generated-display">
<span class="text-muted italic">
When you run a query, the relevant content found & shown below will be used to help generate a smart machine generated response.
</span>
</p>
</div>
<div class="card content-wrap auto-height pb-xl">
<h2 class="list-heading">Relevant Content</h2>
<div refs="query-manager@content-loading">
<div refs="query-manager@content-loading" hidden>
@include('common.loading-icon')
</div>
<div class="book-contents">
<div refs="query-manager@content-display" class="entity-list">
@include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
<p class="text-muted italic mx-m">
Start a query to find relevant matching content.
The items shown here reflect those used to help provide the above response.
</p>
</div>
</div>
</div>
{{-- @if($results)--}}
{{-- <h2>Results</h2>--}}
{{-- <h3>LLM Output</h3>--}}
{{-- <p>{{ $results['llm_result'] }}</p>--}}
{{-- <h3>Entity Matches</h3>--}}
{{-- @foreach($results['entity_matches'] as $match)--}}
{{-- <div>--}}
{{-- <div><strong>{{ $match['entity_type'] }}:{{ $match['entity_id'] }}; Distance: {{ $match['distance'] }}</strong></div>--}}
{{-- <details>--}}
{{-- <summary>match text</summary>--}}
{{-- <div>{{ $match['text'] }}</div>--}}
{{-- </details>--}}
{{-- </div>--}}
{{-- @endforeach--}}
{{-- @endif--}}
</div>
@stop