Skip to content

Commit

Permalink
Generate Image with AI for Image Fields (#30335)
Browse files Browse the repository at this point in the history
### Parent Issue

#30062 

### Summary

This pull request focuses on integrating the `primeng/dynamicdialog`
module into the block editor and AI image prompt functionalities. It
includes changes to import necessary services, update component
providers, and refactor the AI image prompt extension to use dynamic
dialogs.

### Integration of `primeng/dynamicdialog`:

*
[`core-web/libs/block-editor/src/lib/block-editor.module.ts`](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4R9):
Added `DynamicDialogModule` to the imports and module declarations.
[[1]](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4R9)
[[2]](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4R61)
*
[`core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts`](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609R19-R20):
Imported `DialogService` and `DotMessageService` and updated the
component providers to include `DialogService`.
[[1]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609R19-R20)
[[2]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609L34-R36)
[[3]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609R80)
[[4]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609R123-R124)
[[5]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609L461-R466)
*
[`core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.extension.ts`](diffhunk://#diff-689103c18ebe5e9dd00423ab7d493e5bf2570c867ec3b67ffef6dfd51ad62bfcL3-R7):
Refactored `AIImagePromptExtension` to use `DialogService` and
`DotMessageService` for creating dynamic dialogs.
[[1]](diffhunk://#diff-689103c18ebe5e9dd00423ab7d493e5bf2570c867ec3b67ffef6dfd51ad62bfcL3-R7)
[[2]](diffhunk://#diff-689103c18ebe5e9dd00423ab7d493e5bf2570c867ec3b67ffef6dfd51ad62bfcL31-R34)
[[3]](diffhunk://#diff-689103c18ebe5e9dd00423ab7d493e5bf2570c867ec3b67ffef6dfd51ad62bfcL74-R82)
*
[`core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.plugin.ts`](diffhunk://#diff-c7ed359690f2aff854a493cbbe30b539d13e742964e9aa5794c7afd4b0a81c18L7-R23):
Updated the AI image prompt plugin to manage dialog lifecycle and
interactions using `DialogService` and `DotMessageService`.
[[1]](diffhunk://#diff-c7ed359690f2aff854a493cbbe30b539d13e742964e9aa5794c7afd4b0a81c18L7-R23)
[[2]](diffhunk://#diff-c7ed359690f2aff854a493cbbe30b539d13e742964e9aa5794c7afd4b0a81c18L37-L38)
[[3]](diffhunk://#diff-c7ed359690f2aff854a493cbbe30b539d13e742964e9aa5794c7afd4b0a81c18L47-R65)
[[4]](diffhunk://#diff-c7ed359690f2aff854a493cbbe30b539d13e742964e9aa5794c7afd4b0a81c18L107-R109)

### Removal of Deprecated Components:

*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.html`](diffhunk://#diff-23b6d93662589dabff9ce186485da98e3e53b435fcbcf0a889e3f5c3008915fcL2):
Removed the deprecated `dot-ai-image-prompt` component.

### Test and Storybook Updates:

*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts`](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dR12):
Updated test providers to include `DialogService` and removed deprecated
stores.
[[1]](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dR12)
[[2]](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dR26)
[[3]](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dL34-R36)
[[4]](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dL93-R103)
[[5]](diffhunk://#diff-d01a58af8fedbafd245fa8166ab7aeb806f756bb643d7b81c9b0d396bfddb89dL589-R592)
*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.stories.ts`](diffhunk://#diff-24e79acf5790caefe96f0fcf5072694a1be1e6fc85ab88480d8244c78e05e78cL2-R6):
Updated Storybook configuration to use `provideHttpClient` and
`DotMessageService`.
[[1]](diffhunk://#diff-24e79acf5790caefe96f0fcf5072694a1be1e6fc85ab88480d8244c78e05e78cL2-R6)
[[2]](diffhunk://#diff-24e79acf5790caefe96f0fcf5072694a1be1e6fc85ab88480d8244c78e05e78cR35-L37)

### Miscellaneous:

*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts`](diffhunk://#diff-06beb921499e762bd5f4d25d68d0a24a22477acf79372fb0350ee6d9a7b0c01aL4-R10):
Removed unused imports and updated to use `takeUntilDestroyed` for
cleanup.
[[1]](diffhunk://#diff-06beb921499e762bd5f4d25d68d0a24a22477acf79372fb0350ee6d9a7b0c01aL4-R10)
[[2]](diffhunk://#diff-06beb921499e762bd5f4d25d68d0a24a22477acf79372fb0350ee6d9a7b0c01aL24-R28)
[[3]](diffhunk://#diff-06beb921499e762bd5f4d25d68d0a24a22477acf79372fb0350ee6d9a7b0c01aL50-R50)
[[4]](diffhunk://#diff-06beb921499e762bd5f4d25d68d0a24a22477acf79372fb0350ee6d9a7b0c01aL90)
*
[`core-web/libs/dotcms-scss/angular/dotcms-theme/components/_dialog.scss`](diffhunk://#diff-123bc2019d92d71d5d2d93da360b73b30eefd6efb45c2c4686e92f6ed2995c68R112-R121):
Added styles for transparent AI dialog masks.
  • Loading branch information
nicobytes authored Oct 15, 2024
1 parent 9ea0573 commit 17da5c2
Show file tree
Hide file tree
Showing 23 changed files with 727 additions and 472 deletions.
2 changes: 2 additions & 0 deletions core-web/libs/block-editor/src/lib/block-editor.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ConfirmationService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DynamicDialogModule } from 'primeng/dynamicdialog';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { PaginatorModule } from 'primeng/paginator';

Expand Down Expand Up @@ -57,6 +58,7 @@ const initTranslations = (dotMessageService: DotMessageService) => {
ReactiveFormsModule,
SharedModule,
PrimengModule,
DynamicDialogModule,
AssetFormModule,
DotFieldRequiredDirective,
UploadPlaceholderComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

import { DialogService } from 'primeng/dynamicdialog';

import { debounceTime, map, take, takeUntil } from 'rxjs/operators';

import { AnyExtension, Content, Editor, JSONContent } from '@tiptap/core';
Expand All @@ -31,7 +33,7 @@ import { Underline } from '@tiptap/extension-underline';
import { Youtube } from '@tiptap/extension-youtube';
import StarterKit, { StarterKitOptions } from '@tiptap/starter-kit';

import { DotPropertiesService, DotAiService } from '@dotcms/data-access';
import { DotPropertiesService, DotAiService, DotMessageService } from '@dotcms/data-access';
import {
DotCMSContentlet,
DotCMSContentTypeField,
Expand Down Expand Up @@ -75,6 +77,7 @@ import {
templateUrl: './dot-block-editor.component.html',
styleUrls: ['./dot-block-editor.component.scss'],
providers: [
DialogService,
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DotBlockEditorComponent),
Expand Down Expand Up @@ -117,6 +120,8 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy, ControlValueA
private readonly cd = inject(ChangeDetectorRef);
private readonly dotPropertiesService = inject(DotPropertiesService);
private isAIPluginInstalled$: Observable<boolean>;
readonly #dialogService = inject(DialogService);
readonly #dotMessageService = inject(DotMessageService);

constructor(
private readonly viewContainerRef: ViewContainerRef,
Expand Down Expand Up @@ -458,7 +463,7 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy, ControlValueA
if (isAIPluginInstalled) {
extensions.push(
AIContentPromptExtension(this.viewContainerRef),
AIImagePromptExtension(this.viewContainerRef)
AIImagePromptExtension(this.#dialogService, this.#dotMessageService)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PluginKey } from 'prosemirror-state';

import { ViewContainerRef } from '@angular/core';
import { DialogService } from 'primeng/dynamicdialog';

import { Extension } from '@tiptap/core';

import { DotAIImagePromptComponent } from '@dotcms/ui';
import { DotMessageService } from '@dotcms/data-access';

import { aiImagePromptPlugin } from './ai-image-prompt.plugin';

Expand All @@ -28,7 +28,10 @@ export const AI_IMAGE_PROMPT_PLUGIN_KEY = new PluginKey('aiImagePrompt-form');

export const AI_IMAGE_PROMPT_EXTENSION_NAME = 'aiImagePrompt';

export const AIImagePromptExtension = (viewContainerRef: ViewContainerRef) => {
export const AIImagePromptExtension = (
dialogService: DialogService,
dotMessageService: DotMessageService
) => {
return Extension.create<AIImagePromptOptions>({
name: AI_IMAGE_PROMPT_EXTENSION_NAME,

Expand Down Expand Up @@ -71,15 +74,12 @@ export const AIImagePromptExtension = (viewContainerRef: ViewContainerRef) => {
},

addProseMirrorPlugins() {
const component = viewContainerRef.createComponent(DotAIImagePromptComponent);
component.changeDetectorRef.detectChanges();

return [
aiImagePromptPlugin({
pluginKey: this.options.pluginKey,
editor: this.editor,
element: component.location.nativeElement,
component: component
dialogService: dialogService,
dotMessageService: dotMessageService
})
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ import { EditorView } from 'prosemirror-view';
import { Subject } from 'rxjs';
import { Instance, Props } from 'tippy.js';

import { ComponentRef } from '@angular/core';
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';

import { filter, skip, takeUntil } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';

import { Editor } from '@tiptap/core';

import { DotAIImagePromptComponent, DotAiImagePromptStore } from '@dotcms/ui';
import { DotMessageService } from '@dotcms/data-access';
import { DotGeneratedAIImage } from '@dotcms/dotcms-models';
import { DotAIImagePromptComponent } from '@dotcms/ui';

import { AI_IMAGE_PROMPT_PLUGIN_KEY } from './ai-image-prompt.extension';

interface AIImagePromptProps {
pluginKey: PluginKey;
editor: Editor;
element: HTMLElement;
component: ComponentRef<DotAIImagePromptComponent>;
dialogService: DialogService;
dotMessageService: DotMessageService;
}

interface PluginState {
Expand All @@ -34,8 +36,6 @@ export class AIImagePromptView {

public node: Node;

public element: HTMLElement;

public view: EditorView;

public tippy: Instance | undefined;
Expand All @@ -44,58 +44,25 @@ export class AIImagePromptView {

public pluginKey: PluginKey;

public component: ComponentRef<DotAIImagePromptComponent>;

private destroy$ = new Subject<boolean>();

private store: DotAiImagePromptStore;
#dialogService: DialogService | null = null;
#dotMessageService: DotMessageService | null = null;
#dialogRef: DynamicDialogRef | null = null;

/**
* 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;
const { editor, view, pluginKey, dialogService, dotMessageService } = props;

this.editor = editor;
this.element = element;
this.view = view;

this.element.remove();
this.pluginKey = pluginKey;
this.component = component;

this.store = this.component.injector.get(DotAiImagePromptStore);

/**
* Subscription fired by the store when the dialog change of the state
* Handle the manual close of the dialog (esc, click outside, x button)
*/
this.store.isOpenDialog$
.pipe(
skip(1),
filter((value) => value === false),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.editor.commands.closeImagePrompt();
});

/**
* Subscription fired by the store when image is seleted
* from the gallery to be inserted it into the editor
*/
this.store.selectedImage$
.pipe(
filter((selectedImage) => !!selectedImage),
takeUntil(this.destroy$)
)
.subscribe((selectedImage) => {
this.editor.chain().insertImage(selectedImage.response.contentlet).run();
// A new image is being inserted
this.store.hideDialog();
this.editor.chain().closeImagePrompt().run();
});
this.#dialogService = dialogService;
this.#dotMessageService = dotMessageService;
}

update(view: EditorView, prevState: EditorState) {
Expand All @@ -104,15 +71,42 @@ export class AIImagePromptView {

// show the dialog
if (next.aIImagePromptOpen && prev.aIImagePromptOpen === false) {
this.store.showDialog(this.editor.getText());
}
const context = this.editor.getText();

const header = this.#dotMessageService.get(
'block-editor.extension.ai-image.dialog-title'
);

this.#dialogRef = this.#dialogService.open(DotAIImagePromptComponent, {
header,
appendTo: 'body',
closeOnEscape: false,
draggable: false,
keepInViewport: false,
maskStyleClass: 'p-dialog-mask-transparent-ai',
resizable: false,
modal: true,
width: '90%',
style: { 'max-width': '1040px' },
data: { context }
});

// hide the dialog handled by isOpenDialog$ subscription
this.#dialogRef.onClose
.pipe(takeUntil(this.destroy$))
.subscribe((selectedImage: DotGeneratedAIImage) => {
if (selectedImage) {
this.editor.chain().insertImage(selectedImage.response.contentlet).run();
}

this.editor.commands.closeImagePrompt();
});
}
}

destroy() {
this.destroy$.next(true);
this.destroy$.complete();
this.#dialogRef?.close();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@
background-color: transparent;
-webkit-backdrop-filter: blur($blur-md);
backdrop-filter: blur($blur-md);
padding: 0;
align-items: center;
}

.p-dialog-mask.p-dialog-mask-transparent-ai.p-component-overlay {
background-color: transparent;
-webkit-backdrop-filter: blur($blur-md);
backdrop-filter: blur($blur-md);
padding: 0;
align-items: center;
}

.p-dialog-mask.p-dialog-mask-transparent-nested.p-component-overlay {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@if (vm$ | async; as vm) {
<dot-ai-image-prompt />
<div
[ngClass]="{
'binary-field__container--uploading': vm.status === BinaryFieldStatus.UPLOADING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { provideHttpClient } from '@angular/common/http';
import { Component, NgZone } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import {
Expand All @@ -22,6 +23,7 @@ import { By } from '@angular/platform-browser';

import { ButtonModule, Button } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { DialogService } from 'primeng/dynamicdialog';

import {
DotAiService,
Expand All @@ -31,7 +33,7 @@ import {
} from '@dotcms/data-access';
import { DotCMSTempFile } from '@dotcms/dotcms-models';
import { DotEditContentBinaryFieldComponent } from '@dotcms/edit-content';
import { DotAiImagePromptStore, DropZoneErrorType, DropZoneFileEvent } from '@dotcms/ui';
import { DropZoneErrorType, DropZoneFileEvent } from '@dotcms/ui';
import { dotcmsContentletMock } from '@dotcms/utils-testing';

import { DotBinaryFieldPreviewComponent } from './components/dot-binary-field-preview/dot-binary-field-preview.component';
Expand Down Expand Up @@ -90,14 +92,15 @@ describe('DotEditContentBinaryFieldComponent', () => {
component: DotEditContentBinaryFieldComponent,
componentProviders: [
DotBinaryFieldStore,
DotAiImagePromptStore,
DotBinaryFieldEditImageService,
DotAiService
DotAiService,
DialogService
],
componentViewProviders: [
{ provide: ControlContainer, useValue: createFormGroupDirectiveMock() }
],
providers: [
provideHttpClient(),
DotBinaryFieldValidatorService,
{
provide: DotLicenseService,
Expand Down Expand Up @@ -586,7 +589,7 @@ describe('DotEditContentBinaryFieldComponent - ControlValueAccessor', () => {
ReactiveFormsModule,
DotEditContentBinaryFieldComponent
],
providers: [DotAiService, DotAiImagePromptStore]
providers: [DotAiService, provideHttpClient()]
});

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { moduleMetadata, StoryObj, Meta } from '@storybook/angular';
import { moduleMetadata, StoryObj, Meta, applicationConfig } from '@storybook/angular';
import { of } from 'rxjs';

import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { provideHttpClient } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { ButtonModule } from 'primeng/button';
Expand Down Expand Up @@ -32,9 +32,11 @@ const meta: Meta<DotEditContentBinaryFieldComponent> = {
title: 'Library / Edit Content / Binary Field',
component: DotEditContentBinaryFieldComponent,
decorators: [
applicationConfig({
providers: [provideHttpClient(), DotMessageService]
}),
moduleMetadata({
imports: [
HttpClientModule,
BrowserAnimationsModule,
CommonModule,
ButtonModule,
Expand Down
Loading

0 comments on commit 17da5c2

Please sign in to comment.