Files
ChatbotAI/app/Http/Controllers/Admin/ChunkReviewController.php
2026-05-18 08:56:23 +08:00

272 lines
8.7 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\SplitChunkRequest;
use App\Http\Requests\Admin\UpdateChunkRequest;
use App\Models\Document;
use App\Models\DocumentChunk;
use App\Models\DocumentVersion;
use App\Services\Document\ChunkEditingService;
use App\Services\Document\ChunkSplitService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use InvalidArgumentException;
use RuntimeException;
/**
* ChunkReviewController
*
* Menguruskan UI dan operasi Chunk Review & Editing:
* - index() → Senarai chunk per versi (enhanced list dengan status filter)
* - show() → Detail satu chunk + preview 3 teks + form edit
* - update() → Simpan final_text yang diedit
* - exclude() → Kecualikan chunk dari indexing
* - include() → Kembalikan chunk ke indexing
* - reindex() → Trigger reindex manual
* - splitForm() → Form split chunk
* - doSplit() → Jalankan split
*
* NOTA: Hanya admin yang boleh akses (dikuatkuasakan di routes via 'role:admin').
*/
class ChunkReviewController extends Controller
{
public function __construct(
private readonly ChunkEditingService $editor,
private readonly ChunkSplitService $splitter,
) {}
// =========================================================================
// LIST VIEW
// =========================================================================
/**
* Senarai chunk untuk satu versi dokumen.
* Menggantikan DocumentController::chunks().
*
* Route: GET /admin/documents/{document}/versions/{version}/chunks
*/
public function index(Document $document, DocumentVersion $version): View
{
abort_if($version->document_id !== $document->id, 404);
$statusFilter = request('status');
$query = $version->chunks()
->with(['editor', 'parentChunk', 'childChunks'])
->withCount('audits')
->orderBy('chunk_index');
if ($statusFilter) {
$query->where('chunk_status', $statusFilter);
}
$chunks = $query->paginate(20)->withQueryString();
// Bilangan chunk mengikut status untuk filter pills
$statusCounts = $version->chunks()
->selectRaw('chunk_status, count(*) as total')
->groupBy('chunk_status')
->pluck('total', 'chunk_status')
->toArray();
$allStatuses = DocumentChunk::STATUS_PENDING === 'pending'
? [
DocumentChunk::STATUS_PENDING,
DocumentChunk::STATUS_INDEXED,
DocumentChunk::STATUS_NEEDS_REVIEW,
DocumentChunk::STATUS_NEEDS_REINDEX,
DocumentChunk::STATUS_EXCLUDED,
DocumentChunk::STATUS_SUPERSEDED,
DocumentChunk::STATUS_FAILED_EMBEDDING,
]
: [];
return view('admin.documents.chunks', compact(
'document',
'version',
'chunks',
'statusCounts',
'statusFilter'
));
}
// =========================================================================
// DETAIL + EDIT VIEW
// =========================================================================
/**
* Detail satu chunk: preview raw/cleaned/final text + form edit + audit trail.
*
* Route: GET /admin/chunks/{chunk}
*/
public function show(DocumentChunk $chunk): View
{
$chunk->load([
'document.category',
'documentVersion',
'editor',
'parentChunk',
'childChunks.editor',
'audits' => fn($q) => $q->with('user')->limit(10),
]);
return view('admin.chunks.show', compact('chunk'));
}
/**
* Simpan perubahan final_text.
*
* Route: PATCH /admin/chunks/{chunk}
*/
public function update(UpdateChunkRequest $request, DocumentChunk $chunk): RedirectResponse
{
try {
$this->editor->editFinalText(
$chunk,
$request->validated('final_text'),
$request->validated('notes')
);
return redirect()
->route('admin.chunks.show', $chunk)
->with('success', 'final_text berjaya disimpan. Reindex sedang diantrikan dalam queue.');
} catch (RuntimeException $e) {
return back()->withInput()->with('error', $e->getMessage());
}
}
// =========================================================================
// EXCLUDE / INCLUDE
// =========================================================================
/**
* Kecualikan chunk dari indexing.
*
* Route: POST /admin/chunks/{chunk}/exclude
*/
public function exclude(Request $request, DocumentChunk $chunk): RedirectResponse
{
$request->validate(['notes' => ['nullable', 'string', 'max:500']]);
try {
$this->editor->excludeChunk($chunk, $request->input('notes'));
return back()->with(
'success',
"Chunk #{$chunk->chunk_index} berjaya dikecualikan dari indexing."
);
} catch (RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
}
/**
* Kembalikan chunk ke indexing.
*
* Route: POST /admin/chunks/{chunk}/include
*/
public function include(Request $request, DocumentChunk $chunk): RedirectResponse
{
$request->validate(['notes' => ['nullable', 'string', 'max:500']]);
try {
$this->editor->includeChunk($chunk, $request->input('notes'));
return back()->with(
'success',
"Chunk #{$chunk->chunk_index} berjaya dikembalikan ke indexing."
);
} catch (RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
}
// =========================================================================
// REINDEX
// =========================================================================
/**
* Trigger reindex manual untuk satu chunk.
*
* Route: POST /admin/chunks/{chunk}/reindex
*/
public function reindex(Request $request, DocumentChunk $chunk): RedirectResponse
{
$request->validate(['notes' => ['nullable', 'string', 'max:500']]);
try {
$this->editor->triggerReindex($chunk, $request->input('notes'));
return back()->with(
'success',
"Chunk #{$chunk->chunk_index} sedang diantrikan untuk reindex."
);
} catch (RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
}
// =========================================================================
// SPLIT
// =========================================================================
/**
* Form split chunk.
*
* Route: GET /admin/chunks/{chunk}/split
*/
public function splitForm(DocumentChunk $chunk): View
{
if ($chunk->isSuperseded()) {
abort(403, 'Chunk yang telah digantikan tidak boleh di-split semula.');
}
if ($chunk->chunk_status === DocumentChunk::STATUS_EXCLUDED) {
abort(403, 'Chunk yang dikecualikan tidak boleh di-split. Include semula dahulu.');
}
$chunk->load(['document', 'documentVersion', 'childChunks']);
return view('admin.chunks.split', compact('chunk'));
}
/**
* Jalankan split chunk.
*
* Route: POST /admin/chunks/{chunk}/split
*/
public function doSplit(SplitChunkRequest $request, DocumentChunk $chunk): RedirectResponse
{
if ($chunk->isSuperseded()) {
return back()->with('error', 'Chunk yang telah digantikan tidak boleh di-split.');
}
try {
$children = $this->splitter->split(
$chunk,
$request->validated('segments'),
$request->validated('notes')
);
return redirect()
->route('admin.documents.chunks', [
'document' => $chunk->document_id,
'version' => $chunk->document_version_id,
])
->with(
'success',
"Chunk #{$chunk->chunk_index} berjaya di-split kepada "
. count($children)
. " chunk baharu. Reindex sedang dijalankan dalam queue."
);
} catch (InvalidArgumentException $e) {
return back()->withInput()->with('error', $e->getMessage());
} catch (RuntimeException $e) {
return back()->withInput()->with('error', $e->getMessage());
}
}
}