diff --git a/app/Api/Plugins/ApiPluginInterface.php b/app/Api/Plugins/ApiPluginInterface.php index 55f40b0..dd0cbdd 100644 --- a/app/Api/Plugins/ApiPluginInterface.php +++ b/app/Api/Plugins/ApiPluginInterface.php @@ -13,4 +13,5 @@ interface ApiPluginInterface public function getProgress(string $imageUUID): array; public function processImageStyleChange(\App\Models\Image $image, \App\Models\Style $style): array; public function testConnection(array $data): bool; + public function searchModels(string $searchTerm): array; } \ No newline at end of file diff --git a/app/Api/Plugins/ComfyUi.php b/app/Api/Plugins/ComfyUi.php index cbc754b..2a44691 100644 --- a/app/Api/Plugins/ComfyUi.php +++ b/app/Api/Plugins/ComfyUi.php @@ -239,4 +239,10 @@ class ComfyUi implements ApiPluginInterface return false; } } + + public function searchModels(string $searchTerm): array + { + $this->logInfo('ComfyUI does not support model search. Returning empty list.', ['searchTerm' => $searchTerm]); + return []; + } } \ No newline at end of file diff --git a/app/Api/Plugins/RunwareAi.php b/app/Api/Plugins/RunwareAi.php index d5711b3..c0f584e 100644 --- a/app/Api/Plugins/RunwareAi.php +++ b/app/Api/Plugins/RunwareAi.php @@ -106,10 +106,9 @@ class RunwareAi implements ApiPluginInterface 'Content-Type' => 'application/json', 'Accept' => 'application/json', ])->timeout(5)->post($apiUrl, [ - [ 'taskType' => 'authentication', 'apiKey' => $token, - ] + 'taskUUID' => (string) Str::uuid(), ]); $responseData = $response->json(); @@ -135,6 +134,62 @@ class RunwareAi implements ApiPluginInterface } } + + public function searchModels(string $searchTerm): array + { + $this->logInfo('Attempting model search on RunwareAI.', ['searchTerm' => $searchTerm]); + if (!$this->apiProvider->api_url || !$this->apiProvider->token) { + $this->logError('RunwareAI API URL or Token not configured for model search.', ['provider_name' => $this->apiProvider->name]); + return []; + } + + $apiUrl = rtrim($this->apiProvider->api_url, '/'); + $token = $this->apiProvider->token; + + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . $token, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ])->timeout(10)->post($apiUrl, [ + [ + 'taskType' => 'modelSearch', + 'search' => $searchTerm, + 'type' => 'base', + 'category' => 'checkpoint', + 'limit' => 100, + 'taskUUID' => (string) Str::uuid(), + ] + ]); + + $responseData = $response->json(); + + if ($response->successful() && isset($responseData['data'][0]['results']) && !isset($responseData['error'])) { + $models = []; + foreach ($responseData['data'][0]['results'] as $model) { + $models[] = [ + 'name' => $model['name'] ?? 'Unknown', + 'id' => $model['air'] ?? 'Unknown', + 'type' => $model['type'] ?? null, + ]; + } + $this->logInfo('Model search successful on RunwareAI.', ['searchTerm' => $searchTerm, 'modelsFound' => count($models)]); + return $models; + } else { + $errorMessage = $responseData['error'] ?? 'Unknown error'; + $this->logError('Model search failed on RunwareAI: Unsuccessful response.', [ + 'status' => $response->status(), + 'response' => $responseData, + 'error_message' => $errorMessage, + ]); + return []; + } + } catch (\Exception $e) { + $this->logError('Model search failed on RunwareAI: Exception caught.', ['error' => $e->getMessage()]); + return []; + } + } + private function upload(string $imagePath): array { $this->logInfo('Attempting to upload image to RunwareAI.', ['image_path' => $imagePath]); diff --git a/app/Filament/Resources/AiModelResource.php b/app/Filament/Resources/AiModelResource.php index 3e803d1..b4b6d3a 100644 --- a/app/Filament/Resources/AiModelResource.php +++ b/app/Filament/Resources/AiModelResource.php @@ -16,6 +16,9 @@ use Filament\Forms\Components\TextInput; use Filament\Tables\Columns\TextColumn; use Filament\Forms\Components\Select; use Filament\Tables\Actions\Action; +use App\Models\ApiProvider; +use App\Api\Plugins\PluginLoader; +use App\Api\Plugins\ApiPluginInterface; class AiModelResource extends Resource { @@ -27,35 +30,114 @@ class AiModelResource extends Resource { return $form ->schema([ - TextInput::make('name') - ->label(__('filament.resource.ai_model.form.name')) - ->required() - ->maxLength(255), - TextInput::make('model_id') - ->label(__('filament.resource.ai_model.form.model_id')) - ->required() - ->maxLength(255), - TextInput::make('model_type') - ->nullable() - ->maxLength(255), - Forms\Components\Toggle::make('enabled') - ->label(__('filament.resource.ai_model.form.enabled')) - ->default(true), - Select::make('apiProviders') - ->relationship('apiProviders', 'name') - ->multiple() - ->preload() - ->searchable(false) - ->label(__('filament.resource.ai_model.form.api_providers')), - Forms\Components\Textarea::make('parameters') - ->label(__('filament.resource.ai_model.form.parameters')) - ->nullable() - ->rows(15) - ->json(JSON_PRETTY_PRINT) - ->helperText(__('filament.resource.ai_model.form.parameters_help')) + Forms\Components\Section::make() + ->schema([ + Select::make('api_provider_id') + ->label(__('filament.resource.ai_model.form.api_provider')) + ->relationship('apiProviders', 'name') + ->live() + ->nullable() + ->afterStateUpdated(function (callable $set) { + $set('model_search_result', null); + }), + Select::make('model_search_result') + ->label(__('filament.resource.ai_model.form.search_model')) + ->searchable() + ->live() + ->hidden(fn (callable $get) => !static::canSearchModels($get('api_provider_id'))) + ->getSearchResultsUsing(function (string $search, callable $get) { + $apiProviderId = $get('api_provider_id'); + if (!$apiProviderId) { + return []; + } + $pluginInstance = static::getPluginInstance($apiProviderId); + if ($pluginInstance && method_exists($pluginInstance, 'searchModels')) { + $models = $pluginInstance->searchModels($search); + $options = []; + foreach ($models as $model) { + $options[json_encode(['name' => $model['name'], 'id' => $model['id'], 'type' => $model['type'] ?? null])] = $model['name'] . ' (' . $model['id'] . ')'; + } + return $options; + } + return []; + }) + ->getOptionLabelUsing(function ($value) { + $decoded = json_decode($value, true); + return $decoded['name'] . ' (' . $decoded['id'] . ')'; + }) + ->afterStateUpdated(function (callable $set, $state) { + if ($state) { + $selectedModel = json_decode($state, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return; // Stop if JSON is invalid + } + $set('name', $selectedModel['name']); + $set('model_id', $selectedModel['id']); + $set('model_type', $selectedModel['type'] ?? null); + } + }), + TextInput::make('name') + ->label(__('filament.resource.ai_model.form.name')) + ->required() + ->maxLength(255) + ->live(), + TextInput::make('model_id') + ->label(__('filament.resource.ai_model.form.model_id')) + ->required() + ->maxLength(255) + ->live(), + TextInput::make('model_type') + ->nullable() + ->maxLength(255) + ->live(), + Forms\Components\Toggle::make('enabled') + ->label(__('filament.resource.ai_model.form.enabled')) + ->default(true), + Forms\Components\Textarea::make('parameters') + ->label(__('filament.resource.ai_model.form.parameters')) + ->nullable() + ->rows(15) + ->json(JSON_PRETTY_PRINT) + ->helperText(__('filament.resource.ai_model.form.parameters_help')) + ]) ]); } + protected static function canSearchModels(?int $apiProviderId): bool + { + if (!$apiProviderId) { + return false; + } + $apiProvider = ApiProvider::find($apiProviderId); + if (!$apiProvider || !$apiProvider->plugin) { + return false; + } + try { + $pluginInstance = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider); + return method_exists($pluginInstance, 'searchModels'); + } catch (\Exception $e) { + // Log the exception if needed + return false; + } + } + + protected static function getPluginInstance(?int $apiProviderId): ?ApiPluginInterface + { + if (!$apiProviderId) { + return null; + } + $apiProvider = ApiProvider::find($apiProviderId); + if (!$apiProvider || !$apiProvider->plugin) { + return null; + } + try { + return PluginLoader::getPlugin($apiProvider->plugin, $apiProvider); + } catch (\Exception $e) { + // Log the exception if needed + return null; + } + } + public static function table(Table $table): Table { return $table diff --git a/app/Filament/Widgets/AppStatsOverview.php b/app/Filament/Widgets/AppStatsOverview.php new file mode 100644 index 0000000..776285d --- /dev/null +++ b/app/Filament/Widgets/AppStatsOverview.php @@ -0,0 +1,27 @@ +icon('heroicon-o-server') + ->url(route('filament.admin.resources.ai-models.index')), + Stat::make('Anzahl API-Provider', ApiProvider::count()) + ->icon('heroicon-o-cube') + ->url(route('filament.admin.resources.api-providers.index')), + Stat::make('Anzahl Styles', Style::count()) + ->icon('heroicon-o-sparkles') + ->url(route('filament.admin.resources.styles.index')), + ]; + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 532fb93..ccd5993 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -20,6 +20,7 @@ use Illuminate\View\Middleware\ShareErrorsFromSession; use App\Filament\Resources\StyleResource; use App\Filament\Resources\SettingResource\Pages\Settings; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\HtmlString; class AdminPanelProvider extends PanelProvider { @@ -30,6 +31,10 @@ class AdminPanelProvider extends PanelProvider ->id('admin') ->path('admin') ->login() + ->brandLogo(fn () => new HtmlString( + 'App Icon' . + '' . config('app.name') . '' + )) ->colors([ 'primary' => Color::Amber, ]) @@ -47,6 +52,7 @@ class AdminPanelProvider extends PanelProvider ->widgets([ Widgets\AccountWidget::class, Widgets\FilamentInfoWidget::class, + \App\Filament\Widgets\AppStatsOverview::class, ]) ->middleware([ EncryptCookies::class, @@ -68,7 +74,7 @@ class AdminPanelProvider extends PanelProvider ->profile(); if (Auth::check()) { - $user = Auth::user(); + $user = Auth->user(); if ($user->theme_preference === 'dark') { $panel->darkMode(); } else { diff --git a/config/filament.php b/config/filament.php new file mode 100644 index 0000000..1dc656c --- /dev/null +++ b/config/filament.php @@ -0,0 +1,101 @@ + [ + + // 'echo' => [ + // 'broadcaster' => 'pusher', + // 'key' => env('VITE_PUSHER_APP_KEY'), + // 'cluster' => env('VITE_PUSHER_APP_CLUSTER'), + // 'wsHost' => env('VITE_PUSHER_HOST'), + // 'wsPort' => env('VITE_PUSHER_PORT'), + // 'wssPort' => env('VITE_PUSHER_PORT'), + // 'authEndpoint' => '/broadcasting/auth', + // 'disableStats' => true, + // 'encrypted' => true, + // 'forceTLS' => true, + // ], + + ], + + /* + |-------------------------------------------------------------------------- + | Default Filesystem Disk + |-------------------------------------------------------------------------- + | + | This is the storage disk Filament will use to store files. You may use + | any of the disks defined in the `config/filesystems.php`. + | + */ + + 'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'), + + /* + |-------------------------------------------------------------------------- + | Assets Path + |-------------------------------------------------------------------------- + | + | This is the directory where Filament's assets will be published to. It + | is relative to the `public` directory of your Laravel application. + | + | After changing the path, you should run `php artisan filament:assets`. + | + */ + + 'assets_path' => null, + + /* + |-------------------------------------------------------------------------- + | Cache Path + |-------------------------------------------------------------------------- + | + | This is the directory that Filament will use to store cache files that + | are used to optimize the registration of components. + | + | After changing the path, you should run `php artisan filament:cache-components`. + | + */ + + 'cache_path' => base_path('bootstrap/cache/filament'), + + /* + |-------------------------------------------------------------------------- + | Livewire Loading Delay + |-------------------------------------------------------------------------- + | + | This sets the delay before loading indicators appear. + | + | Setting this to 'none' makes indicators appear immediately, which can be + | desirable for high-latency connections. Setting it to 'default' applies + | Livewire's standard 200ms delay. + | + */ + + 'livewire_loading_delay' => 'default', + + /* + |-------------------------------------------------------------------------- + | System Route Prefix + |-------------------------------------------------------------------------- + | + | This is the prefix used for the system routes that Filament registers, + | such as the routes for downloading exports and failed import rows. + | + */ + + 'system_route_prefix' => 'filament', + +]; diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..ca04e6d Binary files /dev/null and b/public/icon.png differ diff --git a/resources/css/filament.css b/resources/css/filament.css new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/Pages/Home.vue b/resources/js/Pages/Home.vue index c74dc26..b6bb753 100644 --- a/resources/js/Pages/Home.vue +++ b/resources/js/Pages/Home.vue @@ -1,4 +1,5 @@