KI-Kennzeichnung: 2 Optionen im Submit-Modal + Labels auf Detailseite
Submit-Modal auf zwei Auswahloptionen reduziert (kein Label vs.
"Ganz oder ueberwiegend mit generativer KI erstellt"); alle vier
Enum-Werte bleiben fuer API/Bestand gueltig und mappen per formOption()
auf die zwei Gruppen. Oeffentliche Detailseite rendert jetzt Text-Label
("KI-generiert: Nach Angaben ...") und Bild-Badge (KI-generiert/
KI-bearbeitet, abgedunkelter Hintergrund) — fehlte dort bisher komplett.
Bildnachweis-Vorschlag ohne "Foto:"-Praefix (stand sonst doppelt).
Einreich-Toast + FAQ: automatische Pruefung in Minuten statt 24 h.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c8836564e9
commit
dc2a3443c1
13 changed files with 172 additions and 50 deletions
|
|
@ -28,15 +28,16 @@ enum AiImageDeclaration: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Öffentlicher Bildhinweis direkt am Bild — null bei Originalfotos
|
* Öffentlicher Bildhinweis direkt am Bild (Badge) — null bei Originalfotos
|
||||||
* (keine Nicht-KI-Zertifizierung in Portalstimme).
|
* (keine Nicht-KI-Zertifizierung in Portalstimme). Kurzform „KI-generiert"
|
||||||
|
* statt Satz-Wortlaut (User-Entscheidung 10.07.2026).
|
||||||
*/
|
*/
|
||||||
public function publicLabel(): ?string
|
public function publicLabel(): ?string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::Original => null,
|
self::Original => null,
|
||||||
self::AiGenerated => __('Bildhinweis: KI-generierte Illustration.'),
|
self::AiGenerated => __('KI-generiert'),
|
||||||
self::AiEdited => __('Bildhinweis: KI-bearbeitetes Bild.'),
|
self::AiEdited => __('KI-bearbeitet'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@ namespace App\Enums;
|
||||||
/**
|
/**
|
||||||
* Selbst-Deklaration des KI-Einsatzes beim PM-Text (Art. 50 Abs. 4 KI-VO).
|
* 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):
|
* Pflichtauswahl im Submit-Flow (Decision-Update KI-Kennzeichnung, 08.07.2026;
|
||||||
* gelabelt wird allein nach dem deklarierten KI-Grad, nicht nach der
|
* vereinfacht auf zwei Auswahloptionen, 10.07.2026): gelabelt wird allein nach
|
||||||
* Public-Interest-Frage — bewusste Über-Erfüllung. Das öffentliche Label
|
* dem deklarierten KI-Grad, nicht nach der Public-Interest-Frage — bewusste
|
||||||
* spricht in Einreicher-Stimme, nie in Portalstimme.
|
* Ü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
|
enum AiTextDeclaration: string
|
||||||
{
|
{
|
||||||
|
|
@ -18,15 +23,13 @@ enum AiTextDeclaration: string
|
||||||
case FullyAi = 'fully_ai';
|
case FullyAi = 'fully_ai';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wortlaut der Auswahloption im Submit-Flow.
|
* Wortlaut der Auswahloption im Submit-Flow (zwei Gruppen).
|
||||||
*/
|
*/
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::None => __('Ohne generative KI erstellt'),
|
self::None, self::Assisted => __('Ohne generative KI erstellt – oder KI nur als Formulierungshilfe, mit anschließender menschlicher Überarbeitung und Verantwortung.'),
|
||||||
self::Assisted => __('Mit KI als Formulierungshilfe erstellt und redaktionell überarbeitet'),
|
self::MostlyAi, self::FullyAi => __('Ganz oder überwiegend mit generativer KI erstellt.'),
|
||||||
self::MostlyAi => __('Überwiegend mit generativer KI erstellt und anschließend geprüft'),
|
|
||||||
self::FullyAi => __('Vollständig KI-generiert (ohne wesentliche menschliche Bearbeitung)'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,11 +42,32 @@ enum AiTextDeclaration: string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::None, self::Assisted => null,
|
self::None, self::Assisted => null,
|
||||||
self::MostlyAi => __('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung überwiegend 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.'),
|
||||||
self::FullyAi => __('Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung vollständig 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<self>
|
||||||
|
*/
|
||||||
|
public static function formCases(): array
|
||||||
|
{
|
||||||
|
return [self::None, self::MostlyAi];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionsliste für Radios/Selects: value => label.
|
* Optionsliste für Radios/Selects: value => label.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ return [
|
||||||
'faq_contact' => 'Direkt Kontakt aufnehmen',
|
'faq_contact' => 'Direkt Kontakt aufnehmen',
|
||||||
'faq_response_note' => 'Antwort werktags per E-Mail',
|
'faq_response_note' => 'Antwort werktags per E-Mail',
|
||||||
'faq_1_q' => 'Wie schnell wird meine Mitteilung veröffentlicht?',
|
'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_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_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?',
|
'faq_3_q' => 'Kann ich auch auf presseecho.de veröffentlichen?',
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ return [
|
||||||
'faq_contact' => 'Get in touch directly',
|
'faq_contact' => 'Get in touch directly',
|
||||||
'faq_response_note' => 'Reply by email on business days',
|
'faq_response_note' => 'Reply by email on business days',
|
||||||
'faq_1_q' => 'How quickly will my release be published?',
|
'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_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_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?',
|
'faq_3_q' => 'Can I also publish on presseecho.de?',
|
||||||
|
|
|
||||||
|
|
@ -126,14 +126,16 @@
|
||||||
<p class="m-0 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
<p class="m-0 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
||||||
{{ __('Wie wurde der Text dieser Pressemitteilung erstellt? Diese Angabe ist gesetzlich verpflichtend (Art. 50 KI-VO).') }}
|
{{ __('Wie wurde der Text dieser Pressemitteilung erstellt? Diese Angabe ist gesetzlich verpflichtend (Art. 50 KI-VO).') }}
|
||||||
</p>
|
</p>
|
||||||
|
{{-- Zwei Gruppen (Entscheidung 10.07.2026): Option 1 = kein
|
||||||
|
Label, Option 2 = Hinweis „KI-generiert" am Pressetext. --}}
|
||||||
<flux:radio.group wire:model="aiTextDeclaration">
|
<flux:radio.group wire:model="aiTextDeclaration">
|
||||||
@foreach (\App\Enums\AiTextDeclaration::cases() as $aiOption)
|
@foreach (\App\Enums\AiTextDeclaration::formCases() as $aiOption)
|
||||||
<flux:radio :value="$aiOption->value" :label="$aiOption->label()" />
|
<flux:radio :value="$aiOption->value" :label="$aiOption->label()" />
|
||||||
@endforeach
|
@endforeach
|
||||||
</flux:radio.group>
|
</flux:radio.group>
|
||||||
<flux:error name="aiTextDeclaration" />
|
<flux:error name="aiTextDeclaration" />
|
||||||
<p class="m-0 pt-2 border-t border-[color:var(--color-bg-rule)] text-[11.5px] leading-[1.5] text-[color:var(--color-ink-3)]">
|
<p class="m-0 pt-2 border-t border-[color:var(--color-bg-rule)] text-[11.5px] leading-[1.5] text-[color:var(--color-ink-3)]">
|
||||||
{{ __('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.') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -198,13 +198,15 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
@php
|
@php
|
||||||
$adminTitleImage = $pr->images->sortByDesc('is_preview')->first();
|
$adminTitleImage = $pr->images->sortByDesc('is_preview')->first();
|
||||||
@endphp
|
@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) --}}
|
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
|
||||||
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
|
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
|
||||||
@if ($adminTitleImage->is_ai_generated)
|
@if ($aiImageBadge = $adminTitleImage->ai_declaration?->publicLabel() ?? ($adminTitleImage->is_ai_generated ? __('KI-generiert') : null))
|
||||||
<span class="badge hub">{{ __('KI-generiert') }}</span>
|
<span class="badge hub">{{ $aiImageBadge }}</span>
|
||||||
|
@endif
|
||||||
|
@if ($adminTitleImage->copyright)
|
||||||
|
<span>{{ $adminTitleImage->copyright }}</span>
|
||||||
@endif
|
@endif
|
||||||
<span>{{ $adminTitleImage->copyright ?? __('Bild: KI-generiert') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ new class extends Component {
|
||||||
$this->resetErrorBag('newLicenseDetail');
|
$this->resetErrorBag('newLicenseDetail');
|
||||||
|
|
||||||
// Automatisch erzeugte Vorschläge weichen dem Vorschlag des neuen Typs.
|
// 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->newCopyright = '';
|
||||||
$this->autoCopyright = '';
|
$this->autoCopyright = '';
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,9 @@ new class extends Component {
|
||||||
return;
|
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;
|
$this->newCopyright = $this->autoCopyright;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,11 +156,11 @@ new class extends Component {
|
||||||
return;
|
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;
|
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
|
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.'),
|
'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.'),
|
'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.'),
|
'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 {
|
||||||
|
|
||||||
<flux:input wire:model="newCopyright" :label="__('Öffentlicher Bildnachweis')"
|
<flux:input wire:model="newCopyright" :label="__('Öffentlicher Bildnachweis')"
|
||||||
:badge="__('Pflicht')"
|
:badge="__('Pflicht')"
|
||||||
:description="__('Wird öffentlich unter dem Bild angezeigt — Vorschlag anpassbar.')"
|
:description="__('Wird öffentlich unter dem Bild angezeigt („Foto: …") — Vorschlag anpassbar.')"
|
||||||
:placeholder="__('z. B. Foto: Max Mustermann / Beispiel GmbH')" required />
|
:placeholder="__('z. B. Max Mustermann / Beispiel GmbH')" required />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{-- ===== Schritt 3 · Personen & Rechte Dritter ===== --}}
|
{{-- ===== Schritt 3 · Personen & Rechte Dritter ===== --}}
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
||||||
|
|
||||||
Flux::toast(
|
Flux::toast(
|
||||||
heading: __('Eingereicht'),
|
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',
|
variant: 'success',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
||||||
|
|
||||||
$this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($pr->placeholder_variant?->value)->value;
|
$this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($pr->placeholder_variant?->value)->value;
|
||||||
|
|
||||||
// KI-Grad vorbelegen (bereits deklariert); die verbindliche
|
// KI-Grad vorbelegen (bereits deklariert), gemappt auf die zwei
|
||||||
// Bestätigung muss bei jedem Submit erneut gesetzt werden.
|
// Formular-Optionen; die verbindliche Bestätigung muss bei jedem
|
||||||
$this->aiTextDeclaration = $pr->ai_text_declaration?->value;
|
// Submit erneut gesetzt werden.
|
||||||
|
$this->aiTextDeclaration = $pr->ai_text_declaration?->formOption()->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[On('placeholder-selected')]
|
#[On('placeholder-selected')]
|
||||||
|
|
@ -337,7 +338,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
||||||
|
|
||||||
Flux::toast(
|
Flux::toast(
|
||||||
heading: __('Eingereicht'),
|
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',
|
variant: 'success',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@
|
||||||
$imageUrl = $image?->variantUrl('detail') ?? $image?->variantUrl('card') ?? $image?->url();
|
$imageUrl = $image?->variantUrl('detail') ?? $image?->variantUrl('card') ?? $image?->url();
|
||||||
$imageCaption = $image?->description ?: $image?->title;
|
$imageCaption = $image?->description ?: $image?->title;
|
||||||
$imageCredit = $image?->copyright ?: $image?->author;
|
$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.
|
// Ohne Pressefoto: die im User-Panel gewählte Platzhalter-Variante.
|
||||||
// Null bei Altbeständen ohne Variante — dann generischer Hero-Fallback.
|
// Null bei Altbeständen ohne Variante — dann generischer Hero-Fallback.
|
||||||
|
|
@ -306,6 +309,13 @@
|
||||||
<div class="relative flex items-end p-[18px] bg-bg-dark overflow-hidden" style="aspect-ratio:21/9;">
|
<div class="relative flex items-end p-[18px] bg-bg-dark overflow-hidden" style="aspect-ratio:21/9;">
|
||||||
<img src="{{ $imageUrl }}" alt="{{ $imageCaption ?: $release->title }}"
|
<img src="{{ $imageUrl }}" alt="{{ $imageCaption ?: $release->title }}"
|
||||||
class="absolute inset-0 h-full w-full object-cover" loading="eager">
|
class="absolute inset-0 h-full w-full object-cover" loading="eager">
|
||||||
|
@if ($imageAiBadge)
|
||||||
|
{{-- KI-Badge am Bild (Art. 50 KI-VO): kräftig abgedunkelter
|
||||||
|
Hintergrund, damit es auch auf hellen Bildern lesbar bleibt. --}}
|
||||||
|
<span class="relative text-[9.5px] font-bold tracking-[0.18em] uppercase text-white px-2.5 py-[3px] border border-white/60 font-mono bg-black/75 shadow-sm">
|
||||||
|
{{ $imageAiBadge }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if ($imageCaption || $imageCredit)
|
@if ($imageCaption || $imageCredit)
|
||||||
<div class="text-[11.5px] text-ink-3 mt-2.5 italic max-w-[720px]">
|
<div class="text-[11.5px] text-ink-3 mt-2.5 italic max-w-[720px]">
|
||||||
|
|
@ -319,6 +329,19 @@
|
||||||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-10 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-10 lg:gap-14">
|
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-10 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-10 lg:gap-14">
|
||||||
{{-- Body --}}
|
{{-- Body --}}
|
||||||
<div>
|
<div>
|
||||||
|
{{-- KI-Label (Art. 50 KI-VO) in Einreicher-Stimme am Anfang
|
||||||
|
des Inhalts; neutrales ℹ-Icon, kein Flux auf Web-Domains. --}}
|
||||||
|
@if ($aiTextLabel = $release->aiTextPublicLabel())
|
||||||
|
<div class="flex items-start gap-2 border border-bg-rule-strong bg-bg-elev px-3 py-2.5 mb-6 max-w-[680px] text-[12px] leading-[1.5] text-ink-3">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true" class="shrink-0 mt-0.5">
|
||||||
|
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8" fill="none" />
|
||||||
|
<path d="M12 11v5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||||
|
<circle cx="12" cy="8" r="1.1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ $aiTextLabel }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="pm-body max-w-[680px]">
|
<div class="pm-body max-w-[680px]">
|
||||||
{!! $bodyHtml !!}
|
{!! $bodyHtml !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,29 @@ function makeAiDeclarationCustomer(): array
|
||||||
}
|
}
|
||||||
|
|
||||||
test('the text declaration maps to the attributed public label per decision update', function () {
|
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::None->publicLabel())->toBeNull();
|
||||||
expect(AiTextDeclaration::Assisted->publicLabel())->toBeNull();
|
expect(AiTextDeclaration::Assisted->publicLabel())->toBeNull();
|
||||||
expect(AiTextDeclaration::MostlyAi->publicLabel())
|
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())
|
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 () {
|
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::Original->publicLabel())->toBeNull();
|
||||||
expect(AiImageDeclaration::AiGenerated->publicLabel())->toBe('Bildhinweis: KI-generierte Illustration.');
|
expect(AiImageDeclaration::AiGenerated->publicLabel())->toBe('KI-generiert');
|
||||||
expect(AiImageDeclaration::AiEdited->publicLabel())->toBe('Bildhinweis: KI-bearbeitetes Bild.');
|
expect(AiImageDeclaration::AiEdited->publicLabel())->toBe('KI-bearbeitet');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit is blocked without a complete ai declaration', function () {
|
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_text_declaration)->toBe(AiTextDeclaration::FullyAi);
|
||||||
expect($pr->ai_declaration_confirmed_at)->not->toBeNull();
|
expect($pr->ai_declaration_confirmed_at)->not->toBeNull();
|
||||||
expect($pr->aiTextPublicLabel())
|
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();
|
$entry = PressReleaseAiDeclaration::query()->sole();
|
||||||
expect($entry->press_release_id)->toBe($pr->id);
|
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])
|
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
|
||||||
->assertSee('KI-Kennzeichnung')
|
->assertSee('KI-Kennzeichnung')
|
||||||
->assertSee('Ohne generative KI erstellt')
|
->assertSee('Ohne generative KI erstellt – oder KI nur als Formulierungshilfe, mit anschließender menschlicher Überarbeitung und Verantwortung.')
|
||||||
->assertSee('Vollständig KI-generiert (ohne wesentliche menschliche Bearbeitung)')
|
->assertSee('Ganz oder überwiegend mit generativer KI erstellt.')
|
||||||
->assertSee('Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.');
|
->assertSee('Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,20 +336,21 @@ test('a valid ai generated upload stores tool and ai flag', function () {
|
||||||
expect($image->is_ai_generated)->toBeTrue();
|
expect($image->is_ai_generated)->toBeTrue();
|
||||||
// Lizenztyp „KI-generiert" setzt die KI-Kennzeichnung automatisch.
|
// Lizenztyp „KI-generiert" setzt die KI-Kennzeichnung automatisch.
|
||||||
expect($image->ai_declaration)->toBe(AiImageDeclaration::AiGenerated);
|
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 () {
|
test('the copyright suggestion follows the author but respects manual input', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
|
// Ohne „Foto:"-Präfix — den stellt die Veröffentlichungsseite selbst voran.
|
||||||
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
|
||||||
$this->actingAs($owner);
|
$this->actingAs($owner);
|
||||||
|
|
||||||
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
->set('newLicenseType', ImageLicenseType::Own->value)
|
->set('newLicenseType', ImageLicenseType::Own->value)
|
||||||
->set('newAuthor', 'Jane Doe')
|
->set('newAuthor', 'Jane Doe')
|
||||||
->assertSet('newCopyright', 'Foto: Jane Doe')
|
->assertSet('newCopyright', 'Jane Doe')
|
||||||
->set('newAuthor', 'John Smith')
|
->set('newAuthor', 'John Smith')
|
||||||
->assertSet('newCopyright', 'Foto: John Smith')
|
->assertSet('newCopyright', 'John Smith')
|
||||||
->set('newCopyright', 'Eigener Nachweis')
|
->set('newCopyright', 'Eigener Nachweis')
|
||||||
->set('newAuthor', 'Jane Doe')
|
->set('newAuthor', 'Jane Doe')
|
||||||
->assertSet('newCopyright', 'Eigener Nachweis');
|
->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])
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
->set('newLicenseType', ImageLicenseType::Own->value)
|
->set('newLicenseType', ImageLicenseType::Own->value)
|
||||||
->set('newAuthor', 'Jane Doe')
|
->set('newAuthor', 'Jane Doe')
|
||||||
->assertSet('newCopyright', 'Foto: Jane Doe')
|
->assertSet('newCopyright', 'Jane Doe')
|
||||||
->set('newLicenseType', ImageLicenseType::AiGenerated->value)
|
->set('newLicenseType', ImageLicenseType::AiGenerated->value)
|
||||||
->assertSet('newCopyright', 'Bild: KI-generiert')
|
->assertSet('newCopyright', 'KI-generiert')
|
||||||
->set('newLicenseType', ImageLicenseType::Own->value)
|
->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 () {
|
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])
|
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
|
||||||
->set('newLicenseType', ImageLicenseType::AiGenerated->value)
|
->set('newLicenseType', ImageLicenseType::AiGenerated->value)
|
||||||
->assertSet('newCopyright', 'Bild: KI-generiert')
|
->assertSet('newCopyright', 'KI-generiert')
|
||||||
->set('newLicenseDetail', 'DALL·E 3')
|
->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('newCopyright', 'Eigener Nachweis')
|
||||||
->set('newLicenseDetail', 'Midjourney v7')
|
->set('newLicenseDetail', 'Midjourney v7')
|
||||||
->assertSet('newCopyright', 'Eigener Nachweis');
|
->assertSet('newCopyright', 'Eigener Nachweis');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\AiImageDeclaration;
|
||||||
|
use App\Enums\AiTextDeclaration;
|
||||||
use App\Enums\CompanyType;
|
use App\Enums\CompanyType;
|
||||||
use App\Enums\Portal;
|
use App\Enums\Portal;
|
||||||
use App\Enums\PressReleasePlaceholder;
|
use App\Enums\PressReleasePlaceholder;
|
||||||
|
|
@ -216,6 +218,59 @@ test('release detail attributes the company itself without a distribution agency
|
||||||
->assertDontSeeText('übermittelt von');
|
->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 () {
|
test('release detail counts a hit', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
|
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue