feat: Enhance Support Reply Service with tone instructions and article details
- 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.
This commit is contained in:
247
resources/views/admin/knowledge-search-demo.blade.php
Normal file
247
resources/views/admin/knowledge-search-demo.blade.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user