mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-10-22 07:52:19 +03:00
Vectors: Finished core fetch & display functionality
This commit is contained in:
@@ -3,7 +3,6 @@
|
|||||||
namespace BookStack\Search\Queries;
|
namespace BookStack\Search\Queries;
|
||||||
|
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Search\SearchOptions;
|
|
||||||
use BookStack\Search\SearchRunner;
|
use BookStack\Search\SearchRunner;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -12,6 +11,13 @@ class QueryController extends Controller
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
protected SearchRunner $searchRunner,
|
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)
|
public function show(Request $request)
|
||||||
{
|
{
|
||||||
// TODO - Validate if query system is active
|
|
||||||
$query = $request->get('ask', '');
|
$query = $request->get('ask', '');
|
||||||
|
|
||||||
// TODO - Placeholder
|
|
||||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString("cat"), 'all', 1, 20)['results'];
|
|
||||||
|
|
||||||
// TODO - Set page title
|
// TODO - Set page title
|
||||||
|
|
||||||
return view('search.query', [
|
return view('search.query', [
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
'entities' => $entities,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,16 +39,23 @@ class QueryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function run(Request $request, VectorSearchRunner $searchRunner, LlmQueryRunner $llmRunner)
|
public function run(Request $request, VectorSearchRunner $searchRunner, LlmQueryRunner $llmRunner)
|
||||||
{
|
{
|
||||||
// TODO - Validate if query system is active
|
// TODO - Rate limiting
|
||||||
$query = $request->get('query', '');
|
$query = $request->get('query', '');
|
||||||
|
|
||||||
return response()->eventStream(function () use ($query, $searchRunner, $llmRunner) {
|
return response()->eventStream(function () use ($query, $searchRunner, $llmRunner) {
|
||||||
$results = $query ? $searchRunner->run($query) : [];
|
$results = $query ? $searchRunner->run($query) : [];
|
||||||
|
|
||||||
$count = count($results);
|
$entities = [];
|
||||||
yield "Found {$count} results for query: {$query}!";
|
foreach ($results as $result) {
|
||||||
$llmResult = $llmRunner->run($query, $results);
|
$entityKey = $result->entity->getMorphClass() . ':' . $result->entity->id;
|
||||||
yield "LLM result: {$llmResult}";
|
if (!isset($entities[$entityKey])) {
|
||||||
|
$entities[$entityKey] = $result->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ['view' => view('entities.list', ['entities' => $entities])->render()];
|
||||||
|
|
||||||
|
yield ['result' => $llmRunner->run($query, $results)];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import {Component} from "./component";
|
import {Component} from "./component";
|
||||||
import {createEventSource} from "eventsource-client";
|
|
||||||
|
|
||||||
export class QueryManager extends Component {
|
export class QueryManager extends Component {
|
||||||
protected input!: HTMLTextAreaElement;
|
protected input!: HTMLTextAreaElement;
|
||||||
@@ -8,33 +7,52 @@ export class QueryManager extends Component {
|
|||||||
protected contentLoading!: HTMLElement;
|
protected contentLoading!: HTMLElement;
|
||||||
protected contentDisplay!: HTMLElement;
|
protected contentDisplay!: HTMLElement;
|
||||||
protected form!: HTMLFormElement;
|
protected form!: HTMLFormElement;
|
||||||
|
protected fieldset!: HTMLFieldSetElement;
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.input = this.$refs.input as HTMLTextAreaElement;
|
this.input = this.$refs.input as HTMLTextAreaElement;
|
||||||
this.form = this.$refs.form as HTMLFormElement;
|
this.form = this.$refs.form as HTMLFormElement;
|
||||||
|
this.fieldset = this.$refs.fieldset as HTMLFieldSetElement;
|
||||||
this.generatedLoading = this.$refs.generatedLoading;
|
this.generatedLoading = this.$refs.generatedLoading;
|
||||||
this.generatedDisplay = this.$refs.generatedDisplay;
|
this.generatedDisplay = this.$refs.generatedDisplay;
|
||||||
this.contentLoading = this.$refs.contentLoading;
|
this.contentLoading = this.$refs.contentLoading;
|
||||||
this.contentDisplay = this.$refs.contentDisplay;
|
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 => {
|
this.form.addEventListener('submit', event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.runQuery();
|
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.contentLoading.hidden = false;
|
||||||
this.generatedLoading.hidden = false;
|
this.generatedLoading.hidden = false;
|
||||||
this.contentDisplay.innerHTML = '';
|
this.contentDisplay.innerHTML = '';
|
||||||
this.generatedDisplay.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});
|
const es = window.$http.eventSource('/query', 'POST', {query});
|
||||||
|
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
@@ -42,16 +60,18 @@ export class QueryManager extends Component {
|
|||||||
messageCount++;
|
messageCount++;
|
||||||
if (messageCount === 1) {
|
if (messageCount === 1) {
|
||||||
// Entity results
|
// Entity results
|
||||||
this.contentDisplay.innerText = data; // TODO - Update to HTML
|
this.contentDisplay.innerHTML = JSON.parse(data).view;
|
||||||
this.contentLoading.hidden = true;
|
this.contentLoading.hidden = true;
|
||||||
} else if (messageCount === 2) {
|
} else if (messageCount === 2) {
|
||||||
// LLM Output
|
// LLM Output
|
||||||
this.generatedDisplay.innerText = data; // TODO - Update to HTML
|
this.generatedDisplay.innerText = JSON.parse(data).result;
|
||||||
this.generatedLoading.hidden = true;
|
this.generatedLoading.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
es.close()
|
es.close();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fieldset.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -614,4 +614,12 @@ input.shortcut-input {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
textarea:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -8,8 +8,8 @@
|
|||||||
<form action="{{ url('/query') }}"
|
<form action="{{ url('/query') }}"
|
||||||
refs="query-manager@form"
|
refs="query-manager@form"
|
||||||
title="Run Query"
|
title="Run Query"
|
||||||
method="post"
|
method="post">
|
||||||
class="query-form">
|
<fieldset class="query-form" refs="query-manager@fieldset">
|
||||||
<textarea name="query"
|
<textarea name="query"
|
||||||
refs="query-manager@input"
|
refs="query-manager@input"
|
||||||
class="input-fill-width"
|
class="input-fill-width"
|
||||||
@@ -17,46 +17,36 @@
|
|||||||
placeholder="Enter a query"
|
placeholder="Enter a query"
|
||||||
autocomplete="off">{{ $query }}</textarea>
|
autocomplete="off">{{ $query }}</textarea>
|
||||||
<button class="button icon">@icon('search')</button>
|
<button class="button icon">@icon('search')</button>
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card content-wrap auto-height pb-xl">
|
<div class="card content-wrap auto-height pb-xl">
|
||||||
<h2 class="list-heading">Generated Response</h2>
|
<h2 class="list-heading">Generated Response</h2>
|
||||||
<div refs="query-manager@generated-loading">
|
<div refs="query-manager@generated-loading" hidden>
|
||||||
@include('common.loading-icon')
|
@include('common.loading-icon')
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
|
||||||
<div class="card content-wrap auto-height pb-xl">
|
<div class="card content-wrap auto-height pb-xl">
|
||||||
<h2 class="list-heading">Relevant Content</h2>
|
<h2 class="list-heading">Relevant Content</h2>
|
||||||
<div refs="query-manager@content-loading">
|
<div refs="query-manager@content-loading" hidden>
|
||||||
@include('common.loading-icon')
|
@include('common.loading-icon')
|
||||||
</div>
|
</div>
|
||||||
<div class="book-contents">
|
<div class="book-contents">
|
||||||
<div refs="query-manager@content-display" class="entity-list">
|
<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>
|
</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>
|
</div>
|
||||||
@stop
|
@stop
|
||||||
|
Reference in New Issue
Block a user