Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
min-width: 500px;
min-height: 400px;
max-width: 800px;
max-height: 600px;
max-height: 790px;
}

.edit_file {
Expand Down
16 changes: 16 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,22 @@ de:
video_settings_tab: Video-Details
files:
common_attributes:
ai_indicator:
blank: "(Nicht angegeben)"
inline_help: |-
Kennzeichne, dass die Datei mit künstlicher Intelligenz
erzeugt oder verändert wurde. Wird als EU-KI-Label an
der Stelle im Beitrag angezeigt, wo die Datei verwendet
wird.
label: KI-Kennzeichnung
values:
ai_generated: Vollständig KI-generiert
ai_modified: Teilweise KI-modifiziert
ai_indicator_text:
inline_help: |-
Erläutert, welche Teile der Datei mit künstlicher
Intelligenz erzeugt oder verändert wurden.
label: Text der KI-Kennzeichnung
alt:
inline_help: Ein Hinweis darauf, was in dem dargestellten Element zum Ausdruck kommt. Der Text ist im Beitrag nicht sichtbar, kann aber von Screen-Readern verarbeitet werden und trägt somit zur barrierearmen Gestaltung der Inhalte bei.
label: Alt-Text
Expand Down
15 changes: 15 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,21 @@ en:
video_settings_tab: Video Details
files:
common_attributes:
ai_indicator:
blank: "(Not specified)"
inline_help: |-
Indicate that the file has been generated or modified
using artificial intelligence. Displayed as EU AI label
at the element where the file is used.
label: AI label
values:
ai_generated: Fully AI generated
ai_modified: Partially AI modified
ai_indicator_text:
inline_help: |-
Explains which parts of the file have been generated or
modified using artificial intelligence.
label: AI label text
alt:
inline_help: A hint describing the contents of the displayed element. Not displayed in the story, but can be used by screen reader software. Improves accessibility of the content.
label: Alt-Text
Expand Down
4 changes: 4 additions & 0 deletions entry_types/scrolled/config/locales/new/public.de.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
de:
pageflow_scrolled:
public:
ai_indicators:
ai: KI
ai_generated: KI-generiert
ai_modified: KI-modifiziert
enter_fullscreen: Vollbildmodus öffnen
exit_fullscreen: Vollbildmodus verlassen
flip_card: Karte wenden
4 changes: 4 additions & 0 deletions entry_types/scrolled/config/locales/new/public.en.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
en:
pageflow_scrolled:
public:
ai_indicators:
ai: AI
ai_generated: AI generated
ai_modified: AI modified
enter_fullscreen: Enter fullscreen
exit_fullscreen: Exit fullscreen
flip_card: Flip card
1 change: 1 addition & 0 deletions entry_types/scrolled/package/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ toc:
description: |
React components for building content elements.
children:
- AiIndicatorIcon
- AudioPlayer
- ContentElementBox
- ContentElementFigure
Expand Down
57 changes: 57 additions & 0 deletions entry_types/scrolled/package/spec/frontend/AiIndicatorIcon-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';

import {AiIndicatorIcon, useAiIndicatorLabel} from 'frontend/AiIndicatorIcon';

import {renderInEntry} from 'support';
import {useFakeTranslations} from 'pageflow/testHelpers';

describe('AiIndicatorIcon', () => {
useFakeTranslations({
'pageflow_scrolled.public.ai_indicators.ai_generated': 'AI generated'
});

it('renders icon for kind', () => {
const {container} = renderInEntry(<AiIndicatorIcon kind="ai_generated" />);

expect(container.querySelector('svg')).toHaveAttribute('data-file-name',
'SvgAiGenerated');
});

it('renders different icon per kind', () => {
const {container} = renderInEntry(<AiIndicatorIcon kind="ai_modified" />);

expect(container.querySelector('svg')).toHaveAttribute('data-file-name',
'SvgAiModified');
});

it('renders nothing for unknown kind', () => {
const {container} = renderInEntry(<AiIndicatorIcon kind="ai_dreamed" />);

expect(container.querySelector('svg')).toBeNull();
});

it('renders translated label for screen readers', () => {
const {queryByRole} = renderInEntry(<AiIndicatorIcon kind="ai_generated" />);

expect(queryByRole('img', {name: 'AI generated'})).not.toBeNull();
});
});

describe('useAiIndicatorLabel', () => {
useFakeTranslations({
'pageflow_scrolled.public.ai_indicators.ai_generated': 'AI generated'
});

function Probe({kind}) {
const aiIndicatorLabel = useAiIndicatorLabel();

return <span>{aiIndicatorLabel(kind)}</span>;
}

it('translates kind', () => {
const {container} = renderInEntry(<Probe kind="ai_generated" />);

expect(container).toHaveTextContent('AI generated');
});
});
221 changes: 221 additions & 0 deletions entry_types/scrolled/package/spec/frontend/InlineFileRights-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {useFakeTranslations} from 'pageflow/testHelpers';

describe('InlineFileRights', () => {
useFakeTranslations({
'pageflow_scrolled.public.inline_file_rights_labels.image': 'Image',
'pageflow_scrolled.public.inline_file_rights_labels.poster': 'Poster',
'pageflow_scrolled.public.inline_file_rights_labels.video': 'Video',
'pageflow_scrolled.public.ai_indicators.ai_generated': 'AI generated',
'pageflow_scrolled.public.ai_indicators.ai_modified': 'AI modified'
});

beforeEach(() => {
Expand All @@ -21,6 +24,17 @@ describe('InlineFileRights', () => {
)
}
});

api.widgetTypes.register('inlineFileRightsFlags', {
component: function ({hasRights, hasAiIndicators, children}) {
return (
<div>
{`${hasRights}|${hasAiIndicators}`}
{children}
</div>
)
}
});
});

const seed = {
Expand Down Expand Up @@ -222,6 +236,213 @@ describe('InlineFileRights', () => {
.toHaveAttribute('href', 'https://stock.example.com/123');
});

it('renders ai indicator icon and text as part of item', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {
ai_indicator: 'ai_modified',
ai_indicator_text: 'Sky replaced'
},
inlineRights: true
};

const {getByRole, queryByRole} = renderInEntry(
<InlineFileRights items={[{label: 'image', file}]} />,
{seed}
);

expect(getByRole('listitem')).toHaveTextContent('Image: My stock images Sky replaced');
expect(queryByRole('img', {name: 'AI modified'})).not.toBeNull();
});

