403 lines
14 KiB
PHP
403 lines
14 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ms">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>Pembantu Maklumat — Sistem Pangkalan Pengetahuan</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
|
|
|
<style>
|
|
body {
|
|
background: linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.chat-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.chat-messages {
|
|
height: 500px;
|
|
overflow-y: auto;
|
|
padding: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .75rem;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
.message {
|
|
max-width: 85%;
|
|
animation: fadeIn .2s ease;
|
|
}
|
|
|
|
.message.user-message {
|
|
align-self: flex-end;
|
|
}
|
|
|
|
.message.bot-message {
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.message-bubble {
|
|
padding: .75rem 1rem;
|
|
border-radius: 1rem;
|
|
line-height: 1.6;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.user-message .message-bubble {
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
border-bottom-right-radius: .25rem;
|
|
}
|
|
|
|
.bot-message .message-bubble {
|
|
background: #fff;
|
|
border: 1px solid #e2e8f0;
|
|
border-bottom-left-radius: .25rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,.05);
|
|
}
|
|
|
|
.message-meta {
|
|
font-size: .7rem;
|
|
color: #94a3b8;
|
|
margin-top: .25rem;
|
|
padding: 0 .25rem;
|
|
}
|
|
|
|
.user-message .message-meta {
|
|
text-align: right;
|
|
}
|
|
|
|
.source-card {
|
|
font-size: .75rem;
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: .5rem;
|
|
padding: .5rem .75rem;
|
|
margin-top: .5rem;
|
|
}
|
|
|
|
.typing-indicator span {
|
|
display: inline-block;
|
|
width: 8px; height: 8px;
|
|
background: #94a3b8;
|
|
border-radius: 50%;
|
|
animation: typing .8s infinite;
|
|
}
|
|
|
|
.typing-indicator span:nth-child(2) { animation-delay: .15s; }
|
|
.typing-indicator span:nth-child(3) { animation-delay: .3s; }
|
|
|
|
@keyframes typing {
|
|
0%, 100% { transform: translateY(0); opacity: .5; }
|
|
50% { transform: translateY(-4px); opacity: 1; }
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.feedback-btn.active { opacity: 1; }
|
|
.feedback-btn { opacity: .6; transition: opacity .15s; }
|
|
.feedback-btn:hover { opacity: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="chat-container py-4 px-3">
|
|
|
|
{{-- Header --}}
|
|
<div class="card border-0 shadow-sm mb-3">
|
|
<div class="card-body py-3">
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center"
|
|
style="width:40px;height:40px">
|
|
<i class="bi bi-robot text-white fs-5"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">Pembantu Maklumat</div>
|
|
<div class="text-muted small">Sistem Pangkalan Pengetahuan</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center gap-2">
|
|
{{-- Pilih Kategori --}}
|
|
<select id="categorySelect" class="form-select form-select-sm" style="max-width:200px">
|
|
<option value="">Semua Kategori</option>
|
|
@foreach($categories as $cat)
|
|
<option value="{{ $cat->id }}"
|
|
{{ ($selectedCatId == $cat->id) ? 'selected' : '' }}>
|
|
{{ $cat->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
|
|
@auth
|
|
<a href="{{ route('admin.dashboard') }}" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-shield me-1"></i>Admin
|
|
</a>
|
|
@endauth
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Chat Window --}}
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="chat-messages" id="chatMessages">
|
|
{{-- Welcome message --}}
|
|
<div class="message bot-message">
|
|
<div class="message-bubble">
|
|
<strong>Selamat datang!</strong> Saya pembantu maklumat anda. 👋<br><br>
|
|
Anda boleh bertanya tentang:
|
|
<ul class="mb-0 mt-1">
|
|
@foreach($categories->take(5) as $cat)
|
|
<li>{{ $cat->name }}</li>
|
|
@endforeach
|
|
@if($categories->count() > 5)
|
|
<li class="text-muted">...dan {{ $categories->count() - 5 }} lagi</li>
|
|
@endif
|
|
</ul>
|
|
<div class="mt-2 text-muted small">
|
|
Pilih kategori di atas untuk soalan yang lebih tepat.
|
|
</div>
|
|
</div>
|
|
<div class="message-meta">Pembantu AI</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Input --}}
|
|
<div class="border-top p-3">
|
|
<form id="chatForm">
|
|
<div class="input-group">
|
|
<input type="text" id="questionInput" class="form-control"
|
|
placeholder="Taip soalan anda di sini..."
|
|
autocomplete="off" maxlength="1000">
|
|
<button type="submit" id="sendBtn" class="btn btn-primary px-3">
|
|
<i class="bi bi-send-fill"></i>
|
|
</button>
|
|
</div>
|
|
<div class="d-flex justify-content-between mt-1">
|
|
<small class="text-muted">Tekan Enter untuk hantar</small>
|
|
<small class="text-muted" id="charCounter">0/1000</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Disclaimer --}}
|
|
<div class="text-center mt-3">
|
|
<small class="text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
Jawapan dijana secara automatik berdasarkan dokumen rasmi.
|
|
Sila semak dokumen asal untuk maklumat muktamad.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script>
|
|
$(document).ready(function () {
|
|
let lastChatLogId = null;
|
|
|
|
// Kira karakter
|
|
$('#questionInput').on('input', function () {
|
|
const len = $(this).val().length;
|
|
$('#charCounter').text(len + '/1000');
|
|
});
|
|
|
|
// Submit borang
|
|
$('#chatForm').on('submit', function (e) {
|
|
e.preventDefault();
|
|
const question = $('#questionInput').val().trim();
|
|
if (!question) return;
|
|
|
|
sendQuestion(question);
|
|
});
|
|
|
|
// Enter key
|
|
$('#questionInput').on('keydown', function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
$('#chatForm').trigger('submit');
|
|
}
|
|
});
|
|
|
|
function sendQuestion(question) {
|
|
const categoryId = $('#categorySelect').val();
|
|
|
|
// Papar mesej user
|
|
appendUserMessage(question);
|
|
$('#questionInput').val('').trigger('input');
|
|
|
|
// Papar typing indicator
|
|
const typingId = appendTypingIndicator();
|
|
|
|
// Disable input
|
|
$('#sendBtn, #questionInput').prop('disabled', true);
|
|
|
|
$.ajax({
|
|
url: '{{ route("chatbot.ask") }}',
|
|
method: 'POST',
|
|
data: {
|
|
_token: $('meta[name="csrf-token"]').attr('content'),
|
|
question: question,
|
|
category_id: categoryId || null,
|
|
},
|
|
success: function (res) {
|
|
removeTypingIndicator(typingId);
|
|
if (res.success) {
|
|
const msgId = 'msg-' + Date.now();
|
|
appendBotMessage(res.answer, res.sources, res.has_answer, msgId);
|
|
// Simpan untuk feedback (dalam real app, dapatkan chat_log_id dari server)
|
|
// Untuk demo, kita simpan session token
|
|
lastChatLogId = null; // akan diisi bila server return log ID
|
|
} else {
|
|
appendErrorMessage(res.message || 'Ralat berlaku.');
|
|
}
|
|
},
|
|
error: function (xhr) {
|
|
removeTypingIndicator(typingId);
|
|
if (xhr.status === 429) {
|
|
appendErrorMessage('Terlalu banyak soalan. Sila tunggu sebentar.');
|
|
} else if (xhr.status === 503) {
|
|
appendErrorMessage('Perkhidmatan AI tidak tersedia. Sila cuba sebentar lagi.');
|
|
} else {
|
|
appendErrorMessage('Ralat sistem. Sila cuba lagi.');
|
|
}
|
|
},
|
|
complete: function () {
|
|
$('#sendBtn, #questionInput').prop('disabled', false);
|
|
$('#questionInput').focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
function appendUserMessage(text) {
|
|
const html = `
|
|
<div class="message user-message">
|
|
<div class="message-bubble">${escapeHtml(text)}</div>
|
|
<div class="message-meta">${formatTime(new Date())}</div>
|
|
</div>`;
|
|
$('#chatMessages').append(html);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function appendBotMessage(answer, sources, hasAnswer, msgId) {
|
|
let sourcesHtml = '';
|
|
if (sources && sources.length > 0) {
|
|
sourcesHtml = '<div class="source-card mt-2">';
|
|
sourcesHtml += '<div class="fw-semibold mb-1 text-muted"><i class="bi bi-book me-1"></i>Sumber Rujukan:</div>';
|
|
sources.forEach(function (s) {
|
|
const icon = s.type === 'pdf' ? 'bi-file-pdf text-danger' : 'bi-lightbulb text-primary';
|
|
const page = s.page_number ? ` — ms. ${s.page_number}` : '';
|
|
sourcesHtml += `<div class="mb-1">
|
|
<i class="bi ${icon} me-1"></i>
|
|
<strong>${escapeHtml(s.title)}</strong>${page}
|
|
<span class="badge bg-light text-dark border ms-1">${escapeHtml(s.category)}</span>
|
|
</div>`;
|
|
});
|
|
sourcesHtml += '</div>';
|
|
}
|
|
|
|
const warningHtml = !hasAnswer
|
|
? '<div class="alert alert-warning py-1 px-2 mt-2 mb-0 small"><i class="bi bi-exclamation-triangle me-1"></i>Jawapan tidak dijumpai dalam pangkalan pengetahuan.</div>'
|
|
: '';
|
|
|
|
const feedbackHtml = `
|
|
<div class="d-flex gap-1 mt-2" id="feedback-${msgId}">
|
|
<small class="text-muted me-1">Adakah ini membantu?</small>
|
|
<button class="btn btn-sm btn-outline-success feedback-btn py-0 px-1" data-rating="helpful" data-msgid="${msgId}">
|
|
<i class="bi bi-hand-thumbs-up"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger feedback-btn py-0 px-1" data-rating="not_helpful" data-msgid="${msgId}">
|
|
<i class="bi bi-hand-thumbs-down"></i>
|
|
</button>
|
|
</div>`;
|
|
|
|
const html = `
|
|
<div class="message bot-message" id="${msgId}">
|
|
<div class="message-bubble">
|
|
<div style="white-space:pre-wrap">${escapeHtml(answer)}</div>
|
|
${warningHtml}
|
|
${sourcesHtml}
|
|
${feedbackHtml}
|
|
</div>
|
|
<div class="message-meta">${formatTime(new Date())}</div>
|
|
</div>`;
|
|
|
|
$('#chatMessages').append(html);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function appendErrorMessage(text) {
|
|
const html = `
|
|
<div class="message bot-message">
|
|
<div class="message-bubble bg-danger-subtle border-danger">
|
|
<i class="bi bi-exclamation-circle me-1 text-danger"></i>
|
|
${escapeHtml(text)}
|
|
</div>
|
|
</div>`;
|
|
$('#chatMessages').append(html);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function appendTypingIndicator() {
|
|
const id = 'typing-' + Date.now();
|
|
const html = `
|
|
<div class="message bot-message" id="${id}">
|
|
<div class="message-bubble">
|
|
<div class="typing-indicator d-flex gap-1 align-items-center">
|
|
<span></span><span></span><span></span>
|
|
<small class="text-muted ms-1">Sedang mencari jawapan...</small>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
$('#chatMessages').append(html);
|
|
scrollToBottom();
|
|
return id;
|
|
}
|
|
|
|
function removeTypingIndicator(id) {
|
|
$('#' + id).remove();
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
const el = document.getElementById('chatMessages');
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return $('<div>').text(text).html();
|
|
}
|
|
|
|
function formatTime(date) {
|
|
return date.toLocaleTimeString('ms-MY', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
// Feedback handler
|
|
$(document).on('click', '.feedback-btn', function () {
|
|
const rating = $(this).data('rating');
|
|
const msgId = $(this).data('msgid');
|
|
|
|
// Note: Untuk feedback berfungsi penuh, perlu simpan chat_log_id
|
|
// dari response server. Ini adalah placeholder UI.
|
|
$(this).addClass('active').siblings('.feedback-btn').removeClass('active');
|
|
|
|
const feedbackDiv = $('#feedback-' + msgId);
|
|
feedbackDiv.html('<small class="text-muted"><i class="bi bi-check2 me-1 text-success"></i>Terima kasih atas maklum balas!</small>');
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|