Skip to content

Commit

Permalink
#26912 [UI] AI actions menu selection is not working as should. (#26995)
Browse files Browse the repository at this point in the history
* progress

* clean up

* style fix

* use vars

* feedback on name convention and change the prop state for the plugin

* adapt status to ComponentStatus

---------

Co-authored-by: Arcadio Quintero <oidacra@gmail.com>
  • Loading branch information
hmoreras and oidacra authored Dec 14, 2023
1 parent b0411cb commit acee057
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@
}
}
}

.ai-loading {
display: flex;
justify-content: center;
align-items: center;
min-width: 100%;
padding: $spacing-1;
border-radius: $spacing-1;
border: 1px solid $color-palette-gray-400;
color: $color-palette-primary;
}
}

.video-container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,13 @@
padding: $spacing-1;
margin-left: $spacing-8;

li {
.p-listbox-list .p-listbox-item {
padding: $spacing-2 $spacing-3;
border-bottom: 1px solid $color-palette-gray-300;
background-color: $white;

// accept option
&:first-child {
background-color: $color-palette-primary-200;
}

// regenerate option
&:nth-child(2) {
padding: $spacing-3;
background-color: $white;
}

// delete option
&:last-child {
border-top: 1px solid $color-palette-gray-300;
background-color: $white;
border-bottom: none;
}

&:hover {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
<span class="p-input-icon-right">
<input
#input
[attr.disabled]="vm.status === 'loading' || null"
[attr.disabled]="vm.status === ComponentStatus.LOADING || null"
(keyup.escape)="handleScape($event)"
(keydown.escape)="$event.stopPropagation()"
autofocus
formControlName="textPrompt"
pInputText
placeholder="{{
'block-editor.extension.ai-content.ask-ai-to-write-something' | dm
}}"
type="text" />

<ng-container *ngIf="vm.status === 'loading'; else submitButton">
<ng-container *ngIf="vm.status === ComponentStatus.LOADING; else submitButton">
<span class="pi pi-spin pi-spinner"></span>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
OnInit,
ViewChild
} from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { FormControl, FormGroup, Validators } from '@angular/forms';

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

import { ComponentStatus } from '@dotcms/dotcms-models';

import { AiContentPromptState, AiContentPromptStore } from './store/ai-content-prompt.store';

