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()); } } }