Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dotCMS/core#26912 [UI] AI actions menu selection is not working as should. #26995

Merged
merged 8 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Loading