306 lines
12 KiB
PHP
306 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources\AiStyles;
|
|
|
|
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
|
|
use App\Filament\Resources\AiStyles\Pages\ManageAiStyles;
|
|
use App\Models\AiStyle;
|
|
use App\Services\AiEditing\RunwareModelSearchService;
|
|
use App\Services\Audit\SuperAdminAuditLogger;
|
|
use BackedEnum;
|
|
use Filament\Actions;
|
|
use Filament\Forms\Components\KeyValue;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Schemas\Components\Utilities\Set;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use UnitEnum;
|
|
|
|
class AiStyleResource extends Resource
|
|
{
|
|
protected static ?string $model = AiStyle::class;
|
|
|
|
protected static ?string $cluster = RareAdminCluster::class;
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-paint-brush';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = null;
|
|
|
|
protected static ?int $navigationSort = 31;
|
|
|
|
public static function getNavigationGroup(): UnitEnum|string|null
|
|
{
|
|
return __('admin.nav.platform');
|
|
}
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return 'AI Styles';
|
|
}
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema->schema([
|
|
Section::make('Style Basics')
|
|
->schema([
|
|
TextInput::make('key')
|
|
->required()
|
|
->maxLength(120)
|
|
->unique(ignoreRecord: true),
|
|
TextInput::make('name')
|
|
->required()
|
|
->maxLength(120),
|
|
TextInput::make('version')
|
|
->numeric()
|
|
->default(1)
|
|
->disabled()
|
|
->dehydrated(false)
|
|
->helperText('Auto-increments when core style configuration changes.'),
|
|
TextInput::make('category')
|
|
->maxLength(50),
|
|
TextInput::make('sort')
|
|
->numeric()
|
|
->default(0)
|
|
->required(),
|
|
Toggle::make('is_active')
|
|
->default(true),
|
|
Toggle::make('is_premium')
|
|
->default(false),
|
|
Toggle::make('requires_source_image')
|
|
->default(true),
|
|
])
|
|
->columns(3),
|
|
Section::make('Provider Binding')
|
|
->schema([
|
|
Select::make('provider')
|
|
->options([
|
|
'runware' => 'runware.ai',
|
|
])
|
|
->required()
|
|
->default('runware'),
|
|
Select::make('provider_model')
|
|
->label('Runware model (AIR)')
|
|
->searchable()
|
|
->getSearchResultsUsing(static fn (string $search): array => app(RunwareModelSearchService::class)->searchOptions($search))
|
|
->getOptionLabelUsing(static fn (mixed $value): ?string => app(RunwareModelSearchService::class)->labelForModel($value))
|
|
->helperText('Start typing to search models from runware.ai.')
|
|
->native(false)
|
|
->live()
|
|
->afterStateUpdated(static function (Set $set, ?string $state): void {
|
|
self::applySelectedRunwareModel($set, $state);
|
|
}),
|
|
])
|
|
->columns(2),
|
|
Section::make('Runware Generation')
|
|
->schema([
|
|
TextInput::make('metadata.runware.generation.width')
|
|
->label('Width')
|
|
->numeric()
|
|
->minValue(64)
|
|
->maxValue(4096)
|
|
->step(64)
|
|
->helperText(static fn (Get $get): ?string => self::dimensionConstraintHint($get, 'width')),
|
|
TextInput::make('metadata.runware.generation.height')
|
|
->label('Height')
|
|
->numeric()
|
|
->minValue(64)
|
|
->maxValue(4096)
|
|
->step(64)
|
|
->helperText(static fn (Get $get): ?string => self::dimensionConstraintHint($get, 'height')),
|
|
TextInput::make('metadata.runware.generation.steps')
|
|
->label('Steps')
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(150)
|
|
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'steps')),
|
|
TextInput::make('metadata.runware.generation.cfg_scale')
|
|
->label('CFG Scale')
|
|
->numeric()
|
|
->minValue(0)
|
|
->maxValue(30)
|
|
->step(0.1)
|
|
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'cfg_scale')),
|
|
TextInput::make('metadata.runware.generation.strength')
|
|
->label('Strength')
|
|
->numeric()
|
|
->minValue(0)
|
|
->maxValue(1)
|
|
->step(0.01)
|
|
->helperText(static fn (Get $get): ?string => self::rangeConstraintHint($get, 'strength')),
|
|
Select::make('metadata.runware.generation.output_format')
|
|
->label('Output format')
|
|
->options([
|
|
'JPG' => 'JPG',
|
|
'PNG' => 'PNG',
|
|
'WEBP' => 'WEBP',
|
|
])
|
|
->default('JPG')
|
|
->native(false),
|
|
Select::make('metadata.runware.generation.delivery_method')
|
|
->label('Delivery method')
|
|
->options([
|
|
'async' => 'async (queue + poll)',
|
|
'sync' => 'sync',
|
|
])
|
|
->default('async')
|
|
->native(false),
|
|
])
|
|
->columns(3),
|
|
Section::make('Prompts')
|
|
->schema([
|
|
Textarea::make('description')
|
|
->rows(2),
|
|
Textarea::make('prompt_template')
|
|
->rows(5),
|
|
Textarea::make('negative_prompt_template')
|
|
->rows(4),
|
|
]),
|
|
Section::make('Metadata')
|
|
->schema([
|
|
KeyValue::make('metadata')
|
|
->nullable(),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->defaultSort('sort')
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('key')
|
|
->searchable()
|
|
->copyable(),
|
|
Tables\Columns\TextColumn::make('name')
|
|
->searchable(),
|
|
Tables\Columns\TextColumn::make('version')
|
|
->sortable()
|
|
->toggleable(),
|
|
Tables\Columns\TextColumn::make('provider')
|
|
->badge(),
|
|
Tables\Columns\TextColumn::make('provider_model')
|
|
->toggleable(),
|
|
Tables\Columns\IconColumn::make('is_active')
|
|
->boolean(),
|
|
Tables\Columns\IconColumn::make('is_premium')
|
|
->boolean(),
|
|
Tables\Columns\TextColumn::make('sort')
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('updated_at')
|
|
->since()
|
|
->toggleable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\TernaryFilter::make('is_active'),
|
|
Tables\Filters\TernaryFilter::make('is_premium'),
|
|
])
|
|
->actions([
|
|
Actions\EditAction::make()
|
|
->after(fn (array $data, AiStyle $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
|
|
'updated',
|
|
$record,
|
|
SuperAdminAuditLogger::fieldsMetadata(array_keys($data)),
|
|
static::class
|
|
)),
|
|
Actions\DeleteAction::make()
|
|
->after(fn (AiStyle $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
|
|
'deleted',
|
|
$record,
|
|
source: static::class
|
|
)),
|
|
])
|
|
->bulkActions([
|
|
Actions\DeleteBulkAction::make(),
|
|
]);
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => ManageAiStyles::route('/'),
|
|
];
|
|
}
|
|
|
|
private static function applySelectedRunwareModel(Set $set, ?string $air): void
|
|
{
|
|
if (! is_string($air) || trim($air) === '') {
|
|
return;
|
|
}
|
|
|
|
$model = app(RunwareModelSearchService::class)->findByAir($air);
|
|
if (! is_array($model)) {
|
|
return;
|
|
}
|
|
|
|
$set('metadata.runware.model.air', $model['air']);
|
|
$set('metadata.runware.model.name', $model['name']);
|
|
$set('metadata.runware.model.architecture', $model['architecture']);
|
|
$set('metadata.runware.model.category', $model['category']);
|
|
|
|
foreach ((array) ($model['constraints'] ?? []) as $key => $value) {
|
|
$set("metadata.runware.constraints.{$key}", $value);
|
|
}
|
|
|
|
self::setIfNumeric($set, 'metadata.runware.generation.width', $model['defaults']['width'] ?? null);
|
|
self::setIfNumeric($set, 'metadata.runware.generation.height', $model['defaults']['height'] ?? null);
|
|
self::setIfNumeric($set, 'metadata.runware.generation.steps', $model['defaults']['steps'] ?? null);
|
|
self::setIfNumeric($set, 'metadata.runware.generation.cfg_scale', $model['defaults']['cfg_scale'] ?? null);
|
|
}
|
|
|
|
private static function setIfNumeric(Set $set, string $path, mixed $value): void
|
|
{
|
|
if (is_numeric($value)) {
|
|
$set($path, $value);
|
|
}
|
|
}
|
|
|
|
private static function dimensionConstraintHint(Get $get, string $dimension): ?string
|
|
{
|
|
$min = $get("metadata.runware.constraints.min_{$dimension}");
|
|
$max = $get("metadata.runware.constraints.max_{$dimension}");
|
|
$step = $get("metadata.runware.constraints.{$dimension}_step");
|
|
|
|
if (! is_numeric($min) && ! is_numeric($max) && ! is_numeric($step)) {
|
|
return null;
|
|
}
|
|
|
|
$parts = [];
|
|
if (is_numeric($min) || is_numeric($max)) {
|
|
$parts[] = sprintf(
|
|
'Model range: %s - %s',
|
|
is_numeric($min) ? (string) (int) $min : '?',
|
|
is_numeric($max) ? (string) (int) $max : '?'
|
|
);
|
|
}
|
|
|
|
if (is_numeric($step) && (int) $step > 0) {
|
|
$parts[] = sprintf('Step: %d', (int) $step);
|
|
}
|
|
|
|
return $parts !== [] ? implode(' | ', $parts) : null;
|
|
}
|
|
|
|
private static function rangeConstraintHint(Get $get, string $field): ?string
|
|
{
|
|
$min = $get("metadata.runware.constraints.min_{$field}");
|
|
$max = $get("metadata.runware.constraints.max_{$field}");
|
|
|
|
if (! is_numeric($min) && ! is_numeric($max)) {
|
|
return null;
|
|
}
|
|
|
|
return sprintf(
|
|
'Model range: %s - %s',
|
|
is_numeric($min) ? trim((string) $min) : '?',
|
|
is_numeric($max) ? trim((string) $max) : '?'
|
|
);
|
|
}
|
|
}
|