Files
fotospiel-app/app/Filament/Resources/AiStyles/AiStyleResource.php
Codex Agent 6cc463fc70
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
tests / ui (push) Waiting to run
feat(ai): add runware model search and model-constrained img2img
2026-02-07 21:16:28 +01:00

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) : '?'
);
}
}