diff --git a/src/AI.php b/src/AI.php index 3961c05..06a1c95 100644 --- a/src/AI.php +++ b/src/AI.php @@ -2,60 +2,69 @@ namespace Backstage\AI; +use Backstage\AI\Prism\SystemMessages\Forms\Components\BaseInstructions; +use Backstage\AI\Prism\SystemMessages\Forms\Components\DateTimePicker; +use Backstage\AI\Prism\SystemMessages\Forms\Components\MarkdownEditor; +use Backstage\AI\Prism\SystemMessages\Forms\Components\RichEditor; +use Backstage\AI\Prism\SystemMessages\Forms\Components\Select; +use Backstage\AI\Prism\SystemMessages\Forms\Components\TextInput; +use Filament\Forms; use Filament\Forms\Components\Actions\Action; -use Filament\Forms\Components\Field; -use Filament\Forms\Components\Section; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Notifications\Notification; +use Prism\Prism\Enums\Provider; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Prism; class AI { - public static function registerMacro(): void + public static function registerFormMacro(): void { - Field::macro('withAI', function ($prompt = null) { - return $this->hintAction( - function (Set $set, Field $component) use ($prompt) { + Forms\Components\Field::macro('withAI', function ($prompt = null, $hint = true) { + return $this->{$hint ? 'hintAction' : 'suffixAction'}( + function (Set $set, Forms\Components\Field $component) use ($prompt, $hint) { return Action::make('ai') + ->visible(fn($operation) => $operation !== 'view') ->icon(config('backstage.ai.action.icon')) ->label(config('backstage.ai.action.label')) ->modalHeading(config('backstage.ai.action.modal.heading')) - ->modalSubmitActionLabel('Generate') + ->modalSubmitActionLabel(__('Generate')) + ->extraAttributes($hint ? [ + 'x-show' => 'focused || hover', + 'x-cloak' => '', + ] : []) ->form([ - Select::make('model') - ->label('Model') + Forms\Components\Select::make('model') + ->label(__('AI Model')) ->options( collect(config('backstage.ai.providers')) - ->mapWithKeys(fn ($provider, $model) => [ + ->mapWithKeys(fn(Provider $provider, $model) => [ $model => $model . ' (' . $provider->name . ')', ]), ) ->default(key(config('backstage.ai.providers'))), - Textarea::make('prompt') - ->label('Prompt') + Forms\Components\Textarea::make('prompt') + ->label(__('Instructions')) ->autosize() ->default($prompt), - Section::make('configuration') - ->heading('Configuration') + Forms\Components\Section::make('configuration') + ->heading(__('Configuration')) ->schema([ - TextInput::make('temperature') + Forms\Components\TextInput::make('temperature') ->numeric() - ->label('Temperature') + ->label(__('AI Temperature')) ->default(config('backstage.ai.configuration.temperature')) ->helperText('The higher the temperature, the more creative the text') ->maxValue(1) ->minValue(0) ->step('0.1'), - TextInput::make('max_tokens') + + Forms\Components\TextInput::make('max_tokens') ->numeric() - ->label('Max tokens') + ->label(__('Max Tokens')) ->default(config('backstage.ai.configuration.max_tokens')) ->helperText('The maximum number of tokens to generate') ->step('10') @@ -63,7 +72,7 @@ function (Set $set, Field $component) use ($prompt) { ->suffixAction( Action::make('increase') ->icon('heroicon-o-plus') - ->action(fn (Set $set, Get $get) => $set('max_tokens', $get('max_tokens') + 100)), + ->action(fn(Set $set, Get $get) => $set('max_tokens', $get('max_tokens') + 100)), ), ]) ->columns(2) @@ -71,12 +80,27 @@ function (Set $set, Field $component) use ($prompt) { ->collapsible(), ]) ->action(function ($data) use ($component, $set) { + $systemPrompts = AI::getSystemPrompts($component); + try { $response = Prism::text() ->using(config('backstage.ai.providers.' . $data['model']), $data['model']) ->withPrompt($data['prompt']) + ->withSystemPrompts($systemPrompts) ->asText(); + $fieldState = $component->getState(); + + if ($fieldState === $response->text) { + Notification::make() + ->title(__('AI generated text is the same as the current state')) + ->body(__('Please be more specific with your prompt or try again.')) + ->danger() + ->send(); + + return; + } + $set($component->getName(), $response->text); } catch (PrismException $exception) { Notification::make() @@ -87,7 +111,41 @@ function (Set $set, Field $component) use ($prompt) { } }); } - ); + ) + ->extraFieldWrapperAttributes([ + 'x-data' => '{focused: false, hover: false}', + 'x-on:mouseover' => 'hover = true', + 'x-on:mouseout' => 'hover = false', + ])->extraInputAttributes([ + 'x-on:focus' => 'focused = true', + 'x-on:blur' => 'focused = false', + ]); }); } + + /** + * Checking the type of the component and returning the specific instructions for each type. + * Allowed types are: + * + * @var Forms\Components\RichEditor + * @var Forms\Components\MarkdownEditor + * @var Forms\Components\DateTimePicker + * @var Forms\Components\TextInput + * @var Forms\Components\Select + */ + public static function getSystemPrompts(Forms\Components\Field $component): array + { + $baseInstructions = BaseInstructions::ask($component); + + $componentInstructions = match (true) { + $component instanceof Forms\Components\RichEditor => RichEditor::ask($component), + $component instanceof Forms\Components\MarkdownEditor => MarkdownEditor::ask($component), + $component instanceof Forms\Components\DateTimePicker => DateTimePicker::ask($component), + $component instanceof Forms\Components\TextInput => TextInput::ask($component), + $component instanceof Forms\Components\Select => Select::ask($component), + default => [], + }; + + return array_merge($baseInstructions, $componentInstructions); + } } diff --git a/src/AIServiceProvider.php b/src/AIServiceProvider.php index 0bb244e..9ef864a 100644 --- a/src/AIServiceProvider.php +++ b/src/AIServiceProvider.php @@ -32,7 +32,7 @@ public function configurePackage(Package $package): void public function packageBooted(): void { - AI::registerMacro(); + AI::registerFormMacro(); } /** diff --git a/src/Prism/SystemMessages/Forms/Components/BaseInstructions.php b/src/Prism/SystemMessages/Forms/Components/BaseInstructions.php new file mode 100644 index 0000000..b436dc2 --- /dev/null +++ b/src/Prism/SystemMessages/Forms/Components/BaseInstructions.php @@ -0,0 +1,19 @@ +getState())), + new SystemMessage('You must only return the value of the field.'), + new SystemMessage('No yapping, no explanations, no extra text.'), + ]; + + return $baseInstructions; + } +} diff --git a/src/Prism/SystemMessages/Forms/Components/DateTimePicker.php b/src/Prism/SystemMessages/Forms/Components/DateTimePicker.php new file mode 100644 index 0000000..36a3486 --- /dev/null +++ b/src/Prism/SystemMessages/Forms/Components/DateTimePicker.php @@ -0,0 +1,20 @@ +getFormat(); + + $instructions = [ + new SystemMessage('You must return a date as output.'), + new SystemMessage('The date format is: ' . $format), + ]; + + return $instructions; + } +} diff --git a/src/Prism/SystemMessages/Forms/Components/MarkdownEditor.php b/src/Prism/SystemMessages/Forms/Components/MarkdownEditor.php new file mode 100644 index 0000000..dd652d7 --- /dev/null +++ b/src/Prism/SystemMessages/Forms/Components/MarkdownEditor.php @@ -0,0 +1,18 @@ + tags.'), + ]; + + return $instructions; + } +} diff --git a/src/Prism/SystemMessages/Forms/Components/Select.php b/src/Prism/SystemMessages/Forms/Components/Select.php new file mode 100644 index 0000000..22c0f55 --- /dev/null +++ b/src/Prism/SystemMessages/Forms/Components/Select.php @@ -0,0 +1,19 @@ +getOptions())), + new SystemMessage('You must return the key of the option as output.'), + ]; + + return $instructions; + } +} diff --git a/src/Prism/SystemMessages/Forms/Components/TextInput.php b/src/Prism/SystemMessages/Forms/Components/TextInput.php new file mode 100644 index 0000000..74e0913 --- /dev/null +++ b/src/Prism/SystemMessages/Forms/Components/TextInput.php @@ -0,0 +1,27 @@ +isPassword()) { + $instructions = [ + new SystemMessage('You must return a password as output.'), + ]; + } + + if ($component->isEmail()) { + $instructions = [ + new SystemMessage('You must return an email as output.'), + ]; + } + + return $instructions; + } +}