diff --git a/app/Http/Controllers/Admin/CreationController.php b/app/Http/Controllers/Admin/CreationController.php index 93f4590310abaaf1fb1c4b4a49123722506eaa34..792b41cf0547811b75577778e475c6e0b8d6b052 100644 --- a/app/Http/Controllers/Admin/CreationController.php +++ b/app/Http/Controllers/Admin/CreationController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Jobs\TranslateCreationJob; use App\Models\Category; use App\Models\Creation; use App\Models\Translation; @@ -350,4 +351,15 @@ public function removeAdditionalImage(int $creationId, int $uploadedPictureId): 'success' => true, ]); } + + public function translateWithAi(int $creationId): JsonResponse + { + $creation = Creation::findOrFail($creationId); + $job = new TranslateCreationJob($creation); + dispatch($job); + + return response()->json([ + 'success' => true, + ]); + } } diff --git a/app/Jobs/TranslateCreationJob.php b/app/Jobs/TranslateCreationJob.php new file mode 100644 index 0000000000000000000000000000000000000000..062936cf27b2573e41a0bd77c7e5b687be8da602 --- /dev/null +++ b/app/Jobs/TranslateCreationJob.php @@ -0,0 +1,72 @@ +<?php + +namespace App\Jobs; + +use App\Models\Creation; +use App\Models\Translation; +use App\Services\AiProviderService; +use Exception; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; + +class TranslateCreationJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(private readonly Creation $creation) {} + + public function handle(AiProviderService $aiProviderService): void + { + $frenchShortDesc = $this->creation->shortDescriptionTranslationKey->getTranslation('fr'); + $frenchDesc = $this->creation->descriptionTranslationKey->getTranslation('fr'); + + $shortDescriptionTranslationKeyId = $this->creation->shortDescriptionTranslationKey->id; + $descriptionTranslationKeyId = $this->creation->descriptionTranslationKey->id; + + if (! empty($frenchShortDesc)) { + Translation::updateOrCreate( + ['translation_key_id' => $shortDescriptionTranslationKeyId, 'locale' => 'en'], + ['text' => $this->translate($frenchShortDesc, $aiProviderService)] + ); + Cache::forget("translation_key_{$shortDescriptionTranslationKeyId}_en"); + } + + if (! empty($frenchDesc)) { + Translation::updateOrCreate( + ['translation_key_id' => $descriptionTranslationKeyId, 'locale' => 'en'], + ['text' => $this->translate($frenchDesc, $aiProviderService)] + ); + Cache::forget("translation_key_{$descriptionTranslationKeyId}_en"); + } + } + + /** + * Translate the given text from French to English + * + * @param string $text The text to translate + * @param AiProviderService $aiProviderService The AI provider service + * @return string The translated text + */ + private function translate(string $text, AiProviderService $aiProviderService): string + { + try { + $result = $aiProviderService->prompt( + 'You are a helpful assistant that translates french markdown text in english and that outputs JSON in the format {message:string}. Markdown is supported.', + $text + ); + + return $result['message'] ?? ''; + } catch (Exception $e) { + Log::error("Failed to translate text: {$text}", [ + 'exception' => $e->getMessage(), + ]); + } + + return ''; + } +} diff --git a/resources/views/admin/creations/index.blade.php b/resources/views/admin/creations/index.blade.php index e13f5b4f9fcb48b70d6605515bbc45addf9ca8a3..c5e81d17f85c53b0cce028eff3327f3618c4f197 100644 --- a/resources/views/admin/creations/index.blade.php +++ b/resources/views/admin/creations/index.blade.php @@ -28,6 +28,11 @@ size="sm" tag="a" variant="danger"> Supprimer </x-bs.button> + <x-bs.button size="sm" variant="secondary" id="translate-{{ $creation->id }}" + onclick="translateWithAi({{ $creation->id }})"> + {{ \Spatie\Emoji\Emoji::flagsForFlagUnitedKingdom() }} + Traduire + </x-bs.button> </x-slot:footer> </x-bs.card> @endforeach @@ -38,3 +43,24 @@ @endif </div> @endsection + +@pushonce('scripts') + <script type="text/javascript"> + function translateWithAi(creationId) { + const button = document.querySelector(`#translate-${creationId}`); + button.disabled = true; + fetch(`/admin/creations/${creationId}/translate-with-ai`) + .then(response => response.json()) + .then(data => { + if (data.success) { + button.classList.remove('btn-secondary'); + button.classList.add('btn-success'); + } else { + button.disabled = false; + button.classList.remove('btn-secondary'); + button.classList.add('btn-warning'); + } + }); + } + </script> +@endpushonce diff --git a/routes/web.php b/routes/web.php index 805fcfe7583eb24bd485229e0da9aea18b81b56a..d006a190a22dd7a0926f7202c57c29cd08fbcdd9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -79,6 +79,8 @@ ->name('delete'); Route::delete('/{creation}/remove-addtionnal-image/{image}', [CreationController::class, 'removeAdditionalImage']) ->name('remove-additionnal-image'); + Route::get('/{creation}/translate-with-ai', [CreationController::class, 'translateWithAi']) + ->name('translate-with-ai'); }); Route::prefix('social-media-links')->name('social-media-links.')->group(function () {