First commit

This commit is contained in:
Saufi
2026-05-18 08:56:23 +08:00
commit fd3d3a4d2b
147 changed files with 22099 additions and 0 deletions

11
resources/css/app.css Normal file
View File

@@ -0,0 +1,11 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

1
resources/js/app.js Normal file
View File

@@ -0,0 +1 @@
import './bootstrap';

4
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@@ -0,0 +1,100 @@
@extends('layouts.admin')
@section('title', 'Log Audit')
@section('breadcrumb')
<li class="breadcrumb-item active">Log Audit</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">Log Audit Sistem</h4>
<small class="text-muted">Log tidak boleh diubah atau dipadam</small>
</div>
{{-- Filter --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-3">
<select name="event" class="form-select form-select-sm">
<option value="">Semua Event</option>
@foreach($eventTypes as $event)
<option value="{{ $event }}" {{ request('event') == $event ? 'selected' : '' }}>
{{ $event }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<input type="date" name="date_from" class="form-control form-control-sm"
value="{{ request('date_from') }}" placeholder="Dari">
</div>
<div class="col-md-2">
<input type="date" name="date_to" class="form-control form-control-sm"
value="{{ request('date_to') }}" placeholder="Hingga">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-search me-1"></i>Tapis
</button>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Event</th>
<th>Deskripsi</th>
<th>Pengguna</th>
<th>IP</th>
<th>Tarikh</th>
<th class="text-end pe-3">Detail</th>
</tr>
</thead>
<tbody>
@forelse($logs as $log)
<tr>
<td class="ps-3">
<code class="small" style="font-size:.75rem">{{ $log->event }}</code>
</td>
<td>
<small>{{ $log->description ?? '—' }}</small>
</td>
<td>
<small>{{ $log->user?->name ?? 'Sistem' }}</small>
</td>
<td>
<small class="text-muted">{{ $log->ip_address ?? '—' }}</small>
</td>
<td>
<small class="text-muted" title="{{ $log->created_at->format('d/m/Y H:i:s') }}">
{{ $log->created_at->diffForHumans() }}
</small>
</td>
<td class="text-end pe-3">
<a href="{{ route('admin.audit-logs.show', $log) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-info-circle"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted py-5">Tiada log ditemui.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($logs->hasPages())
<div class="card-footer bg-white border-top py-3">
{{ $logs->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,57 @@
@extends('layouts.admin')
@section('title', 'Log Audit Detail')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.audit-logs.index') }}">Log Audit</a></li>
<li class="breadcrumb-item active">#{{ $auditLog->id }}</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Log Audit #{{ $auditLog->id }}</h6>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-3 text-muted">Event</dt>
<dd class="col-9"><code>{{ $auditLog->event }}</code></dd>
<dt class="col-3 text-muted">Deskripsi</dt>
<dd class="col-9">{{ $auditLog->description ?? '—' }}</dd>
<dt class="col-3 text-muted">Pengguna</dt>
<dd class="col-9">{{ $auditLog->user?->name ?? 'Sistem' }}</dd>
<dt class="col-3 text-muted">IP</dt>
<dd class="col-9">{{ $auditLog->ip_address ?? '—' }}</dd>
<dt class="col-3 text-muted">Tarikh</dt>
<dd class="col-9">{{ $auditLog->created_at->format('d/m/Y H:i:s') }}</dd>
@if($auditLog->auditable_type)
<dt class="col-3 text-muted">Model</dt>
<dd class="col-9"><code>{{ class_basename($auditLog->auditable_type) }} #{{ $auditLog->auditable_id }}</code></dd>
@endif
</dl>
@if($auditLog->old_values)
<div class="mt-3">
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Nilai Lama</label>
<pre class="bg-light rounded p-2 mt-1 small">{{ json_encode($auditLog->old_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</div>
@endif
@if($auditLog->new_values)
<div class="mt-3">
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Nilai Baru</label>
<pre class="bg-light rounded p-2 mt-1 small">{{ json_encode($auditLog->new_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</div>
@endif
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,59 @@
{{-- Partial form untuk create dan edit kategori --}}
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-semibold">Nama Kategori <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $category->name ?? '') }}" required
placeholder="Contoh: Pelesenan">
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Slug</label>
<input type="text" name="slug" class="form-control @error('slug') is-invalid @enderror"
value="{{ old('slug', $category->slug ?? '') }}"
placeholder="Auto-generated jika kosong (contoh: pelesenan)">
<div class="form-text">Huruf kecil, angka, dan tanda (-) sahaja. Digunakan untuk URL.</div>
@error('slug')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Deskripsi</label>
<textarea name="description" class="form-control" rows="2"
placeholder="Huraian ringkas tentang kategori ini...">{{ old('description', $category->description ?? '') }}</textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Warna</label>
<div class="input-group">
<input type="color" name="color" class="form-control form-control-color"
value="{{ old('color', $category->color ?? '#6c757d') }}" style="width:50px">
<input type="text" class="form-control" id="colorText"
value="{{ old('color', $category->color ?? '#6c757d') }}"
placeholder="#6c757d" readonly>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Urutan Paparan</label>
<input type="number" name="sort_order" class="form-control"
value="{{ old('sort_order', $category->sort_order ?? 0) }}" min="0">
</div>
<div class="col-12">
<div class="form-check form-switch">
<input type="hidden" name="is_active" value="0">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="isActive" {{ old('is_active', $category->is_active ?? true) ? 'checked' : '' }}>
<label class="form-check-label" for="isActive">Kategori Aktif</label>
</div>
</div>
</div>
@push('scripts')
<script>
$('input[type="color"]').on('input', function() {
$('#colorText').val($(this).val());
});
</script>
@endpush

View File

@@ -0,0 +1,32 @@
@extends('layouts.admin')
@section('title', 'Tambah Kategori')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.categories.index') }}">Kategori</a></li>
<li class="breadcrumb-item active">Tambah</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-folder-plus me-2"></i>Tambah Kategori Baru</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.categories.store') }}">
@csrf
@include('admin.categories._form')
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Simpan
</button>
<a href="{{ route('admin.categories.index') }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,32 @@
@extends('layouts.admin')
@section('title', 'Edit Kategori')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.categories.index') }}">Kategori</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-pencil me-2"></i>Edit Kategori: {{ $category->name }}</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.categories.update', $category) }}">
@csrf @method('PUT')
@include('admin.categories._form')
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Kemaskini
</button>
<a href="{{ route('admin.categories.index') }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,101 @@
@extends('layouts.admin')
@section('title', 'Kategori')
@section('breadcrumb')
<li class="breadcrumb-item active">Kategori</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">Pengurusan Kategori</h4>
<a href="{{ route('admin.categories.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i>Tambah Kategori
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Nama</th>
<th>Slug</th>
<th>Dokumen</th>
<th>Knowledge Items</th>
<th>Status</th>
<th>Urutan</th>
<th class="text-end pe-3">Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($categories as $cat)
<tr class="{{ $cat->trashed() ? 'opacity-50' : '' }}">
<td class="ps-3">
<div class="d-flex align-items-center gap-2">
<span class="d-inline-block rounded-circle"
style="width:12px;height:12px;background:{{ $cat->color ?? '#6c757d' }}"></span>
<span class="fw-semibold">{{ $cat->name }}</span>
</div>
@if($cat->description)
<small class="text-muted">{{ Str::limit($cat->description, 60) }}</small>
@endif
</td>
<td><code class="small">{{ $cat->slug }}</code></td>
<td>
<span class="badge bg-light text-dark border">{{ $cat->total_documents ?? 0 }}</span>
<small class="text-muted ms-1">({{ $cat->active_documents ?? 0 }} aktif)</small>
</td>
<td>
<span class="badge bg-light text-dark border">{{ $cat->total_knowledge_items ?? 0 }}</span>
</td>
<td>
@if($cat->trashed())
<span class="badge bg-danger">Dipadam</span>
@elseif($cat->is_active)
<span class="badge bg-success">Aktif</span>
@else
<span class="badge bg-secondary">Tidak Aktif</span>
@endif
</td>
<td><small>{{ $cat->sort_order }}</small></td>
<td class="text-end pe-3">
@unless($cat->trashed())
<a href="{{ route('admin.categories.edit', $cat) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
<form method="POST" action="{{ route('admin.categories.toggle-status', $cat) }}" class="d-inline">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $cat->is_active ? 'btn-outline-warning' : 'btn-outline-success' }}"
title="{{ $cat->is_active ? 'Nyahaktifkan' : 'Aktifkan' }}">
<i class="bi {{ $cat->is_active ? 'bi-toggle-on' : 'bi-toggle-off' }}"></i>
</button>
</form>
<form method="POST" action="{{ route('admin.categories.destroy', $cat) }}" class="d-inline"
onsubmit="return confirm('Padam kategori ini? Dokumen aktif tidak boleh dipadam.')">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
@endunless
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted py-5">
Tiada kategori. <a href="{{ route('admin.categories.create') }}">Tambah kategori pertama.</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($categories->hasPages())
<div class="card-footer bg-white border-top py-3">
{{ $categories->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,164 @@
@extends('layouts.admin')
@section('title', 'Semakan Chat')
@section('breadcrumb')
<li class="breadcrumb-item active">Semakan Chat</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">Semakan Log Chat</h4>
<div class="d-flex gap-2">
<a href="{{ route('admin.chat-feedback.index', ['is_flagged' => 1]) }}"
class="btn btn-sm {{ request('is_flagged') ? 'btn-warning' : 'btn-outline-warning' }}">
<i class="bi bi-flag me-1"></i>Ditanda
</a>
<a href="{{ route('admin.chat-feedback.index', ['has_answer' => 0]) }}"
class="btn btn-sm {{ request('has_answer') === '0' ? 'btn-secondary' : 'btn-outline-secondary' }}">
<i class="bi bi-question-circle me-1"></i>Tidak Terjawab
</a>
</div>
</div>
{{-- Filter --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-3">
<select name="category_id" class="form-select form-select-sm">
<option value="">Semua Kategori</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<select name="rating" class="form-select form-select-sm">
<option value="">Semua Rating</option>
<option value="helpful" {{ request('rating') == 'helpful' ? 'selected' : '' }}>Membantu</option>
<option value="partially_helpful" {{ request('rating') == 'partially_helpful' ? 'selected' : '' }}>Separa Membantu</option>
<option value="not_helpful" {{ request('rating') == 'not_helpful' ? 'selected' : '' }}>Tidak Membantu</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-search me-1"></i>Tapis
</button>
@if(request()->hasAny(['category_id', 'rating', 'is_flagged', 'has_answer']))
<a href="{{ route('admin.chat-feedback.index') }}" class="btn btn-sm btn-link text-muted">Reset</a>
@endif
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Soalan</th>
<th>Kategori</th>
<th>Jawapan</th>
<th>Feedback</th>
<th>Masa</th>
<th class="text-end pe-3">Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($logs as $log)
<tr class="{{ $log->is_flagged ? 'table-warning' : '' }}">
<td class="ps-3" style="max-width:250px">
<div class="small fw-semibold text-truncate">{{ $log->question }}</div>
@if($log->is_flagged)
<span class="badge bg-warning-subtle text-warning border border-warning" style="font-size:.6rem">
<i class="bi bi-flag-fill me-1"></i>Ditanda
</span>
@endif
@if(!$log->has_answer)
<span class="badge bg-secondary-subtle text-secondary border" style="font-size:.6rem">
Tidak Terjawab
</span>
@endif
</td>
<td>
@if($log->category)
<span class="badge" style="background:{{ $log->category->color ?? '#6c757d' }}">
{{ $log->category->name }}
</span>
@else
<span class="text-muted small">Semua</span>
@endif
</td>
<td style="max-width:200px">
<small class="text-muted text-truncate d-block">{{ Str::limit($log->answer, 80) }}</small>
@if($log->sources_used)
<small class="text-success">
<i class="bi bi-book me-1"></i>{{ count($log->sources_used) }} sumber
</small>
@endif
</td>
<td>
@if($log->feedback)
@php
$ratingMap = [
'helpful' => ['bg-success', 'Membantu', 'bi-hand-thumbs-up'],
'partially_helpful' => ['bg-warning', 'Separa', 'bi-hand-thumbs-up'],
'not_helpful' => ['bg-danger', 'Tidak Membantu', 'bi-hand-thumbs-down'],
];
$r = $ratingMap[$log->feedback->rating] ?? ['bg-secondary', $log->feedback->rating, 'bi-dash'];
@endphp
<span class="badge {{ $r[0] }}">
<i class="bi {{ $r[2] }} me-1"></i>{{ $r[1] }}
</span>
@if($log->feedback->converted_to_faq)
<br><span class="badge bg-success-subtle text-success border border-success mt-1" style="font-size:.6rem">
<i class="bi bi-check2 me-1"></i>Dijadikan FAQ
</span>
@endif
@else
<span class="text-muted small"></span>
@endif
</td>
<td>
<small class="text-muted">{{ $log->created_at->format('d/m/Y H:i') }}</small>
@if($log->response_time)
<br><small class="text-muted">{{ $log->response_time }}s</small>
@endif
</td>
<td class="text-end pe-3">
<a href="{{ route('admin.chat-feedback.show', $log) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
<form method="POST" action="{{ route('admin.chat-feedback.toggle-flag', $log) }}" class="d-inline">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $log->is_flagged ? 'btn-warning' : 'btn-outline-warning' }}"
title="{{ $log->is_flagged ? 'Buang Flag' : 'Tandakan' }}">
<i class="bi bi-flag"></i>
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted py-5">
<i class="bi bi-chat-dots fs-2 d-block mb-2 opacity-25"></i>
Tiada log chat ditemui.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($logs->hasPages())
<div class="card-footer bg-white border-top py-3">
{{ $logs->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,161 @@
@extends('layouts.admin')
@section('title', 'Butiran Log Chat')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.chat-feedback.index') }}">Semakan Chat</a></li>
<li class="breadcrumb-item active">Log #{{ $chatLog->id }}</li>
@endsection
@section('content')
<div class="row g-4">
<div class="col-lg-8">
{{-- Soalan & Jawapan --}}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-semibold">Perbualan</h6>
<div class="d-flex gap-2">
@if($chatLog->category)
<span class="badge" style="background:{{ $chatLog->category->color ?? '#6c757d' }}">
{{ $chatLog->category->name }}
</span>
@endif
@if(!$chatLog->has_answer)
<span class="badge bg-warning">Tidak Terjawab</span>
@endif
</div>
</div>
<div class="card-body">
<div class="mb-3">
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Soalan User</label>
<div class="p-3 bg-light rounded mt-1">{{ $chatLog->question }}</div>
</div>
<div>
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Jawapan AI</label>
<div class="p-3 bg-white border rounded mt-1" style="white-space:pre-wrap">{{ $chatLog->answer }}</div>
</div>
</div>
</div>
{{-- Sumber --}}
@if($chatLog->sources_used)
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Sumber Digunakan ({{ count($chatLog->sources_used) }})</h6>
</div>
<div class="list-group list-group-flush">
@foreach($chatLog->sources_used as $i => $source)
<div class="list-group-item py-2 px-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge {{ $source['type'] === 'pdf' ? 'bg-danger-subtle text-danger' : 'bg-primary-subtle text-primary' }}">
<i class="bi {{ $source['type'] === 'pdf' ? 'bi-file-pdf' : 'bi-lightbulb' }} me-1"></i>
{{ strtoupper($source['type'] ?? 'unknown') }}
</span>
<span class="fw-semibold small">{{ $source['title'] }}</span>
@if(isset($source['page_number']))
<span class="badge bg-light text-dark border small">ms. {{ $source['page_number'] }}</span>
@endif
<span class="badge bg-light text-dark border small">{{ $source['category'] }}</span>
<span class="text-muted small ms-auto">Skor: {{ $source['score'] }}</span>
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- Feedback --}}
@if($chatLog->feedback)
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom"><h6 class="mb-0 fw-semibold">Feedback Pengguna</h6></div>
<div class="card-body">
@php
$ratingLabels = ['helpful' => 'Membantu', 'not_helpful' => 'Tidak Membantu', 'partially_helpful' => 'Separa Membantu'];
@endphp
<p><strong>Rating:</strong> {{ $ratingLabels[$chatLog->feedback->rating] ?? $chatLog->feedback->rating }}</p>
@if($chatLog->feedback->comment)
<p><strong>Komen:</strong> {{ $chatLog->feedback->comment }}</p>
@endif
@if($chatLog->feedback->correct_answer)
<div>
<strong>Jawapan Betul (dari user):</strong>
<div class="p-2 bg-light rounded mt-1 small">{{ $chatLog->feedback->correct_answer }}</div>
</div>
@endif
</div>
</div>
@endif
{{-- Convert ke FAQ --}}
@if(!($chatLog->feedback && $chatLog->feedback->converted_to_faq))
<div class="card border-warning shadow-sm">
<div class="card-header bg-warning-subtle border-warning">
<h6 class="mb-0 fw-semibold"><i class="bi bi-lightbulb me-2"></i>Convert kepada FAQ Rasmi</h6>
</div>
<div class="card-body">
<p class="small text-muted mb-3">Gunakan soalan ini sebagai asas FAQ baru yang akan dimasukkan ke dalam knowledge base.</p>
<form method="POST" action="{{ route('admin.chat-feedback.convert-to-faq', $chatLog) }}">
@csrf
<div class="mb-2">
<label class="form-label small fw-semibold">Kategori</label>
<select name="category_id" class="form-select form-select-sm" required>
@foreach(\App\Models\Category::active()->ordered()->get() as $cat)
<option value="{{ $cat->id }}" {{ optional($chatLog->category)->id == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Soalan (Tajuk FAQ)</label>
<input type="text" name="title" class="form-control form-control-sm"
value="{{ $chatLog->question }}" required>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Jawapan Rasmi</label>
<textarea name="content" class="form-control form-control-sm" rows="5" required>{{ $chatLog->feedback?->correct_answer ?? $chatLog->answer }}</textarea>
</div>
<button type="submit" class="btn btn-warning btn-sm">
<i class="bi bi-check-circle me-1"></i>Cipta FAQ dari Log Ini
</button>
</form>
</div>
</div>
@else
<div class="alert alert-success">
<i class="bi bi-check-circle me-2"></i>
Log ini telah dijadikan FAQ.
<a href="{{ route('admin.knowledge-items.show', $chatLog->feedback->convertedFaq) }}">Lihat FAQ</a>
</div>
@endif
</div>
{{-- Meta Info --}}
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom"><h6 class="mb-0 fw-semibold">Maklumat</h6></div>
<div class="card-body small">
<dl class="row mb-0">
<dt class="col-5 text-muted">Model AI</dt>
<dd class="col-7">{{ $chatLog->model_used ?? '—' }}</dd>
<dt class="col-5 text-muted">Masa Respons</dt>
<dd class="col-7">{{ $chatLog->response_time ? $chatLog->response_time . 's' : '—' }}</dd>
<dt class="col-5 text-muted">Token</dt>
<dd class="col-7">{{ $chatLog->tokens_used ? number_format($chatLog->tokens_used) : '—' }}</dd>
<dt class="col-5 text-muted">Tarikh</dt>
<dd class="col-7">{{ $chatLog->created_at->format('d/m/Y H:i') }}</dd>
<dt class="col-5 text-muted">Flag</dt>
<dd class="col-7">
<form method="POST" action="{{ route('admin.chat-feedback.toggle-flag', $chatLog) }}">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $chatLog->is_flagged ? 'btn-warning' : 'btn-outline-warning' }}">
<i class="bi bi-flag me-1"></i>{{ $chatLog->is_flagged ? 'Buang Flag' : 'Tandakan' }}
</button>
</form>
</dd>
</dl>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,474 @@
@extends('layouts.admin')
@section('title', 'Chunk #' . ($chunk->chunk_index + 1) . ' — ' . Str::limit($chunk->document->title, 30))
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item">
<a href="{{ route('admin.documents.show', $chunk->document) }}">{{ Str::limit($chunk->document->title, 25) }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.documents.chunks', ['document' => $chunk->document, 'version' => $chunk->documentVersion]) }}">
Chunks v{{ $chunk->documentVersion->version_number }}
</a>
</li>
<li class="breadcrumb-item active">Chunk #{{ $chunk->chunk_index + 1 }}</li>
@endsection
@section('content')
{{-- Flash Messages --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show py-2">
<i class="bi bi-check-circle me-1"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show py-2">
<i class="bi bi-exclamation-triangle me-1"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Header Row --}}
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
<div>
<div class="d-flex align-items-center gap-2 flex-wrap mb-1">
<h4 class="mb-0 fw-bold">Chunk #{{ $chunk->chunk_index + 1 }}</h4>
<span class="badge {{ $chunk->getStatusBadgeClass() }}">{{ $chunk->getStatusLabel() }}</span>
@if($chunk->is_edited)
<span class="badge bg-primary-subtle text-primary border border-primary">
<i class="bi bi-pencil me-1"></i>Telah Diedit
</span>
@endif
@if($chunk->parent_chunk_id)
<span class="badge bg-warning-subtle text-warning border border-warning">
<i class="bi bi-scissors me-1"></i>Hasil Split
</span>
@endif
</div>
<p class="text-muted small mb-0">
{{ $chunk->document->title }} &mdash; v{{ $chunk->documentVersion->version_number }}
@if($chunk->page_number) &middot; ms. {{ $chunk->page_number }} @endif
@if($chunk->section_heading) &middot; {{ $chunk->section_heading }} @endif
</p>
</div>
{{-- Action buttons --}}
<div class="d-flex gap-2 flex-wrap">
<a href="{{ route('admin.documents.chunks', ['document' => $chunk->document, 'version' => $chunk->documentVersion]) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Senarai Chunk
</a>
@if(!$chunk->isSuperseded())
<a href="{{ route('admin.chunks.split', $chunk) }}"
class="btn btn-sm btn-warning">
<i class="bi bi-scissors me-1"></i>Split Chunk
</a>
@if($chunk->exclude_from_index)
<form method="POST" action="{{ route('admin.chunks.include', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-success"
onclick="return confirm('Kembalikan chunk ini ke indexing?')">
<i class="bi bi-check-circle me-1"></i>Include
</button>
</form>
@else
<form method="POST" action="{{ route('admin.chunks.exclude', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-secondary"
onclick="return confirm('Kecualikan chunk ini dari indexing?')">
<i class="bi bi-slash-circle me-1"></i>Exclude
</button>
</form>
@endif
@if($chunk->isIndexable())
<form method="POST" action="{{ route('admin.chunks.reindex', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-info"
onclick="return confirm('Trigger reindex untuk chunk ini?')">
<i class="bi bi-arrow-repeat me-1"></i>Reindex
</button>
</form>
@endif
@endif
</div>
</div>
{{-- Metadata Cards --}}
<div class="row g-2 mb-3">
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-light h-100">
<div class="card-body py-2 px-3">
<p class="text-muted mb-0" style="font-size:.7rem">CHUNK INDEX</p>
<p class="mb-0 fw-bold">#{{ $chunk->chunk_index + 1 }}</p>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-light h-100">
<div class="card-body py-2 px-3">
<p class="text-muted mb-0" style="font-size:.7rem">QDRANT POINT ID</p>
<p class="mb-0 fw-bold" style="font-size:.75rem;word-break:break-all">
{{ $chunk->qdrant_point_id ?? '—' }}
</p>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-light h-100">
<div class="card-body py-2 px-3">
<p class="text-muted mb-0" style="font-size:.7rem">LAST EMBEDDED</p>
<p class="mb-0 fw-bold" style="font-size:.82rem">
{{ $chunk->last_embedded_at?->format('d/m/Y H:i') ?? '—' }}
</p>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-light h-100">
<div class="card-body py-2 px-3">
<p class="text-muted mb-0" style="font-size:.7rem">DIEDIT OLEH</p>
<p class="mb-0 fw-bold" style="font-size:.82rem">
{{ $chunk->editor?->name ?? '—' }}
@if($chunk->edited_at)
<br><span class="text-muted fw-normal" style="font-size:.7rem">{{ $chunk->edited_at->format('d/m/Y H:i') }}</span>
@endif
</p>
</div>
</div>
</div>
</div>
{{-- Parent Chunk Info (jika ini adalah hasil split) --}}
@if($chunk->parentChunk)
<div class="alert alert-warning py-2 mb-3">
<i class="bi bi-scissors me-1"></i>
Chunk ini adalah hasil split daripada
<a href="{{ route('admin.chunks.show', $chunk->parentChunk) }}" class="alert-link">
Chunk #{{ $chunk->parentChunk->chunk_index + 1 }}
</a>
({{ $chunk->parentChunk->getStatusLabel() }}).
Urutan dalam split: {{ $chunk->split_order + 1 }} / dalam kumpulan yang sama.
</div>
@endif
{{-- Superseded Warning --}}
@if($chunk->isSuperseded())
<div class="alert alert-dark py-2 mb-3">
<i class="bi bi-exclamation-triangle me-1"></i>
Chunk ini telah <strong>digantikan</strong> oleh chunk-chunk baharu hasil split. Ia tidak lagi digunakan dalam Qdrant.
</div>
@endif
{{-- ===================================================================== --}}
{{-- TEXT PREVIEW 3 Panel --}}
{{-- ===================================================================== --}}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" id="textTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link {{ !$chunk->final_text && !$chunk->cleaned_text ? 'active' : '' }}"
id="raw-tab" data-bs-toggle="tab" data-bs-target="#raw-panel"
type="button" role="tab">
<i class="bi bi-file-earmark-text me-1"></i>
raw_text
<span class="badge bg-light text-dark border ms-1" style="font-size:.65rem">
{{ number_format(str_word_count($chunk->content)) }} patah
</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {{ $chunk->cleaned_text && !$chunk->final_text ? 'active' : '' }}"
id="cleaned-tab" data-bs-toggle="tab" data-bs-target="#cleaned-panel"
type="button" role="tab">
<i class="bi bi-magic me-1"></i>
cleaned_text
@if(!$chunk->cleaned_text)
<span class="badge bg-light text-muted border ms-1" style="font-size:.65rem">null</span>
@endif
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {{ $chunk->final_text ? 'active' : '' }}"
id="final-tab" data-bs-toggle="tab" data-bs-target="#final-panel"
type="button" role="tab">
<i class="bi bi-check-circle me-1"></i>
final_text
@if($chunk->final_text)
<span class="badge bg-primary-subtle text-primary border border-primary ms-1" style="font-size:.65rem">
Aktif
</span>
@else
<span class="badge bg-light text-muted border ms-1" style="font-size:.65rem">null</span>
@endif
</button>
</li>
</ul>
</div>
<div class="card-body p-0">
<div class="tab-content" id="textTabsContent">
{{-- raw_text --}}
<div class="tab-pane fade {{ !$chunk->final_text && !$chunk->cleaned_text ? 'show active' : '' }}"
id="raw-panel" role="tabpanel">
<div class="p-3">
<p class="text-muted small mb-2">
Teks asal hasil extraction PDF. <strong>Tidak boleh diubah.</strong>
Ini adalah source of truth untuk chunk ini.
</p>
<pre class="mb-0 p-3 bg-light rounded" style="white-space:pre-wrap;max-height:300px;overflow-y:auto;font-family:inherit;font-size:.82rem">{{ $chunk->content }}</pre>
</div>
</div>
{{-- cleaned_text --}}
<div class="tab-pane fade {{ $chunk->cleaned_text && !$chunk->final_text ? 'show active' : '' }}"
id="cleaned-panel" role="tabpanel">
<div class="p-3">
@if($chunk->cleaned_text)
<p class="text-muted small mb-2">
Teks selepas auto cleanup. Jika final_text tidak ditetapkan, ini digunakan untuk embedding.
</p>
<pre class="mb-0 p-3 bg-light rounded" style="white-space:pre-wrap;max-height:300px;overflow-y:auto;font-family:inherit;font-size:.82rem">{{ $chunk->cleaned_text }}</pre>
@else
<div class="text-center py-4 text-muted">
<i class="bi bi-dash-circle display-6 d-block mb-2"></i>
<p class="mb-0">cleaned_text tidak ditetapkan.</p>
<p class="small">raw_text (content) digunakan untuk embedding.</p>
</div>
@endif
</div>
</div>
{{-- final_text --}}
<div class="tab-pane fade {{ $chunk->final_text ? 'show active' : '' }}"
id="final-panel" role="tabpanel">
<div class="p-3">
@if($chunk->final_text)
<p class="text-muted small mb-2">
Teks yang <strong>sebenarnya dihantar ke Qdrant</strong> untuk embedding.
Diedit oleh admin.
</p>
<pre class="mb-0 p-3 bg-success-subtle rounded" style="white-space:pre-wrap;max-height:300px;overflow-y:auto;font-family:inherit;font-size:.82rem">{{ $chunk->final_text }}</pre>
@else
<div class="text-center py-4 text-muted">
<i class="bi bi-dash-circle display-6 d-block mb-2"></i>
<p class="mb-0">final_text tidak ditetapkan.</p>
<p class="small">Sistem menggunakan cleaned_text atau raw_text untuk embedding.</p>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
{{-- ===================================================================== --}}
{{-- EDIT FINAL TEXT FORM --}}
{{-- ===================================================================== --}}
@if(!$chunk->isSuperseded())
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom py-2">
<h6 class="mb-0">
<i class="bi bi-pencil-square me-2"></i>Edit final_text
</h6>
<p class="text-muted small mb-0 mt-1">
Edit teks yang akan dihantar untuk embedding. raw_text tidak akan diubah.
Selepas simpan, reindex akan diantrikan secara automatik.
</p>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.chunks.update', $chunk) }}" id="editForm">
@csrf
@method('PATCH')
@error('final_text')
<div class="alert alert-danger py-2 mb-3">{{ $message }}</div>
@enderror
<div class="mb-3">
<label for="final_text" class="form-label fw-semibold">
final_text
<small class="text-muted fw-normal">
(kosong = guna {{ $chunk->cleaned_text ? 'cleaned_text' : 'raw_text' }})
</small>
</label>
<textarea name="final_text" id="final_text" rows="10"
class="form-control font-monospace @error('final_text') is-invalid @enderror"
style="font-size:.82rem;line-height:1.5"
placeholder="Masukkan final_text yang telah dibersihkan...">{{ old('final_text', $chunk->final_text ?? $chunk->getEmbeddableText()) }}</textarea>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted">Minimum 20 aksara, maksimum 10,000 aksara.</small>
<small class="text-muted" id="charCount">0 aksara</small>
</div>
</div>
{{-- Quick-fill dari raw_text --}}
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnFillRaw">
<i class="bi bi-arrow-down me-1"></i>Isi dari raw_text
</button>
<button type="button" class="btn btn-sm btn-outline-secondary ms-1" id="btnClearFinal">
<i class="bi bi-x me-1"></i>Kosongkan
</button>
<span class="text-muted ms-2" style="font-size:.75rem">
raw_text: {{ number_format(mb_strlen($chunk->content)) }} aksara,
{{ number_format(str_word_count($chunk->content)) }} patah
</span>
</div>
<div class="mb-3">
<label for="notes" class="form-label">Nota perubahan <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" name="notes" id="notes" class="form-control"
value="{{ old('notes') }}"
placeholder="Contoh: Buang header halaman, nombor muka surat...">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Simpan & Queue Reindex
</button>
<button type="button" class="btn btn-outline-secondary" onclick="history.back()">
Batal
</button>
</div>
</form>
</div>
</div>
@endif
{{-- ===================================================================== --}}
{{-- CHILD CHUNKS (jika chunk ini pernah di-split) --}}
{{-- ===================================================================== --}}
@if($chunk->childChunks->count() > 0)
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom py-2">
<h6 class="mb-0">
<i class="bi bi-diagram-2 me-2"></i>
Chunk Baharu Hasil Split
<span class="badge bg-secondary ms-1">{{ $chunk->childChunks->count() }}</span>
</h6>
</div>
<div class="card-body p-0">
@foreach($chunk->childChunks as $child)
<div class="d-flex align-items-center gap-3 px-3 py-2 border-bottom">
<span class="badge {{ $child->getStatusBadgeClass() }}" style="font-size:.7rem">
{{ $child->getStatusLabel() }}
</span>
<span class="text-muted" style="font-size:.75rem">#{{ $child->chunk_index + 1 }}</span>
<p class="mb-0 text-truncate flex-grow-1" style="font-size:.82rem">
{{ Str::limit($child->getEmbeddableText(), 100) }}
</p>
<a href="{{ route('admin.chunks.show', $child) }}"
class="btn btn-sm btn-outline-primary flex-shrink-0">
Lihat
</a>
</div>
@endforeach
</div>
</div>
@endif
{{-- ===================================================================== --}}
{{-- AUDIT TRAIL --}}
{{-- ===================================================================== --}}
@if($chunk->audits->count() > 0)
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom py-2">
<h6 class="mb-0">
<i class="bi bi-clock-history me-2"></i>Audit Trail
<span class="badge bg-light text-dark border ms-1">{{ $chunk->audits->count() }} rekod</span>
</h6>
</div>
<div class="card-body p-0">
@foreach($chunk->audits as $audit)
<div class="px-3 py-2 border-bottom">
<div class="d-flex align-items-center gap-2 flex-wrap mb-1">
<span class="badge {{ $audit->getOperationBadgeClass() }}" style="font-size:.7rem">
{{ $audit->getOperationLabel() }}
</span>
@if($audit->old_status && $audit->new_status)
<span class="text-muted" style="font-size:.75rem">
{{ $audit->old_status ?? '—' }} {{ $audit->new_status }}
</span>
@endif
<span class="ms-auto text-muted" style="font-size:.72rem">
{{ $audit->user?->name ?? 'System' }}
&middot; {{ $audit->created_at->format('d/m/Y H:i') }}
</span>
</div>
@if($audit->notes)
<p class="mb-0 text-muted" style="font-size:.75rem">
<i class="bi bi-chat-left-text me-1"></i>{{ $audit->notes }}
</p>
@endif
@if($audit->operation === \App\Models\ChunkAudit::OP_EDIT_FINAL_TEXT && $audit->old_final_text)
<details class="mt-1">
<summary class="text-muted" style="font-size:.72rem;cursor:pointer">
Lihat perubahan teks
</summary>
<div class="row g-2 mt-1">
<div class="col-6">
<p class="text-muted mb-1" style="font-size:.68rem">SEBELUM</p>
<pre class="bg-danger-subtle p-2 rounded mb-0" style="font-size:.72rem;white-space:pre-wrap;max-height:100px;overflow-y:auto">{{ Str::limit($audit->old_final_text, 300) }}</pre>
</div>
<div class="col-6">
<p class="text-muted mb-1" style="font-size:.68rem">SELEPAS</p>
<pre class="bg-success-subtle p-2 rounded mb-0" style="font-size:.72rem;white-space:pre-wrap;max-height:100px;overflow-y:auto">{{ Str::limit($audit->new_final_text, 300) }}</pre>
</div>
</div>
</details>
@endif
</div>
@endforeach
</div>
</div>
@endif
@endsection
@push('scripts')
<script>
// ── Char counter ──────────────────────────────────────────────────────────────
const textarea = document.getElementById('final_text');
const charCount = document.getElementById('charCount');
function updateCount() {
if (textarea && charCount) {
charCount.textContent = textarea.value.length.toLocaleString() + ' aksara';
}
}
if (textarea) {
textarea.addEventListener('input', updateCount);
updateCount();
}
// ── Quick-fill dari raw_text ──────────────────────────────────────────────────
const rawText = @json($chunk->content);
document.getElementById('btnFillRaw')?.addEventListener('click', function() {
if (textarea.value.trim() !== '' && !confirm('Gantikan teks sedia ada dengan raw_text?')) {
return;
}
textarea.value = rawText;
updateCount();
textarea.focus();
});
document.getElementById('btnClearFinal')?.addEventListener('click', function() {
if (!confirm('Kosongkan final_text? Sistem akan guna raw_text untuk embedding.')) return;
textarea.value = '';
updateCount();
});
</script>
@endpush

View File

@@ -0,0 +1,434 @@
@extends('layouts.admin')
@section('title', 'Split Chunk #' . ($chunk->chunk_index + 1))
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item">
<a href="{{ route('admin.documents.show', $chunk->document) }}">{{ Str::limit($chunk->document->title, 25) }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.documents.chunks', ['document' => $chunk->document, 'version' => $chunk->documentVersion]) }}">
Chunks v{{ $chunk->documentVersion->version_number }}
</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.chunks.show', $chunk) }}">Chunk #{{ $chunk->chunk_index + 1 }}</a>
</li>
<li class="breadcrumb-item active">Split</li>
@endsection
@section('content')
{{-- Flash / Validation errors --}}
@if($errors->any())
<div class="alert alert-danger alert-dismissible fade show py-2">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Ralat:</strong>
<ul class="mb-0 mt-1 ps-3">
@foreach($errors->all() as $err)
<li>{{ $err }}</li>
@endforeach
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Header --}}
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
<div>
<h4 class="mb-0 fw-bold">
<i class="bi bi-scissors me-2 text-warning"></i>Split Chunk
</h4>
<p class="text-muted small mb-0">
Chunk #{{ $chunk->chunk_index + 1 }} &mdash;
{{ $chunk->document->title }} v{{ $chunk->documentVersion->version_number }}
@if($chunk->page_number) &middot; ms. {{ $chunk->page_number }} @endif
</p>
</div>
<a href="{{ route('admin.chunks.show', $chunk) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Kembali
</a>
</div>
{{-- Warning panel --}}
<div class="alert alert-warning py-2 mb-3">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Perhatian:</strong> Selepas split, chunk asal akan ditandakan sebagai
<strong>Superseded</strong> dan tidak lagi digunakan dalam Qdrant.
Chunk-chunk baharu akan di-embed secara automatik. Tindakan ini <strong>tidak boleh diundo</strong>.
</div>
<div class="row g-3">
{{-- ── Kolum kiri: Teks asal (rujukan) ─────────────────────────────── --}}
<div class="col-lg-5">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-bottom py-2">
<h6 class="mb-0">
<i class="bi bi-file-earmark-text me-1"></i>Teks Asal (Rujukan)
</h6>
<p class="text-muted small mb-0 mt-1">
Salin bahagian yang diperlukan ke segmen di sebelah kanan.
</p>
</div>
<div class="card-body p-0">
{{-- Stats --}}
<div class="d-flex gap-3 px-3 py-2 border-bottom bg-light">
<span class="text-muted small">
<strong id="origWordCount">{{ str_word_count($chunk->getEmbeddableText()) }}</strong> patah
</span>
<span class="text-muted small">
<strong>{{ number_format(mb_strlen($chunk->getEmbeddableText())) }}</strong> aksara
</span>
@if($chunk->is_edited)
<span class="badge bg-primary-subtle text-primary border border-primary" style="font-size:.65rem">
final_text
</span>
@else
<span class="badge bg-light text-muted border" style="font-size:.65rem">raw_text</span>
@endif
</div>
{{-- Teks asal selectable untuk copy --}}
<textarea id="originalText" class="form-control border-0 rounded-0 font-monospace"
rows="20" readonly
style="font-size:.8rem;line-height:1.5;resize:none;background:#fafafa"
>{{ $chunk->getEmbeddableText() }}</textarea>
</div>
<div class="card-footer bg-white border-top py-2">
<button type="button" class="btn btn-sm btn-outline-secondary w-100" id="btnAutoSplit">
<i class="bi bi-magic me-1"></i>Auto-split mengikut perenggan
</button>
<p class="text-muted mb-0 mt-1" style="font-size:.7rem">
Pecahkan teks mengikut baris kosong berganda (\n\n) sebagai permulaan.
</p>
</div>
</div>
</div>
{{-- ── Kolum kanan: Form segmen ─────────────────────────────────────── --}}
<div class="col-lg-7">
<form method="POST" action="{{ route('admin.chunks.do-split', $chunk) }}" id="splitForm">
@csrf
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom py-2 d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0">
<i class="bi bi-collection me-1"></i>Segmen Baharu
<span class="badge bg-primary ms-1" id="segmentCount">2</span>
</h6>
<p class="text-muted small mb-0 mt-1">
Minimum 2, maksimum 10 segmen. Setiap segmen akan menjadi satu chunk baharu.
</p>
</div>
<div class="d-flex gap-1">
<button type="button" class="btn btn-sm btn-outline-primary" id="btnAddSegment">
<i class="bi bi-plus-lg me-1"></i>Tambah
</button>
<button type="button" class="btn btn-sm btn-outline-danger" id="btnRemoveSegment">
<i class="bi bi-dash-lg me-1"></i>Buang
</button>
</div>
</div>
<div class="card-body p-3" id="segmentsContainer">
{{-- Segmen 1 (default) --}}
<div class="segment-item mb-3" data-index="0">
<div class="d-flex align-items-center justify-content-between mb-1">
<label class="form-label fw-semibold mb-0 small">
<i class="bi bi-1-circle me-1 text-primary"></i>Segmen 1
</label>
<span class="text-muted segment-stats" style="font-size:.72rem">0 patah · 0 aksara</span>
</div>
<textarea name="segments[]"
class="form-control font-monospace segment-textarea @error('segments.0') is-invalid @enderror"
rows="6"
placeholder="Teks untuk segmen 1..."
style="font-size:.8rem;line-height:1.5">{{ old('segments.0') }}</textarea>
@error('segments.0')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- Segmen 2 (default) --}}
<div class="segment-item mb-3" data-index="1">
<div class="d-flex align-items-center justify-content-between mb-1">
<label class="form-label fw-semibold mb-0 small">
<i class="bi bi-2-circle me-1 text-primary"></i>Segmen 2
</label>
<span class="text-muted segment-stats" style="font-size:.72rem">0 patah · 0 aksara</span>
</div>
<textarea name="segments[]"
class="form-control font-monospace segment-textarea @error('segments.1') is-invalid @enderror"
rows="6"
placeholder="Teks untuk segmen 2..."
style="font-size:.8rem;line-height:1.5">{{ old('segments.1') }}</textarea>
@error('segments.1')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
{{-- Summary bar --}}
<div class="card border-0 bg-light mb-3">
<div class="card-body py-2 px-3">
<div class="d-flex gap-3 flex-wrap align-items-center">
<span class="small text-muted">
Jumlah patah dalam segmen:
<strong id="totalSegmentWords">0</strong>
<span class="text-muted">/ {{ str_word_count($chunk->getEmbeddableText()) }}</span>
</span>
<span class="small text-muted">
Jumlah aksara:
<strong id="totalSegmentChars">0</strong>
</span>
<span class="ms-auto small" id="coverageIndicator"></span>
</div>
</div>
</div>
{{-- Notes --}}
<div class="mb-3">
<label for="notes" class="form-label">
Sebab split <span class="text-muted fw-normal">(optional)</span>
</label>
<input type="text" name="notes" id="notes" class="form-control"
value="{{ old('notes') }}"
placeholder="Contoh: Chunk terlalu panjang, gabungan dua topik berbeza...">
</div>
{{-- Submit --}}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-warning fw-semibold" id="btnSubmit">
<i class="bi bi-scissors me-1"></i>
Jalankan Split
<span class="badge bg-white text-warning ms-1" id="btnSegmentCount">2</span>
Chunk Baharu
</button>
<a href="{{ route('admin.chunks.show', $chunk) }}" class="btn btn-outline-secondary">
Batal
</a>
</div>
<p class="text-muted small mt-2">
<i class="bi bi-info-circle me-1"></i>
Chunk asal akan ditandakan <strong>Superseded</strong>.
Setiap segmen akan di-embed secara automatik dalam queue.
</p>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
(function () {
'use strict';
// ── Data ─────────────────────────────────────────────────────────────────
const originalText = @json($chunk->getEmbeddableText());
const origWordCount = {{ str_word_count($chunk->getEmbeddableText()) }};
const MAX_SEGMENTS = 10;
const MIN_SEGMENTS = 2;
// Icon numerals — Bootstrap Icons
const icons = ['1-circle','2-circle','3-circle','4-circle','5-circle',
'6-circle','7-circle','8-circle','9-circle','10-circle'];
// ── Helpers ───────────────────────────────────────────────────────────────
function countWords(str) {
return str.trim() === '' ? 0 : str.trim().split(/\s+/).length;
}
function updateSegmentStats(textarea) {
const parent = textarea.closest('.segment-item');
const stats = parent.querySelector('.segment-stats');
const words = countWords(textarea.value);
const chars = textarea.value.length;
stats.textContent = words.toLocaleString() + ' patah · ' + chars.toLocaleString() + ' aksara';
}
function updateGlobalStats() {
const textareas = document.querySelectorAll('.segment-textarea');
let totalWords = 0, totalChars = 0;
textareas.forEach(ta => {
totalWords += countWords(ta.value);
totalChars += ta.value.length;
});
document.getElementById('totalSegmentWords').textContent = totalWords.toLocaleString();
document.getElementById('totalSegmentChars').textContent = totalChars.toLocaleString();
document.getElementById('segmentCount').textContent = textareas.length;
document.getElementById('btnSegmentCount').textContent = textareas.length;
// Coverage indicator
const indicator = document.getElementById('coverageIndicator');
if (origWordCount > 0) {
const pct = Math.round((totalWords / origWordCount) * 100);
const cls = pct >= 90 ? 'text-success' : (pct >= 60 ? 'text-warning' : 'text-danger');
indicator.innerHTML = `<span class="${cls}"><i class="bi bi-bar-chart me-1"></i>Liputan: ${pct}%</span>`;
}
}
function relabelAll() {
const items = document.querySelectorAll('.segment-item');
items.forEach((item, i) => {
const label = item.querySelector('label');
const iconClass = icons[i] || (i + 1) + '-circle';
label.innerHTML = `<i class="bi bi-${iconClass} me-1 text-primary"></i>Segmen ${i + 1}`;
item.dataset.index = i;
// Update error class name (not critical — server-side validates)
const ta = item.querySelector('textarea');
ta.placeholder = `Teks untuk segmen ${i + 1}...`;
});
}
// ── Attach listeners kepada textarea ─────────────────────────────────────
function attachListeners(textarea) {
textarea.addEventListener('input', function () {
updateSegmentStats(this);
updateGlobalStats();
});
// Initial update
updateSegmentStats(textarea);
}
// Init existing textareas
document.querySelectorAll('.segment-textarea').forEach(attachListeners);
updateGlobalStats();
// ── Add segment ───────────────────────────────────────────────────────────
document.getElementById('btnAddSegment').addEventListener('click', function () {
const items = document.querySelectorAll('.segment-item');
if (items.length >= MAX_SEGMENTS) {
alert('Maksimum ' + MAX_SEGMENTS + ' segmen dibenarkan.');
return;
}
const newIndex = items.length;
const iconClass = icons[newIndex] || (newIndex + 1) + '-circle';
const div = document.createElement('div');
div.className = 'segment-item mb-3';
div.dataset.index = newIndex;
div.innerHTML = `
<div class="d-flex align-items-center justify-content-between mb-1">
<label class="form-label fw-semibold mb-0 small">
<i class="bi bi-${iconClass} me-1 text-primary"></i>Segmen ${newIndex + 1}
</label>
<span class="text-muted segment-stats" style="font-size:.72rem">0 patah · 0 aksara</span>
</div>
<textarea name="segments[]"
class="form-control font-monospace segment-textarea"
rows="6"
placeholder="Teks untuk segmen ${newIndex + 1}..."
style="font-size:.8rem;line-height:1.5"></textarea>
`;
document.getElementById('segmentsContainer').appendChild(div);
attachListeners(div.querySelector('textarea'));
updateGlobalStats();
div.querySelector('textarea').focus();
});
// ── Remove last segment ───────────────────────────────────────────────────
document.getElementById('btnRemoveSegment').addEventListener('click', function () {
const items = document.querySelectorAll('.segment-item');
if (items.length <= MIN_SEGMENTS) {
alert('Minimum ' + MIN_SEGMENTS + ' segmen diperlukan.');
return;
}
if (!confirm('Buang segmen terakhir?')) return;
items[items.length - 1].remove();
relabelAll();
updateGlobalStats();
});
// ── Auto-split mengikut perenggan ─────────────────────────────────────────
document.getElementById('btnAutoSplit').addEventListener('click', function () {
// Pecahkan teks asal mengikut baris kosong berganda
const paragraphs = originalText
.split(/\n\s*\n/)
.map(p => p.trim())
.filter(p => p.length >= 20);
if (paragraphs.length < 2) {
alert('Teks asal tidak mempunyai cukup perenggan untuk auto-split (keperluan: sekurang-kurangnya 2 perenggan dengan 20+ aksara).');
return;
}
const maxAllowed = Math.min(paragraphs.length, MAX_SEGMENTS);
const msg = `Auto-split akan menghasilkan ${maxAllowed} segmen berdasarkan perenggan. Ini akan menggantikan semua segmen semasa. Teruskan?`;
if (!confirm(msg)) return;
// Buang semua segmen sedia ada
document.getElementById('segmentsContainer').innerHTML = '';
// Cipta segmen baru
paragraphs.slice(0, MAX_SEGMENTS).forEach((para, i) => {
const iconClass = icons[i] || (i + 1) + '-circle';
const div = document.createElement('div');
div.className = 'segment-item mb-3';
div.dataset.index = i;
div.innerHTML = `
<div class="d-flex align-items-center justify-content-between mb-1">
<label class="form-label fw-semibold mb-0 small">
<i class="bi bi-${iconClass} me-1 text-primary"></i>Segmen ${i + 1}
</label>
<span class="text-muted segment-stats" style="font-size:.72rem">0 patah · 0 aksara</span>
</div>
<textarea name="segments[]"
class="form-control font-monospace segment-textarea"
rows="5"
placeholder="Teks untuk segmen ${i + 1}..."
style="font-size:.8rem;line-height:1.5"></textarea>
`;
document.getElementById('segmentsContainer').appendChild(div);
div.querySelector('textarea').value = para;
attachListeners(div.querySelector('textarea'));
updateSegmentStats(div.querySelector('textarea'));
});
updateGlobalStats();
});
// ── Confirm on submit ────────────────────────────────────────────────────
document.getElementById('splitForm').addEventListener('submit', function (e) {
const items = document.querySelectorAll('.segment-item');
const count = items.length;
// Semak semua segmen tidak kosong
let allFilled = true;
items.forEach(item => {
if (item.querySelector('textarea').value.trim() === '') {
allFilled = false;
}
});
if (!allFilled) {
e.preventDefault();
alert('Semua segmen mesti diisi sebelum split boleh dijalankan.');
return;
}
if (!confirm(
`Jalankan split chunk #{{ $chunk->chunk_index + 1 }} kepada ${count} chunk baharu?\n\n` +
`Chunk asal akan ditandakan Superseded dan tidak lagi aktif dalam Qdrant.\n` +
`Tindakan ini tidak boleh diundo.`
)) {
e.preventDefault();
}
});
})();
</script>
@endpush

View File

@@ -0,0 +1,221 @@
@extends('layouts.admin')
@section('title', 'Dashboard')
@section('breadcrumb')
<li class="breadcrumb-item active">Dashboard</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">Dashboard</h4>
<span class="text-muted small">{{ now()->format('d M Y, H:i') }}</span>
</div>
{{-- Status Perkhidmatan --}}
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body py-2 px-3">
<div class="d-flex align-items-center justify-content-between">
<span class="small fw-semibold text-muted">Ollama</span>
@if($health['ollama']['online'])
<span class="badge bg-success"><i class="bi bi-circle-fill me-1" style="font-size:.5rem"></i>Online</span>
@else
<span class="badge bg-danger"><i class="bi bi-circle-fill me-1" style="font-size:.5rem"></i>Offline</span>
@endif
</div>
@if($health['ollama']['online'])
<div class="d-flex gap-2 mt-1">
<span class="badge {{ $health['ollama']['chat_model'] ? 'bg-success-subtle text-success' : 'bg-warning-subtle text-warning' }} small">
Chat: {{ config('ollama.chat_model') }}
</span>
<span class="badge {{ $health['ollama']['embed_model'] ? 'bg-success-subtle text-success' : 'bg-warning-subtle text-warning' }} small">
Embed: {{ config('ollama.embedding_model') }}
</span>
</div>
@elseif($health['ollama']['error'])
<small class="text-danger">{{ $health['ollama']['error'] }}</small>
@endif
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body py-2 px-3">
<div class="d-flex align-items-center justify-content-between">
<span class="small fw-semibold text-muted">Qdrant</span>
@if($health['qdrant']['online'])
<span class="badge bg-success"><i class="bi bi-circle-fill me-1" style="font-size:.5rem"></i>Online</span>
@else
<span class="badge bg-danger"><i class="bi bi-circle-fill me-1" style="font-size:.5rem"></i>Offline</span>
@endif
</div>
@if($health['qdrant']['online'])
<div class="mt-1">
<span class="badge bg-info-subtle text-info small">
{{ number_format($health['qdrant']['points_count'] ?? 0) }} vectors
</span>
</div>
@elseif($health['qdrant']['error'])
<small class="text-danger">{{ $health['qdrant']['error'] }}</small>
@endif
</div>
</div>
</div>
</div>
{{-- Stats Cards --}}
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-muted small">Dokumen Aktif</div>
<div class="fs-3 fw-bold text-primary">{{ $stats['active_documents'] }}</div>
<div class="text-muted small">dari {{ $stats['total_documents'] }} dokumen</div>
</div>
<i class="bi bi-file-pdf fs-1 text-primary opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-muted small">FAQ & Pengetahuan</div>
<div class="fs-3 fw-bold text-success">{{ $stats['total_knowledge_items'] }}</div>
<div class="text-muted small">item aktif</div>
</div>
<i class="bi bi-lightbulb fs-1 text-success opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-muted small">Chat Hari Ini</div>
<div class="fs-3 fw-bold text-info">{{ $stats['total_chats_today'] }}</div>
<div class="text-muted small">pertanyaan</div>
</div>
<i class="bi bi-chat-dots fs-1 text-info opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm {{ $stats['flagged_chats'] > 0 ? 'border-warning' : '' }}">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-muted small">Perlu Semakan</div>
<div class="fs-3 fw-bold {{ $stats['flagged_chats'] > 0 ? 'text-warning' : 'text-muted' }}">
{{ $stats['flagged_chats'] }}
</div>
<div class="text-muted small">log ditanda</div>
</div>
<i class="bi bi-flag fs-1 text-warning opacity-25"></i>
</div>
</div>
</div>
</div>
</div>
{{-- Processing Status & Recent Activity --}}
<div class="row g-3">
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Status Pemprosesan</h6>
</div>
<div class="card-body">
@if($stats['processing_documents'] > 0)
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="small">Sedang diproses</span>
<span class="badge bg-warning">{{ $stats['processing_documents'] }}</span>
</div>
@endif
@if($stats['failed_documents'] > 0)
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="small text-danger">Gagal diproses</span>
<a href="{{ route('admin.documents.index', ['status' => 'failed']) }}"
class="badge bg-danger text-decoration-none">{{ $stats['failed_documents'] }}</a>
</div>
@endif
@if($stats['unanswered_chats'] > 0)
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="small">Soalan tidak terjawab</span>
<a href="{{ route('admin.chat-feedback.index', ['has_answer' => 0]) }}"
class="badge bg-secondary text-decoration-none">{{ $stats['unanswered_chats'] }}</a>
</div>
@endif
@if($stats['processing_documents'] == 0 && $stats['failed_documents'] == 0)
<p class="text-muted small mb-0"><i class="bi bi-check-circle text-success me-1"></i>Semua dokumen diproses.</p>
@endif
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Chat Terkini</h6>
</div>
<ul class="list-group list-group-flush">
@forelse($recentChats as $chat)
<li class="list-group-item py-2 px-3">
<div class="text-truncate small fw-semibold">{{ $chat->question }}</div>
<div class="d-flex align-items-center gap-2 mt-1">
@if($chat->category)
<span class="badge bg-light text-dark border" style="font-size:.65rem">{{ $chat->category->name }}</span>
@endif
@if(!$chat->has_answer)
<span class="badge bg-warning-subtle text-warning border border-warning" style="font-size:.65rem">Tidak Terjawab</span>
@endif
<span class="text-muted ms-auto" style="font-size:.65rem">{{ $chat->created_at->diffForHumans() }}</span>
</div>
</li>
@empty
<li class="list-group-item py-3 text-center text-muted small">Tiada chat lagi.</li>
@endforelse
</ul>
</div>
</div>
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-semibold">Aktiviti Terkini</h6>
<a href="{{ route('admin.audit-logs.index') }}" class="btn btn-sm btn-link p-0 text-muted">
Lihat semua
</a>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody>
@forelse($recentActivity as $log)
<tr>
<td class="ps-3 py-2">
<div class="small fw-semibold">{{ $log->description ?? $log->event }}</div>
@if($log->user)
<small class="text-muted">{{ $log->user->name }}</small>
@endif
</td>
<td class="text-end pe-3 py-2 text-muted small" style="white-space:nowrap">
{{ $log->created_at->diffForHumans() }}
</td>
</tr>
@empty
<tr><td colspan="2" class="text-center text-muted py-4 small">Tiada aktiviti.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,253 @@
@extends('layouts.admin')
@section('title', 'Chunk Review — ' . Str::limit($document->title, 40))
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.documents.show', $document) }}">{{ Str::limit($document->title, 30) }}</a></li>
<li class="breadcrumb-item active">Chunk Review v{{ $version->version_number }}</li>
@endsection
@section('content')
{{-- Header --}}
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
<div>
<h4 class="mb-0 fw-bold">Chunk Review</h4>
<p class="text-muted small mb-0">
{{ $document->title }} &mdash; Versi {{ $version->version_number }}
@if($version->page_count)
&middot; {{ $version->page_count }} muka surat
@endif
</p>
</div>
<a href="{{ route('admin.documents.show', $document) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Kembali ke Dokumen
</a>
</div>
{{-- Flash Messages --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show py-2" role="alert">
<i class="bi bi-check-circle me-1"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<i class="bi bi-exclamation-triangle me-1"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Status Summary Pills --}}
<div class="d-flex flex-wrap gap-2 mb-3">
<a href="{{ route('admin.documents.chunks', ['document' => $document, 'version' => $version]) }}"
class="btn btn-sm {{ !$statusFilter ? 'btn-dark' : 'btn-outline-dark' }}">
Semua <span class="badge bg-secondary ms-1">{{ array_sum($statusCounts) }}</span>
</a>
@php
$statusDef = [
\App\Models\DocumentChunk::STATUS_INDEXED => ['label' => 'Diindex', 'class' => 'btn-success'],
\App\Models\DocumentChunk::STATUS_NEEDS_REINDEX => ['label' => 'Perlu Reindex', 'class' => 'btn-warning'],
\App\Models\DocumentChunk::STATUS_NEEDS_REVIEW => ['label' => 'Perlu Semak', 'class' => 'btn-info'],
\App\Models\DocumentChunk::STATUS_PENDING => ['label' => 'Menunggu', 'class' => 'btn-light border'],
\App\Models\DocumentChunk::STATUS_EXCLUDED => ['label' => 'Dikecualikan', 'class' => 'btn-secondary'],
\App\Models\DocumentChunk::STATUS_SUPERSEDED => ['label' => 'Digantikan', 'class' => 'btn-dark'],
\App\Models\DocumentChunk::STATUS_FAILED_EMBEDDING => ['label' => 'Gagal Embed', 'class' => 'btn-danger'],
];
@endphp
@foreach($statusDef as $statusKey => $def)
@if(isset($statusCounts[$statusKey]) && $statusCounts[$statusKey] > 0)
<a href="{{ route('admin.documents.chunks', ['document' => $document, 'version' => $version, 'status' => $statusKey]) }}"
class="btn btn-sm {{ $statusFilter === $statusKey ? $def['class'] : 'btn-outline-' . str_replace('btn-', '', $def['class']) }}">
{{ $def['label'] }}
<span class="badge bg-white text-dark ms-1">{{ $statusCounts[$statusKey] }}</span>
</a>
@endif
@endforeach
</div>
{{-- Chunk List --}}
@forelse($chunks as $chunk)
<div class="card border-0 shadow-sm mb-2 {{ $chunk->isSuperseded() ? 'opacity-50' : '' }}">
<div class="card-body py-2 px-3">
<div class="row align-items-center g-2">
{{-- Metadata --}}
<div class="col-12 col-md-7">
<div class="d-flex align-items-center gap-2 flex-wrap">
{{-- Status badge --}}
<span class="badge {{ $chunk->getStatusBadgeClass() }}" style="font-size:.7rem">
{{ $chunk->getStatusLabel() }}
</span>
{{-- Chunk number --}}
<span class="badge bg-light text-dark border fw-normal" style="font-size:.7rem">
#{{ $chunk->chunk_index + 1 }}
</span>
{{-- Edited badge --}}
@if($chunk->is_edited)
<span class="badge bg-primary-subtle text-primary border border-primary" style="font-size:.65rem">
<i class="bi bi-pencil me-1"></i>Edited
</span>
@endif
{{-- Split badge --}}
@if($chunk->parent_chunk_id)
<span class="badge bg-warning-subtle text-warning border border-warning" style="font-size:.65rem">
<i class="bi bi-scissors me-1"></i>Split
</span>
@endif
@if($chunk->childChunks->count() > 0)
<span class="badge bg-dark-subtle text-dark border" style="font-size:.65rem">
<i class="bi bi-diagram-2 me-1"></i>{{ $chunk->childChunks->count() }} anak
</span>
@endif
{{-- Metadata kecil --}}
@if($chunk->page_number)
<span class="text-muted" style="font-size:.75rem">ms.{{ $chunk->page_number }}</span>
@endif
@if($chunk->section_heading)
<span class="text-muted text-truncate" style="font-size:.75rem;max-width:200px"
title="{{ $chunk->section_heading }}">
{{ Str::limit($chunk->section_heading, 40) }}
</span>
@endif
</div>
{{-- Preview teks --}}
<p class="text-muted mb-0 mt-1" style="font-size:.8rem;line-height:1.4">
{{ Str::limit($chunk->getEmbeddableText(), 150) }}
</p>
{{-- Editor info --}}
@if($chunk->is_edited && $chunk->editor)
<p class="mb-0 mt-1" style="font-size:.7rem;color:#aaa">
Diedit oleh {{ $chunk->editor->name }}
{{ $chunk->edited_at?->diffForHumans() }}
</p>
@endif
</div>
{{-- Actions --}}
<div class="col-12 col-md-5">
<div class="d-flex gap-1 flex-wrap justify-content-md-end">
{{-- View detail --}}
<a href="{{ route('admin.chunks.show', $chunk) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Detail
</a>
@if(!$chunk->isSuperseded())
{{-- Split --}}
<a href="{{ route('admin.chunks.split', $chunk) }}"
class="btn btn-sm btn-outline-warning"
title="Split chunk ini">
<i class="bi bi-scissors"></i>
</a>
{{-- Exclude / Include --}}
@if($chunk->exclude_from_index)
<form method="POST" action="{{ route('admin.chunks.include', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-success"
title="Kembalikan ke indexing"
onclick="return confirm('Kembalikan chunk ini ke indexing?')">
<i class="bi bi-check-circle"></i>
</button>
</form>
@else
<form method="POST" action="{{ route('admin.chunks.exclude', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-secondary"
title="Kecualikan dari indexing"
onclick="return confirm('Kecualikan chunk #{{ $chunk->chunk_index + 1 }} dari indexing?')">
<i class="bi bi-slash-circle"></i>
</button>
</form>
@endif
{{-- Reindex --}}
@if($chunk->isIndexable())
<form method="POST" action="{{ route('admin.chunks.reindex', $chunk) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-info"
title="Trigger reindex"
onclick="return confirm('Reindex chunk #{{ $chunk->chunk_index + 1 }} sekarang?')">
<i class="bi bi-arrow-repeat"></i>
</button>
</form>
@endif
@else
{{-- Superseded tunjukkan link ke children --}}
@if($chunk->childChunks->count() > 0)
<span class="text-muted small">
<i class="bi bi-arrow-down-right me-1"></i>
{{ $chunk->childChunks->count() }} chunk baharu
</span>
@endif
@endif
</div>
</div>
</div>
{{-- Child chunks inline (jika chunk ini pernah di-split) --}}
@if($chunk->childChunks->count() > 0)
<div class="mt-2 ps-3 border-start border-2 border-warning">
<p class="text-muted mb-1" style="font-size:.72rem">
<i class="bi bi-diagram-2 me-1"></i>Dipecahkan kepada:
</p>
@foreach($chunk->childChunks as $child)
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge {{ $child->getStatusBadgeClass() }}" style="font-size:.65rem">
{{ $child->getStatusLabel() }}
</span>
<span class="text-muted" style="font-size:.75rem">#{{ $child->chunk_index + 1 }}</span>
<span class="text-muted text-truncate" style="font-size:.75rem">
{{ Str::limit($child->getEmbeddableText(), 80) }}
</span>
<a href="{{ route('admin.chunks.show', $child) }}"
class="btn btn-sm btn-link p-0 ms-auto" style="font-size:.72rem">
Lihat
</a>
</div>
@endforeach
</div>
@endif
</div>
</div>
@empty
<div class="text-center py-5 text-muted">
<i class="bi bi-collection display-6 d-block mb-2"></i>
@if($statusFilter)
Tiada chunk dengan status "{{ $statusFilter }}".
<a href="{{ route('admin.documents.chunks', ['document' => $document, 'version' => $version]) }}">
Lihat semua
</a>
@else
Tiada chunk untuk versi ini.
@endif
</div>
@endforelse
{{-- Pagination --}}
@if($chunks->hasPages())
<div class="mt-3">
{{ $chunks->links() }}
</div>
@endif
@endsection

View File

@@ -0,0 +1,180 @@
@extends('layouts.admin')
@section('title', 'Upload Dokumen')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item active">Upload Baru</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-upload me-2"></i>Upload Dokumen PDF</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.documents.store') }}" enctype="multipart/form-data">
@csrf
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-semibold">Kategori <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror" required>
<option value=""> Pilih Kategori </option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Tajuk Dokumen <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title') }}" placeholder="Contoh: Garis Panduan Pelesenan Perniagaan 2024" required>
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Fail PDF <span class="text-danger">*</span></label>
<input type="file" name="file" id="fileInput"
class="form-control @error('file') is-invalid @enderror"
accept=".pdf" required>
<div class="form-text">
Hanya fail PDF dibenarkan. Saiz maksimum:
{{ round(config('knowledgebase.upload.max_file_size', 20480) / 1024) }}MB
</div>
@error('file')<div class="invalid-feedback">{{ $message }}</div>@enderror
<div id="filePreview" class="mt-2 d-none">
<div class="alert alert-info py-2 mb-0 small">
<i class="bi bi-file-pdf me-1"></i>
<span id="fileName"></span>
<span id="fileSize"></span>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Deskripsi</label>
<textarea name="description" class="form-control" rows="2"
placeholder="Deskripsi ringkas dokumen ini...">{{ old('description') }}</textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Kuat Kuasa</label>
<input type="date" name="effective_date" class="form-control @error('effective_date') is-invalid @enderror"
value="{{ old('effective_date') }}">
@error('effective_date')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Luput</label>
<input type="date" name="expiry_date" class="form-control @error('expiry_date') is-invalid @enderror"
value="{{ old('expiry_date') }}">
@error('expiry_date')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Bahasa</label>
<select name="language" class="form-select">
<option value="ms" {{ old('language', 'ms') == 'ms' ? 'selected' : '' }}>Bahasa Melayu</option>
<option value="en" {{ old('language') == 'en' ? 'selected' : '' }}>English</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tag</label>
<input type="text" id="tagsInput" class="form-control"
placeholder="Taip dan tekan Enter..."
value="{{ old('tags') ? implode(', ', old('tags')) : '' }}">
<div id="tagsContainer" class="mt-1"></div>
<input type="hidden" name="tags" id="tagsHidden">
<div class="form-text">Tekan Enter atau koma untuk tambah tag.</div>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Nota Upload</label>
<input type="text" name="change_notes" class="form-control"
value="{{ old('change_notes', 'Versi pertama.') }}"
placeholder="Nota ringkas tentang upload ini...">
</div>
</div>
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload me-1"></i>Upload & Proses
</button>
<a href="{{ route('admin.documents.index') }}" class="btn btn-outline-secondary">
Batal
</a>
</div>
</form>
</div>
</div>
<div class="alert alert-light border mt-3 small">
<i class="bi bi-info-circle me-2 text-info"></i>
Dokumen akan diproses secara <strong>background</strong> selepas upload. Proses extraction, chunking dan embedding mungkin mengambil masa beberapa minit bergantung pada saiz fail.
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Preview fail yang dipilih
$('#fileInput').on('change', function() {
const file = this.files[0];
if (file) {
const size = file.size < 1048576
? (file.size / 1024).toFixed(1) + ' KB'
: (file.size / 1048576).toFixed(1) + ' MB';
$('#fileName').text(file.name);
$('#fileSize').text(size);
$('#filePreview').removeClass('d-none');
}
});
// Tag input
let tags = [];
function renderTags() {
const container = $('#tagsContainer');
container.empty();
tags.forEach((tag, i) => {
container.append(
`<span class="badge bg-secondary me-1 mb-1">${tag}
<i class="bi bi-x-circle ms-1" style="cursor:pointer" data-i="${i}"></i></span>`
);
});
$('#tagsHidden').val(tags.map((t, i) => `tags[${i}]=${t}`).join('&'));
// Rebuild hidden inputs properly
$('[name^="tags["]').remove();
tags.forEach((tag, i) => {
$('form').append(`<input type="hidden" name="tags[${i}]" value="${tag}">`);
});
}
$('#tagsInput').on('keydown', function(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const val = $(this).val().trim().replace(/,$/, '');
if (val && !tags.includes(val)) {
tags.push(val);
renderTags();
}
$(this).val('');
}
});
$(document).on('click', '.bi-x-circle', function() {
const i = $(this).data('i');
tags.splice(i, 1);
renderTags();
});
</script>
@endpush

View File

@@ -0,0 +1,80 @@
@extends('layouts.admin')
@section('title', 'Edit Dokumen')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.documents.show', $document) }}">{{ Str::limit($document->title, 30) }}</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-pencil me-2"></i>Edit Maklumat Dokumen</h5>
<small class="text-muted">Untuk upload fail PDF baru, gunakan butang "Upload Versi Baru" pada halaman dokumen.</small>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.documents.update', $document) }}">
@csrf @method('PUT')
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-semibold">Kategori <span class="text-danger">*</span></label>
<select name="category_id" class="form-select" required>
@foreach($categories as $cat)
<option value="{{ $cat->id }}"
{{ old('category_id', $document->category_id) == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Tajuk <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title', $document->title) }}" required>
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Deskripsi</label>
<textarea name="description" class="form-control" rows="2">{{ old('description', $document->description) }}</textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Kuat Kuasa</label>
<input type="date" name="effective_date" class="form-control"
value="{{ old('effective_date', $document->effective_date?->toDateString()) }}">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Luput</label>
<input type="date" name="expiry_date" class="form-control"
value="{{ old('expiry_date', $document->expiry_date?->toDateString()) }}">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Bahasa</label>
<select name="language" class="form-select">
<option value="ms" {{ old('language', $document->language) == 'ms' ? 'selected' : '' }}>BM</option>
<option value="en" {{ old('language', $document->language) == 'en' ? 'selected' : '' }}>EN</option>
</select>
</div>
</div>
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Kemaskini
</button>
<a href="{{ route('admin.documents.show', $document) }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,153 @@
@extends('layouts.admin')
@section('title', 'Dokumen')
@section('breadcrumb')
<li class="breadcrumb-item active">Dokumen PDF</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">Dokumen PDF</h4>
<a href="{{ route('admin.documents.create') }}" class="btn btn-primary">
<i class="bi bi-upload me-1"></i>Upload Dokumen
</a>
</div>
{{-- Filter --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="search" class="form-control form-control-sm"
placeholder="Cari tajuk..." value="{{ request('search') }}">
</div>
<div class="col-md-3">
<select name="category_id" class="form-select form-select-sm">
<option value="">Semua Kategori</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<select name="status" class="form-select form-select-sm">
<option value="">Semua Status</option>
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Aktif</option>
<option value="processing" {{ request('status') == 'processing' ? 'selected' : '' }}>Sedang Diproses</option>
<option value="inactive" {{ request('status') == 'inactive' ? 'selected' : '' }}>Tidak Aktif</option>
<option value="failed" {{ request('status') == 'failed' ? 'selected' : '' }}>Gagal</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-search me-1"></i>Tapis
</button>
@if(request()->hasAny(['search', 'category_id', 'status']))
<a href="{{ route('admin.documents.index') }}" class="btn btn-sm btn-link text-muted">Reset</a>
@endif
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Tajuk</th>
<th>Kategori</th>
<th>Versi</th>
<th>Status</th>
<th>Tarikh</th>
<th class="text-end pe-3">Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($documents as $doc)
<tr>
<td class="ps-3">
<div class="fw-semibold">{{ $doc->title }}</div>
@if($doc->description)
<small class="text-muted text-truncate d-block" style="max-width:300px">{{ $doc->description }}</small>
@endif
@if($doc->tags)
@foreach(array_slice($doc->tags, 0, 3) as $tag)
<span class="badge bg-light text-dark border me-1" style="font-size:.65rem">{{ $tag }}</span>
@endforeach
@endif
</td>
<td>
<span class="badge" style="background: {{ $doc->category->color ?? '#6c757d' }}">
{{ $doc->category->name }}
</span>
</td>
<td>
<span class="badge bg-light text-dark border">
v{{ $doc->currentVersion?->version_number ?? '—' }}
</span>
@if($doc->versions_count > 1)
<small class="text-muted">({{ $doc->versions_count }} versi)</small>
@endif
</td>
<td>
@php
$statusMap = [
'active' => ['class' => 'bg-success', 'label' => 'Aktif'],
'inactive' => ['class' => 'bg-secondary', 'label' => 'Tidak Aktif'],
'processing' => ['class' => 'badge-processing', 'label' => 'Diproses'],
'draft' => ['class' => 'bg-light text-dark border', 'label' => 'Draf'],
'failed' => ['class' => 'badge-failed', 'label' => 'Gagal'],
];
$s = $statusMap[$doc->status] ?? ['class' => 'bg-light', 'label' => $doc->status];
@endphp
<span class="badge {{ $s['class'] }}">{{ $s['label'] }}</span>
@if($doc->currentVersion && $doc->currentVersion->processing_status !== 'indexed' && $doc->status == 'processing')
<br><small class="text-muted" style="font-size:.65rem">{{ $doc->currentVersion->processing_status }}</small>
@endif
</td>
<td>
<small class="text-muted">{{ $doc->created_at->format('d/m/Y') }}</small>
@if($doc->effective_date)
<br><small class="text-muted">Kuat kuasa: {{ $doc->effective_date->format('d/m/Y') }}</small>
@endif
</td>
<td class="text-end pe-3">
<a href="{{ route('admin.documents.show', $doc) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
@if($doc->status !== 'processing')
<form method="POST" action="{{ route('admin.documents.toggle-status', $doc) }}" class="d-inline">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $doc->is_active ? 'btn-outline-warning' : 'btn-outline-success' }}"
title="{{ $doc->is_active ? 'Nyahaktifkan' : 'Aktifkan' }}">
<i class="bi {{ $doc->is_active ? 'bi-pause-circle' : 'bi-play-circle' }}"></i>
</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted py-5">
<i class="bi bi-file-pdf fs-2 d-block mb-2 opacity-25"></i>
Tiada dokumen ditemui.
<a href="{{ route('admin.documents.create') }}">Upload yang pertama.</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($documents->hasPages())
<div class="card-footer bg-white border-top py-3">
{{ $documents->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,223 @@
@extends('layouts.admin')
@section('title', $document->title)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.documents.index') }}">Dokumen</a></li>
<li class="breadcrumb-item active">{{ Str::limit($document->title, 40) }}</li>
@endsection
@section('content')
<div class="d-flex align-items-start justify-content-between mb-4">
<div>
<h4 class="mb-1 fw-bold">{{ $document->title }}</h4>
<div class="d-flex gap-2 align-items-center flex-wrap">
<span class="badge" style="background:{{ $document->category->color }}">{{ $document->category->name }}</span>
@php
$statusMap = [
'active' => 'bg-success',
'inactive' => 'bg-secondary',
'processing' => 'bg-warning',
'failed' => 'bg-danger',
];
@endphp
<span class="badge {{ $statusMap[$document->status] ?? 'bg-light text-dark' }}">{{ ucfirst($document->status) }}</span>
@if($document->effective_date)
<small class="text-muted">Kuat kuasa: {{ $document->effective_date->format('d/m/Y') }}</small>
@endif
</div>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.documents.edit', $document) }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-pencil me-1"></i>Edit
</a>
@if(auth()->user()->isAdmin())
<form method="POST" action="{{ route('admin.documents.reindex', $document) }}">
@csrf
<button type="submit" class="btn btn-outline-info btn-sm"
onclick="return confirm('Adakah anda pasti untuk reindex dokumen ini?')">
<i class="bi bi-arrow-repeat me-1"></i>Reindex
</button>
</form>
@endif
<form method="POST" action="{{ route('admin.documents.toggle-status', $document) }}">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $document->is_active ? 'btn-warning' : 'btn-success' }}">
@if($document->is_active)
<i class="bi bi-pause-circle me-1"></i>Nyahaktif
@else
<i class="bi bi-play-circle me-1"></i>Aktifkan
@endif
</button>
</form>
</div>
</div>
<div class="row g-4">
{{-- Info Dokumen --}}
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom"><h6 class="mb-0 fw-semibold">Maklumat Dokumen</h6></div>
<div class="card-body small">
@if($document->description)
<p>{{ $document->description }}</p>
@endif
<dl class="row mb-0">
<dt class="col-5 text-muted">Bahasa</dt>
<dd class="col-7">{{ $document->language == 'ms' ? 'Bahasa Melayu' : 'English' }}</dd>
@if($document->expiry_date)
<dt class="col-5 text-muted">Tarikh Luput</dt>
<dd class="col-7">{{ $document->expiry_date->format('d/m/Y') }}</dd>
@endif
<dt class="col-5 text-muted">Dicipta</dt>
<dd class="col-7">{{ $document->created_at->format('d/m/Y H:i') }}</dd>
</dl>
@if($document->tags)
<div class="mt-2">
@foreach($document->tags as $tag)
<span class="badge bg-light text-dark border me-1">{{ $tag }}</span>
@endforeach
</div>
@endif
</div>
</div>
{{-- Upload versi baru --}}
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Upload Versi Baru</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.documents.upload-version', $document) }}" enctype="multipart/form-data">
@csrf
<div class="mb-2">
<input type="file" name="file" class="form-control form-control-sm" accept=".pdf" required>
</div>
<div class="mb-2">
<input type="text" name="change_notes" class="form-control form-control-sm"
placeholder="Nota perubahan (optional)">
</div>
<button type="submit" class="btn btn-sm btn-primary w-100">
<i class="bi bi-upload me-1"></i>Upload Versi Baru
</button>
</form>
<small class="text-muted mt-1 d-block">Versi lama tidak akan dipadam.</small>
</div>
</div>
</div>
{{-- Senarai Versi --}}
<div class="col-lg-8">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Sejarah Versi</h6>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Versi</th>
<th>Fail</th>
<th>Status</th>
<th>Diupload</th>
<th class="text-end pe-3">Tindakan</th>
</tr>
</thead>
<tbody>
@foreach($document->versions->sortByDesc('version_number') as $version)
<tr class="{{ $version->is_current ? 'table-primary' : '' }}">
<td class="ps-3">
<span class="badge bg-light text-dark border">v{{ $version->version_number }}</span>
@if($version->is_current)
<span class="badge bg-success-subtle text-success border border-success ms-1">Semasa</span>
@endif
</td>
<td>
<div class="small">{{ $version->original_filename }}</div>
<small class="text-muted">{{ $version->file_size_formatted }}</small>
@if($version->page_count)
<small class="text-muted"> · {{ $version->page_count }} ms.</small>
@endif
</td>
<td>
@php
$ps = [
'indexed' => ['bg-success', 'Selesai'],
'processing' => ['bg-warning', 'Diproses'],
'embedding' => ['bg-warning', 'Embedding'],
'extraction_failed' => ['bg-danger', 'Gagal Extract'],
'failed' => ['bg-danger', 'Gagal'],
'pending' => ['bg-secondary', 'Menunggu'],
];
$pInfo = $ps[$version->processing_status] ?? ['bg-light text-dark', $version->processing_status];
@endphp
<span class="badge {{ $pInfo[0] }}">{{ $pInfo[1] }}</span>
@if($version->hasFailed() && $version->processing_error)
<br><small class="text-danger d-block" style="max-width:200px"
title="{{ $version->processing_error }}">
{{ Str::limit($version->processing_error, 50) }}
</small>
@endif
</td>
<td>
<small>{{ $version->created_at->format('d/m/Y') }}</small>
@if($version->uploader)
<br><small class="text-muted">{{ $version->uploader->name }}</small>
@endif
</td>
<td class="text-end pe-3">
@if($version->processing_status === 'indexed')
<a href="{{ route('admin.documents.chunks', [$document, $version]) }}"
class="btn btn-sm btn-outline-secondary"
title="Lihat Chunk">
<i class="bi bi-list-ul"></i>
</a>
@endif
<a href="{{ route('admin.documents.download', [$document, $version]) }}"
class="btn btn-sm btn-outline-primary" title="Muat Turun">
<i class="bi bi-download"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
{{-- Preview Chunks (versi semasa) --}}
@if($document->currentVersion && $document->currentVersion->processing_status === 'indexed')
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-semibold">Chunk Terkini ({{ $document->currentVersion->chunks->count() }} preview)</h6>
<a href="{{ route('admin.documents.chunks', [$document, $document->currentVersion]) }}"
class="btn btn-sm btn-link p-0 text-muted">Lihat semua</a>
</div>
<div class="list-group list-group-flush">
@foreach($document->currentVersion->chunks->take(5) as $chunk)
<div class="list-group-item py-2 px-3">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-light text-dark border" style="font-size:.65rem">
Chunk #{{ $chunk->chunk_index + 1 }}
</span>
@if($chunk->page_number)
<span class="badge bg-light text-dark border" style="font-size:.65rem">
ms. {{ $chunk->page_number }}
</span>
@endif
@if($chunk->section_heading)
<span class="text-muted small text-truncate">{{ $chunk->section_heading }}</span>
@endif
<span class="badge {{ $chunk->is_embedded ? 'bg-success-subtle text-success' : 'bg-warning-subtle text-warning' }} ms-auto" style="font-size:.65rem">
{{ $chunk->is_embedded ? 'Embedded' : 'Belum Embed' }}
</span>
</div>
<p class="mb-0 small text-muted" style="white-space:pre-wrap">{{ Str::limit($chunk->content, 200) }}</p>
</div>
@endforeach
</div>
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,142 @@
@extends('layouts.admin')
@section('title', 'Tambah Knowledge Item')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.knowledge-items.index') }}">FAQ & Pengetahuan</a></li>
<li class="breadcrumb-item active">Tambah</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-plus-circle me-2"></i>Tambah Knowledge Item</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.knowledge-items.store') }}">
@csrf
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Kategori <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror" required>
<option value=""> Pilih Kategori </option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}"
{{ (old('category_id', $prefillData['category_id'] ?? '') == $cat->id) ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Jenis Item <span class="text-danger">*</span></label>
<select name="item_type" class="form-select @error('item_type') is-invalid @enderror" required>
<option value=""> Pilih Jenis </option>
@foreach($typeLabels as $value => $label)
<option value="{{ $value }}"
{{ (old('item_type', $prefillData['item_type'] ?? '') == $value) ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
@error('item_type')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">
<span id="titleLabel">Tajuk</span> <span class="text-danger">*</span>
<small class="text-muted fw-normal ms-1">(untuk FAQ: tuliskan soalannya)</small>
</label>
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title', $prefillData['title'] ?? '') }}"
placeholder="Contoh: Apakah syarat untuk memohon lesen perniagaan?" required>
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">
<span id="contentLabel">Kandungan / Jawapan</span> <span class="text-danger">*</span>
</label>
<textarea name="content" class="form-control @error('content') is-invalid @enderror"
rows="8" required
placeholder="Tulis jawapan atau kandungan penuh di sini...">{{ old('content', $prefillData['content'] ?? '') }}</textarea>
<div class="d-flex justify-content-between">
@error('content')<div class="invalid-feedback">{{ $message }}</div>@enderror
<small class="text-muted ms-auto mt-1" id="charCount">0 / 10,000</small>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Bahasa</label>
<select name="language" class="form-select">
<option value="ms" {{ old('language', 'ms') == 'ms' ? 'selected' : '' }}>Bahasa Melayu</option>
<option value="en" {{ old('language') == 'en' ? 'selected' : '' }}>English</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Kuat Kuasa</label>
<input type="date" name="effective_date" class="form-control"
value="{{ old('effective_date') }}">
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-2">
<input type="hidden" name="is_active" value="0">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="isActive" {{ old('is_active', '1') ? 'checked' : '' }}>
<label class="form-check-label" for="isActive">Aktifkan selepas simpan</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-2">
<input type="hidden" name="is_public" value="0">
<input class="form-check-input" type="checkbox" name="is_public" value="1"
id="isPublic" {{ old('is_public', '1') ? 'checked' : '' }}>
<label class="form-check-label" for="isPublic">Boleh dicari oleh public</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Simpan & Embed
</button>
<a href="{{ route('admin.knowledge-items.index') }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Kira karakter
$('textarea[name="content"]').on('input', function() {
const count = $(this).val().length;
$('#charCount').text(count.toLocaleString() + ' / 10,000');
if (count > 9000) $('#charCount').addClass('text-warning');
else $('#charCount').removeClass('text-warning');
});
$('textarea[name="content"]').trigger('input');
// Update label ikut jenis
$('select[name="item_type"]').on('change', function() {
if ($(this).val() === 'faq') {
$('#titleLabel').text('Soalan');
$('#contentLabel').text('Jawapan');
} else {
$('#titleLabel').text('Tajuk');
$('#contentLabel').text('Kandungan');
}
});
$('select[name="item_type"]').trigger('change');
</script>
@endpush

View File

@@ -0,0 +1,107 @@
@extends('layouts.admin')
@section('title', 'Edit Knowledge Item')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.knowledge-items.index') }}">FAQ & Pengetahuan</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.knowledge-items.show', $knowledgeItem) }}">{{ Str::limit($knowledgeItem->title, 30) }}</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0 fw-bold"><i class="bi bi-pencil me-2"></i>Edit Knowledge Item</h5>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.knowledge-items.update', $knowledgeItem) }}">
@csrf @method('PUT')
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Kategori <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror" required>
<option value=""> Pilih Kategori </option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}"
{{ old('category_id', $knowledgeItem->category_id) == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Jenis Item <span class="text-danger">*</span></label>
<select name="item_type" class="form-select @error('item_type') is-invalid @enderror" required>
@foreach($typeLabels as $value => $label)
<option value="{{ $value }}"
{{ old('item_type', $knowledgeItem->item_type) == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
@error('item_type')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Tajuk / Soalan <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title', $knowledgeItem->title) }}" required>
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-semibold">Kandungan / Jawapan <span class="text-danger">*</span></label>
<textarea name="content" class="form-control @error('content') is-invalid @enderror"
rows="10" required>{{ old('content', $knowledgeItem->content) }}</textarea>
@error('content')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Bahasa</label>
<select name="language" class="form-select">
<option value="ms" {{ old('language', $knowledgeItem->language) == 'ms' ? 'selected' : '' }}>BM</option>
<option value="en" {{ old('language', $knowledgeItem->language) == 'en' ? 'selected' : '' }}>EN</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Tarikh Kuat Kuasa</label>
<input type="date" name="effective_date" class="form-control"
value="{{ old('effective_date', $knowledgeItem->effective_date?->toDateString()) }}">
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-2">
<input type="hidden" name="is_active" value="0">
<input class="form-check-input" type="checkbox" name="is_active" value="1"
id="isActive" {{ old('is_active', $knowledgeItem->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="isActive">Aktif</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-2">
<input type="hidden" name="is_public" value="0">
<input class="form-check-input" type="checkbox" name="is_public" value="1"
id="isPublic" {{ old('is_public', $knowledgeItem->is_public) ? 'checked' : '' }}>
<label class="form-check-label" for="isPublic">Awam</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Kemaskini & Re-embed
</button>
<a href="{{ route('admin.knowledge-items.show', $knowledgeItem) }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,142 @@
@extends('layouts.admin')
@section('title', 'FAQ & Pengetahuan')
@section('breadcrumb')
<li class="breadcrumb-item active">FAQ & Pengetahuan</li>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0 fw-bold">FAQ & Pengetahuan</h4>
<a href="{{ route('admin.knowledge-items.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i>Tambah Item
</a>
</div>
{{-- Filter --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="search" class="form-control form-control-sm"
placeholder="Cari tajuk atau kandungan..." value="{{ request('search') }}">
</div>
<div class="col-md-3">
<select name="category_id" class="form-select form-select-sm">
<option value="">Semua Kategori</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<select name="item_type" class="form-select form-select-sm">
<option value="">Semua Jenis</option>
@foreach($typeLabels as $value => $label)
<option value="{{ $value }}" {{ request('item_type') == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-search me-1"></i>Tapis
</button>
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Tajuk / Soalan</th>
<th>Kategori</th>
<th>Jenis</th>
<th>Status</th>
<th>Embed</th>
<th class="text-end pe-3">Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($items as $item)
<tr class="{{ $item->trashed() ? 'opacity-50' : '' }}">
<td class="ps-3">
<div class="fw-semibold">{{ Str::limit($item->title, 80) }}</div>
<small class="text-muted">{{ Str::limit(strip_tags($item->content), 80) }}</small>
</td>
<td>
<span class="badge" style="background:{{ $item->category->color ?? '#6c757d' }}">
{{ $item->category->name }}
</span>
</td>
<td>
@php
$typeColors = ['faq' => 'bg-primary', 'policy' => 'bg-success', 'note' => 'bg-warning', 'announcement' => 'bg-info'];
@endphp
<span class="badge {{ $typeColors[$item->item_type] ?? 'bg-secondary' }}">
{{ $typeLabels[$item->item_type] ?? $item->item_type }}
</span>
</td>
<td>
@if($item->trashed())
<span class="badge bg-danger">Dipadam</span>
@elseif($item->is_active)
<span class="badge bg-success">Aktif</span>
@else
<span class="badge bg-secondary">Tidak Aktif</span>
@endif
</td>
<td>
@if($item->is_embedded)
<i class="bi bi-check-circle-fill text-success" title="Sudah di-embed"></i>
@else
<i class="bi bi-clock text-warning" title="Belum di-embed"></i>
@endif
</td>
<td class="text-end pe-3">
@unless($item->trashed())
<a href="{{ route('admin.knowledge-items.show', $item) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
<a href="{{ route('admin.knowledge-items.edit', $item) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
<form method="POST" action="{{ route('admin.knowledge-items.toggle-status', $item) }}" class="d-inline">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $item->is_active ? 'btn-outline-warning' : 'btn-outline-success' }}"
title="{{ $item->is_active ? 'Nyahaktifkan' : 'Aktifkan' }}">
<i class="bi {{ $item->is_active ? 'bi-toggle-on' : 'bi-toggle-off' }}"></i>
</button>
</form>
@endunless
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted py-5">
<i class="bi bi-lightbulb fs-2 d-block mb-2 opacity-25"></i>
Tiada knowledge item ditemui.
<a href="{{ route('admin.knowledge-items.create') }}">Tambah yang pertama.</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($items->hasPages())
<div class="card-footer bg-white border-top py-3">
{{ $items->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,112 @@
@extends('layouts.admin')
@section('title', 'Knowledge Item')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.knowledge-items.index') }}">FAQ & Pengetahuan</a></li>
<li class="breadcrumb-item active">{{ Str::limit($knowledgeItem->title, 40) }}</li>
@endsection
@section('content')
<div class="d-flex align-items-start justify-content-between mb-4">
<div>
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
@php $typeColors = ['faq' => 'bg-primary', 'policy' => 'bg-success', 'note' => 'bg-warning', 'announcement' => 'bg-info']; @endphp
<span class="badge {{ $typeColors[$knowledgeItem->item_type] ?? 'bg-secondary' }}">
{{ $knowledgeItem->type_label }}
</span>
<span class="badge" style="background:{{ $knowledgeItem->category->color ?? '#6c757d' }}">
{{ $knowledgeItem->category->name }}
</span>
<span class="badge {{ $knowledgeItem->is_active ? 'bg-success' : 'bg-secondary' }}">
{{ $knowledgeItem->is_active ? 'Aktif' : 'Tidak Aktif' }}
</span>
@if($knowledgeItem->is_embedded)
<span class="badge bg-success-subtle text-success border border-success">
<i class="bi bi-check2 me-1"></i>Di-embed
</span>
@else
<span class="badge bg-warning-subtle text-warning border border-warning">
<i class="bi bi-clock me-1"></i>Belum Embed
</span>
@endif
</div>
<h4 class="mb-0 fw-bold">{{ $knowledgeItem->title }}</h4>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.knowledge-items.edit', $knowledgeItem) }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<form method="POST" action="{{ route('admin.knowledge-items.reindex', $knowledgeItem) }}">
@csrf
<button type="submit" class="btn btn-outline-info btn-sm">
<i class="bi bi-arrow-repeat me-1"></i>Re-embed
</button>
</form>
<form method="POST" action="{{ route('admin.knowledge-items.toggle-status', $knowledgeItem) }}">
@csrf @method('PATCH')
<button type="submit" class="btn btn-sm {{ $knowledgeItem->is_active ? 'btn-warning' : 'btn-success' }}">
{{ $knowledgeItem->is_active ? 'Nyahaktifkan' : 'Aktifkan' }}
</button>
</form>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0 fw-semibold">Kandungan</h6>
</div>
<div class="card-body">
@if($knowledgeItem->item_type === 'faq')
<div class="mb-3">
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Soalan</label>
<div class="p-3 bg-light rounded mt-1 fw-semibold">{{ $knowledgeItem->title }}</div>
</div>
<div>
<label class="text-muted small fw-semibold text-uppercase" style="font-size:.7rem">Jawapan</label>
<div class="p-3 border rounded mt-1" style="white-space:pre-wrap">{{ $knowledgeItem->content }}</div>
</div>
@else
<div style="white-space:pre-wrap">{{ $knowledgeItem->content }}</div>
@endif
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom"><h6 class="mb-0 fw-semibold">Maklumat</h6></div>
<div class="card-body small">
<dl class="row mb-0">
<dt class="col-5 text-muted">Bahasa</dt>
<dd class="col-7">{{ $knowledgeItem->language == 'ms' ? 'BM' : 'EN' }}</dd>
@if($knowledgeItem->effective_date)
<dt class="col-5 text-muted">Kuat Kuasa</dt>
<dd class="col-7">{{ $knowledgeItem->effective_date->format('d/m/Y') }}</dd>
@endif
<dt class="col-5 text-muted">Dicipta</dt>
<dd class="col-7">{{ $knowledgeItem->created_at->format('d/m/Y H:i') }}</dd>
@if($knowledgeItem->embedded_at)
<dt class="col-5 text-muted">Di-embed</dt>
<dd class="col-7">{{ $knowledgeItem->embedded_at->format('d/m/Y H:i') }}</dd>
@endif
@if($knowledgeItem->creator)
<dt class="col-5 text-muted">Dibuat oleh</dt>
<dd class="col-7">{{ $knowledgeItem->creator->name }}</dd>
@endif
<dt class="col-5 text-muted">Awam</dt>
<dd class="col-7">{{ $knowledgeItem->is_public ? 'Ya' : 'Tidak' }}</dd>
</dl>
@if($knowledgeItem->tags)
<div class="mt-2">
@foreach($knowledgeItem->tags as $tag)
<span class="badge bg-light text-dark border me-1">{{ $tag }}</span>
@endforeach
</div>
@endif
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Masuk 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, #1e293b 0%, #334155 100%);
min-height: 100vh;
display: flex;
align-items: center;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="text-center mb-4">
<i class="bi bi-database-fill-gear text-white fs-1"></i>
<h4 class="text-white mt-2 fw-bold">Pangkalan Pengetahuan</h4>
<p class="text-secondary">Panel Pentadbir</p>
</div>
<div class="card border-0 shadow-lg">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">Log Masuk</h5>
@if($errors->any())
<div class="alert alert-danger">
{{ $errors->first() }}
</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="mb-3">
<label class="form-label fw-semibold">E-mel</label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}" required autofocus autocomplete="email">
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Kata Laluan</label>
<input type="password" name="password" class="form-control"
required autocomplete="current-password">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="bi bi-box-arrow-in-right me-2"></i>Log Masuk
</button>
</form>
<div class="text-center mt-3">
<a href="{{ route('chatbot.index') }}" class="text-muted small">
<i class="bi bi-chat-dots me-1"></i>Pergi ke Chatbot Awam
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,402 @@
<!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>

View File

@@ -0,0 +1,195 @@
<!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>@yield('title', 'Panel Admin') Sistem Pangkalan Pengetahuan</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.sidebar {
min-height: 100vh;
background: #1e293b;
width: 260px;
position: fixed;
top: 0; left: 0;
z-index: 1000;
overflow-y: auto;
}
.sidebar .nav-link {
color: #94a3b8;
padding: .6rem 1.25rem;
border-radius: .375rem;
margin: 0 .5rem .125rem;
font-size: .9rem;
transition: all .15s;
}
.sidebar .nav-link:hover,
.sidebar .nav-link.active {
background: #334155;
color: #fff;
}
.sidebar .nav-link i { width: 1.25rem; }
.sidebar .nav-section {
color: #475569;
font-size: .7rem;
text-transform: uppercase;
letter-spacing: .05em;
padding: .75rem 1.25rem .25rem;
font-weight: 600;
}
.main-content {
margin-left: 260px;
min-height: 100vh;
}
.topbar {
background: #fff;
border-bottom: 1px solid #e2e8f0;
padding: .75rem 1.5rem;
position: sticky;
top: 0;
z-index: 999;
}
.badge-processing { background: #f59e0b; }
.badge-indexed { background: #10b981; }
.badge-failed { background: #ef4444; }
.badge-pending { background: #6b7280; }
.badge-inactive { background: #94a3b8; }
</style>
@stack('styles')
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar p-0">
<div class="p-3 border-bottom border-secondary">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-database-fill-gear text-primary fs-4"></i>
<div>
<div class="text-white fw-bold small">Pangkalan Pengetahuan</div>
<div class="text-muted" style="font-size:.7rem">Panel Admin</div>
</div>
</div>
</div>
<div class="pt-2 pb-4">
<div class="nav-section">Utama</div>
<a href="{{ route('admin.dashboard') }}"
class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-grid-1x2 me-2"></i>Dashboard
</a>
@if(auth()->user()->isAdmin())
<div class="nav-section mt-2">Kandungan</div>
<a href="{{ route('admin.categories.index') }}"
class="nav-link {{ request()->routeIs('admin.categories.*') ? 'active' : '' }}">
<i class="bi bi-folder2 me-2"></i>Kategori
</a>
@endif
<a href="{{ route('admin.documents.index') }}"
class="nav-link {{ request()->routeIs('admin.documents.*') ? 'active' : '' }}">
<i class="bi bi-file-pdf me-2"></i>Dokumen PDF
</a>
<a href="{{ route('admin.knowledge-items.index') }}"
class="nav-link {{ request()->routeIs('admin.knowledge-items.*') ? 'active' : '' }}">
<i class="bi bi-lightbulb me-2"></i>FAQ & Pengetahuan
</a>
<div class="nav-section mt-2">Chat & Audit</div>
<a href="{{ route('admin.chat-feedback.index') }}"
class="nav-link {{ request()->routeIs('admin.chat-feedback.*') ? 'active' : '' }}">
<i class="bi bi-chat-dots me-2"></i>Semakan Chat
@php $flagged = cache()->remember('flagged_chat_count', 60, fn() => \App\Models\ChatLog::where('is_flagged', true)->count()); @endphp
@if($flagged > 0)
<span class="badge bg-danger ms-1">{{ $flagged }}</span>
@endif
</a>
@if(auth()->user()->isAdmin())
<a href="{{ route('admin.audit-logs.index') }}"
class="nav-link {{ request()->routeIs('admin.audit-logs.*') ? 'active' : '' }}">
<i class="bi bi-journal-text me-2"></i>Log Audit
</a>
@endif
<div class="nav-section mt-2">Awam</div>
<a href="{{ route('chatbot.index') }}" target="_blank"
class="nav-link">
<i class="bi bi-robot me-2"></i>Buka Chatbot
<i class="bi bi-box-arrow-up-right ms-1 small"></i>
</a>
</div>
</nav>
<!-- Main Content -->
<div class="main-content">
<!-- Topbar -->
<div class="topbar d-flex align-items-center justify-content-between">
<div>
<nav aria-label="breadcrumb" class="mb-0">
<ol class="breadcrumb mb-0 small">
@yield('breadcrumb')
</ol>
</nav>
</div>
<div class="d-flex align-items-center gap-3">
<span class="small text-muted">
<i class="bi bi-person-circle me-1"></i>
{{ auth()->user()->name }}
<span class="badge bg-secondary ms-1">{{ auth()->user()->role_label }}</span>
</span>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-box-arrow-right"></i> Log Keluar
</button>
</form>
</div>
</div>
<!-- Page Content -->
<div class="p-4">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@yield('content')
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
@stack('scripts')
</body>
</html>

File diff suppressed because one or more lines are too long