Files
ChatbotAI/resources/views/chatbot/index.blade.php
2026-05-18 08:56:23 +08:00

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>