user()?->can('manage_ai_agent') ?? false; } public function table(Table $table): Table { return $table ->records(function (array $filters, int $page, int $recordsPerPage): LengthAwarePaginator { $wasUsed = $filters['was_used']['value'] ?? null; $result = app(BnfexpressAdminClient::class)->listSuggestionMisses( wasUsed: $wasUsed === null || $wasUsed === '' ? null : (bool) $wasUsed, limit: $recordsPerPage, offset: ($page - 1) * $recordsPerPage, ); return $this->paginateBareList($result, 'misses', 'id', $page, $recordsPerPage); }) ->columns([ TextColumn::make('id'), TextColumn::make('text_norm') ->limit(80) ->wrap(), TextColumn::make('lang') ->placeholder('—'), TextColumn::make('syllables') ->placeholder('—'), IconColumn::make('was_used') ->boolean(), TextColumn::make('created_at') ->dateTime() ->sortable(), ]) ->defaultSort('created_at', 'desc') // SelectFilter (not TernaryFilter) so the value lands in // $filters['was_used']['value'] predictably — TernaryFilter's // internal field key isn't documented for the custom-data path. ->filters([ SelectFilter::make('was_used') ->label('Used?') ->options(['1' => 'Used', '0' => 'Not used']), ]) ->recordActions([ $this->dismissAction(), ]) ->toolbarActions([ $this->promoteBulkAction(), ]); } protected function dismissAction(): Action { return Action::make('dismiss') ->color('danger') ->icon(Heroicon::OutlinedTrash) ->requiresConfirmation() ->action(function (array $record): void { $this->callBnfexpress( fn () => app(BnfexpressAdminClient::class)->dismissSuggestionMiss($record['id']), successTitle: 'Miss dismissed', failureTitle: 'Failed to dismiss miss', ); $this->resetTable(); }); } /** * Opens the same "bulk add" modal shape as ManageSuggestions' * createManyAction() (info text + editable items textarea + lang/intent), * prefilled with the selected misses' own text — one per line — so an * admin can review/tweak the batch before it's created. Submits via * batchCreateSuggestions() (same endpoint createManyAction uses, and per * its own comment, bnfexpress's thin wrapper around the misses-promotion * promote()), not promoteSuggestionMisses() — editing the textarea means * the created rows no longer map 1:1 to the original miss ids. Once that * succeeds, the selected misses are deleted via batchDeleteSuggestionMisses() * so they don't linger in this list now that they're suggestions — best * effort, since editing the textarea already broke the id mapping and the * promotion itself is done by that point either way. */ protected function promoteBulkAction(): BulkAction { return BulkAction::make('promote') ->label('Promote Selected') ->icon(Heroicon::OutlinedArrowUp) ->fillForm(fn (Collection $records): array => [ 'items_raw' => $records->pluck('text_norm')->implode("\n"), 'lang' => $records->pluck('lang')->filter()->unique()->count() === 1 ? $records->pluck('lang')->filter()->first() : null, ]) ->schema([ Text::make('Created rows are tagged source: mined by bnfexpress.') ->color('gray'), Textarea::make('items_raw') ->label('Phrases (one per line)') ->required() ->rows(8), TextInput::make('lang') ->required() ->maxLength(10), TextInput::make('intent'), ]) ->deselectRecordsAfterCompletion() ->action(function (array $data, Collection $records): void { $items = collect(preg_split('/\r\n|\r|\n/', (string) $data['items_raw'])) ->map(fn (string $line): string => trim($line)) ->filter() ->map(fn (string $text): array => array_filter([ 'text' => $text, 'lang' => $data['lang'], 'intent' => $data['intent'] ?: null, ], fn (mixed $value): bool => $value !== null)) ->values() ->all(); $this->callBnfexpressForResult( fn () => app(BnfexpressAdminClient::class)->batchCreateSuggestions($items), function (array $result) use ($records): void { Notification::make() ->title("{$result['created']} promoted, {$result['skipped']} skipped") ->success() ->send(); try { app(BnfexpressAdminClient::class)->batchDeleteSuggestionMisses($records->keys()->all()); } catch (BnfexpressApiException $exception) { Notification::make() ->title('Promoted, but failed to remove the original misses') ->body($exception->getMessage()) ->warning() ->send(); } $this->resetTable(); }, failureTitle: 'Failed to promote suggestion misses', ); }); } }