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>
|
||||
25
resources/views/components/admin/confidence-badge.blade.php
Normal file
25
resources/views/components/admin/confidence-badge.blade.php
Normal file
@@ -0,0 +1,25 @@
|
||||
@props([
|
||||
'confidence',
|
||||
'threshold' => \App\Services\KnowledgeGapService::CONFIDENCE_THRESHOLD,
|
||||
])
|
||||
|
||||
@php
|
||||
$score = (float) $confidence;
|
||||
$passes = $score >= (float) $threshold;
|
||||
@endphp
|
||||
|
||||
<span @class([
|
||||
'inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium',
|
||||
'bg-green-100 text-green-800' => $passes,
|
||||
'bg-amber-100 text-amber-900' => ! $passes,
|
||||
])>
|
||||
<span @class([
|
||||
'h-2 w-2 rounded-full',
|
||||
'bg-green-600' => $passes,
|
||||
'bg-amber-600' => ! $passes,
|
||||
])></span>
|
||||
Confidence {{ number_format($score, 2) }}
|
||||
<span class="text-[11px] opacity-80">
|
||||
{{ $passes ? 'haalt drempel' : 'onder drempel' }} {{ number_format((float) $threshold, 2) }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -17,6 +17,7 @@
|
||||
<nav class="flex gap-3 text-sm">
|
||||
<a href="{{ route('admin.dashboard') }}" class="hover:underline">Dashboard</a>
|
||||
<a href="{{ route('admin.articles') }}" class="hover:underline">Articles</a>
|
||||
<a href="{{ route('admin.knowledge-search-demo') }}" class="hover:underline">KB demo</a>
|
||||
<a href="{{ route('admin.quick-replies') }}" class="hover:underline">Snelantwoorden</a>
|
||||
<a href="{{ route('admin.tickets') }}" class="hover:underline">Tickets</a>
|
||||
<a href="{{ route('admin.process') }}" class="hover:underline">Proces</a>
|
||||
|
||||
@@ -35,91 +35,142 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h2 class="font-semibold mb-3">Artikelen</h2>
|
||||
<div class="flex flex-col gap-3 mb-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 class="font-semibold">Artikelen</h2>
|
||||
<p class="text-sm text-slate-500">Zoek op titel, inhoud, bron of artikelnummer.</p>
|
||||
</div>
|
||||
<div class="w-full lg:w-96">
|
||||
<label for="article-search" class="block text-xs font-semibold text-slate-500 mb-1">Zoeken</label>
|
||||
<input id="article-search" wire:model.live.debounce.300ms="search" type="search"
|
||||
class="w-full border rounded p-2" placeholder="Bijvoorbeeld: DNS, e-mail, #42">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@foreach ($articles as $article)
|
||||
<div class="border rounded p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium">#{{ $article->id }} {{ $article->title }}</div>
|
||||
@if (($article->status ?? 'published') === 'draft')
|
||||
<span
|
||||
class="inline-block mt-1 text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-800">Concept
|
||||
(AI)</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if (($article->status ?? 'published') === 'draft')
|
||||
<button type="button" wire:click="approveDraft({{ $article->id }})"
|
||||
class="text-sm text-green-700 hover:underline">
|
||||
Valideren & publiceren
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" wire:click="deleteArticle({{ $article->id }})"
|
||||
wire:confirm="Weet je zeker dat je dit artikel wilt verwijderen?"
|
||||
class="text-sm text-red-600 hover:underline">
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if ($article->note)
|
||||
<div class="mt-2 text-xs rounded bg-slate-50 p-2 text-slate-600">
|
||||
<span class="font-semibold">LLM note:</span>
|
||||
{{ \Illuminate\Support\Str::limit($article->note, 180) }}
|
||||
</div>
|
||||
@endif
|
||||
@if (($article->allowed_actions ?? []) !== [])
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
@foreach ($article->allowed_actions as $action)
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded bg-blue-100 text-blue-800">{{ $action }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-sm text-slate-600">{{ \Illuminate\Support\Str::limit($article->content, 140) }}
|
||||
</div>
|
||||
<div class="mt-3 rounded bg-slate-50 p-3 space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-500">LLM note</label>
|
||||
<textarea wire:model="articleNotes.{{ $article->id }}" class="w-full border rounded p-2 min-h-20 text-sm"
|
||||
placeholder="Interne aanwijzingen voor dit artikel"></textarea>
|
||||
@error("articleNotes.{$article->id}")
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
<label class="flex items-start gap-2 text-sm">
|
||||
<input wire:model="articleAllowedActions.{{ $article->id }}" type="checkbox"
|
||||
value="domain_inf" class="mt-1 rounded border-slate-300">
|
||||
<span>domain_inf toestaan</span>
|
||||
</label>
|
||||
@error("articleAllowedActions.{$article->id}.*")
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Gekoppelde
|
||||
snelantwoorden</label>
|
||||
@if ($quickReplyOptions->isEmpty())
|
||||
<p class="text-xs text-slate-500">Nog geen actieve snelantwoorden beschikbaar.</p>
|
||||
@else
|
||||
<div class="grid gap-1 sm:grid-cols-2">
|
||||
@foreach ($quickReplyOptions as $quickReply)
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input wire:model="articleQuickReplies.{{ $article->id }}" type="checkbox"
|
||||
value="{{ $quickReply->id }}" class="rounded border-slate-300">
|
||||
<span>{{ $quickReply->title }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@forelse ($articles as $article)
|
||||
<div class="border rounded p-3" wire:key="article-{{ $article->id }}">
|
||||
@if ($editingArticleId === $article->id)
|
||||
<form wire:submit="saveEdit" class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium">Artikel #{{ $article->id }} bewerken</div>
|
||||
<p class="text-sm text-slate-500">Na opslaan wordt het artikel opnieuw geindexeerd.</p>
|
||||
</div>
|
||||
@endif
|
||||
@error("articleQuickReplies.{$article->id}.*")
|
||||
<button type="button" wire:click="cancelEdit"
|
||||
class="text-sm text-slate-600 hover:underline">
|
||||
Annuleren
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Titel</label>
|
||||
<input wire:model="editTitle" type="text" class="w-full border rounded p-2">
|
||||
@error('editTitle')
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Inhoud</label>
|
||||
<textarea wire:model="editContent" class="w-full border rounded p-2 min-h-64"></textarea>
|
||||
@error('editContent')
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<button class="bg-slate-900 text-white px-4 py-2 rounded" type="submit">
|
||||
Wijzigingen opslaan
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium">#{{ $article->id }} {{ $article->title }}</div>
|
||||
@if (($article->status ?? 'published') === 'draft')
|
||||
<span
|
||||
class="inline-block mt-1 text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-800">Concept
|
||||
(AI)</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if (($article->status ?? 'published') === 'draft')
|
||||
<button type="button" wire:click="approveDraft({{ $article->id }})"
|
||||
class="text-sm text-green-700 hover:underline">
|
||||
Valideren & publiceren
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" wire:click="startEdit({{ $article->id }})"
|
||||
class="text-sm text-slate-700 hover:underline">
|
||||
Bewerken
|
||||
</button>
|
||||
<button type="button" wire:click="deleteArticle({{ $article->id }})"
|
||||
wire:confirm="Weet je zeker dat je dit artikel wilt verwijderen?"
|
||||
class="text-sm text-red-600 hover:underline">
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if ($article->note)
|
||||
<div class="mt-2 text-xs rounded bg-slate-50 p-2 text-slate-600">
|
||||
<span class="font-semibold">LLM note:</span>
|
||||
{{ \Illuminate\Support\Str::limit($article->note, 180) }}
|
||||
</div>
|
||||
@endif
|
||||
@if (($article->allowed_actions ?? []) !== [])
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
@foreach ($article->allowed_actions as $action)
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded bg-blue-100 text-blue-800">{{ $action }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-sm text-slate-600">{{ \Illuminate\Support\Str::limit($article->content, 140) }}
|
||||
</div>
|
||||
<div class="mt-3 rounded bg-slate-50 p-3 space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-500">LLM note</label>
|
||||
<textarea wire:model="articleNotes.{{ $article->id }}" class="w-full border rounded p-2 min-h-20 text-sm"
|
||||
placeholder="Interne aanwijzingen voor dit artikel"></textarea>
|
||||
@error("articleNotes.{$article->id}")
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
<label class="flex items-start gap-2 text-sm">
|
||||
<input wire:model="articleAllowedActions.{{ $article->id }}" type="checkbox"
|
||||
value="domain_inf" class="mt-1 rounded border-slate-300">
|
||||
<span>domain_inf toestaan</span>
|
||||
</label>
|
||||
@error("articleAllowedActions.{$article->id}.*")
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Gekoppelde
|
||||
snelantwoorden</label>
|
||||
@if ($quickReplyOptions->isEmpty())
|
||||
<p class="text-xs text-slate-500">Nog geen actieve snelantwoorden beschikbaar.</p>
|
||||
@else
|
||||
<div class="grid gap-1 sm:grid-cols-2">
|
||||
@foreach ($quickReplyOptions as $quickReply)
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input wire:model="articleQuickReplies.{{ $article->id }}"
|
||||
type="checkbox" value="{{ $quickReply->id }}"
|
||||
class="rounded border-slate-300">
|
||||
<span>{{ $quickReply->title }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@error("articleQuickReplies.{$article->id}.*")
|
||||
<p class="text-red-600 text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<button type="button" wire:click="saveMetadata({{ $article->id }})"
|
||||
class="text-sm px-3 py-1 rounded bg-slate-900 text-white">
|
||||
Metadata opslaan
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" wire:click="saveMetadata({{ $article->id }})"
|
||||
class="text-sm px-3 py-1 rounded bg-slate-900 text-white">
|
||||
Metadata opslaan
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@empty
|
||||
<div class="border rounded p-4 text-sm text-slate-500">
|
||||
Geen artikelen gevonden.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-4">{{ $articles->links() }}</div>
|
||||
</div>
|
||||
|
||||
@@ -64,8 +64,12 @@
|
||||
</div>
|
||||
<div class="text-sm text-slate-700 mb-2">{{ $ticket->message }}</div>
|
||||
@if ($ticket->bestArticle)
|
||||
<div class="text-sm">Article: #{{ $ticket->bestArticle->id }} | Confidence:
|
||||
{{ number_format((float) $ticket->confidence, 2) }}</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span>Article: #{{ $ticket->bestArticle->id }}</span>
|
||||
@if ($ticket->confidence !== null)
|
||||
<x-admin.confidence-badge :confidence="$ticket->confidence" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-slate-500">{{ $ticket->explanation }}</div>
|
||||
@endif
|
||||
<a class="text-sm underline"
|
||||
|
||||
@@ -67,10 +67,20 @@
|
||||
@endif
|
||||
@if ($ticket->bestArticle)
|
||||
<div class="mt-3 text-sm rounded bg-green-100 text-green-800 p-2">
|
||||
Beste artikel: #{{ $ticket->bestArticle->id }} - {{ $ticket->bestArticle->title }}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span>Beste artikel: #{{ $ticket->bestArticle->id }} - {{ $ticket->bestArticle->title }}</span>
|
||||
@if ($ticket->confidence !== null)
|
||||
(confidence {{ number_format($ticket->confidence, 2) }})
|
||||
<x-admin.confidence-badge :confidence="$ticket->confidence" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
Bron:
|
||||
@if ($ticket->bestArticle->source_url)
|
||||
<a href="{{ $ticket->bestArticle->source_url }}" target="_blank" class="underline">{{ $ticket->bestArticle->source_url }}</a>
|
||||
@else
|
||||
niet beschikbaar
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user