it('keeps label, rights and license together for line breaking', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {ai_indicator: 'ai_generated'},
inlineRights: true,
license: {
name: 'CC0',
url: 'https://creativecommons.org/publicdomain/zero/1.0/'
}
};

const {container} = renderInEntry(
<InlineFileRights items={[{label: 'image', file}]} />,
{seed}
);

expect(container.querySelector('[data-part="rights"]'))
.toHaveTextContent('Image: My stock images (CC0)');
});

it('keeps ai indicator icon and text together for line breaking', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {
ai_indicator: 'ai_modified',
ai_indicator_text: 'Sky replaced'
},
inlineRights: true
};

const {container, getByRole} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed}
);

expect(getByRole('img', {name: 'AI modified'}).parentElement)
.toHaveAttribute('data-part', 'ai-indicator');
expect(container.querySelector('[data-part="ai-indicator"]'))
.toHaveTextContent('Sky replaced');
});

it('renders item for file with ai indicator even without inline rights', () => {
const file = {
id: 10,
rights: '',
configuration: {
ai_indicator: 'ai_generated',
ai_indicator_text: 'Voices cloned'
},
inlineRights: false
};

const {getByRole} = renderInEntry(
<InlineFileRights items={[{label: 'video', file}]} />,
{seed}
);

expect(getByRole('listitem')).toHaveTextContent('Video: Voices cloned');
});

it('renders one item per file with ai indicator', () => {
const videoFile = {
id: 10,
configuration: {ai_indicator: 'ai_generated', ai_indicator_text: 'Voices'},
inlineRights: false
};
const posterFile = {
id: 11,
configuration: {ai_indicator: 'ai_generated', ai_indicator_text: 'Voices'},
inlineRights: false
};

const {queryAllByRole} = renderInEntry(
<InlineFileRights items={[{label: 'video', file: videoFile},
{label: 'poster', file: posterFile}]} />,
{seed}
);

const listItems = queryAllByRole('listitem');

expect(listItems).toHaveLength(2);
expect(listItems[0]).toHaveTextContent('Video: Voices');
expect(listItems[1]).toHaveTextContent('Poster: Voices');
});

it('marks items that have rights', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {},
inlineRights: true
};

const {getByRole} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed}
);

expect(getByRole('listitem')).toHaveAttribute('data-rights', '');
});

it('does not mark items that only have an ai indicator', () => {
const file = {
id: 10,
rights: '',
configuration: {ai_indicator: 'ai_generated'},
inlineRights: false
};

const {getByRole} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed}
);

expect(getByRole('listitem')).not.toHaveAttribute('data-rights');
});

it('marks labels of items to allow hiding them', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {},
inlineRights: true
};

const {container} = renderInEntry(
<InlineFileRights items={[{label: 'image', file}]} />,
{seed}
);

expect(container.querySelector('[data-label]')).toHaveTextContent('Image:');
});

it('does not render license of file without inline rights', () => {
const file = {
id: 10,
rights: 'My stock images',
configuration: {ai_indicator: 'ai_generated'},
inlineRights: false,
license: {
name: 'CC0',
url: 'https://creativecommons.org/publicdomain/zero/1.0/'
}
};

const {getByRole} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed}
);

expect(getByRole('listitem')).not.toHaveTextContent('CC0');
});

it('passes flags for rights and ai indicators to widget', () => {
const file = {
id: 10,
rights: '',
configuration: {ai_indicator: 'ai_generated'},
inlineRights: false
};

const {container} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed: {widgets: [{role: 'inlineFileRights', typeName: 'inlineFileRightsFlags'}]}}
);

expect(container).toHaveTextContent('false|true');
});

it('does not render widget for files without rights and ai indicator', () => {
const file = {
id: 10,
rights: '',
configuration: {},
inlineRights: true
};

const {container} = renderInEntry(
<InlineFileRights items={[{file}]} />,
{seed}
);

expect(container.textContent).toEqual('');
});

it('renders license links for for files with license', () => {
const file = {
id: 10,
Expand Down
Loading
Loading