diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/panel/Accordion.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/panel/Accordion.stories.ts
new file mode 100644
index 000000000000..35d0a3d6ab00
--- /dev/null
+++ b/core-web/apps/dotcms-ui/src/stories/primeng/panel/Accordion.stories.ts
@@ -0,0 +1,50 @@
+import { Meta, Story } from '@storybook/angular';
+
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { Accordion, AccordionModule } from 'primeng/accordion';
+
+export default {
+ title: 'PrimeNG/Panel/Accordion',
+ component: Accordion,
+ args: { activeIndex: 0, expandIcon: 'pi pi-angle-down', collapseIcon: 'pi pi-angle-up' },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Accordion groups a collection of contents in tabs.: https://www.primefaces.org/primeng-v15-lts/accordion'
+ }
+ }
+ }
+} as Meta;
+
+const BasicTemplate = `
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
+ ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
+ architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
+ voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi.
+
+
+ At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati
+ cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
+ Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus.
+
+ `;
+
+export const Main: Story = (args) => {
+ return {
+ props: {
+ activeIndex: args.activeIndex,
+ expandIcon: args.expandIcon,
+ collapseIcon: args.collapseIcon
+ },
+ moduleMetadata: { imports: [AccordionModule, BrowserAnimationsModule] },
+ template: BasicTemplate
+ };
+};
diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts
index 77d773ebe481..2a5cd943fc3d 100644
--- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts
+++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts
@@ -68,7 +68,6 @@ import {
DotMarketingConfigService,
formatHTML,
removeInvalidNodes,
- removeLoadingNodes,
RestoreDefaultDOMAttrs,
SetDocAttrStep
} from '../../shared';
@@ -485,19 +484,10 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy, ControlValueA
}
private setEditorJSONContent(content: Content) {
- //TODO: remove this when the AI content is generated exclusively in popups and not in the editor directly.
- const filterContent = removeLoadingNodes(
- content
- ? Array.isArray(content)
- ? [...content]
- : [...(content as JSONContent).content]
- : []
- );
-
this.content =
this.allowedBlocks?.length > 1
- ? removeInvalidNodes(filterContent, this.allowedBlocks)
- : filterContent;
+ ? removeInvalidNodes(content, this.allowedBlocks)
+ : content;
}
private setEditorContent(content: Content) {
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/plugins/ai-content-actions.plugin.ts b/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/plugins/ai-content-actions.plugin.ts
index 24c78c4fd282..29e7cab84ca1 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/plugins/ai-content-actions.plugin.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/plugins/ai-content-actions.plugin.ts
@@ -10,12 +10,9 @@ import { takeUntil } from 'rxjs/operators';
import { Editor } from '@tiptap/core';
-import { getAIPlaceholderImage } from '../../../shared';
import { DOT_AI_TEXT_CONTENT_KEY } from '../../ai-content-prompt/ai-content-prompt.extension';
import { AiContentPromptStore } from '../../ai-content-prompt/store/ai-content-prompt.store';
-import { DOT_AI_IMAGE_CONTENT_KEY } from '../../ai-image-prompt/ai-image-prompt.extension';
import { DotAiImagePromptStore } from '../../ai-image-prompt/ai-image-prompt.store';
-import { AI_IMAGE_PLACEHOLDER_PROPERTY } from '../../ai-image-prompt/plugins/ai-image-prompt.plugin';
import { ACTIONS, AIContentActionsComponent } from '../ai-content-actions.component';
import { AI_CONTENT_ACTIONS_PLUGIN_KEY } from '../ai-content-actions.extension';
import { TIPPY_OPTIONS } from '../utils';
@@ -152,20 +149,6 @@ export class AIContentActionsView {
case DOT_AI_TEXT_CONTENT_KEY:
this.aiContentPromptStore.setAcceptContent(true);
break;
-
- case DOT_AI_IMAGE_CONTENT_KEY:
- // eslint-disable-next-line no-case-declarations
- const placeholder = getAIPlaceholderImage(this.editor);
-
- delete placeholder.node.attrs.data[AI_IMAGE_PLACEHOLDER_PROPERTY];
-
- this.view.dispatch(
- this.view.state.tr.setNodeMarkup(placeholder.from, undefined, {
- data: placeholder.node.attrs.data
- })
- );
-
- break;
}
}
@@ -178,10 +161,6 @@ export class AIContentActionsView {
case DOT_AI_TEXT_CONTENT_KEY:
this.aiContentPromptStore.reGenerateContent();
break;
-
- case DOT_AI_IMAGE_CONTENT_KEY:
- this.dotAiImagePromptStore.reGenerateContent();
- break;
}
}
@@ -191,16 +170,6 @@ export class AIContentActionsView {
case DOT_AI_TEXT_CONTENT_KEY:
this.aiContentPromptStore.setDeleteContent(true);
break;
-
- case DOT_AI_IMAGE_CONTENT_KEY:
- // eslint-disable-next-line no-case-declarations
- const placeholder = getAIPlaceholderImage(this.editor);
- this.editor.commands.deleteRange({
- from: placeholder.from,
- to: placeholder.to
- });
-
- break;
}
this.editor.commands.closeAIContentActions();
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html
index 8461c709cc7b..2e4ebe599609 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html
@@ -4,28 +4,35 @@
[dismissableMask]="true"
[draggable]="false"
[resizable]="false"
- [style]="{ width: '800px' }"
- (onHide)="hideDialog()"
+ [style]="{ width: '1040px' }"
+ (onHide)="store.hideDialog()"
appendTo="body"
header="{{ 'block-editor.extension.ai-image.dialog-title' | dm }}">
-
-
-
-
+
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss
index 2c1e61c03419..fe6088cbb4fe 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss
@@ -1,5 +1,20 @@
@use "variables" as *;
-.dialog-prompts__wrapper {
+.dialog-prompt__wrapper {
gap: $spacing-5;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ > * {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ justify-content: center;
+ }
+}
+
+.dialog-prompt_gallery {
+ > * {
+ margin-top: auto;
+ }
}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts
index 751022633036..0108e11524c9 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts
@@ -1,28 +1,85 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
+import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator';
+import { of } from 'rxjs';
+
+import { Dialog } from 'primeng/dialog';
import { AIImagePromptComponent } from './ai-image-prompt.component';
+import { DotAiImagePromptStore } from './ai-image-prompt.store';
+import { AiImagePromptFormComponent } from './components/ai-image-prompt-form/ai-image-prompt-form.component';
-import { DotAiService } from '../../shared';
+import {
+ AIImagePrompt,
+ DotAIImageOrientation,
+ DotGeneratedAIImage
+} from '../../shared/services/dot-ai/dot-ai.models';
describe('AIImagePromptComponent', () => {
- let component: AIImagePromptComponent;
- let fixture: ComponentFixture
;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [ReactiveFormsModule, HttpClientTestingModule],
- declarations: [AIImagePromptComponent],
- providers: [DotAiService]
- }).compileComponents();
-
- fixture = TestBed.createComponent(AIImagePromptComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
+ let spectator: Spectator;
+ let store: DotAiImagePromptStore;
+
+ const imagesMock: DotGeneratedAIImage[] = [
+ { name: 'image1', url: 'image_url' },
+ { name: 'image2', url: 'image_url_2' }
+ ] as unknown as DotGeneratedAIImage[];
+
+ const createComponent = createComponentFactory({
+ component: AIImagePromptComponent,
+ providers: [
+ {
+ provide: DotAiImagePromptStore,
+ useValue: {
+ vm$: of({
+ showDialog: true,
+ isLoading: false,
+ images: imagesMock,
+ galleryActiveIndex: 0,
+ orientation: DotAIImageOrientation.VERTICAL
+ }),
+ generateImage: jasmine.createSpy('generateImage'),
+ hideDialog: jasmine.createSpy('hideDialog'),
+ patchState: jasmine.createSpy('patchState'),
+ cleanError: jasmine.createSpy('cleanError')
+ }
+ }
+ ]
+ });
+
+ beforeEach(() => {
+ spectator = createComponent();
+ store = spectator.inject(DotAiImagePromptStore);
+ });
+
+ it('should hide dialog', () => {
+ const dialog = spectator.query(Dialog);
+ dialog.onHide.emit('true');
+ expect(store.hideDialog).toHaveBeenCalled();
+ });
+
+ it('should generate image', () => {
+ const promptForm = spectator.query(AiImagePromptFormComponent);
+ const formMock: AIImagePrompt = {
+ text: 'Test',
+ type: 'input',
+ size: DotAIImageOrientation.VERTICAL
+ };
+
+ promptForm.value.emit(formMock);
+
+ expect(store.generateImage).toHaveBeenCalledWith(formMock);
+ });
+
+ it('should inset image', () => {
+ const submitBtn = spectator.query(byTestId('submit-btn'));
+
+ spectator.click(submitBtn);
+ expect(store.patchState).toHaveBeenCalledWith({
+ selectedImage: imagesMock[0]
+ });
});
- it('should create', () => {
- expect(component).toBeTruthy();
+ it('should clear error on hide confirm', () => {
+ const dialog = spectator.query(Dialog);
+ dialog.onHide.emit('true');
+ expect(store.cleanError).toHaveBeenCalled();
});
});
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts
index 142dc10ecd17..8c978f730759 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts
@@ -2,22 +2,19 @@ import { Observable } from 'rxjs';
import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
-import { ReactiveFormsModule } from '@angular/forms';
+import { FormGroupDirective } from '@angular/forms';
import { ConfirmationService } from 'primeng/api';
-import { ButtonModule } from 'primeng/button';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
-import { OverlayPanelModule } from 'primeng/overlaypanel';
-import { TooltipModule } from 'primeng/tooltip';
import { DotMessageService } from '@dotcms/data-access';
import { ComponentStatus } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';
-import { PromptType } from './ai-image-prompt.models';
import { DotAiImagePromptStore, VmAiImagePrompt } from './ai-image-prompt.store';
-import { AiImagePromptInputComponent } from './components/ai-image-prompt-input/ai-image-prompt-input.component';
+import { AiImagePromptFormComponent } from './components/ai-image-prompt-form/ai-image-prompt-form.component';
+import { AiImagePromptGalleryComponent } from './components/ai-image-prompt-gallery/ai-image-prompt-gallery.component';
@Component({
selector: 'dot-ai-image-prompt',
@@ -25,17 +22,15 @@ import { AiImagePromptInputComponent } from './components/ai-image-prompt-input/
templateUrl: './ai-image-prompt.component.html',
styleUrls: ['./ai-image-prompt.component.scss'],
imports: [
- ButtonModule,
- TooltipModule,
- ReactiveFormsModule,
- OverlayPanelModule,
NgIf,
DialogModule,
- AiImagePromptInputComponent,
AsyncPipe,
DotMessagePipe,
- ConfirmDialogModule
+ ConfirmDialogModule,
+ AiImagePromptFormComponent,
+ AiImagePromptGalleryComponent
],
+ providers: [FormGroupDirective],
changeDetection: ChangeDetectionStrategy.OnPush
})
@@ -44,36 +39,7 @@ export class AIImagePromptComponent {
protected readonly ComponentStatus = ComponentStatus;
private confirmationService = inject(ConfirmationService);
private dotMessageService = inject(DotMessageService);
- private store: DotAiImagePromptStore = inject(DotAiImagePromptStore);
-
- /**
- * Hides the dialog.
- * @return {void}
- */
- hideDialog(): void {
- this.store.hideDialog();
- }
-
- /**
- * Selects the prompt type
- *
- * @return {void}
- */
- selectType(promptType: PromptType, current: PromptType): void {
- if (current != promptType) {
- this.store.setPromptType(promptType);
- }
- }
-
- /**
- * Generates an image based on the provided prompt.
- *
- * @param {string} prompt - The text prompt used to generate the image.
- * @return {void} - This method does not return any value.
- */
- generateImage(prompt: string): void {
- this.store.generateImage(prompt);
- }
+ store: DotAiImagePromptStore = inject(DotAiImagePromptStore);
/**
* Clears the error at the store on hiding the confirmation dialog.
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.models.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.models.ts
index 6ccd86b30821..a6c0d4cbda7d 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.models.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.models.ts
@@ -1 +1,4 @@
-export type PromptType = 'input' | 'auto';
+export const enum PromptType {
+ INPUT = 'input',
+ AUTO = 'auto'
+}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.store.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.store.ts
index 22fa3ed391d2..a89eb964f40a 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.store.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.store.ts
@@ -5,38 +5,50 @@ import { Injectable } from '@angular/core';
import { switchMap, tap, withLatestFrom } from 'rxjs/operators';
-import { ComponentStatus, DotCMSContentlet } from '@dotcms/dotcms-models';
+import { ComponentStatus } from '@dotcms/dotcms-models';
import { PromptType } from './ai-image-prompt.models';
import { DotAiService } from '../../shared';
+import {
+ AIImagePrompt,
+ DotAIImageOrientation,
+ DotGeneratedAIImage
+} from '../../shared/services/dot-ai/dot-ai.models';
-const DEFAULT_INPUT_PROMPT: PromptType = 'input';
+const DEFAULT_INPUT_PROMPT = PromptType.INPUT;
export interface DotAiImagePromptComponentState {
showDialog: boolean;
- selectedPromptType: PromptType | null;
prompt: string | null; // we always have the final prompt here
editorContent: string | null;
- contentlets: DotCMSContentlet[] | [];
+ images: DotGeneratedAIImage[];
status: ComponentStatus;
error: string;
+ selectedImage: DotGeneratedAIImage | null;
+ galleryActiveIndex: number;
+ orientation: DotAIImageOrientation;
}
export interface VmAiImagePrompt {
- selectedPromptType: PromptType | null;
showDialog: boolean;
status: ComponentStatus;
+ images: DotGeneratedAIImage[];
+ galleryActiveIndex: number;
+ orientation: DotAIImageOrientation;
+ isLoading: boolean;
}
const initialState: DotAiImagePromptComponentState = {
- selectedPromptType: null,
showDialog: false,
status: ComponentStatus.INIT,
- contentlets: [],
+ images: [],
prompt: null,
editorContent: null,
- error: ''
+ error: '',
+ selectedImage: null,
+ galleryActiveIndex: 0,
+ orientation: DotAIImageOrientation.HORIZONTAL
};
@Injectable({ providedIn: 'root' })
@@ -47,14 +59,13 @@ export class DotAiImagePromptStore extends ComponentStore status === ComponentStatus.LOADING
);
+
+ readonly selectedImage$ = this.select(this.state$, ({ selectedImage }) => selectedImage);
+
readonly errorMsg$ = this.select(this.state$, ({ error }) => error);
- readonly getContentlets$ = this.select(this.state$, ({ contentlets }) => contentlets);
+ readonly getImages$ = this.select(this.state$, ({ images }) => images);
//Updaters
- readonly setPromptType = this.updater((state, selectedPromptType: PromptType) => ({
- ...state,
- selectedPromptType
- }));
readonly showDialog = this.updater((state, editorContent: string) => ({
...state,
@@ -68,18 +79,26 @@ export class DotAiImagePromptStore extends ComponentStore ({
- ...state,
- showDialog: false,
- selectedPromptType: null
+ readonly setSelectedImage = this.updater(
+ (state: DotAiImagePromptComponentState, selectedImage: DotGeneratedAIImage) => {
+ return { ...state, selectedImage };
+ }
+ );
+
+ readonly hideDialog = this.updater(() => ({
+ ...initialState
}));
readonly vm$: Observable = this.select(
this.state$,
- ({ selectedPromptType, showDialog, status }) => ({
- selectedPromptType,
+ this.isLoading$,
+ ({ showDialog, status, images, galleryActiveIndex, orientation }, isLoading) => ({
showDialog,
- status
+ status,
+ images,
+ galleryActiveIndex,
+ orientation,
+ isLoading
})
);
@@ -91,14 +110,14 @@ export class DotAiImagePromptStore extends ComponentStore} prompt$ - An observable representing the prompt.
* @returns {Observable} - An observable that emits the generated image.
*/
- readonly generateImage = this.effect((prompt$: Observable) => {
+ readonly generateImage = this.effect((prompt$: Observable) => {
return prompt$.pipe(
withLatestFrom(this.state$),
- switchMap(([prompt, { selectedPromptType, editorContent }]) => {
- const cleanPrompt = prompt?.trim() ?? '';
+ switchMap(([prompt, { editorContent }]) => {
+ const cleanPrompt = prompt.text?.trim() ?? '';
const finalPrompt =
- selectedPromptType === 'auto' && editorContent
+ prompt.type === 'auto' && editorContent
? `${cleanPrompt} to illustrate the following content: ${editorContent}`
: cleanPrompt;
@@ -108,13 +127,14 @@ export class DotAiImagePromptStore extends ComponentStore {
- this.patchState({
+ (response) => {
+ this.patchState((state) => ({
status: ComponentStatus.IDLE,
- contentlets: contentLets
- });
+ images: [...state.images, { request: prompt, response: response }],
+ galleryActiveIndex: state.images.length
+ }));
},
(error: string) => {
this.patchState({ status: ComponentStatus.IDLE, error });
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.html b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.html
new file mode 100644
index 000000000000..c50328625c37
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.html
@@ -0,0 +1,106 @@
+
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.scss b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.scss
new file mode 100644
index 000000000000..3e41eff71989
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.scss
@@ -0,0 +1,53 @@
+@use "variables" as *;
+
+form {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ justify-content: center;
+}
+
+.ai-image-prompt__input {
+ display: flex;
+ flex-direction: column;
+
+ > div {
+ display: flex;
+ align-items: center;
+ }
+
+ > span {
+ display: flex;
+ margin: $spacing-0 0 $spacing-1 $spacing-5;
+ font-size: $font-size-sm;
+ color: $color-palette-gray-700;
+ }
+}
+
+.ai-image-prompt__text {
+ .field:last-child {
+ margin-bottom: 0;
+ }
+
+ .ai-image-prompt__textarea {
+ height: 23rem;
+ }
+
+ &.ai-image-prompt__text--generated {
+ .ai-image-prompt__textarea {
+ height: 6rem;
+ }
+
+ .ai-image-prompt__rewritten-textarea {
+ height: 14rem;
+ }
+ }
+}
+
+.ai-image-prompt__orientation {
+ margin-bottom: 9rem;
+}
+
+p-accordion {
+ min-height: 37rem;
+}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts
new file mode 100644
index 000000000000..0d2a12203754
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts
@@ -0,0 +1,119 @@
+import { createComponentFactory, Spectator } from '@ngneat/spectator';
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { ButtonModule } from 'primeng/button';
+
+import { DotMessageService } from '@dotcms/data-access';
+import { DotMessagePipe } from '@dotcms/ui';
+
+import { AiImagePromptFormComponent } from './ai-image-prompt-form.component';
+
+import {
+ DotAIImageOrientation,
+ DotGeneratedAIImage
+} from '../../../../shared/services/dot-ai/dot-ai.models';
+import { PromptType } from '../../ai-image-prompt.models';
+
+describe('AiImagePromptFormComponent', () => {
+ let spectator: Spectator;
+ let generateButton;
+ const formValue = {
+ text: 'Test',
+ type: PromptType.INPUT,
+ size: DotAIImageOrientation.HORIZONTAL
+ };
+ const createComponent = createComponentFactory({
+ component: AiImagePromptFormComponent,
+ imports: [HttpClientTestingModule, ButtonModule, ReactiveFormsModule],
+ providers: [DotMessageService],
+ mocks: [DotMessagePipe]
+ });
+
+ beforeEach(() => {
+ spectator = createComponent();
+ generateButton = spectator.query('button');
+ });
+
+ it('should initialize the form properly', () => {
+ expect(spectator.component.form.get('text').value).toEqual('');
+ expect(spectator.component.form.get('type').value).toEqual(PromptType.INPUT);
+ expect(spectator.component.form.get('size').value).toEqual(
+ DotAIImageOrientation.HORIZONTAL
+ );
+ });
+
+ it('should emit value on form submission', () => {
+ const emitSpy = jest.spyOn(spectator.component.value, 'emit');
+ spectator.component.form.setValue(formValue);
+
+ spectator.detectChanges();
+
+ spectator.click(generateButton);
+
+ expect(emitSpy).toHaveBeenCalledWith(formValue);
+ });
+
+ it('should emit orientation on size control value change', () => {
+ const emitSpy = jest.spyOn(spectator.component.orientation, 'emit');
+ spectator.component.form.get('size').setValue(DotAIImageOrientation.SQUARE);
+ expect(emitSpy).toHaveBeenCalledWith(DotAIImageOrientation.SQUARE);
+ });
+
+ it('should clear validators for text control when type is auto', () => {
+ spectator.component.form.get('type').setValue('auto');
+ expect(spectator.component.form.get('text').validator).toBeNull();
+ });
+
+ it('should update form when changes come', () => {
+ const newGeneratedValue = {
+ request: formValue,
+ response: { revised_prompt: 'New Prompt' }
+ } as DotGeneratedAIImage;
+
+ spectator.setInput('generatedValue', newGeneratedValue);
+ spectator.setInput('isLoading', false);
+
+ expect(spectator.component.form.value).toEqual(newGeneratedValue.request);
+ expect(spectator.component.aiProcessedPrompt).toBe(
+ newGeneratedValue.response.revised_prompt
+ );
+ });
+
+ it('should disable form controls when isLoading is true', () => {
+ spectator.setInput('isLoading', true);
+ expect(spectator.query('form').getAttribute('disabled')).toBeDefined();
+ });
+
+ it('should enable form controls when isLoading is false', () => {
+ spectator.setInput('isLoading', false);
+ expect(spectator.query('form').getAttribute('disabled')).toBeNull();
+ });
+
+ it('should disable button when form is invalid or isLoading is true', () => {
+ spectator.setInput('isLoading', false);
+ spectator.component.form.setErrors({ invalid: true });
+ spectator.detectChanges();
+
+ expect(generateButton.disabled).toEqual(true);
+ });
+
+ it('should enable button when form is valid and isLoading is false', () => {
+ spectator.setInput({ isLoading: false });
+ spectator.component.form.setValue(formValue);
+ spectator.detectChanges();
+
+ expect(generateButton.disabled).toEqual(false);
+ });
+
+ it('should call submitForm method on button click', () => {
+ const valueSpy = jest.spyOn(spectator.component.value, 'emit');
+ spectator.setInput({ isLoading: false });
+ spectator.component.form.setValue(formValue);
+ spectator.detectChanges();
+
+ spectator.click(generateButton);
+ expect(valueSpy).toHaveBeenCalled();
+ });
+});
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts
new file mode 100644
index 000000000000..e95a6f440e01
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts
@@ -0,0 +1,180 @@
+import { NgIf } from '@angular/common';
+import {
+ Component,
+ DestroyRef,
+ EventEmitter,
+ inject,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ SimpleChange,
+ SimpleChanges
+} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ FormControl,
+ FormGroup,
+ FormsModule,
+ ReactiveFormsModule,
+ Validators
+} from '@angular/forms';
+
+import { AccordionModule } from 'primeng/accordion';
+import { SelectItem } from 'primeng/api';
+import { ButtonModule } from 'primeng/button';
+import { DropdownModule } from 'primeng/dropdown';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { RadioButtonModule } from 'primeng/radiobutton';
+
+import { DotMessageService } from '@dotcms/data-access';
+import { DotFieldRequiredDirective, DotMessagePipe } from '@dotcms/ui';
+
+import {
+ AIImagePrompt,
+ DotAIImageOrientation,
+ DotGeneratedAIImage
+} from '../../../../shared/services/dot-ai/dot-ai.models';
+import { PromptType } from '../../ai-image-prompt.models';
+
+@Component({
+ selector: 'dot-ai-image-prompt-form',
+ standalone: true,
+ templateUrl: './ai-image-prompt-form.component.html',
+ imports: [
+ ButtonModule,
+ AccordionModule,
+ RadioButtonModule,
+ ReactiveFormsModule,
+ FormsModule,
+ DropdownModule,
+ NgIf,
+ InputTextareaModule,
+ DotFieldRequiredDirective,
+ DotMessagePipe
+ ],
+ styleUrls: ['./ai-image-prompt-form.component.scss']
+})
+export class AiImagePromptFormComponent implements OnChanges, OnInit {
+ @Input()
+ generatedValue: DotGeneratedAIImage;
+
+ @Input()
+ isLoading = false;
+
+ @Output()
+ value = new EventEmitter();
+
+ @Output()
+ orientation = new EventEmitter();
+
+ form: FormGroup;
+ aiProcessedPrompt: string;
+ dotMessageService = inject(DotMessageService);
+ destroyRef = inject(DestroyRef);
+ promptTextAreaPlaceholder = 'block-editor.extension.ai-image.custom.placeholder';
+ promptLabel = 'block-editor.extension.ai-image.prompt';
+ submitButtonLabel = 'block-editor.extension.ai-image.generate';
+ requiredPrompt = true;
+
+ orientationOptions: SelectItem[] = [
+ {
+ value: DotAIImageOrientation.HORIZONTAL,
+ label: this.dotMessageService.get(
+ 'block-editor.extension.ai-image.orientation.horizontal'
+ )
+ },
+ {
+ value: DotAIImageOrientation.SQUARE,
+ label: this.dotMessageService.get('block-editor.extension.ai-image.orientation.square')
+ },
+ {
+ value: DotAIImageOrientation.VERTICAL,
+ label: this.dotMessageService.get(
+ 'block-editor.extension.ai-image.orientation.vertical'
+ )
+ }
+ ];
+
+ ngOnInit(): void {
+ this.initForm();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const { generatedValue, isLoading } = changes;
+
+ this.updatedFormValues(generatedValue, isLoading?.currentValue);
+
+ this.setSubmitButtonLabel(isLoading?.currentValue);
+
+ this.toggleFormState(isLoading?.currentValue && !isLoading.firstChange);
+ }
+
+ private initForm(): void {
+ this.form = new FormGroup({
+ text: new FormControl('', Validators.required),
+ type: new FormControl(PromptType.INPUT, Validators.required),
+ size: new FormControl(DotAIImageOrientation.HORIZONTAL, Validators.required)
+ });
+
+ const sizeControl = this.form.get('size');
+ const typeControl = this.form.get('type');
+
+ sizeControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((size) => {
+ this.orientation.emit(size);
+ });
+
+ typeControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((type) => {
+ const promptControl = this.form.get('text');
+ type === PromptType.AUTO
+ ? promptControl.clearValidators()
+ : promptControl.setValidators(Validators.required);
+
+ promptControl.updateValueAndValidity();
+
+ this.setTypeLabels(type);
+ this.requiredPrompt = type === PromptType.INPUT;
+ });
+ }
+
+ private setTypeLabels(type: PromptType): void {
+ if (type === PromptType.INPUT) {
+ this.promptLabel = 'block-editor.extension.ai-image.prompt';
+ this.promptTextAreaPlaceholder = 'block-editor.extension.ai-image.custom.placeholder';
+ } else {
+ this.promptLabel = 'block-editor.extension.ai-image.custom.props';
+ this.promptTextAreaPlaceholder = 'block-editor.extension.ai-image.placeholder';
+ }
+ }
+
+ private toggleFormState(isLoading: boolean): void {
+ isLoading ? this.form?.disable() : this.form?.enable();
+ }
+
+ /**
+ * Updates the form values based on the generated content that comes from
+ * the endpoint.
+ *
+ * @param {SimpleChange} generatedValue - The generated value.
+ * @param {boolean} isLoading - The loading status.
+ * @return {void}
+ */
+ private updatedFormValues(generatedValue: SimpleChange, isLoading: boolean): void {
+ if (generatedValue?.currentValue && !generatedValue.firstChange && !isLoading) {
+ const updatedValue: DotGeneratedAIImage = generatedValue.currentValue;
+ this.form.patchValue(updatedValue.request);
+ this.form.clearValidators();
+ this.form.updateValueAndValidity();
+
+ this.aiProcessedPrompt = updatedValue.response.revised_prompt;
+ }
+ }
+
+ private setSubmitButtonLabel(isLoading: boolean): void {
+ this.submitButtonLabel = isLoading
+ ? 'block-editor.extension.ai-image.generating'
+ : this.aiProcessedPrompt
+ ? 'block-editor.extension.ai-image.regenerate'
+ : 'block-editor.extension.ai-image.generate';
+ }
+}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html
new file mode 100644
index 000000000000..6e86797f9913
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeImageIndex + 1 }} {{ 'of' | dm }} {{ images.length }}
+
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.scss b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.scss
new file mode 100644
index 000000000000..d0fe8ace19e8
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.scss
@@ -0,0 +1,60 @@
+@use "variables" as *;
+
+:host {
+ display: flex;
+ flex-direction: column;
+}
+
+.ai-image-gallery__placeholder {
+ background-color: $color-palette-gray-200;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: $border-radius-md;
+
+ &.s1024x1792 {
+ aspect-ratio: 1/1.25;
+ }
+
+ &.s1792x1024 {
+ aspect-ratio: 1/0.5;
+ }
+
+ &.s1024x1024 {
+ aspect-ratio: 1/1;
+ }
+}
+
+p-galleria {
+ height: 400px;
+ background-color: $color-palette-gray-200;
+ display: flex;
+ align-items: center;
+}
+
+.ai-image-gallery__count {
+ margin-top: $spacing-1;
+}
+
+:host ::ng-deep {
+ .ai-image-gallery__img {
+ width: 100%;
+ max-height: 400px;
+ }
+
+ p-galleriacontent {
+ width: 100%;
+ }
+
+ .ai-image-gallery_skeleton {
+ width: 100%;
+ height: 400px;
+ display: flex;
+ }
+
+ .ai-image-gallery__count-skeleton {
+ margin-top: $spacing-1;
+ align-self: end;
+ }
+}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.spec.ts
new file mode 100644
index 000000000000..4c3a504b8a81
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.spec.ts
@@ -0,0 +1,82 @@
+import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator';
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+import { Galleria, GalleriaModule } from 'primeng/galleria';
+
+import { AiImagePromptGalleryComponent } from './ai-image-prompt-gallery.component';
+
+import { DotGeneratedAIImage } from '../../../../shared/services/dot-ai/dot-ai.models';
+
+describe('AiImagePromptGalleryComponent', () => {
+ let spectator: Spectator;
+
+ const createComponent = createComponentFactory({
+ component: AiImagePromptGalleryComponent,
+ imports: [HttpClientTestingModule, GalleriaModule]
+ });
+
+ const imagesMock: DotGeneratedAIImage[] = [
+ {
+ response: {
+ contentlet: { assetVersion: 'image_url' }
+ }
+ } as unknown as DotGeneratedAIImage,
+ {
+ response: { assetVersion: 'image_url_2' }
+ } as unknown as DotGeneratedAIImage
+ ];
+
+ beforeEach(() => {
+ spectator = createComponent();
+ });
+
+ it('should display placeholder when isLoading is false and images is empty', () => {
+ spectator.setInput({
+ isLoading: false,
+ images: []
+ });
+ spectator.detectChanges();
+ const placeholderElement = spectator.query(byTestId('ai-image-gallery__placeholder'));
+ expect(placeholderElement).toBeTruthy();
+ });
+
+ it('should display skeleton when isLoading is true', () => {
+ spectator.setInput({
+ isLoading: true,
+ images: []
+ });
+ spectator.detectChanges();
+
+ const skeletonElement = spectator.query(byTestId('ai-image-gallery_skeleton'));
+ const skeletonElementCount = spectator.query(byTestId('ai-image-gallery__count-skeleton'));
+
+ expect(skeletonElement).toBeTruthy();
+ expect(skeletonElementCount).toBeTruthy();
+ });
+
+ it('should display Galleria when isLoading is false and images is not empty', () => {
+ spectator.setInput({
+ isLoading: false,
+ images: imagesMock
+ });
+ spectator.detectChanges();
+ const galleriaElement = spectator.query('p-galleria');
+ expect(galleriaElement).toBeTruthy();
+ });
+
+ it('should emit activeIndexChange event when galleria active index changes', () => {
+ const emitterSpy = jest.spyOn(spectator.component.activeIndexChange, 'emit');
+
+ spectator.setInput({
+ isLoading: false,
+ images: imagesMock
+ });
+ spectator.detectChanges();
+ const galleria = spectator.query(Galleria);
+
+ galleria.activeIndexChange.emit(1);
+
+ expect(emitterSpy).toHaveBeenCalledWith(1);
+ });
+});
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.ts
new file mode 100644
index 000000000000..d441cff84368
--- /dev/null
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.ts
@@ -0,0 +1,41 @@
+import { NgIf } from '@angular/common';
+import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
+
+import { SharedModule } from 'primeng/api';
+import { GalleriaModule } from 'primeng/galleria';
+import { ImageModule } from 'primeng/image';
+import { SkeletonModule } from 'primeng/skeleton';
+
+import { DotMessageService } from '@dotcms/data-access';
+import { DotMessagePipe } from '@dotcms/ui';
+
+import {
+ DotAIImageOrientation,
+ DotGeneratedAIImage
+} from '../../../../shared/services/dot-ai/dot-ai.models';
+
+@Component({
+ selector: 'dot-ai-image-prompt-gallery',
+ standalone: true,
+ templateUrl: './ai-image-prompt-gallery.component.html',
+ imports: [GalleriaModule, ImageModule, NgIf, SharedModule, SkeletonModule, DotMessagePipe],
+ styleUrls: ['./ai-image-prompt-gallery.component.scss']
+})
+export class AiImagePromptGalleryComponent {
+ @Input()
+ isLoading = false;
+
+ @Input()
+ images: DotGeneratedAIImage[] = [];
+
+ @Input()
+ activeImageIndex = 0;
+
+ @Input()
+ orientation: DotAIImageOrientation;
+
+ @Output()
+ activeIndexChange = new EventEmitter();
+
+ dotMessageService = inject(DotMessageService);
+}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.html b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.html
deleted file mode 100644
index ff4a224f46d2..000000000000
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.html
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.scss b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.scss
deleted file mode 100644
index d9f8d166f71c..000000000000
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.scss
+++ /dev/null
@@ -1,44 +0,0 @@
-@use "variables" as *;
-
-:host {
- form {
- position: relative;
- .field {
- margin-bottom: $spacing-5;
- }
- dot-field-validation-message {
- position: absolute;
- left: 0;
- bottom: 50px;
- }
- }
-
- .prompt-input__wrapper {
- border-radius: $border-radius-lg;
- border: 2px solid $color-palette-gray-300;
- cursor: pointer;
- padding: $spacing-4;
- gap: $spacing-4;
- transition: all $basic-speed ease;
- min-height: 540px;
-
- .pi {
- display: inline-flex;
- color: $color-palette-primary-500;
- }
-
- p {
- margin: 0;
- }
-
- &:hover {
- border-color: $color-palette-secondary-200;
- }
-
- &.selected {
- border-color: $color-palette-secondary-300;
- box-shadow: $shadow-l;
- cursor: default;
- }
- }
-}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.spec.ts
deleted file mode 100644
index df327369766e..000000000000
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Spectator, createComponentFactory } from '@ngneat/spectator';
-
-import { AiImagePromptInputComponent } from './ai-image-prompt-input.component';
-
-describe('AiImagePromptInputComponent', () => {
- let spectator: Spectator;
- const createComponent = createComponentFactory(AiImagePromptInputComponent);
-
- it('should create', () => {
- spectator = createComponent();
-
- expect(spectator.component).toBeTruthy();
- });
-});
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.ts
deleted file mode 100644
index 8923c3161b10..000000000000
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/components/ai-image-prompt-input/ai-image-prompt-input.component.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { AsyncPipe, NgIf, NgTemplateOutlet } from '@angular/common';
-import {
- ChangeDetectionStrategy,
- Component,
- EventEmitter,
- inject,
- Input,
- OnChanges,
- Output,
- SimpleChanges
-} from '@angular/core';
-import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
-
-import { AutoFocusModule } from 'primeng/autofocus';
-import { ButtonModule } from 'primeng/button';
-import { InputTextareaModule } from 'primeng/inputtextarea';
-import { TooltipModule } from 'primeng/tooltip';
-
-import { DotMessageService } from '@dotcms/data-access';
-import {
- DotAutofocusDirective,
- DotFieldValidationMessageComponent,
- DotMessagePipe
-} from '@dotcms/ui';
-
-import { PromptType } from '../../ai-image-prompt.models';
-
-//TODO: make this component more flexible is we need more PromptType
-//TODO: disable in auto if you dont have a prompt and content in BlockEditor
-@Component({
- selector: 'dot-ai-image-prompt-input',
- standalone: true,
- templateUrl: './ai-image-prompt-input.component.html',
- styleUrls: ['./ai-image-prompt-input.component.scss'],
- imports: [
- ButtonModule,
- NgIf,
- ReactiveFormsModule,
- TooltipModule,
- NgTemplateOutlet,
- InputTextareaModule,
- DotMessagePipe,
- AsyncPipe,
- DotAutofocusDirective,
- AutoFocusModule,
- DotFieldValidationMessageComponent
- ],
-
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class AiImagePromptInputComponent implements OnChanges {
- isSelected = false;
-
- @Input()
- placeholder: string;
-
- @Input()
- isLoading: boolean;
-
- @Input({ required: true })
- type: PromptType;
-
- @Output()
- promptChanged = new EventEmitter();
-
- form = inject(FormBuilder).group({
- prompt: ['', Validators.required]
- });
-
- dotMessageService = inject(DotMessageService);
-
- @Input()
- set selected(isSelected: boolean) {
- this.isSelected = isSelected;
- this.resetForm();
- }
-
- get promptControl(): FormControl {
- return this.form.get('prompt') as FormControl;
- }
-
- /**
- * Emit the prompt
- *
- * @return {void}
- */
- generateImage(): void {
- if (this.form.valid) {
- this.promptChanged.emit(this.promptControl.value);
- this.disableForm();
- }
- }
-
- ngOnChanges(changes: SimpleChanges): void {
- const { type } = changes;
- if (type && type.currentValue === 'auto') {
- this.promptControl.clearValidators();
- this.form.updateValueAndValidity();
- }
- }
-
- private resetForm() {
- this.form.enable();
- this.form.reset();
- }
-
- private disableForm() {
- this.form.disable();
- }
-}
diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts
index 315dcaece01e..a9b84201991b 100644
--- a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts
+++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts
@@ -6,18 +6,12 @@ import { Instance, Props } from 'tippy.js';
import { ComponentRef } from '@angular/core';
-import { ConfirmationService } from 'primeng/api';
-
import { filter, skip, takeUntil } from 'rxjs/operators';
import { Editor } from '@tiptap/core';
-import { DotMessageService } from '@dotcms/data-access';
-
-import { findNodeByType, getAIPlaceholderImage } from '../../../shared';
-import { NodeTypes } from '../../bubble-menu/models';
import { AIImagePromptComponent } from '../ai-image-prompt.component';
-import { AI_IMAGE_PROMPT_PLUGIN_KEY, DOT_AI_IMAGE_CONTENT_KEY } from '../ai-image-prompt.extension';
+import { AI_IMAGE_PROMPT_PLUGIN_KEY } from '../ai-image-prompt.extension';
import { DotAiImagePromptStore } from '../ai-image-prompt.store';
interface AIImagePromptProps {
@@ -35,8 +29,6 @@ export type AIImagePromptViewProps = AIImagePromptProps & {
view: EditorView;
};
-export const AI_IMAGE_PLACEHOLDER_PROPERTY = 'isAIPlaceholder';
-
export class AIImagePromptView {
public editor: Editor;
@@ -58,6 +50,10 @@ export class AIImagePromptView {
private store: DotAiImagePromptStore;
+ /**
+ * Creates a new instance of the AIImagePromptView class.
+ * @param {AIImagePromptViewProps} props - The properties for the component.
+ */
constructor(props: AIImagePromptViewProps) {
const { editor, element, view, pluginKey, component } = props;
@@ -86,83 +82,19 @@ export class AIImagePromptView {
});
/**
- * Subscription fired by the store when the dialog change of the state
- * Handle the click of Generate button
- */
- this.store.isLoading$
- .pipe(
- filter((isLoading) => isLoading === true),
- takeUntil(this.destroy$)
- )
- .subscribe(() => {
- const placeholder = getAIPlaceholderImage(this.editor);
-
- if (placeholder) {
- // A regenerate has been requested, so we need to delete the placeholder image
- this.editor
- .chain()
- .deleteRange({
- from: placeholder.from,
- to: placeholder.to
- })
- .insertLoaderNode(true, placeholder.from)
- .run();
- } else {
- // A new image is being inserted
- this.store.hideDialog();
- this.editor.chain().insertLoaderNode().closeImagePrompt().run();
- }
- });
-
- /**
- * Subscription fired by an error and remove the loader node
- */
- this.store.errorMsg$
- .pipe(
- filter((hasError) => !!hasError),
- takeUntil(this.destroy$)
- )
- .subscribe((error) => {
- const loaderNodes = findNodeByType(this.editor, NodeTypes.LOADER);
- this.editor.commands.deleteRange({
- from: loaderNodes[0].from,
- to: loaderNodes[0].to
- });
- // call the confirmation service
-
- this.component.injector.get(ConfirmationService).confirm({
- key: 'ai-image-prompt-msg',
- message: this.component.injector.get(DotMessageService).get(error),
- header: 'Error',
- rejectVisible: false,
- acceptVisible: false
- });
- });
-
- /**
- * Subscription fired by the store when the prompt get a new contentlet to show
+ * Subscription fired by the store when image is seleted
+ * from the gallery to be inserted it into the editor
*/
- this.store.getContentlets$
+ this.store.selectedImage$
.pipe(
- filter((contentlets) => contentlets.length > 0),
+ filter((selectedImage) => !!selectedImage),
takeUntil(this.destroy$)
)
- .subscribe((contentlets) => {
- const data = Object.values(contentlets[0])[0];
-
- const loaderNodes = findNodeByType(this.editor, NodeTypes.LOADER);
-
- //Trust in this property to identify the image as a placeholder, until the user accept the content.
- data[AI_IMAGE_PLACEHOLDER_PROPERTY] = true;
-
- if (loaderNodes) {
- this.editor
- .chain()
- .deleteRange({ from: loaderNodes[0].from, to: loaderNodes[0].to })
- .insertImage(data, loaderNodes[0].from)
- .openAIContentActions(DOT_AI_IMAGE_CONTENT_KEY)
- .run();
- }
+ .subscribe((selectedImage) => {
+ this.editor.chain().insertImage(selectedImage.response.contentlet).run();
+ // A new image is being inserted
+ this.store.hideDialog();
+ this.editor.chain().closeImagePrompt().run();
});
}
diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.models.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.models.ts
index 46cb392684d3..06d0a7f631a1 100644
--- a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.models.ts
+++ b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.models.ts
@@ -1,3 +1,7 @@
+import { DotCMSContentlet } from '@dotcms/dotcms-models';
+
+import { PromptType } from '../../../extensions/ai-image-prompt/ai-image-prompt.models';
+
export interface AiPluginResponse {
id: string;
object: string;
@@ -25,6 +29,9 @@ interface Usage {
total_tokens: number;
}
+/**
+ * Represents the response received from the DotAI Image API.
+ */
export interface DotAIImageResponse {
originalPrompt: string;
response: string;
@@ -33,6 +40,41 @@ export interface DotAIImageResponse {
url: string;
}
+/**
+ * Represents an AI image prompt and the possible config options.
+ */
+export interface AIImagePrompt {
+ text: string;
+ type: PromptType;
+ size: DotAIImageOrientation;
+}
+
+/**
+ Represents the response received from the DotAI Image API plus
+ the contentle generated by DotCMS with the response data
+ */
+export interface DotAIImageContent extends DotAIImageResponse {
+ contentlet: DotCMSContentlet;
+}
+
+/**
+ * Represents the response and request of a generated AI image,
+ * to keep sync when the user change between the gallery and the form
+ */
+export interface DotGeneratedAIImage {
+ request: AIImagePrompt;
+ response: DotAIImageContent;
+}
+
+/**
+ * Represents the possible orientations of a Dot AI image.
+ */
+export const enum DotAIImageOrientation {
+ HORIZONTAL = '1792x1024',
+ SQUARE = '1024x1024',
+ VERTICAL = '1024x1792'
+}
+
export interface DotAICompletionsConfig {
apiImageUrl: string;
apiKey: string;
diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.spec.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.spec.ts
index 7dae8a9419b5..7df1fb6476ac 100644
--- a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.spec.ts
+++ b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.spec.ts
@@ -4,6 +4,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/
import { DotCMSContentlet } from '@dotcms/dotcms-models';
+import { DotAIImageContent, DotAIImageOrientation, DotAIImageResponse } from './dot-ai.models';
import { DotAiService } from './dot-ai.service';
describe('DotAiService', () => {
@@ -51,26 +52,50 @@ describe('DotAiService', () => {
req.flush(null, { status: 500, statusText: 'Server Error' });
});
- it('should generate image', () => {
+ it('should generate and publish an image', () => {
const mockPrompt = 'Test prompt';
- const mockResponse = 'Test response';
-
- spectator.service.generateImage(mockPrompt).subscribe((response) => {
- expect(response).toEqual(mockResponse);
+ const mockGenerateResponse: DotAIImageResponse = {
+ response: 'temp_file123',
+ tempFileName: 'Test Imagae'
+ } as unknown as DotAIImageContent;
+ const mockPublishResponse = [{ '123': 'testContent' }] as unknown as DotCMSContentlet[];
+ const mockPublishRequest = [
+ {
+ baseType: 'dotAsset',
+ asset: mockGenerateResponse.response,
+ title: mockGenerateResponse.tempFileName,
+ hostFolder: '',
+ indexPolicy: 'WAIT_FOR'
+ }
+ ];
+ const size = DotAIImageOrientation.SQUARE;
+
+ spectator.service.generateAndPublishImage(mockPrompt, size).subscribe((response) => {
+ expect(response).toEqual({
+ contentlet: Object.values(mockPublishResponse[0])[0],
+ ...mockGenerateResponse
+ });
});
- const req = httpTestingController.expectOne('/api/v1/ai/image/generate');
+ const generateRequest = httpTestingController.expectOne('/api/v1/ai/image/generate');
+ const publishRequest = httpTestingController.expectOne(
+ '/api/v1/workflow/actions/default/fire/PUBLISH'
+ );
- expect(req.request.method).toEqual('POST');
- expect(JSON.parse(req.request.body)).toEqual({ prompt: mockPrompt });
+ expect(generateRequest.request.method).toEqual('POST');
+ expect(JSON.parse(generateRequest.request.body)).toEqual({ prompt: mockPrompt, size });
- req.flush({ response: mockResponse });
+ expect(publishRequest.request.method).toEqual('POST');
+ expect(JSON.parse(generateRequest.request.body)).toEqual({ mockPublishRequest });
+
+ generateRequest.flush({ response: mockGenerateResponse });
+ publishRequest.flush({ response: mockPublishResponse });
});
it('should handle errors while generating image', () => {
const mockPrompt = 'Test prompt';
- spectator.service.generateImage(mockPrompt).subscribe(
+ spectator.service.generateAndPublishImage(mockPrompt).subscribe(
() => fail('Expected an error, but received a response'),
(error) => {
expect(error).toBe('Error fetching AI content');
@@ -82,36 +107,10 @@ describe('DotAiService', () => {
req.flush(null, { status: 500, statusText: 'Server Error' });
});
- it('should create and publish contentlet', () => {
- const mockFileId = '123';
- const mockResponse = ['contentlet'] as unknown as DotCMSContentlet[];
-
- spectator.service.createAndPublishContentlet(mockFileId).subscribe((response) => {
- expect(response).toEqual(mockResponse);
- });
-
- const req = httpTestingController.expectOne(
- '/api/v1/workflow/actions/default/fire/PUBLISH'
- );
-
- expect(req.request.method).toEqual('POST');
- expect(JSON.parse(req.request.body)).toEqual({
- contentlets: [
- {
- contentType: 'dotAsset',
- asset: mockFileId,
- hostFolder: '',
- indexPolicy: 'WAIT_FOR'
- }
- ]
- });
- req.flush({ entity: { results: mockResponse } });
- });
-
it('should handle errors while creating and publishing contentlet', () => {
- const mockFileId = '123';
+ const mockPrompt = 'Test prompt' as unknown as DotAIImageResponse;
- spectator.service.createAndPublishContentlet(mockFileId).subscribe(
+ spectator.service.createAndPublishContentlet(mockPrompt).subscribe(
() => fail('Expected an error, but received a response'),
(error) => {
expect(error).toBe('Test Error');
diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.ts
index 98ac8a48c66d..769aa25a66b3 100644
--- a/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.ts
+++ b/core-web/libs/block-editor/src/lib/shared/services/dot-ai/dot-ai.service.ts
@@ -7,7 +7,13 @@ import { catchError, map, pluck, switchMap } from 'rxjs/operators';
import { DotCMSContentlet } from '@dotcms/dotcms-models';
-import { AiPluginResponse, DotAICompletionsConfig, DotAIImageResponse } from './dot-ai.models';
+import {
+ AiPluginResponse,
+ DotAICompletionsConfig,
+ DotAIImageContent,
+ DotAIImageOrientation,
+ DotAIImageResponse
+} from './dot-ai.models';
import { AI_PLUGIN_KEY } from '../../utils';
@@ -17,8 +23,6 @@ const headers = new HttpHeaders({
'Content-Type': 'application/json'
});
-type ImageSize = '1024x1024' | '1024x1792' | '1792x1024';
-
@Injectable()
export class DotAiService {
private http: HttpClient = inject(HttpClient);
@@ -65,12 +69,12 @@ export class DotAiService {
*
* @param {string} prompt - The prompt for generating the image.
* @param {string} size - The size of the image to be generated (default: '1024x1024').
- * @returns {Observable} - An observable that emits an array of DotCMSContentlet objects.
+ * @returns {Observable} - An observable that emits an array of DotCMSContentlet objects.
*/
public generateAndPublishImage(
prompt: string,
- size: ImageSize = '1024x1024'
- ): Observable {
+ size = DotAIImageOrientation.HORIZONTAL
+ ): Observable {
return this.http
.post(
`${API_ENDPOINT}/image/generate`,
@@ -107,8 +111,8 @@ export class DotAiService {
);
}
- private createAndPublishContentlet(image: DotAIImageResponse): Observable {
- const { response, tempFileName } = image;
+ createAndPublishContentlet(aiResponse: DotAIImageResponse): Observable {
+ const { response, tempFileName } = aiResponse;
const contentlets: Partial[] = [
{
baseType: 'dotAsset',
@@ -125,11 +129,15 @@ export class DotAiService {
})
.pipe(
pluck('entity', 'results'),
+ map((contentlets: DotCMSContentlet[]) => ({
+ contentlet: Object.values(contentlets[0])[0],
+ ...aiResponse
+ })),
catchError(() =>
throwError(
'block-editor.extension.ai-image.api-error.error-publishing-ai-image'
)
)
- ) as Observable;
+ ) as Observable;
}
}
diff --git a/core-web/libs/block-editor/src/lib/shared/utils/parser.utils.ts b/core-web/libs/block-editor/src/lib/shared/utils/parser.utils.ts
index 474c59b84ca8..05b9447fa645 100644
--- a/core-web/libs/block-editor/src/lib/shared/utils/parser.utils.ts
+++ b/core-web/libs/block-editor/src/lib/shared/utils/parser.utils.ts
@@ -1,7 +1,5 @@
import { Content, JSONContent } from '@tiptap/core';
-import { AI_IMAGE_PLACEHOLDER_PROPERTY, NodeTypes } from '../../extensions';
-
interface BlockMap {
[key: string]: boolean;
}
@@ -96,44 +94,6 @@ export const purifyNodeTree = (content: JSONContent[], blocksMap: BlockMap): JSO
return allowedContent;
};
-/**
- * Removes the loading nodes from the provided JSONContent array recursively.
- * This is needed because the AI placeholder content are part of the editor,
- * but they are not really valid content.
- *
- * @param {JSONContent[]} content - An array of JSONContent objects representing the content with loading nodes.
- * @return {JSONContent[]} The content array with loading nodes removed.
- */
-export const removeLoadingNodes = (content: JSONContent[]): JSONContent[] => {
- if (!content?.length) {
- return content;
- }
-
- const nodesToRemove = [NodeTypes.AI_CONTENT, NodeTypes.LOADER];
- const allowedContent = [];
-
- for (const i in content) {
- const node = content[i];
-
- if (
- node &&
- !nodesToRemove.includes(node?.type as NodeTypes) &&
- !isAIPlaceholderImage(node)
- ) {
- allowedContent.push({
- ...node,
- content: removeLoadingNodes(node.content)
- });
- }
- }
-
- return allowedContent;
-};
-
-const isAIPlaceholderImage = (node: JSONContent): boolean => {
- return node.type === NodeTypes.DOT_IMAGE && node.attrs?.data?.[AI_IMAGE_PLACEHOLDER_PROPERTY];
-};
-
/**
* Convert the allowBlock array to an object.
* Since we are going to traverse a tree of nodes (using recursion),
diff --git a/core-web/libs/block-editor/src/lib/shared/utils/prosemirror.utils.ts b/core-web/libs/block-editor/src/lib/shared/utils/prosemirror.utils.ts
index 0f1b801263cd..2635f3c582f4 100644
--- a/core-web/libs/block-editor/src/lib/shared/utils/prosemirror.utils.ts
+++ b/core-web/libs/block-editor/src/lib/shared/utils/prosemirror.utils.ts
@@ -4,7 +4,7 @@ import { EditorView } from 'prosemirror-view';
import { Editor } from '@tiptap/core';
-import { AI_IMAGE_PLACEHOLDER_PROPERTY, CustomNodeTypes, NodeTypes } from '../../extensions';
+import { CustomNodeTypes, NodeTypes } from '../../extensions';
const aTagRex = new RegExp(/]*)>(\s|\n| ]*src="[^"]*"[^>]*>)*?<\/a>/gm);
const imgTagRex = new RegExp(/ ]*src="[^"]*"[^>]*>/gm);
@@ -274,21 +274,6 @@ export const findNodeByType = (
return nodes.length ? nodes : null;
};
-/**
- * Get the information about the first occurrence of an AI placeholder image node in the TipTap editor.
- *
- * @param {Editor} editor - The TipTap editor instance.
- * @returns {DotTiptapNodeInformation | null}
- */
-export const getAIPlaceholderImage = (editor: Editor): DotTiptapNodeInformation => {
- const nodes = findNodeByType(editor, NodeTypes.DOT_IMAGE);
- const aIPlaceholderImages = nodes
- ? nodes.filter((nodeInfo) => nodeInfo.node.attrs.data[AI_IMAGE_PLACEHOLDER_PROPERTY])
- : null;
-
- return aIPlaceholderImages?.[0];
-};
-
/**
* Check if the text is an image URL.
*
diff --git a/core-web/libs/block-editor/src/test-setup.ts b/core-web/libs/block-editor/src/test-setup.ts
index 3d6428dcb9d8..d69ec33e305c 100644
--- a/core-web/libs/block-editor/src/test-setup.ts
+++ b/core-web/libs/block-editor/src/test-setup.ts
@@ -1,2 +1,13 @@
// Configure Jest for Angular [https://medium.com/@kyjungok/setup-jest-in-angular-application-22b22609cbcd]
import 'jest-preset-angular/setup-jest';
+
+import { NgModule } from '@angular/core';
+
+// This is needed to mock the PrimeNG SplitButton component to avoid errors while running tests.
+// https://github.com/primefaces/primeng/issues/12945
+@NgModule()
+export class SplitButtonMockModule {}
+
+jest.mock('primeng/splitbutton', () => ({
+ SplitButtonModule: SplitButtonMockModule
+}));
diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_accordion.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_accordion.scss
new file mode 100644
index 000000000000..418a55184ccf
--- /dev/null
+++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_accordion.scss
@@ -0,0 +1,85 @@
+@use "variables" as *;
+
+.p-accordion .p-accordion-header .p-accordion-header-link {
+ padding: $spacing-3;
+ border: 1px solid $color-palette-gray-400;
+ color: $black;
+ font-weight: $font-weight-semi-bold;
+ border-radius: $border-radius-md;
+ transition: box-shadow 0.2s;
+ font-size: $font-size-lg;
+}
+.p-accordion .p-accordion-header .p-accordion-header-link .p-accordion-toggle-icon {
+ margin-right: $spacing-1;
+}
+.p-accordion .p-accordion-header .p-accordion-header-link:focus {
+ outline: 0 none;
+ outline-offset: 0;
+}
+
+.p-accordion .p-accordion-header.p-highlight .p-accordion-header-link {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ border-bottom: none;
+}
+
+.p-accordion .p-accordion-content {
+ padding: $spacing-3;
+ border: 1px solid $color-palette-gray-400;
+ border-top: 0;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ border-bottom-right-radius: $border-radius-md;
+ border-bottom-left-radius: $border-radius-md;
+}
+.p-accordion p-accordiontab .p-accordion-tab {
+ margin-bottom: $spacing-3;
+}
+
+.p-accordion p-accordiontab:first-child .p-accordion-header .p-accordion-header-link {
+ border-top-right-radius: $border-radius-md;
+ border-top-left-radius: $border-radius-md;
+}
+.p-accordion
+ p-accordiontab:last-child
+ .p-accordion-header:not(.p-highlight)
+ .p-accordion-header-link {
+ border-bottom-right-radius: $border-radius-md;
+ border-bottom-left-radius: $border-radius-md;
+}
+.p-accordion p-accordiontab:last-child .p-accordion-content {
+ border-bottom-right-radius: $border-radius-md;
+ border-bottom-left-radius: $border-radius-md;
+}
+
+//Custom
+.p-accordion {
+ .p-accordion-header {
+ .p-accordion-header-link .p-accordion-toggle-icon-end {
+ &.pi {
+ border: 1px solid;
+ border-radius: 100px;
+ height: $spacing-5;
+ width: $spacing-5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $color-palette-primary;
+ }
+
+ &.pi:before {
+ font-size: $spacing-2;
+ }
+ }
+
+ &.p-disabled {
+ .p-accordion-header-link {
+ color: $color-palette-gray-400;
+
+ .p-accordion-toggle-icon-end.pi {
+ color: $color-palette-gray-400;
+ }
+ }
+ }
+ }
+}
diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_galleria.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_galleria.scss
new file mode 100644
index 000000000000..b92cfbc49ca5
--- /dev/null
+++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_galleria.scss
@@ -0,0 +1 @@
+//TODO: Implement Galleria styles.
diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_image.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_image.scss
new file mode 100644
index 000000000000..fd316c982c6f
--- /dev/null
+++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_image.scss
@@ -0,0 +1 @@
+//TODO: implement image styles
diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/theme.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/theme.scss
index 11096ab4830f..410e9647248b 100644
--- a/core-web/libs/dotcms-scss/angular/dotcms-theme/theme.scss
+++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/theme.scss
@@ -1,7 +1,7 @@
@charset "UTF-8";
@use "misc";
-
+@use "components/accordion";
@use "components/avatar";
@use "components/badge";
@use "components/breadcrumb";
@@ -16,6 +16,8 @@
@use "components/divider";
@use "components/dynamicdialog";
@use "components/form";
+@use "components/galleria";
+@use "components/image";
@use "components/inplace";
@use "components/listbox";
@use "components/menu";
diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
index 7354e94894e9..eb890f31dabf 100644
--- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
+++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
@@ -5616,6 +5616,29 @@ block-editor.common.input-prompt-required-error=The information provided is insu
block-editor.extension.ai-content.ask-ai-to-write-something=Ask AI to write something
block-editor.extension.ai-image.dialog-title=Generate AI Image
+block-editor.extension.ai-image.settings=Settings
+block-editor.extension.ai-image.input=Input
+block-editor.extension.ai-image.custom.prompt=Create with Custom Prompt
+block-editor.extension.ai-image.custom.prompt.desc=Write your own prompt and have full creative control over the image generation.
+block-editor.extension.ai-image.existing.content=Create based on Existing Content
+block-editor.extension.ai-image.existing.content.desc=Generate and image based on provided content. You may Customize with mood, colors and size.
+block-editor.extension.ai-image.orientation=Orientation
+
+block-editor.extension.ai-image.prompt=Prompt
+block-editor.extension.ai-image.custom.props=Customize images Properties ( Optional )
+
+block-editor.extension.ai-image.placeholder=Enhance the given content. Detail your desired mood, color preferences.
+block-editor.extension.ai-image.custom.placeholder=Describe the image you want to create, including details like subjects, colors, mood, and style. The more specific, the better!
+
+block-editor.extension.ai-image.rewritten=AI Prompt (Rewritten)
+block-editor.extension.ai-image.generating=Generating
+block-editor.extension.ai-image.generate=Generate
+block-editor.extension.ai-image.regenerate=Regenerate
+block-editor.extension.ai-image.insert=Insert
+
+block-editor.extension.ai-image.orientation.horizontal=Horizontal (1792 x 1024)
+block-editor.extension.ai-image.orientation.square=Square (1024 x 1024)
+block-editor.extension.ai-image.orientation.vertical=Vertical (1024 x 1792)
block-editor.extension.ai-image.input-text.placeholder=Create a realistic image of a cow in the snow
block-editor.extension.ai-image.input-text.tooltip=Describe the type of image you want to generate.