- Added tone instruction retrieval to SupportReplyService. - Improved user feedback when no relevant article is found. - Included article URL and tone instruction in LLM prompt. - Updated response format to include source information. - Enhanced article management UI with search functionality and editing capabilities. - Introduced a new API endpoint for nearest articles based on vector search. - Added confidence badge component to display article confidence levels. - Implemented tests for article searching, editing, and nearest article API. - Removed obsolete .htaccess file.
248 lines
11 KiB
PHP
248 lines
11 KiB
PHP
<x-layouts.admin title="Knowledge Base Search Demo">
|
|
<div class="space-y-6">
|
|
<section class="bg-white border border-slate-200 rounded-lg p-6">
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Website kennisbank search</h2>
|
|
<p class="text-sm text-slate-600 mt-1">
|
|
Demo voor het ophalen van de dichtstbijzijnde gepubliceerde artikelen via vector search.
|
|
</p>
|
|
</div>
|
|
<span class="inline-flex w-fit items-center rounded bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">
|
|
GET /api/articles/nearest
|
|
</span>
|
|
</div>
|
|
|
|
<form id="nearest-articles-form" class="mt-6 grid gap-4 lg:grid-cols-[1fr_140px_170px_auto]">
|
|
<label class="block">
|
|
<span class="text-sm font-medium text-slate-700">Zoekvraag</span>
|
|
<input
|
|
id="query"
|
|
name="query"
|
|
type="search"
|
|
value="Hoe stel ik DNS in?"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900"
|
|
required
|
|
minlength="2"
|
|
maxlength="1000"
|
|
>
|
|
</label>
|
|
|
|
<label class="block">
|
|
<span class="text-sm font-medium text-slate-700">Aantal</span>
|
|
<input
|
|
id="limit"
|
|
name="limit"
|
|
type="number"
|
|
min="1"
|
|
max="20"
|
|
value="5"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900"
|
|
>
|
|
</label>
|
|
|
|
<label class="block">
|
|
<span class="text-sm font-medium text-slate-700">Min. similarity</span>
|
|
<input
|
|
id="min_similarity"
|
|
name="min_similarity"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value="0"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900"
|
|
>
|
|
</label>
|
|
|
|
<button
|
|
type="submit"
|
|
class="mt-6 inline-flex h-10 items-center justify-center rounded-md bg-slate-900 px-4 text-sm font-medium text-white hover:bg-slate-800"
|
|
>
|
|
Zoek
|
|
</button>
|
|
</form>
|
|
|
|
<div id="status" class="mt-4 hidden rounded-md border px-3 py-2 text-sm"></div>
|
|
</section>
|
|
|
|
<section class="grid gap-6 lg:grid-cols-[1fr_420px]">
|
|
<div class="bg-white border border-slate-200 rounded-lg p-6">
|
|
<div class="flex items-center justify-between gap-3">
|
|
<h3 class="font-semibold">Resultaten</h3>
|
|
<span id="result-count" class="text-xs text-slate-500">Nog niet gezocht</span>
|
|
</div>
|
|
<div id="results" class="mt-4 space-y-3"></div>
|
|
</div>
|
|
|
|
<aside class="bg-white border border-slate-200 rounded-lg p-6">
|
|
<h3 class="font-semibold">Call documentatie</h3>
|
|
|
|
<div class="mt-4 space-y-4 text-sm">
|
|
<div>
|
|
<p class="font-medium text-slate-700">Endpoint</p>
|
|
<pre class="mt-1 overflow-auto rounded bg-slate-950 p-3 text-xs text-slate-100">GET /api/articles/nearest</pre>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="font-medium text-slate-700">Query parameters</p>
|
|
<dl class="mt-2 divide-y divide-slate-100 border-y border-slate-100">
|
|
<div class="grid grid-cols-[130px_1fr] gap-3 py-2">
|
|
<dt class="font-mono text-xs text-slate-700">query</dt>
|
|
<dd class="text-slate-600">Verplicht. De zoekvraag die wordt ge-embed.</dd>
|
|
</div>
|
|
<div class="grid grid-cols-[130px_1fr] gap-3 py-2">
|
|
<dt class="font-mono text-xs text-slate-700">limit</dt>
|
|
<dd class="text-slate-600">Optioneel. 1 t/m 20, standaard 5.</dd>
|
|
</div>
|
|
<div class="grid grid-cols-[130px_1fr] gap-3 py-2">
|
|
<dt class="font-mono text-xs text-slate-700">min_similarity</dt>
|
|
<dd class="text-slate-600">Optioneel. 0 t/m 1, standaard 0.</dd>
|
|
</div>
|
|
<div class="grid grid-cols-[130px_1fr] gap-3 py-2">
|
|
<dt class="font-mono text-xs text-slate-700">include_content</dt>
|
|
<dd class="text-slate-600">Optioneel. Boolean, standaard false.</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="font-medium text-slate-700">Voorbeeld</p>
|
|
<pre id="request-example" class="mt-1 overflow-auto rounded bg-slate-950 p-3 text-xs text-slate-100">GET /api/articles/nearest?query=Hoe%20stel%20ik%20DNS%20in%3F&limit=5&min_similarity=0</pre>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="font-medium text-slate-700">Response shape</p>
|
|
<pre class="mt-1 overflow-auto rounded bg-slate-950 p-3 text-xs text-slate-100">{
|
|
"data": [
|
|
{
|
|
"article_id": 12,
|
|
"title": "DNS instellen",
|
|
"similarity": 0.8421,
|
|
"distance": 0.1579,
|
|
"snippet": "Korte preview van het artikel...",
|
|
"content": null,
|
|
"source_url": "https://...",
|
|
"source_article_id": 12345,
|
|
"note": null,
|
|
"allowed_actions": []
|
|
}
|
|
],
|
|
"meta": {
|
|
"query": "Hoe stel ik DNS in?",
|
|
"limit": 5,
|
|
"min_similarity": 0,
|
|
"published_only": true,
|
|
"embedding_provider_instance_id": "default",
|
|
"embedding_model": "nomic-embed-text"
|
|
}
|
|
}</pre>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('nearest-articles-form');
|
|
const resultsEl = document.getElementById('results');
|
|
const statusEl = document.getElementById('status');
|
|
const resultCountEl = document.getElementById('result-count');
|
|
const requestExampleEl = document.getElementById('request-example');
|
|
|
|
function setStatus(message, type = 'info') {
|
|
statusEl.classList.remove('hidden', 'border-red-200', 'bg-red-50', 'text-red-700', 'border-slate-200', 'bg-slate-50', 'text-slate-700');
|
|
statusEl.classList.add(type === 'error' ? 'border-red-200' : 'border-slate-200');
|
|
statusEl.classList.add(type === 'error' ? 'bg-red-50' : 'bg-slate-50');
|
|
statusEl.classList.add(type === 'error' ? 'text-red-700' : 'text-slate-700');
|
|
statusEl.textContent = message;
|
|
}
|
|
|
|
function clearStatus() {
|
|
statusEl.classList.add('hidden');
|
|
statusEl.textContent = '';
|
|
}
|
|
|
|
function buildUrl() {
|
|
const params = new URLSearchParams({
|
|
query: document.getElementById('query').value,
|
|
limit: document.getElementById('limit').value || '5',
|
|
min_similarity: document.getElementById('min_similarity').value || '0',
|
|
});
|
|
|
|
return `/api/articles/nearest?${params.toString()}`;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
const div = document.createElement('div');
|
|
div.textContent = value ?? '';
|
|
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function renderResults(results) {
|
|
resultCountEl.textContent = `${results.length} resultaat${results.length === 1 ? '' : 'en'}`;
|
|
|
|
if (results.length === 0) {
|
|
resultsEl.innerHTML = '<div class="rounded-md border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600">Geen artikelen gevonden voor deze zoekvraag of threshold.</div>';
|
|
return;
|
|
}
|
|
|
|
resultsEl.innerHTML = results.map((result) => {
|
|
const url = result.source_url
|
|
? `<a href="${escapeHtml(result.source_url)}" target="_blank" rel="noreferrer" class="text-sm text-slate-900 underline">Bron openen</a>`
|
|
: '';
|
|
|
|
return `
|
|
<article class="rounded-md border border-slate-200 p-4">
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<h4 class="font-medium">${escapeHtml(result.title)}</h4>
|
|
<p class="mt-1 text-sm text-slate-600">${escapeHtml(result.snippet)}</p>
|
|
</div>
|
|
<span class="w-fit rounded bg-slate-100 px-2 py-1 text-xs font-medium text-slate-700">
|
|
${(result.similarity * 100).toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-slate-500">
|
|
<span>Article #${result.article_id}</span>
|
|
<span>Distance ${result.distance}</span>
|
|
${url}
|
|
</div>
|
|
</article>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
clearStatus();
|
|
resultsEl.innerHTML = '';
|
|
resultCountEl.textContent = 'Zoeken...';
|
|
|
|
const url = buildUrl();
|
|
requestExampleEl.textContent = `GET ${url}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
const payload = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(payload.message || 'De request is mislukt.');
|
|
}
|
|
|
|
renderResults(payload.data || []);
|
|
setStatus('Request afgerond via vector search.');
|
|
} catch (error) {
|
|
resultCountEl.textContent = 'Mislukt';
|
|
setStatus(error.message, 'error');
|
|
}
|
|
});
|
|
</script>
|
|
</x-layouts.admin>
|