diff --git a/app/Enums/AiImageDeclaration.php b/app/Enums/AiImageDeclaration.php index 22c03742..0bfb7ffc 100644 --- a/app/Enums/AiImageDeclaration.php +++ b/app/Enums/AiImageDeclaration.php @@ -28,15 +28,16 @@ enum AiImageDeclaration: string } /** - * Öffentlicher Bildhinweis direkt am Bild — null bei Originalfotos - * (keine Nicht-KI-Zertifizierung in Portalstimme). + * Öffentlicher Bildhinweis direkt am Bild (Badge) — null bei Originalfotos + * (keine Nicht-KI-Zertifizierung in Portalstimme). Kurzform „KI-generiert" + * statt Satz-Wortlaut (User-Entscheidung 10.07.2026). */ public function publicLabel(): ?string { return match ($this) { self::Original => null, - self::AiGenerated => __('Bildhinweis: KI-generierte Illustration.'), - self::AiEdited => __('Bildhinweis: KI-bearbeitetes Bild.'), + self::AiGenerated => __('KI-generiert'), + self::AiEdited => __('KI-bearbeitet'), }; } diff --git a/app/Enums/AiTextDeclaration.php b/app/Enums/AiTextDeclaration.php index cd93346b..2f214ab0 100644 --- a/app/Enums/AiTextDeclaration.php +++ b/app/Enums/AiTextDeclaration.php @@ -5,10 +5,15 @@ namespace App\Enums; /** * Selbst-Deklaration des KI-Einsatzes beim PM-Text (Art. 50 Abs. 4 KI-VO). * - * Pflichtauswahl im Submit-Flow (Decision-Update KI-Kennzeichnung, 08.07.2026): - * gelabelt wird allein nach dem deklarierten KI-Grad, nicht nach der - * Public-Interest-Frage — bewusste Über-Erfüllung. Das öffentliche Label - * spricht in Einreicher-Stimme, nie in Portalstimme. + * Pflichtauswahl im Submit-Flow (Decision-Update KI-Kennzeichnung, 08.07.2026; + * vereinfacht auf zwei Auswahloptionen, 10.07.2026): gelabelt wird allein nach + * dem deklarierten KI-Grad, nicht nach der Public-Interest-Frage — bewusste + * Über-Erfüllung. Das öffentliche Label spricht in Einreicher-Stimme, nie in + * Portalstimme. + * + * Alle vier Werte bleiben gültig (API-Vertrag v1 + Bestandsdaten); UI und + * Label-Logik fassen sie zu zwei Gruppen zusammen: None/Assisted = kein Label, + * MostlyAi/FullyAi = KI-Label. */ enum AiTextDeclaration: string { @@ -18,15 +23,13 @@ enum AiTextDeclaration: string case FullyAi = 'fully_ai'; /** - * Wortlaut der Auswahloption im Submit-Flow. + * Wortlaut der Auswahloption im Submit-Flow (zwei Gruppen). */ public function label(): string { return match ($this) { - self::None => __('Ohne generative KI erstellt'), - self::Assisted => __('Mit KI als Formulierungshilfe erstellt und redaktionell überarbeitet'), - self::MostlyAi => __('Überwiegend mit generativer KI erstellt und anschließend geprüft'), - self::FullyAi => __('Vollständig KI-generiert (ohne wesentliche menschliche Bearbeitung)'), + self::None, self::Assisted => __('Ohne generative KI erstellt – oder KI nur als Formulierungshilfe, mit anschließender menschlicher Überarbeitung und Verantwortung.'), + self::MostlyAi, self::FullyAi => __('Ganz oder überwiegend mit generativer KI erstellt.'), }; } @@ -39,11 +42,32 @@ enum AiTextDeclaration: string { return match ($this) { self::None, self::Assisted => null, - self::MostlyAi => __('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung überwiegend mit generativer KI erstellt.'), - self::FullyAi => __('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung vollständig mit generativer KI erstellt.'), + self::MostlyAi, self::FullyAi => __('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'), }; } + /** + * Kanonische Formular-Option der Gruppe — mappt Alt-/API-Werte + * (assisted, fully_ai) auf die zwei im Modal angebotenen Radios. + */ + public function formOption(): self + { + return match ($this) { + self::None, self::Assisted => self::None, + self::MostlyAi, self::FullyAi => self::MostlyAi, + }; + } + + /** + * Die zwei im Submit-Modal angebotenen Auswahloptionen. + * + * @return list + */ + public static function formCases(): array + { + return [self::None, self::MostlyAi]; + } + /** * Optionsliste für Radios/Selects: value => label. * diff --git a/lang/de/static.php b/lang/de/static.php index db10557b..b6f40adc 100644 --- a/lang/de/static.php +++ b/lang/de/static.php @@ -224,7 +224,7 @@ return [ 'faq_contact' => 'Direkt Kontakt aufnehmen', 'faq_response_note' => 'Antwort werktags per E-Mail', 'faq_1_q' => 'Wie schnell wird meine Mitteilung veröffentlicht?', - 'faq_1_a' => 'Werktags üblicherweise innerhalb von 24 Stunden nach Einreichung. Bei zeitkritischen Mitteilungen ist eine Vorab-Absprache möglich.', + 'faq_1_a' => 'Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen — Ihre Mitteilung erscheint direkt danach. Nur in Einzelfällen prüfen wir manuell nach, dann werktags üblicherweise innerhalb von 24 Stunden.', 'faq_2_q' => 'Bleibt meine Mitteilung dauerhaft online?', 'faq_2_a' => 'Ja. Mitteilungen werden nicht gelöscht. Korrekturen sind jederzeit möglich — die URL bleibt erhalten.', 'faq_3_q' => 'Kann ich auch auf presseecho.de veröffentlichen?', diff --git a/lang/en/static.php b/lang/en/static.php index cc20b0a3..91073097 100644 --- a/lang/en/static.php +++ b/lang/en/static.php @@ -223,7 +223,7 @@ return [ 'faq_contact' => 'Get in touch directly', 'faq_response_note' => 'Reply by email on business days', 'faq_1_q' => 'How quickly will my release be published?', - 'faq_1_a' => 'On business days usually within 24 hours of submission. For time-critical releases, prior arrangement is possible.', + 'faq_1_a' => 'The automated content review is usually completed within a few minutes — your release goes live right after. Only in exceptional cases do we review manually, typically within 24 hours on business days.', 'faq_2_q' => 'Does my release stay online permanently?', 'faq_2_a' => 'Yes. Releases are not deleted. Corrections are possible at any time — the URL is preserved.', 'faq_3_q' => 'Can I also publish on presseecho.de?', diff --git a/resources/views/components/press-release-submit-modal.blade.php b/resources/views/components/press-release-submit-modal.blade.php index 2eb8d590..99d9cfed 100644 --- a/resources/views/components/press-release-submit-modal.blade.php +++ b/resources/views/components/press-release-submit-modal.blade.php @@ -126,14 +126,16 @@

{{ __('Wie wurde der Text dieser Pressemitteilung erstellt? Diese Angabe ist gesetzlich verpflichtend (Art. 50 KI-VO).') }}

+ {{-- Zwei Gruppen (Entscheidung 10.07.2026): Option 1 = kein + Label, Option 2 = Hinweis „KI-generiert" am Pressetext. --}} - @foreach (\App\Enums\AiTextDeclaration::cases() as $aiOption) + @foreach (\App\Enums\AiTextDeclaration::formCases() as $aiOption) @endforeach

- {{ __('Bei überwiegend oder vollständig KI-generierten Texten erscheint an der Veröffentlichung ein neutraler Hinweis in Ihrer Stimme („Nach Angaben des einreichenden Unternehmens …"). KI-generierte oder KI-bearbeitete Bilder kennzeichnen Sie direkt beim Bild-Upload.') }} + {{ __('Bei der ersten Option erscheint kein Label an der Veröffentlichung. Bei der zweiten Option wird der Pressetext mit dem Hinweis „KI-generiert" in Ihrer Stimme gekennzeichnet („Nach Angaben des einreichenden Unternehmens …"). KI-generierte oder KI-bearbeitete Bilder kennzeichnen Sie direkt beim Bild-Upload.') }}

diff --git a/resources/views/livewire/admin/press-releases/show.blade.php b/resources/views/livewire/admin/press-releases/show.blade.php index f61fa5d1..acbaeeb6 100644 --- a/resources/views/livewire/admin/press-releases/show.blade.php +++ b/resources/views/livewire/admin/press-releases/show.blade.php @@ -198,13 +198,15 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends @php $adminTitleImage = $pr->images->sortByDesc('is_preview')->first(); @endphp - @if ($adminTitleImage && ($adminTitleImage->copyright || $adminTitleImage->is_ai_generated)) + @if ($adminTitleImage && ($adminTitleImage->copyright || $adminTitleImage->is_ai_generated || $adminTitleImage->ai_declaration?->publicLabel())) {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
- @if ($adminTitleImage->is_ai_generated) - {{ __('KI-generiert') }} + @if ($aiImageBadge = $adminTitleImage->ai_declaration?->publicLabel() ?? ($adminTitleImage->is_ai_generated ? __('KI-generiert') : null)) + {{ $aiImageBadge }} + @endif + @if ($adminTitleImage->copyright) + {{ $adminTitleImage->copyright }} @endif - {{ $adminTitleImage->copyright ?? __('Bild: KI-generiert') }}
@endif @endif diff --git a/resources/views/livewire/components/press-release-images-manager.blade.php b/resources/views/livewire/components/press-release-images-manager.blade.php index bbfce6ce..00374ac8 100644 --- a/resources/views/livewire/components/press-release-images-manager.blade.php +++ b/resources/views/livewire/components/press-release-images-manager.blade.php @@ -101,7 +101,7 @@ new class extends Component { $this->resetErrorBag('newLicenseDetail'); // Automatisch erzeugte Vorschläge weichen dem Vorschlag des neuen Typs. - if ($this->newCopyright === $this->autoCopyright || str_starts_with($this->newCopyright, __('Bild: KI-generiert'))) { + if ($this->newCopyright === $this->autoCopyright || str_starts_with($this->newCopyright, __('KI-generiert'))) { $this->newCopyright = ''; $this->autoCopyright = ''; } @@ -135,7 +135,9 @@ new class extends Component { return; } - $this->autoCopyright = filled($this->newAuthor) ? __('Foto: :author', ['author' => trim($this->newAuthor)]) : ''; + // Ohne „Foto:"-Präfix — die Veröffentlichungsseite stellt es dem + // Bildnachweis bereits selbst voran (sonst stünde dort „Foto: Foto: …"). + $this->autoCopyright = filled($this->newAuthor) ? trim($this->newAuthor) : ''; $this->newCopyright = $this->autoCopyright; } @@ -154,11 +156,11 @@ new class extends Component { return; } - if (filled($this->newCopyright) && !str_starts_with($this->newCopyright, __('Bild: KI-generiert'))) { + if (filled($this->newCopyright) && !str_starts_with($this->newCopyright, __('KI-generiert'))) { return; } - $this->newCopyright = filled($this->newLicenseDetail) ? __('Bild: KI-generiert (:tool)', ['tool' => trim($this->newLicenseDetail)]) : __('Bild: KI-generiert'); + $this->newCopyright = filled($this->newLicenseDetail) ? __('KI-generiert (:tool)', ['tool' => trim($this->newLicenseDetail)]) : __('KI-generiert'); } public function closeUploadForm(): void @@ -214,7 +216,7 @@ new class extends Component { ]), ], [ - 'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'), + 'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Max Mustermann / Beispiel GmbH.'), 'newAuthor.required' => $isAiGenerated ? __('Bitte angeben, wer für die Erstellung verantwortlich ist (Person oder Firma).') : __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'), 'newLicenseType.required' => __('Bitte einen Lizenztyp wählen.'), 'newLicenseDetail.required' => $isAiGenerated ? __('Bitte das verwendete KI-Tool angeben, z. B. Midjourney, Gemini, ChatGPT, DALL·E, Adobe Firefly.') : __('Bitte die Lizenz genauer angeben.'), @@ -624,8 +626,8 @@ new class extends Component { + :description="__('Wird öffentlich unter dem Bild angezeigt („Foto: …") — Vorschlag anpassbar.')" + :placeholder="__('z. B. Max Mustermann / Beispiel GmbH')" required /> {{-- ===== Schritt 3 · Personen & Rechte Dritter ===== --}} diff --git a/resources/views/livewire/customer/press-releases/create.blade.php b/resources/views/livewire/customer/press-releases/create.blade.php index 17bdcf85..d2fd03af 100644 --- a/resources/views/livewire/customer/press-releases/create.blade.php +++ b/resources/views/livewire/customer/press-releases/create.blade.php @@ -341,7 +341,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex Flux::toast( heading: __('Eingereicht'), - text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'), + text: __('Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen.'), variant: 'success', ); } else { diff --git a/resources/views/livewire/customer/press-releases/edit.blade.php b/resources/views/livewire/customer/press-releases/edit.blade.php index 98018527..79bdaf53 100644 --- a/resources/views/livewire/customer/press-releases/edit.blade.php +++ b/resources/views/livewire/customer/press-releases/edit.blade.php @@ -125,9 +125,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl $this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($pr->placeholder_variant?->value)->value; - // KI-Grad vorbelegen (bereits deklariert); die verbindliche - // Bestätigung muss bei jedem Submit erneut gesetzt werden. - $this->aiTextDeclaration = $pr->ai_text_declaration?->value; + // KI-Grad vorbelegen (bereits deklariert), gemappt auf die zwei + // Formular-Optionen; die verbindliche Bestätigung muss bei jedem + // Submit erneut gesetzt werden. + $this->aiTextDeclaration = $pr->ai_text_declaration?->formOption()->value; } #[On('placeholder-selected')] @@ -337,7 +338,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl Flux::toast( heading: __('Eingereicht'), - text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'), + text: __('Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen.'), variant: 'success', ); } else { diff --git a/resources/views/web/release-detail.blade.php b/resources/views/web/release-detail.blade.php index dc80e4ef..417d2b66 100644 --- a/resources/views/web/release-detail.blade.php +++ b/resources/views/web/release-detail.blade.php @@ -42,6 +42,9 @@ $imageUrl = $image?->variantUrl('detail') ?? $image?->variantUrl('card') ?? $image?->url(); $imageCaption = $image?->description ?: $image?->title; $imageCredit = $image?->copyright ?: $image?->author; + // KI-Bildkennzeichnung (Art. 50 KI-VO): Badge direkt am Bild + // („KI-generiert" / „KI-bearbeitet"), null bei Originalfotos. + $imageAiBadge = $image?->ai_declaration?->publicLabel(); // Ohne Pressefoto: die im User-Panel gewählte Platzhalter-Variante. // Null bei Altbeständen ohne Variante — dann generischer Hero-Fallback. @@ -306,6 +309,13 @@
{{ $imageCaption ?: $release->title }} + @if ($imageAiBadge) + {{-- KI-Badge am Bild (Art. 50 KI-VO): kräftig abgedunkelter + Hintergrund, damit es auch auf hellen Bildern lesbar bleibt. --}} + + {{ $imageAiBadge }} + + @endif
@if ($imageCaption || $imageCredit)
@@ -319,6 +329,19 @@
{{-- Body --}}
+ {{-- KI-Label (Art. 50 KI-VO) in Einreicher-Stimme am Anfang + des Inhalts; neutrales ℹ-Icon, kein Flux auf Web-Domains. --}} + @if ($aiTextLabel = $release->aiTextPublicLabel()) +
+ + {{ $aiTextLabel }} +
+ @endif +
{!! $bodyHtml !!}
diff --git a/tests/Feature/PressReleaseAiDeclarationTest.php b/tests/Feature/PressReleaseAiDeclarationTest.php index 0aea4faf..3ff78e86 100644 --- a/tests/Feature/PressReleaseAiDeclarationTest.php +++ b/tests/Feature/PressReleaseAiDeclarationTest.php @@ -41,18 +41,29 @@ function makeAiDeclarationCustomer(): array } test('the text declaration maps to the attributed public label per decision update', function () { + // Zwei Gruppen (Vereinfachung 10.07.2026): None/Assisted → kein Label, + // MostlyAi/FullyAi → gemeinsames „KI-generiert"-Label in Einreicher-Stimme. expect(AiTextDeclaration::None->publicLabel())->toBeNull(); expect(AiTextDeclaration::Assisted->publicLabel())->toBeNull(); expect(AiTextDeclaration::MostlyAi->publicLabel()) - ->toBe('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung überwiegend mit generativer KI erstellt.'); + ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); expect(AiTextDeclaration::FullyAi->publicLabel()) - ->toBe('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung vollständig mit generativer KI erstellt.'); + ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); +}); + +test('every stored value maps onto one of the two form options', function () { + expect(AiTextDeclaration::None->formOption())->toBe(AiTextDeclaration::None); + expect(AiTextDeclaration::Assisted->formOption())->toBe(AiTextDeclaration::None); + expect(AiTextDeclaration::MostlyAi->formOption())->toBe(AiTextDeclaration::MostlyAi); + expect(AiTextDeclaration::FullyAi->formOption())->toBe(AiTextDeclaration::MostlyAi); + expect(AiTextDeclaration::formCases())->toBe([AiTextDeclaration::None, AiTextDeclaration::MostlyAi]); }); test('the image declaration maps to the public image hint per decision update', function () { + // Kurzform als Badge direkt am Bild (User-Entscheidung 10.07.2026). expect(AiImageDeclaration::Original->publicLabel())->toBeNull(); - expect(AiImageDeclaration::AiGenerated->publicLabel())->toBe('Bildhinweis: KI-generierte Illustration.'); - expect(AiImageDeclaration::AiEdited->publicLabel())->toBe('Bildhinweis: KI-bearbeitetes Bild.'); + expect(AiImageDeclaration::AiGenerated->publicLabel())->toBe('KI-generiert'); + expect(AiImageDeclaration::AiEdited->publicLabel())->toBe('KI-bearbeitet'); }); test('submit is blocked without a complete ai declaration', function () { @@ -199,7 +210,7 @@ test('submitting from the create editor persists the declaration and the audit e expect($pr->ai_text_declaration)->toBe(AiTextDeclaration::FullyAi); expect($pr->ai_declaration_confirmed_at)->not->toBeNull(); expect($pr->aiTextPublicLabel()) - ->toBe('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung vollständig mit generativer KI erstellt.'); + ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); $entry = PressReleaseAiDeclaration::query()->sole(); expect($entry->press_release_id)->toBe($pr->id); @@ -254,8 +265,8 @@ test('the submit modal renders the declaration options and the binding confirmat LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->assertSee('KI-Kennzeichnung') - ->assertSee('Ohne generative KI erstellt') - ->assertSee('Vollständig KI-generiert (ohne wesentliche menschliche Bearbeitung)') + ->assertSee('Ohne generative KI erstellt – oder KI nur als Formulierungshilfe, mit anschließender menschlicher Überarbeitung und Verantwortung.') + ->assertSee('Ganz oder überwiegend mit generativer KI erstellt.') ->assertSee('Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.'); }); diff --git a/tests/Feature/PressReleaseImageLicenseTest.php b/tests/Feature/PressReleaseImageLicenseTest.php index 7c19f927..c603c943 100644 --- a/tests/Feature/PressReleaseImageLicenseTest.php +++ b/tests/Feature/PressReleaseImageLicenseTest.php @@ -336,20 +336,21 @@ test('a valid ai generated upload stores tool and ai flag', function () { expect($image->is_ai_generated)->toBeTrue(); // Lizenztyp „KI-generiert" setzt die KI-Kennzeichnung automatisch. expect($image->ai_declaration)->toBe(AiImageDeclaration::AiGenerated); - expect($image->copyright)->toBe('Bild: KI-generiert (Midjourney v7)'); + expect($image->copyright)->toBe('KI-generiert (Midjourney v7)'); }); test('the copyright suggestion follows the author but respects manual input', function () { /** @var TestCase $this */ + // Ohne „Foto:"-Präfix — den stellt die Veröffentlichungsseite selbst voran. ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); $this->actingAs($owner); LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) ->set('newLicenseType', ImageLicenseType::Own->value) ->set('newAuthor', 'Jane Doe') - ->assertSet('newCopyright', 'Foto: Jane Doe') + ->assertSet('newCopyright', 'Jane Doe') ->set('newAuthor', 'John Smith') - ->assertSet('newCopyright', 'Foto: John Smith') + ->assertSet('newCopyright', 'John Smith') ->set('newCopyright', 'Eigener Nachweis') ->set('newAuthor', 'Jane Doe') ->assertSet('newCopyright', 'Eigener Nachweis'); @@ -363,11 +364,11 @@ test('switching to ai replaces an automatically suggested author copyright', fun LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) ->set('newLicenseType', ImageLicenseType::Own->value) ->set('newAuthor', 'Jane Doe') - ->assertSet('newCopyright', 'Foto: Jane Doe') + ->assertSet('newCopyright', 'Jane Doe') ->set('newLicenseType', ImageLicenseType::AiGenerated->value) - ->assertSet('newCopyright', 'Bild: KI-generiert') + ->assertSet('newCopyright', 'KI-generiert') ->set('newLicenseType', ImageLicenseType::Own->value) - ->assertSet('newCopyright', 'Foto: Jane Doe'); + ->assertSet('newCopyright', 'Jane Doe'); }); test('the ai copyright suggestion follows the tool but respects manual input', function () { @@ -377,9 +378,9 @@ test('the ai copyright suggestion follows the tool but respects manual input', f LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) ->set('newLicenseType', ImageLicenseType::AiGenerated->value) - ->assertSet('newCopyright', 'Bild: KI-generiert') + ->assertSet('newCopyright', 'KI-generiert') ->set('newLicenseDetail', 'DALL·E 3') - ->assertSet('newCopyright', 'Bild: KI-generiert (DALL·E 3)') + ->assertSet('newCopyright', 'KI-generiert (DALL·E 3)') ->set('newCopyright', 'Eigener Nachweis') ->set('newLicenseDetail', 'Midjourney v7') ->assertSet('newCopyright', 'Eigener Nachweis'); diff --git a/tests/Feature/Web/ReleaseDetailTest.php b/tests/Feature/Web/ReleaseDetailTest.php index dd02b8a5..3e5b6a54 100644 --- a/tests/Feature/Web/ReleaseDetailTest.php +++ b/tests/Feature/Web/ReleaseDetailTest.php @@ -1,5 +1,7 @@ assertDontSeeText('übermittelt von'); }); +test('release detail shows the ai label only for ai-generated declarations', function () { + /** @var TestCase $this */ + // Art. 50 KI-VO: Option 2 (ganz/überwiegend KI) trägt das Label + // „KI-generiert" am Anfang des Inhalts, Option 1 bleibt ohne Label. + // KI-deklarierte Bilder tragen das Badge „KI-generiert" direkt am Bild. + [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); + + $aiRelease = PressRelease::factory() + ->published() + ->forPortal(Portal::Businessportal24) + ->for($category) + ->for($company) + ->create([ + 'slug' => 'ki-generierte-meldung', + 'ai_text_declaration' => AiTextDeclaration::MostlyAi->value, + 'published_at' => now()->subDay(), + ]); + + $aiRelease->images()->create([ + 'disk' => 'public', + 'path' => 'press-releases/'.$aiRelease->id.'/images/title.jpg', + 'author' => 'Beispiel GmbH', + 'license_type' => 'ai_generated', + 'rights_confirmed_at' => now(), + 'is_ai_generated' => true, + 'ai_declaration' => AiImageDeclaration::AiGenerated->value, + 'is_preview' => true, + 'sort_order' => 1, + ]); + + $this->get('https://businessportal24.test/de/press-release/'.$aiRelease->slug) + ->assertSuccessful() + ->assertSeeText('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.') + // Badge am Bild — Kurzform, nicht mehr „KI-generierte Illustration". + ->assertSeeText('KI-generiert') + ->assertDontSeeText('KI-generierte Illustration'); + + $plainRelease = PressRelease::factory() + ->published() + ->forPortal(Portal::Businessportal24) + ->for($category) + ->for($company) + ->create([ + 'slug' => 'meldung-ohne-ki', + 'ai_text_declaration' => AiTextDeclaration::None->value, + 'published_at' => now()->subDay(), + ]); + + $this->get('https://businessportal24.test/de/press-release/'.$plainRelease->slug) + ->assertSuccessful() + ->assertDontSeeText('KI-generiert'); +}); + test('release detail counts a hit', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);