From 5bd76355d4c1e28d53908608764bbe9049e6d848 Mon Sep 17 00:00:00 2001 From: Freddy Montes <751424+fmontes@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:59:04 -0600 Subject: [PATCH] feat(block-editor): AI Image prompt Block (#26405) (#26485) * Created AI content node * Created ai-content-service * Created AI prompt content extension * Update ai-content.service.ts * Optimised and aligned extension code * Resolved comments on PR * Removed ai content node file * Update ai-content.service.ts * Remove import path for ai-content-node * Updated the ai-prompt form * align ai text prompt extension * Resovled comments on pull * Fix the aiTextPrompt form * Updated extension related code * Update ai-content-prompt.plugin.ts * Remove some extra code * Fixed outside click for aiContentPrompt extension * Created ai content node * Updated ai-content node * Remove unused code, resolve comments on PR * ai-content-prompt extension code optimisation * Update ai-content-prompt.plugin.ts * Integrated with AI api * insert ai node on response * textPrompt extension code optimsation * Update ai-content.service.ts * Resolve comments on pull req * handle close extension on outside click and content flip fix * Added focus field method and type for form * Change name of ai node creation command * Update the ai-content icon in the actions menu list * Update ai-content-prompt.component.scss * Resolved comments on pull * Added pening status and update the name of destroy var * Implement ai-content-actions extension * ai-prompt extension code optimisation * ai-text-prompt block optimisation * Update main.ts * feat: Created ai image prompt extension * Resolve comments on pull * Created loader node and handle loading image * Resolve comments on PR * Code optimisation for ai-content-actions extension * Fix css for p-listbox * Handle ai content actions to work in two diff context, code optimisation * Code optimisation * clean code from extra code --------- Co-authored-by: Nikola Trajkovic <82508651+nikolatrajkovic24@users.noreply.github.com> Co-authored-by: Nikola Co-authored-by: Will Ezell --- .../src/lib/block-editor.module.ts | 7 +- .../dot-block-editor.component.scss | 32 ++++ .../dot-block-editor.component.ts | 8 +- .../action-button/actions-menu.extension.ts | 3 +- .../ai-content-actions.component.ts | 17 +- .../plugins/ai-content-actions.plugin.ts | 31 ++-- .../ai-image-prompt.component.html | 118 ++++++++++++ .../ai-image-prompt.component.scss | 88 +++++++++ .../ai-image-prompt.component.spec.ts | 28 +++ .../ai-image-prompt.component.ts | 73 ++++++++ .../ai-image-prompt.extension.ts | 84 +++++++++ .../plugins/ai-image-prompt.plugin.ts | 175 ++++++++++++++++++ .../extensions/ai-image-prompt/utils/index.ts | 20 ++ .../extensions/bubble-menu/models/index.ts | 1 + .../lib/extensions/bubble-menu/utils/index.ts | 3 +- .../block-editor/src/lib/extensions/index.ts | 3 + .../src/lib/nodes/image-node/helpers/index.ts | 8 +- .../libs/block-editor/src/lib/nodes/index.ts | 1 + .../src/lib/nodes/loader/loader.node.ts | 77 ++++++++ .../suggestions/suggestion-icons.ts | 3 + .../services/ai-content/ai-content.service.ts | 65 ++++++- .../src/lib/shared/utils/suggestion.utils.ts | 8 +- 22 files changed, 819 insertions(+), 34 deletions(-) create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.extension.ts create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts create mode 100644 core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/utils/index.ts create mode 100644 core-web/libs/block-editor/src/lib/nodes/loader/loader.node.ts diff --git a/core-web/libs/block-editor/src/lib/block-editor.module.ts b/core-web/libs/block-editor/src/lib/block-editor.module.ts index 49f38a572009..6d74032dda67 100644 --- a/core-web/libs/block-editor/src/lib/block-editor.module.ts +++ b/core-web/libs/block-editor/src/lib/block-editor.module.ts @@ -12,6 +12,7 @@ import { DotEditorCountBarComponent } from './components/dot-editor-count-bar/do import { AIContentPromptComponent, AIContentActionsComponent, + AIImagePromptComponent, BubbleLinkFormComponent, BubbleMenuButtonComponent, BubbleMenuComponent, @@ -54,7 +55,8 @@ import { SharedModule } from './shared/shared.module'; DotEditorCountBarComponent, FloatingButtonComponent, AIContentPromptComponent, - AIContentActionsComponent + AIContentActionsComponent, + AIImagePromptComponent ], providers: [DotUploadFileService, LoggerService, StringUtils, AiContentService], exports: [ @@ -66,7 +68,8 @@ import { SharedModule } from './shared/shared.module'; BubbleFormComponent, DotBlockEditorComponent, AIContentPromptComponent, - AIContentActionsComponent + AIContentActionsComponent, + AIImagePromptComponent ] }) export class BlockEditorModule {} diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss index 21d7425722d4..26403a09ba57 100644 --- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss +++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss @@ -313,6 +313,38 @@ border: 1px solid $color-palette-primary-300; } } + + .loader-style { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + min-height: 12.5rem; + min-width: 25rem; + width: 25.5625rem; + height: 17.375rem; + padding: 0.5rem; + border-radius: 0.5rem; + border: 1.5px solid $color-palette-gray-400; + } + + .p-progress-spinner { + border: 5px solid $color-palette-gray-300; + border-radius: 50%; + border-top: 5px solid $color-palette-primary; + width: 2.4rem; + height: 2.4rem; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } } tiptap-editor { 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 73294816a0af..013b319581d5 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 @@ -51,10 +51,11 @@ import { AssetUploader, DotComands, AIContentPromptExtension, + AIImagePromptExtension, AIContentActionsExtension } from '../../extensions'; import { DotPlaceholder } from '../../extensions/dot-placeholder/dot-placeholder-plugin'; -import { ContentletBlock, ImageNode, VideoNode, AIContentNode } from '../../nodes'; +import { ContentletBlock, ImageNode, VideoNode, AIContentNode, LoaderNode } from '../../nodes'; import { formatHTML, removeInvalidNodes, @@ -62,6 +63,7 @@ import { DotMarketingConfigService, RestoreDefaultDOMAttrs } from '../../shared'; + @Component({ selector: 'dot-block-editor', templateUrl: './dot-block-editor.component.html', @@ -111,7 +113,8 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy { ['image', ImageNode], ['video', VideoNode], ['table', DotTableExtension()], - ['aiContent', AIContentNode] + ['aiContent', AIContentNode], + ['loader', LoaderNode] ]); private destroy$: Subject = new Subject(); @@ -383,6 +386,7 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy { DotBubbleMenuExtension(this.viewContainerRef), BubbleFormExtension(this.viewContainerRef), AIContentPromptExtension(this.viewContainerRef), + AIImagePromptExtension(this.viewContainerRef), AIContentActionsExtension(this.viewContainerRef), DotFloatingButton(this.injector, this.viewContainerRef), DotTableCellExtension(this.viewContainerRef), diff --git a/core-web/libs/block-editor/src/lib/extensions/action-button/actions-menu.extension.ts b/core-web/libs/block-editor/src/lib/extensions/action-button/actions-menu.extension.ts index e3f05f6463bc..6c6d30617e4c 100644 --- a/core-web/libs/block-editor/src/lib/extensions/action-button/actions-menu.extension.ts +++ b/core-web/libs/block-editor/src/lib/extensions/action-button/actions-menu.extension.ts @@ -174,7 +174,8 @@ function execCommand({ superscript: () => editor.chain().setSuperscript().focus().run(), video: () => editor.commands.openAssetForm({ type: 'video' }), aiContentPrompt: () => editor.commands.openAIPrompt(), - aiContent: () => editor.commands.insertAINode() + aiContent: () => editor.commands.insertAINode(), + aiImagePrompt: () => editor.commands.openImagePrompt() }; getCustomActions(customBlocks).forEach((option) => { diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/ai-content-actions.component.ts b/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/ai-content-actions.component.ts index 96e8e2615b57..c95252054764 100644 --- a/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/ai-content-actions.component.ts +++ b/core-web/libs/block-editor/src/lib/extensions/ai-content-actions/ai-content-actions.component.ts @@ -1,4 +1,4 @@ -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { Component, EventEmitter, Output, OnInit } from '@angular/core'; @@ -26,6 +26,7 @@ export class AIContentActionsComponent implements OnInit { @Output() actionEmitter = new EventEmitter(); actionOptions!: ActionOption[]; + tooltipContent = 'Describe the size, color palette, style, mood, etc.'; constructor(private aiContentService: AiContentService) {} @@ -56,17 +57,11 @@ export class AIContentActionsComponent implements OnInit { this.actionEmitter.emit(action); } - getLatestContent() { - return this.aiContentService.getLastContentResponse(); + getLatestContent(): string { + return this.aiContentService.getLatestContent(); } - getNewContent(): Observable { - const promptToUse: string = this.aiContentService.getLastUsedPrompt(); - - if (promptToUse) { - return this.aiContentService.getIAContent(promptToUse); - } - - return of(''); + getNewContent(contentType: string): Observable { + return this.aiContentService.getNewContent(contentType); } } 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 dac816801b4f..198b7c44daf7 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 @@ -6,7 +6,7 @@ import tippy, { Instance, Props } from 'tippy.js'; import { ComponentRef } from '@angular/core'; -import { take, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { Editor } from '@tiptap/core'; @@ -88,18 +88,27 @@ export class AIContentActionsView { } private generateContent() { + const nodeType = this.getNodeType(); + this.editor.commands.closeAIContentActions(); - this.component.instance - .getNewContent() - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((newContent) => { - if (newContent) { - this.editor.commands.deleteSelection(); - this.editor.commands.insertAINode(newContent); - this.editor.commands.openAIContentActions(); - } - }); + this.component.instance.getNewContent(nodeType).subscribe((newContent) => { + if (newContent) { + this.editor.commands.deleteSelection(); + this.editor.commands.insertAINode(newContent); + this.editor.commands.openAIContentActions(); + } + }); + } + + private getNodeType() { + const { state } = this.editor.view; + const { doc, selection } = state; + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const node = doc?.nodeAt(from); + + return node.type.name; } private deleteContent() { 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 new file mode 100644 index 000000000000..303fc31a4c12 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.html @@ -0,0 +1,118 @@ +
+

Generate AI Image

+
+
+
+ + + + + + +
+ + Generate an AI image based on your input and requests. + + + + + + +
+
+ +
+ +
+ +
+
+
+ +
+
+ + + + + +
+ Auto-Generate an Image based on the content created within the + block editor. + + + + + +
+
+ +
+ +
+ +
+
+
+
+
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 new file mode 100644 index 000000000000..fc1f3e306781 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.scss @@ -0,0 +1,88 @@ +@use "variables" as *; + +.ai-image-dialog { + position: fixed; + top: 0%; + left: 0%; + transform: translate(-55%, -5%); + width: 90vw; + height: 75vh; + padding: $spacing-4; + margin: $spacing-5; + border: 2px solid $color-palette-gray-400; + box-shadow: 0px 8px 16px 0px rgba(20, 21, 26, 0.08); + border-radius: $border-radius-lg; + z-index: 9999; + background: $white; +} + +.title { + font-size: $font-size-xl; + font-weight: $font-weight-semi-bold; + margin-bottom: $spacing-3 0; +} + +form { + width: 100%; +} + +.input-field-wrapper { + width: 100%; + height: 7rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: $spacing-3 0; + + .input-field { + width: 100%; + height: 100%; + padding: $spacing-1; + line-height: normal; + background-color: $white; + border: 1px solid #ccc; + border-radius: $border-radius-sm; + color: $color-palette-gray-700; + } +} + +.choice-div-container { + display: flex; + justify-content: space-between; + height: 80%; + margin-bottom: $spacing-1; +} + +.choice-div { + height: 100%; + width: 48%; + padding: $spacing-2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: $border-radius-lg; + border: 2px solid $color-palette-gray-400; + cursor: pointer; + + &:hover { + border: 2px solid #c6b3f9; + } +} + +.choice-div .icon-wrapper, +.submit-btn-container { + margin-bottom: $spacing-3; + display: flex; + justify-content: center; + align-items: center; +} + +span { + text-align: center; +} + +.icon-wrapper { + background-color: $white; +} 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 new file mode 100644 index 000000000000..2c731c999a6f --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.spec.ts @@ -0,0 +1,28 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { AIImagePromptComponent } from './ai-image-prompt.component'; + +import { AiContentService } from '../../shared'; + +describe('AIImagePromptComponent', () => { + let component: AIImagePromptComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, HttpClientTestingModule], + declarations: [AIImagePromptComponent], + providers: [AiContentService] + }).compileComponents(); + + fixture = TestBed.createComponent(AIImagePromptComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000000..5a2edfe2bacd --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.component.ts @@ -0,0 +1,73 @@ +import { of } from 'rxjs'; + +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +import { catchError, switchMap } from 'rxjs/operators'; + +import { AiContentService } from '../../shared/services/ai-content/ai-content.service'; + +@Component({ + selector: 'dot-ai-image-prompt', + templateUrl: './ai-image-prompt.component.html', + styleUrls: ['./ai-image-prompt.component.scss'] +}) +export class AIImagePromptComponent { + form = this.fb.group({ + promptGenerate: ['', Validators.required], + promptAutoGenerate: ['', Validators.required] + }); + + isFormSubmitting = false; + showFormOne = true; + showFormTwo = false; + + @Output() formSubmission = new EventEmitter(); + @Output() aiResponse = new EventEmitter(); + + constructor(private fb: FormBuilder, private aiContentService: AiContentService) {} + + onSubmit() { + this.isFormSubmitting = true; + const promptGenerate = this.form.value.promptGenerate; + const promptAutoGenerate = this.form.value.promptAutoGenerate; + const combinedPrompt = `${promptGenerate} ${promptAutoGenerate}`; + + this.isFormSubmitting = false; + this.formSubmission.emit(true); + + if (prompt) { + this.aiContentService + .getAIImage(combinedPrompt) + .pipe( + catchError(() => of(null)), + switchMap((imageId) => { + if (!imageId) { + return of(null); + } + + return this.aiContentService.createAndPublishContentlet(imageId); + }) + ) + .subscribe((contentlet) => { + this.aiResponse.emit(contentlet); + }); + } + } + + openFormOne() { + this.showFormOne = true; + this.showFormTwo = false; + } + + openFormTwo() { + this.showFormTwo = true; + this.showFormOne = false; + } + + cleanForm() { + this.form.reset(); + this.showFormOne = true; + this.showFormTwo = false; + } +} diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.extension.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.extension.ts new file mode 100644 index 000000000000..e50c5e537752 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/ai-image-prompt.extension.ts @@ -0,0 +1,84 @@ +import { PluginKey } from 'prosemirror-state'; +import { Props } from 'tippy.js'; + +import { ViewContainerRef } from '@angular/core'; + +import { Extension } from '@tiptap/core'; + +import { AIImagePromptComponent } from './ai-image-prompt.component'; +import { aiImagePromptPlugin } from './plugins/ai-image-prompt.plugin'; + +export interface AIImagePromptOptions { + pluginKey: PluginKey; + tippyOptions?: Partial; + element: HTMLElement | null; +} + +declare module '@tiptap/core' { + interface Commands { + AIImagePrompt: { + openImagePrompt: () => ReturnType; + closeImagePrompt: () => ReturnType; + }; + } +} + +export const AI_IMAGE_PROMPT_PLUGIN_KEY = new PluginKey('aiImagePrompt-form'); + +export const AIImagePromptExtension = (viewContainerRef: ViewContainerRef) => { + return Extension.create({ + name: 'aiImagePrompt', + + addOptions() { + return { + element: null, + tippyOptions: {}, + pluginKey: AI_IMAGE_PROMPT_PLUGIN_KEY + }; + }, + + addCommands() { + return { + openImagePrompt: + () => + ({ chain }) => { + return chain() + .command(({ tr }) => { + tr.setMeta(AI_IMAGE_PROMPT_PLUGIN_KEY, { open: true }); + + return true; + }) + .freezeScroll(true) + .run(); + }, + closeImagePrompt: + () => + ({ chain }) => { + return chain() + .command(({ tr }) => { + tr.setMeta(AI_IMAGE_PROMPT_PLUGIN_KEY, { open: false }); + + return true; + }) + .freezeScroll(false) + .run(); + } + }; + }, + + addProseMirrorPlugins() { + const component = viewContainerRef.createComponent(AIImagePromptComponent); + component.changeDetectorRef.detectChanges(); + + return [ + aiImagePromptPlugin({ + pluginKey: this.options.pluginKey, + editor: this.editor, + element: component.location.nativeElement, + tippyOptions: this.options.tippyOptions, + component: component + }) + ]; + } + }); +}; 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 new file mode 100644 index 000000000000..94fcb3eb81d7 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/plugins/ai-image-prompt.plugin.ts @@ -0,0 +1,175 @@ +import { Node } from 'prosemirror-model'; +import { EditorState, Plugin, PluginKey, Transaction } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; +import { Subject } from 'rxjs'; +import tippy, { Instance, Props } from 'tippy.js'; + +import { ComponentRef } from '@angular/core'; + +import { takeUntil } from 'rxjs/operators'; + +import { Editor } from '@tiptap/core'; + +import { AIImagePromptComponent } from '../ai-image-prompt.component'; +import { AI_IMAGE_PROMPT_PLUGIN_KEY } from '../ai-image-prompt.extension'; +import { TIPPY_OPTIONS } from '../utils'; + +interface AIImagePromptProps { + pluginKey: PluginKey; + editor: Editor; + element: HTMLElement; + tippyOptions: Partial; + component: ComponentRef; +} + +interface PluginState { + open: boolean; + form: []; +} + +export type AIImagePromptViewProps = AIImagePromptProps & { + view: EditorView; +}; + +export class AIImagePromptView { + public editor: Editor; + + public node: Node; + + public element: HTMLElement; + + public view: EditorView; + + public tippy: Instance | undefined; + + public tippyOptions: Partial; + + public pluginKey: PluginKey; + + public component: ComponentRef; + + private destroy$ = new Subject(); + + constructor(props: AIImagePromptViewProps) { + const { editor, element, view, tippyOptions = {}, pluginKey, component } = props; + + this.editor = editor; + this.element = element; + this.view = view; + + this.tippyOptions = tippyOptions; + + this.element.remove(); + this.pluginKey = pluginKey; + this.component = component; + + this.view.dom.addEventListener('keydown', this.handleKeyDown.bind(this)); + + this.component.instance.formSubmission.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.editor.commands.closeImagePrompt(); + this.editor.commands.insertLoaderNode(); + }); + + this.component.instance.aiResponse + .pipe(takeUntil(this.destroy$)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .subscribe((contentlet: any) => { + this.editor.commands.deleteSelection(); + const data = Object.values(contentlet[0])[0]; + + if (data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.editor.commands.insertImage(data as any); + this.editor.commands.openAIContentActions(); + } + }); + } + + private handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Backspace') { + this.editor.commands.closeAIContentActions(); + } + } + + update(view: EditorView, prevState?: EditorState) { + const next = this.pluginKey?.getState(view.state); + const prev = prevState ? this.pluginKey?.getState(prevState) : { open: false }; + + if (next?.open === prev?.open) { + this.tippy?.popperInstance?.forceUpdate(); + + return; + } + + if (!next.open) { + this.component.instance.cleanForm(); + } + + this.createTooltip(); + + next.open ? this.show() : this.hide(); + } + + createTooltip() { + const { element: editorElement } = this.editor.options; + const editorIsAttached = !!editorElement.parentElement; + + if (this.tippy || !editorIsAttached) { + return; + } + + this.tippy = tippy(document.body, { + ...TIPPY_OPTIONS, + ...this.tippyOptions, + content: this.element, + onHide: () => { + this.editor.commands.closeImagePrompt(); + } + }); + } + + show() { + this.tippy?.show(); + } + + hide() { + this.tippy?.hide(); + this.editor.view.focus(); + } + + destroy() { + this.tippy?.destroy(); + this.destroy$.next(true); + this.destroy$.complete(); + } +} + +export const aiImagePromptPlugin = (options: AIImagePromptProps) => { + return new Plugin({ + key: options.pluginKey as PluginKey, + view: (view) => new AIImagePromptView({ view, ...options }), + state: { + init(): PluginState { + return { + open: false, + form: [] + }; + }, + + apply( + transaction: Transaction, + value: PluginState, + oldState: EditorState + ): PluginState { + const { open, form } = transaction.getMeta(AI_IMAGE_PROMPT_PLUGIN_KEY) || {}; + const state = AI_IMAGE_PROMPT_PLUGIN_KEY.getState(oldState); + + if (typeof open === 'boolean') { + return { open, form }; + } + + return state || value; + } + } + }); +}; diff --git a/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/utils/index.ts b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/utils/index.ts new file mode 100644 index 000000000000..8caf34a9396f --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/ai-image-prompt/utils/index.ts @@ -0,0 +1,20 @@ +import { Props } from 'tippy.js'; + +export const TIPPY_OPTIONS: Partial = { + duration: [500, 0], + interactive: true, + maxWidth: '100%', + trigger: 'manual', + hideOnClick: true, + placement: 'top', + popperOptions: { + modifiers: [ + { + name: 'preventOverflow', + options: { + altAxis: true + } + } + ] + } +}; diff --git a/core-web/libs/block-editor/src/lib/extensions/bubble-menu/models/index.ts b/core-web/libs/block-editor/src/lib/extensions/bubble-menu/models/index.ts index c11a311b7ece..65fa5381d3fe 100644 --- a/core-web/libs/block-editor/src/lib/extensions/bubble-menu/models/index.ts +++ b/core-web/libs/block-editor/src/lib/extensions/bubble-menu/models/index.ts @@ -84,4 +84,5 @@ export interface HideBubbleMenuExtensions { dotVideo: boolean; youtube: boolean; aiContent: boolean; + loader: boolean; } diff --git a/core-web/libs/block-editor/src/lib/extensions/bubble-menu/utils/index.ts b/core-web/libs/block-editor/src/lib/extensions/bubble-menu/utils/index.ts index 2b42a7644349..f26d2f547871 100644 --- a/core-web/libs/block-editor/src/lib/extensions/bubble-menu/utils/index.ts +++ b/core-web/libs/block-editor/src/lib/extensions/bubble-menu/utils/index.ts @@ -10,7 +10,8 @@ const hideBubbleMenuOn: HideBubbleMenuExtensions = { table: true, youtube: true, dotVideo: true, - aiContent: true + aiContent: true, + loader: true }; /** diff --git a/core-web/libs/block-editor/src/lib/extensions/index.ts b/core-web/libs/block-editor/src/lib/extensions/index.ts index d22b75aa27e9..b752ccd0b069 100644 --- a/core-web/libs/block-editor/src/lib/extensions/index.ts +++ b/core-web/libs/block-editor/src/lib/extensions/index.ts @@ -33,3 +33,6 @@ export * from './ai-content-prompt/plugins/ai-content-prompt.plugin'; export * from './ai-content-actions/ai-content-actions.component'; export * from './ai-content-actions/ai-content-actions.extension'; export * from './ai-content-actions/plugins/ai-content-actions.plugin'; +export * from './ai-image-prompt/ai-image-prompt.component'; +export * from './ai-image-prompt/ai-image-prompt.extension'; +export * from './ai-image-prompt/plugins/ai-image-prompt.plugin'; diff --git a/core-web/libs/block-editor/src/lib/nodes/image-node/helpers/index.ts b/core-web/libs/block-editor/src/lib/nodes/image-node/helpers/index.ts index e815787eb420..b19dc4848d5a 100644 --- a/core-web/libs/block-editor/src/lib/nodes/image-node/helpers/index.ts +++ b/core-web/libs/block-editor/src/lib/nodes/image-node/helpers/index.ts @@ -19,12 +19,14 @@ export const imageElement = (attrs, newAttrs): DOMOutputSpec => { export const addImageLanguageId = (src: string, languageId: number) => src.includes(LANGUAGE_ID) ? src : `${src}?${LANGUAGE_ID}=${languageId}`; -export const getImageAttr = (attrs: DotCMSContentlet | string) => { +export const getImageAttr = ( + attrs: DotCMSContentlet | string | { url: string; base64: string } +) => { if (typeof attrs === 'string') { - return { src: attrs }; + return { src: attrs, data: 'null' }; } - const { fileAsset, asset, title, languageId } = attrs; + const { fileAsset, asset, title, languageId } = attrs as DotCMSContentlet; return { data: attrs, diff --git a/core-web/libs/block-editor/src/lib/nodes/index.ts b/core-web/libs/block-editor/src/lib/nodes/index.ts index 6e8cdfdc74fd..7396ae140b1b 100644 --- a/core-web/libs/block-editor/src/lib/nodes/index.ts +++ b/core-web/libs/block-editor/src/lib/nodes/index.ts @@ -3,3 +3,4 @@ export * from './contentlet-block/contentlet-block.node'; export * from './image-node/image.node'; export * from './video/video.node'; export * from './ai-content/ai-content.node'; +export * from './loader/loader.node'; diff --git a/core-web/libs/block-editor/src/lib/nodes/loader/loader.node.ts b/core-web/libs/block-editor/src/lib/nodes/loader/loader.node.ts new file mode 100644 index 000000000000..fc355b124837 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/nodes/loader/loader.node.ts @@ -0,0 +1,77 @@ +import { Node } from '@tiptap/core'; + +declare module '@tiptap/core' { + interface Commands { + LoaderNode: { + insertLoaderNode: (isLoading?: boolean) => ReturnType; + }; + } +} + +export const LoaderNode = Node.create({ + name: 'loader', + + addAttributes() { + return { + isLoading: { + default: true + } + }; + }, + + parseHTML() { + return [ + { + tag: 'div.p-d-flex.p-jc-center' + } + ]; + }, + + addOptions() { + return { + inline: false + }; + }, + + inline() { + return this.options.inline; + }, + + group() { + return 'block'; + }, + + addCommands() { + return { + ...this.parent?.(), + insertLoaderNode: + (isLoading?: boolean) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: { isLoading: isLoading } + }); + } + }; + }, + + renderHTML() { + return ['div', { class: 'p-d-flex p-jc-center' }]; + }, + + addNodeView() { + return ({ node }) => { + const dom = document.createElement('div'); + + dom.classList.add('loader-style'); + + if (node.attrs.isLoading) { + const spinner = document.createElement('div'); + spinner.classList.add('p-progress-spinner'); + dom.append(spinner); + } + + return { dom }; + }; + } +}); diff --git a/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestion-icons.ts b/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestion-icons.ts index 5202da8474ed..ad654ffc65e9 100644 --- a/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestion-icons.ts +++ b/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestion-icons.ts @@ -32,3 +32,6 @@ export const squarePlus = export const listStarsIcon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAyMCAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1LjA4NDEgOC43MjQ5M0wxMi41MjIgOS41MDc0MkMxMi40NTU2IDkuNTI3NjggMTIuMzk3MiA5LjU2OTczIDEyLjM1NTcgOS42MjcyOEMxMi4zMTQyIDkuNjg0ODMgMTIuMjkxOCA5Ljc1NDc4IDEyLjI5MTggOS44MjY2NkMxMi4yOTE4IDkuODk4NTQgMTIuMzE0MiA5Ljk2ODQ5IDEyLjM1NTcgMTAuMDI2QzEyLjM5NzIgMTAuMDgzNiAxMi40NTU2IDEwLjEyNTYgMTIuNTIyIDEwLjE0NTlMMTUuMDg0MSAxMC45Mjg0TDE1LjgzODEgMTMuNTg3M0MxNS44NTc3IDEzLjY1NjIgMTUuODk4MiAxMy43MTY4IDE1Ljk1MzYgMTMuNzU5OEMxNi4wMDkxIDEzLjgwMjkgMTYuMDc2NSAxMy44MjYyIDE2LjE0NTcgMTMuODI2MkMxNi4yMTUgMTMuODI2MiAxNi4yODI0IDEzLjgwMjkgMTYuMzM3OCAxMy43NTk4QzE2LjM5MzMgMTMuNzE2OCAxNi40MzM4IDEzLjY1NjIgMTYuNDUzMyAxMy41ODczTDE3LjIwNzcgMTAuOTI4NEwxOS43Njk3IDEwLjE0NTlDMTkuODM2MiAxMC4xMjU2IDE5Ljg5NDYgMTAuMDgzNiAxOS45MzYxIDEwLjAyNkMxOS45Nzc2IDkuOTY4NDkgMjAgOS44OTg1NCAyMCA5LjgyNjY2QzIwIDkuNzU0NzggMTkuOTc3NiA5LjY4NDgzIDE5LjkzNjEgOS42MjcyOEMxOS44OTQ2IDkuNTY5NzMgMTkuODM2MiA5LjUyNzY4IDE5Ljc2OTcgOS41MDc0MkwxNy4yMDc3IDguNzI0OTNMMTYuNDUzMyA2LjA2NjA0QzE2LjQzMzggNS45OTcwNyAxNi4zOTMzIDUuOTM2NTIgMTYuMzM3OCA1Ljg5MzQ1QzE2LjI4MjQgNS44NTAzNyAxNi4yMTUgNS44MjcwOSAxNi4xNDU3IDUuODI3MDlDMTYuMDc2NSA1LjgyNzA5IDE2LjAwOTEgNS44NTAzNyAxNS45NTM2IDUuODkzNDVDMTUuODk4MiA1LjkzNjUyIDE1Ljg1NzYgNS45OTcwNyAxNS44MzgxIDYuMDY2MDRMMTUuMDg0MSA4LjcyNDkzWiIgZmlsbD0iIzhEOTJBNSIvPgo8cGF0aCBkPSJNMTguMjE4NSAzLjk1MjMzTDE5LjYwODQgMy41Mjc0M0MxOS42NzQ5IDMuNTA3MTYgMTkuNzMzMiAzLjQ2NTExIDE5Ljc3NDcgMy40MDc1N0MxOS44MTYyIDMuMzUwMDIgMTkuODM4NiAzLjI4MDA4IDE5LjgzODYgMy4yMDgyQzE5LjgzODYgMy4xMzYzMyAxOS44MTYyIDMuMDY2MzggMTkuNzc0NyAzLjAwODg0QzE5LjczMzIgMi45NTEyOSAxOS42NzQ5IDIuOTA5MjQgMTkuNjA4NCAyLjg4ODk4TDE4LjIxODUgMi40NjQ0MUwxNy44MDkxIDEuMDIxNjdDMTcuNzg5NiAwLjk1MjY5OCAxNy43NDkxIDAuODkyMTQ0IDE3LjY5MzYgMC44NDkwNjlDMTcuNjM4MiAwLjgwNTk5NSAxNy41NzA3IDAuNzgyNzE1IDE3LjUwMTUgMC43ODI3MTVDMTcuNDMyMiAwLjc4MjcxNSAxNy4zNjQ4IDAuODA1OTk1IDE3LjMwOTQgMC44NDkwNjlDMTcuMjUzOSAwLjg5MjE0NCAxNy4yMTM0IDAuOTUyNjk4IDE3LjE5MzkgMS4wMjE2N0wxNi43ODQ4IDIuNDY0NDFMMTUuMzk0NiAyLjg4ODk4QzE1LjMyODEgMi45MDkyMyAxNS4yNjk4IDIuOTUxMjggMTUuMjI4MyAzLjAwODgzQzE1LjE4NjcgMy4wNjYzNyAxNS4xNjQzIDMuMTM2MzIgMTUuMTY0MyAzLjIwODJDMTUuMTY0MyAzLjI4MDA4IDE1LjE4NjcgMy4zNTAwMyAxNS4yMjgzIDMuNDA3NThDMTUuMjY5OCAzLjQ2NTEyIDE1LjMyODEgMy41MDcxNyAxNS4zOTQ2IDMuNTI3NDNMMTYuNzg0OCAzLjk1MjMzTDE3LjE5MzkgNS4zOTQ3NEMxNy4yMTM0IDUuNDYzNzEgMTcuMjUzOSA1LjUyNDI2IDE3LjMwOTQgNS41NjczM0MxNy4zNjQ4IDUuNjEwNDEgMTcuNDMyMiA1LjYzMzY5IDE3LjUwMTUgNS42MzM2OUMxNy41NzA3IDUuNjMzNjkgMTcuNjM4MiA1LjYxMDQxIDE3LjY5MzYgNS41NjczM0MxNy43NDkxIDUuNTI0MjYgMTcuNzg5NiA1LjQ2MzcxIDE3LjgwOTEgNS4zOTQ3NEwxOC4yMTg1IDMuOTUyMzNaIiBmaWxsPSIjOEQ5MkE1Ii8+CjxwYXRoIGQ9Ik0xMS4xNjcyIDQuNTU2MzVMOS43NjQ3OCA1LjE0OTk2QzkuNzA1NzggNS4xNzQ5NCA5LjY1NTI5IDUuMjE3NiA5LjYxOTc0IDUuMjcyNDlDOS41ODQyIDUuMzI3MzcgOS41NjUyMiA1LjM5MjAxIDkuNTY1MjIgNS40NTgxNEM5LjU2NTIyIDUuNTI0MjYgOS41ODQyIDUuNTg4OSA5LjYxOTc0IDUuNjQzNzhDOS42NTUyOSA1LjY5ODY3IDkuNzA1NzggNS43NDEzMyA5Ljc2NDc4IDUuNzY2MzFMMTEuMTY3MiA2LjM1OTkyTDExLjczOTIgNy44MTUzNEMxMS43NjMzIDcuODc2NTcgMTEuODA0NCA3LjkyODk3IDExLjg1NzMgNy45NjU4NUMxMS45MTAyIDguMDAyNzQgMTEuOTcyNCA4LjAyMjQzIDEyLjAzNjIgOC4wMjI0M0MxMi4wOTk5IDguMDIyNDMgMTIuMTYyMiA4LjAwMjc0IDEyLjIxNSA3Ljk2NTg1QzEyLjI2NzkgNy45Mjg5NyAxMi4zMDkgNy44NzY1NyAxMi4zMzMxIDcuODE1MzRMMTIuOTA1MSA2LjM1OTkyTDE0LjMwNzIgNS43NjYzMUMxNC4zNjYyIDUuNzQxMzIgMTQuNDE2NyA1LjY5ODY3IDE0LjQ1MjMgNS42NDM3OEMxNC40ODc4IDUuNTg4ODkgMTQuNTA2OCA1LjUyNDI2IDE0LjUwNjggNS40NTgxNEMxNC41MDY4IDUuMzkyMDEgMTQuNDg3OCA1LjMyNzM4IDE0LjQ1MjMgNS4yNzI0OUMxNC40MTY3IDUuMjE3NiAxNC4zNjYyIDUuMTc0OTUgMTQuMzA3MiA1LjE0OTk2TDEyLjkwNTEgNC41NTYzNUwxMi4zMzMxIDMuMTAxMjZDMTIuMzA5IDMuMDQwMDMgMTIuMjY3OSAyLjk4NzY0IDEyLjIxNSAyLjk1MDc1QzEyLjE2MjIgMi45MTM4NyAxMi4wOTk5IDIuODk0MTcgMTIuMDM2MiAyLjg5NDE3QzExLjk3MjQgMi44OTQxNyAxMS45MTAyIDIuOTEzODcgMTEuODU3MyAyLjk1MDc1QzExLjgwNDQgMi45ODc2NCAxMS43NjMzIDMuMDQwMDMgMTEuNzM5MiAzLjEwMTI2TDExLjE2NzIgNC41NTYzNVoiIGZpbGw9IiM4RDkyQTUiLz4KPHJlY3QgeT0iNC4yNjEyMyIgd2lkdGg9IjguNjk1NjUiIGhlaWdodD0iMS41NjUyMiIgcng9IjAuNzgyNjA5IiBmaWxsPSIjOEQ5MkE1Ii8+CjxyZWN0IHk9IjguOTU3MDMiIHdpZHRoPSIxMS4zMDQzIiBoZWlnaHQ9IjEuNTY1MjIiIHJ4PSIwLjc4MjYwOSIgZmlsbD0iIzhEOTJBNSIvPgo8cmVjdCB5PSIxMy42NTIzIiB3aWR0aD0iMTMuOTEzIiBoZWlnaHQ9IjEuNTY1MjIiIHJ4PSIwLjc4MjYwOSIgZmlsbD0iIzhEOTJBNSIvPgo8L3N2Zz4K'; + +export const mountsStarsIcon = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yLjUxNDI5IDUuNUMxLjg0NzQ2IDUuNSAxLjIwNzk0IDUuNzYxNjkgMC43MzY0MTcgNi4yMjc1MUMwLjI2NDg5NyA2LjY5MzMyIDAgNy4zMjUxMSAwIDcuOTgzODdWMTcuMDE2MUMwIDE3LjY3NDkgMC4yNjQ4OTcgMTguMzA2NyAwLjczNjQxNyAxOC43NzI1QzEuMjA3OTQgMTkuMjM4MyAxLjg0NzQ2IDE5LjUgMi41MTQyOSAxOS41SDEzLjQ4NTdDMTQuMTUyNSAxOS41IDE0Ljc5MjEgMTkuMjM4MyAxNS4yNjM2IDE4Ljc3MjVDMTUuNzM1MSAxOC4zMDY3IDE2IDE3LjY3NDkgMTYgMTcuMDE2MVYxN0wxNiAxNi43TDE0LjYyODYgMTUuMzgxM0wxMi4xNDE3IDEyLjkyNDVDMTIuMDc2NyAxMi44NTU5IDExLjk5NyAxMi44MDI0IDExLjkwODQgMTIuNzY4QzExLjgxOTkgMTIuNzMzNyAxMS43MjQ2IDEyLjcxOTIgMTEuNjI5NyAxMi43MjU4QzExLjUzMzcgMTIuNzMxMyAxMS40Mzk3IDEyLjc1NTcgMTEuMzUzNCAxMi43OTc2QzExLjI2NyAxMi44Mzk1IDExLjE5IDEyLjg5OCAxMS4xMjY5IDEyLjk2OTdMOS45NDc0MyAxNC4zNjk3TDUuNzQxNzEgMTAuMjE0OEM1LjY3OTc0IDEwLjE0OTggNS42MDQ1MSAxMC4wOTg0IDUuNTIwOTkgMTAuMDY0MkM1LjQzNzQ2IDEwLjAyOTkgNS4zNDc1NCAxMC4wMTM1IDUuMjU3MTQgMTAuMDE2MUM1LjE2MTExIDEwLjAyMTYgNS4wNjcxNiAxMC4wNDYxIDQuOTgwODEgMTAuMDg3OUM0Ljg5NDQ2IDEwLjEyOTggNC44MTc0NCAxMC4xODgzIDQuNzU0MjkgMTAuMjZMMS4zNzE0MyAxNC4yNDMyVjcuOTgzODdDMS4zNzE0MyA3LjY4NDQzIDEuNDkxODQgNy4zOTcyNiAxLjcwNjE2IDcuMTg1NTJDMS45MjA0OSA2Ljk3Mzc5IDIuMjExMTggNi44NTQ4NCAyLjUxNDI5IDYuODU0ODRINy4xMDk2OVY2LjE3NzQyVjUuNUgyLjUxNDI5Wk0xLjM3MTQgMTcuMDE2VjE2LjM1NjdMNS4zMDI4MyAxMS42OTZMOS4wNjk2OCAxNS40MTczTDYuNzY1NjkgMTguMTI3SDIuNTE0MjZDMi4yMTQyOSAxOC4xMjcxIDEuOTI2MzQgMTguMDEwNiAxLjcxMjUzIDE3LjgwMjdDMS40OTg3MiAxNy41OTQ5IDEuMzc2MiAxNy4zMTIzIDEuMzcxNCAxNy4wMTZaTTguNTQ4NTQgMTguMTQ1MUwxMS43MDI4IDE0LjQwNTdMMTQuNTgyOCAxNy4yNTA5QzE0LjUzMjMgMTcuNTAyIDE0LjM5NTQgMTcuNzI4MiAxNC4xOTU1IDE3Ljg5MTFDMTMuOTk1NiAxOC4wNTQgMTMuNzQ0OSAxOC4xNDM4IDEzLjQ4NTcgMTguMTQ1MUgxMS4wMTcxSDguNTQ4NTRaIiBmaWxsPSIjOEQ5MkE1Ii8+CjxwYXRoIGQ9Ik0xNC4zNDY3IDkuNjMzNTVMMTEuNDAwMyAxMC41MzM0QzExLjMyMzkgMTAuNTU2NyAxMS4yNTY4IDEwLjYwNTEgMTEuMjA5MSAxMC42NzEyQzExLjE2MTMgMTAuNzM3NCAxMS4xMzU1IDEwLjgxNzkgMTEuMTM1NSAxMC45MDA1QzExLjEzNTUgMTAuOTgzMiAxMS4xNjEzIDExLjA2MzYgMTEuMjA5MSAxMS4xMjk4QzExLjI1NjggMTEuMTk2IDExLjMyMzkgMTEuMjQ0NCAxMS40MDAzIDExLjI2NzdMMTQuMzQ2NyAxMi4xNjc1TDE1LjIxMzggMTUuMjI1M0MxNS4yMzYzIDE1LjMwNDYgMTUuMjgyOSAxNS4zNzQyIDE1LjM0NjcgMTUuNDIzN0MxNS40MTA0IDE1LjQ3MzIgMTUuNDg3OSAxNS41IDE1LjU2NzYgMTUuNUMxNS42NDcyIDE1LjUgMTUuNzI0NyAxNS40NzMyIDE1Ljc4ODUgMTUuNDIzN0MxNS44NTIzIDE1LjM3NDIgMTUuODk4OSAxNS4zMDQ2IDE1LjkyMTMgMTUuMjI1M0wxNi43ODg4IDEyLjE2NzVMMTkuNzM1MiAxMS4yNjc3QzE5LjgxMTYgMTEuMjQ0NCAxOS44Nzg3IDExLjE5NiAxOS45MjY1IDExLjEyOThDMTkuODc4NyAxMC42MDUxIDE5LjgxMTYgMTAuNTU2NyAxOS43MzUyIDEwLjUzMzRMMTYuNzg4OCA5LjYzMzU1TDE1LjkyMTMgNi41NzU4M0MxNS44OTg5IDYuNDk2NTEgMTUuODUyMyA2LjQyNjg4IDE1Ljc4ODUgNi4zNzczNEMxNS43MjQ4IDYuMzI3ODEgMTUuNjQ3MiA2LjMwMTA0IDE1LjU2NzYgNi4zMDEwNEMxNS40ODc5IDYuMzAxMDQgMTUuNDEwNCA2LjMyNzgxIDE1LjM0NjcgNi4zNzczNEMxNS4yODI5IDYuNDI2ODggMTUuMjM2MyA2LjQ5NjUxIDE1LjIxMzggNi41NzU4M0wxNC4zNDY3IDkuNjMzNTVaIiBmaWxsPSIjOEQ5MkE1Ii8+Cjwvc3ZnPg=='; diff --git a/core-web/libs/block-editor/src/lib/shared/services/ai-content/ai-content.service.ts b/core-web/libs/block-editor/src/lib/shared/services/ai-content/ai-content.service.ts index 069f67af40d7..bc8c6c60c48f 100644 --- a/core-web/libs/block-editor/src/lib/shared/services/ai-content/ai-content.service.ts +++ b/core-web/libs/block-editor/src/lib/shared/services/ai-content/ai-content.service.ts @@ -3,17 +3,19 @@ import { Observable, throwError } from 'rxjs'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, pluck } from 'rxjs/operators'; + +import { DotCMSContentlet } from '@dotcms/dotcms-models'; interface OpenAIResponse { model: string; prompt: string; response: string; } - @Injectable() export class AiContentService { private lastUsedPrompt: string | null = null; + private lastImagePrompt: string | null = null; private lastContentResponse: string | null = null; constructor(private http: HttpClient) {} @@ -49,4 +51,63 @@ export class AiContentService { }) ); } + + getAIImage(prompt: string) { + const url = 'api/ai/image/generate'; + const body = JSON.stringify({ + prompt + }); + + const headers = new HttpHeaders({ + 'Content-Type': 'application/json' + }); + + return this.http.post(url, body, { headers }).pipe( + catchError(() => { + return throwError('Error fetching AI content'); + }), + map(({ response }) => { + this.lastContentResponse = response; + + return response; + }) + ); + } + + getLatestContent() { + return this.lastContentResponse; + } + + getNewContent(contentType: string): Observable { + if (contentType === 'aiContent') { + return this.getIAContent(this.lastUsedPrompt); + } + + if (contentType === 'dotImage') { + return this.getAIImage(this.lastImagePrompt); + } + } + + createAndPublishContentlet(fileId: string): Observable { + const contentlets = [ + { + contentType: 'dotAsset', + asset: fileId, + hostFolder: '', + indexPolicy: 'WAIT_FOR' + } + ]; + + return this.http + .post('api/v1/workflow/actions/default/fire/PUBLISH', JSON.stringify({ contentlets }), { + headers: { + Origin: window.location.hostname, + 'Content-Type': 'application/json;charset=UTF-8' + } + }) + .pipe( + pluck('entity', 'results'), + catchError((error) => throwError(error)) + ) as Observable; + } } diff --git a/core-web/libs/block-editor/src/lib/shared/utils/suggestion.utils.ts b/core-web/libs/block-editor/src/lib/shared/utils/suggestion.utils.ts index 0bc23f997146..08d31afe48f7 100644 --- a/core-web/libs/block-editor/src/lib/shared/utils/suggestion.utils.ts +++ b/core-web/libs/block-editor/src/lib/shared/utils/suggestion.utils.ts @@ -11,7 +11,8 @@ import { pIcon, quoteIcon, ulIcon, - listStarsIcon + listStarsIcon, + mountsStarsIcon } from '../components/suggestions/suggestion-icons'; import { DotMenuItem } from '../components/suggestions/suggestions.component'; @@ -80,6 +81,11 @@ const block: DotMenuItem[] = [ icon: sanitizeUrl(listStarsIcon), id: 'aiContentPrompt' }, + { + label: 'AI Image', + icon: sanitizeUrl(mountsStarsIcon), + id: 'aiImagePrompt' + }, { label: 'Blockquote', icon: sanitizeUrl(quoteIcon),