2026-09-01 00:16:17 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\AiAgent\Filament\Pages;
|
|
|
|
|
|
|
|
|
|
use BackedEnum;
|
|
|
|
|
use Filament\Actions\Action;
|
|
|
|
|
use Filament\Actions\BulkAction;
|
2026-09-03 20:42:56 +07:00
|
|
|
use Filament\Forms\Components\Textarea;
|
2026-09-01 00:16:17 +07:00
|
|
|
use Filament\Forms\Components\TextInput;
|
|
|
|
|
use Filament\Notifications\Notification;
|
|
|
|
|
use Filament\Pages\Page;
|
2026-09-03 20:42:56 +07:00
|
|
|
use Filament\Schemas\Components\Text;
|
2026-09-01 00:16:17 +07:00
|
|
|
use Filament\Support\Icons\Heroicon;
|
|
|
|
|
use Filament\Tables\Columns\IconColumn;
|
|
|
|
|
use Filament\Tables\Columns\TextColumn;
|
|
|
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
|
|
|
use Filament\Tables\Contracts\HasTable;
|
|
|
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
|
|
|
use Filament\Tables\Table;
|
|
|
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
use Modules\AiAgent\Filament\Concerns\HandlesBnfexpressErrors;
|
|
|
|
|
use Modules\AiAgent\Filament\Concerns\PaginatesBnfexpressLists;
|
|
|
|
|
use Modules\Shared\Bnfexpress\BnfexpressAdminClient;
|
2026-09-03 20:42:56 +07:00
|
|
|
use Modules\Shared\Bnfexpress\Exceptions\BnfexpressApiException;
|
2026-09-01 00:16:17 +07:00
|
|
|
use UnitEnum;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Browse bnfexpress's "suggestion misses" — queries typed by real users that
|
|
|
|
|
* no suggestion tier answered — and either dismiss them (noise) or promote
|
|
|
|
|
* a batch straight into the suggestions bank. Read-mostly: no create/edit,
|
|
|
|
|
* these rows are only ever produced by bnfexpress's own suggest pipeline.
|
|
|
|
|
*/
|
|
|
|
|
class ManageSuggestionMisses extends Page implements HasTable
|
|
|
|
|
{
|
|
|
|
|
use HandlesBnfexpressErrors;
|
|
|
|
|
use InteractsWithTable;
|
|
|
|
|
use PaginatesBnfexpressLists;
|
|
|
|
|
|
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedMagnifyingGlassCircle;
|
|
|
|
|
|
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'AI Agent';
|
|
|
|
|
|
|
|
|
|
protected static ?string $navigationLabel = 'Suggestion Misses';
|
|
|
|
|
|
|
|
|
|
protected static ?string $title = 'Suggestion Misses';
|
|
|
|
|
|
|
|
|
|
protected string $view = 'ai-agent::filament.pages.manage-suggestion-misses';
|
|
|
|
|
|
|
|
|
|
public static function canAccess(): bool
|
|
|
|
|
{
|
|
|
|
|
return auth()->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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 20:42:56 +07:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2026-09-01 00:16:17 +07:00
|
|
|
protected function promoteBulkAction(): BulkAction
|
|
|
|
|
{
|
|
|
|
|
return BulkAction::make('promote')
|
|
|
|
|
->label('Promote Selected')
|
|
|
|
|
->icon(Heroicon::OutlinedArrowUp)
|
2026-09-03 20:42:56 +07:00
|
|
|
->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,
|
|
|
|
|
])
|
2026-09-01 00:16:17 +07:00
|
|
|
->schema([
|
2026-09-03 20:42:56 +07:00
|
|
|
Text::make('Created rows are tagged source: mined by bnfexpress.')
|
|
|
|
|
->color('gray'),
|
|
|
|
|
Textarea::make('items_raw')
|
|
|
|
|
->label('Phrases (one per line)')
|
|
|
|
|
->required()
|
|
|
|
|
->rows(8),
|
2026-09-01 00:16:17 +07:00
|
|
|
TextInput::make('lang')
|
2026-09-03 20:42:56 +07:00
|
|
|
->required()
|
2026-09-01 00:16:17 +07:00
|
|
|
->maxLength(10),
|
|
|
|
|
TextInput::make('intent'),
|
|
|
|
|
])
|
|
|
|
|
->deselectRecordsAfterCompletion()
|
|
|
|
|
->action(function (array $data, Collection $records): void {
|
2026-09-03 20:42:56 +07:00
|
|
|
$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();
|
|
|
|
|
|
2026-09-01 00:16:17 +07:00
|
|
|
$this->callBnfexpressForResult(
|
2026-09-03 20:42:56 +07:00
|
|
|
fn () => app(BnfexpressAdminClient::class)->batchCreateSuggestions($items),
|
|
|
|
|
function (array $result) use ($records): void {
|
2026-09-01 00:16:17 +07:00
|
|
|
Notification::make()
|
|
|
|
|
->title("{$result['created']} promoted, {$result['skipped']} skipped")
|
|
|
|
|
->success()
|
|
|
|
|
->send();
|
|
|
|
|
|
2026-09-03 20:42:56 +07:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 00:16:17 +07:00
|
|
|
$this->resetTable();
|
|
|
|
|
},
|
|
|
|
|
failureTitle: 'Failed to promote suggestion misses',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|