interface AIContentForm {
Expand All @@ -26,6 +28,7 @@ interface AIContentForm {
})
export class AIContentPromptComponent implements OnInit, OnDestroy {
vm$: Observable<AiContentPromptState> = this.aiContentPromptStore.vm$;
readonly ComponentStatus = ComponentStatus;
private destroy$: Subject<boolean> = new Subject<boolean>();

@ViewChild('input') private input: ElementRef;
Expand All @@ -40,7 +43,7 @@ export class AIContentPromptComponent implements OnInit, OnDestroy {
this.aiContentPromptStore.status$
.pipe(
takeUntil(this.destroy$),
filter((status) => status === 'open')
filter((status) => status === ComponentStatus.IDLE)
)
.subscribe(() => {
this.form.reset();
Expand Down Expand Up @@ -71,7 +74,7 @@ export class AIContentPromptComponent implements OnInit, OnDestroy {
* @memberof AIContentPromptComponent
*/
handleScape(event: KeyboardEvent): void {
this.aiContentPromptStore.setStatus('exit');
this.aiContentPromptStore.setStatus(ComponentStatus.INIT);
event.stopPropagation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export const AIContentPromptExtension = (viewContainerRef: ViewContainerRef) =>
({ chain }) => {
return chain()
.command(({ tr }) => {
tr.setMeta(AI_CONTENT_PROMPT_PLUGIN_KEY, { open: true });
tr.setMeta(AI_CONTENT_PROMPT_PLUGIN_KEY, {
aIContentPromptOpen: true
});

return true;
})
Expand All @@ -58,7 +60,9 @@ export const AIContentPromptExtension = (viewContainerRef: ViewContainerRef) =>
({ chain }) => {
return chain()
.command(({ tr }) => {
tr.setMeta(AI_CONTENT_PROMPT_PLUGIN_KEY, { open: false });
tr.setMeta(AI_CONTENT_PROMPT_PLUGIN_KEY, {
aIContentPromptOpen: false
});

return true;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { filter, skip, takeUntil, tap } from 'rxjs/operators';

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

import { ComponentStatus } from '@dotcms/dotcms-models';

import { DotTiptapNodeInformation, findNodeByType, replaceNodeWithContent } from '../../../shared';
import { NodeTypes } from '../../bubble-menu/models';
import { AIContentPromptComponent } from '../ai-content-prompt.component';
Expand All @@ -29,7 +31,7 @@ interface AIContentPromptProps {
}

interface PluginState {
open: boolean;
aIContentPromptOpen: boolean;
}

export type AIContentPromptViewProps = AIContentPromptProps & {
Expand Down Expand Up @@ -127,15 +129,13 @@ export class AIContentPromptView {
* template in ai-content-prompt.component.html
*/

this.componentStore.status$
.pipe(
skip(1),
takeUntil(this.destroy$),
filter((status) => status === 'exit')
)
.subscribe(() => {
this.componentStore.status$.pipe(skip(1), takeUntil(this.destroy$)).subscribe((status) => {
if (status === ComponentStatus.INIT) {
this.tippy?.hide();
});
} else if (status === ComponentStatus.LOADING) {
this.editor.commands.setLoadingAIContentNode(true);
}
});

/**
* Subscription to delete AI_CONTENT node.
Expand Down Expand Up @@ -166,17 +166,20 @@ export class AIContentPromptView {

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();
const prev = prevState
? this.pluginKey?.getState(prevState)
: { aIContentPromptOpen: false };

if (next?.aIContentPromptOpen === prev?.aIContentPromptOpen) {
return;
}

next.open
next.aIContentPromptOpen
? this.show()
: this.hide(this.storeSate.status === 'open' || this.storeSate.status === 'loaded');
: this.hide(
this.storeSate.status === ComponentStatus.IDLE ||
this.storeSate.status === ComponentStatus.LOADED
);
}

createTooltip() {
Expand Down Expand Up @@ -221,7 +224,7 @@ export class AIContentPromptView {
this.manageClickListener(true);
this.editor.setEditable(false);
this.tippy?.show();
this.componentStore.setStatus('open');
this.componentStore.setStatus(ComponentStatus.IDLE);
}

/**
Expand All @@ -231,11 +234,12 @@ export class AIContentPromptView {
* @param notifyStore
*/
hide(notifyStore = true) {
this.tippy?.hide();
this.editor.setEditable(true);

this.editor.view.focus();
if (notifyStore) {
this.componentStore.setStatus('close');
this.componentStore.setStatus(ComponentStatus.INIT);
}

this.manageClickListener(false);
Expand All @@ -253,7 +257,10 @@ export class AIContentPromptView {
* and not in a loading state, this function hides the associated Tippy tooltip.
*/
handleClick(): void {
if (this.storeSate.status === 'open' || this.storeSate.status === 'loaded') {
if (
this.storeSate.status === ComponentStatus.IDLE ||
this.storeSate.status === ComponentStatus.LOADED
) {
this.tippy.hide();
}
}
Expand All @@ -278,7 +285,7 @@ export const aiContentPromptPlugin = (options: AIContentPromptProps) => {
state: {
init(): PluginState {
return {
open: false
aIContentPromptOpen: false
};
},

Expand All @@ -287,11 +294,12 @@ export const aiContentPromptPlugin = (options: AIContentPromptProps) => {
value: PluginState,
oldState: EditorState
): PluginState {
const { open } = transaction.getMeta(AI_CONTENT_PROMPT_PLUGIN_KEY) || {};
const { aIContentPromptOpen } =
transaction.getMeta(AI_CONTENT_PROMPT_PLUGIN_KEY) || {};
const state = AI_CONTENT_PROMPT_PLUGIN_KEY.getState(oldState);

if (typeof open === 'boolean') {
return { open };
if (typeof aIContentPromptOpen === 'boolean') {
return { aIContentPromptOpen };
}

// keep the old state in case we do not receive a new one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { Injectable } from '@angular/core';

import { catchError, switchMap, tap, withLatestFrom } from 'rxjs/operators';

import { ComponentStatus } from '@dotcms/dotcms-models';

import { DotAiService } from '../../../shared';

export interface AiContentPromptState {
prompt: string;
content: string;
acceptContent: boolean;
deleteContent: boolean;
status: 'loading' | 'loaded' | 'open' | 'exit' | 'close';
status: ComponentStatus;
}

@Injectable({
Expand All @@ -25,7 +27,7 @@ export class AiContentPromptStore extends ComponentStore<AiContentPromptState> {
content: '',
acceptContent: false,
deleteContent: false,
status: 'close'
status: ComponentStatus.INIT
});
}

Expand All @@ -37,7 +39,7 @@ export class AiContentPromptStore extends ComponentStore<AiContentPromptState> {
readonly vm$ = this.select((state) => state);

//Updaters
readonly setStatus = this.updater((state, status: AiContentPromptState['status']) => ({
readonly setStatus = this.updater((state, status: ComponentStatus) => ({
...state,
status
}));
Expand All @@ -55,13 +57,13 @@ export class AiContentPromptStore extends ComponentStore<AiContentPromptState> {
readonly generateContent = this.effect((prompt$: Observable<string>) => {
return prompt$.pipe(
switchMap((prompt) => {
this.patchState({ status: 'loading', prompt });
this.patchState({ status: ComponentStatus.LOADING, prompt });

return this.dotAiService.generateContent(prompt).pipe(
tap((content) => this.patchState({ status: 'loaded', content })),
tap((content) => this.patchState({ status: ComponentStatus.LOADED, content })),
catchError(() => {
//TODO: Notify to handle error in the UI.
this.patchState({ status: 'loaded', content: '' });
this.patchState({ status: ComponentStatus.LOADED, content: '' });

return of(null);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ declare module '@tiptap/core' {
interface Commands<ReturnType> {
AIContentNode: {
insertAINode: (content?: string) => ReturnType;
setLoadingAIContentNode: (loading: boolean) => ReturnType;
};
}
}

const AI_LOADING_CLASS = 'ai-loading';
export const AIContentNode = Node.create({
name: 'aiContent',

addAttributes() {
return {
content: {
default: ''
},
loading: {
default: false
}
};
},
Expand Down Expand Up @@ -70,7 +75,8 @@ export const AIContentNode = Node.create({
// If an AI_CONTENT node is found, replace its content.
if (nodeInformation) {
tr.setNodeMarkup(nodeInformation.from, undefined, {
content: content
content: content,
loading: false
});
// Set the node selection to the beginning of the replaced content.
commands.setNodeSelection(nodeInformation.from);
Expand All @@ -83,6 +89,20 @@ export const AIContentNode = Node.create({
type: this.name,
attrs: { content: content }
});
},
setLoadingAIContentNode:
(loading: boolean) =>
({ tr, editor }) => {
const nodeInformation = findNodeByType(editor, NodeTypes.AI_CONTENT);
// Set the loading attribute to the specified value.
if (nodeInformation) {
tr.setNodeMarkup(nodeInformation.from, undefined, {
...nodeInformation.node.attrs,
loading: loading
});
}

return true;
}
};
},
Expand All @@ -96,13 +116,18 @@ export const AIContentNode = Node.create({
const dom = document.createElement('div');
const div = document.createElement('div');

div.innerHTML = node.attrs.content || '';
div.innerHTML = node.attrs.loading
? `<span class="pi pi-spin pi-spinner"></span>`
: node.attrs.content;

dom.contentEditable = 'true';
dom.classList.add('ai-content-container');
dom.className = `ai-content-container ${node.attrs.loading ? AI_LOADING_CLASS : ''}`;

dom.append(div);

return { dom };
return {
dom
};
};
}
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResolvedPos } from 'prosemirror-model';
import { ResolvedPos, Node } from 'prosemirror-model';
import { SelectionRange, TextSelection } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';

Expand Down
2 changes: 1 addition & 1 deletion dotCMS/src/main/webapp/html/dotcms-block-editor.js

Large diffs are not rendered by default.

0 comments on commit acee057

Please sign in to comment.