diff --git a/modules/app/src/main/resources/admin/tools/main/main.html b/modules/app/src/main/resources/admin/tools/main/main.html index b12fbe9015..188aa063af 100644 --- a/modules/app/src/main/resources/admin/tools/main/main.html +++ b/modules/app/src/main/resources/admin/tools/main/main.html @@ -43,6 +43,7 @@ {{#aiTranslatorAssetsUrl}} + {{/aiTranslatorAssetsUrl}} diff --git a/modules/lib/src/main/resources/assets/js/app/ai/AI.ts b/modules/lib/src/main/resources/assets/js/app/ai/AI.ts index 72f8118b46..952954416f 100644 --- a/modules/lib/src/main/resources/assets/js/app/ai/AI.ts +++ b/modules/lib/src/main/resources/assets/js/app/ai/AI.ts @@ -2,18 +2,19 @@ import {AiHelper} from '@enonic/lib-admin-ui/ai/AiHelper'; import {AiHelperState} from '@enonic/lib-admin-ui/ai/AiHelperState'; import {ApplicationConfig} from '@enonic/lib-admin-ui/application/ApplicationConfig'; import {PropertyTree} from '@enonic/lib-admin-ui/data/PropertyTree'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {Locale} from '@enonic/lib-admin-ui/locale/Locale'; import {IsAuthenticatedRequest} from '@enonic/lib-admin-ui/security/auth/IsAuthenticatedRequest'; import {LoginResult} from '@enonic/lib-admin-ui/security/auth/LoginResult'; import {CONFIG} from '@enonic/lib-admin-ui/util/Config'; import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; import {Content} from '../content/Content'; import {ContentType} from '../inputtype/schema/ContentType'; +import {GetLocalesRequest} from '../resource/GetLocalesRequest'; +import {ContentData, ContentLanguage, ContentSchema} from './event/data/AiData'; import {EnonicAiAppliedData} from './event/data/EnonicAiAppliedData'; -import {ContentData} from './event/data/EnonicAiContentData'; import {EnonicAiContentOperatorSetupData} from './event/data/EnonicAiContentOperatorSetupData'; import {EnonicAiTranslatorSetupData} from './event/data/EnonicAiTranslatorSetupData'; -import {AiContentOperatorDialogShownEvent} from './event/incoming/AiContentOperatorDialogShownEvent'; -import {AiContentOperatorRenderedEvent} from './event/incoming/AiContentOperatorRenderedEvent'; import {AiContentOperatorResultAppliedEvent} from './event/incoming/AiContentOperatorResultAppliedEvent'; import {AiTranslatorCompletedEvent} from './event/incoming/AiTranslatorCompletedEvent'; import {AiTranslatorStartedEvent} from './event/incoming/AiTranslatorStartedEvent'; @@ -38,7 +39,6 @@ interface EnonicAi { setup(setupData: EnonicAiTranslatorSetupData): void; render(container: HTMLElement): void; translate(language?: string): Promise; - isAvailable(): boolean; } } @@ -53,12 +53,14 @@ export class AI { private static instance: AI; - private content: Content; - private currentData: ContentData | undefined; + private content: Content; + private contentType: ContentType; + private locales: Locale[]; + private instructions: Record; private resultReceivedListeners: ((data: EnonicAiAppliedData) => void)[] = []; @@ -71,8 +73,6 @@ export class AI { return; } - AiContentOperatorRenderedEvent.on(this.showContentOperatorEventListener); - AiContentOperatorDialogShownEvent.on(this.showContentOperatorEventListener); AiContentOperatorResultAppliedEvent.on(this.applyContentOperatorEventListener); AiTranslatorStartedEvent.on(this.translatorStartedEventListener); AiTranslatorCompletedEvent.on(this.translatorCompletedEventListener); @@ -85,33 +85,42 @@ export class AI { const fullName = currentUser.getDisplayName(); const names = fullName.split(' ').map(word => word.substring(0, 1)); const shortName = (names.length >= 2 ? names.join('') : fullName).substring(0, 2).toUpperCase(); + const user = {fullName, shortName} as const; + new AiContentOperatorConfigureEvent({user}).fire(); + }).catch(DefaultErrorHandler.handle); - new AiContentOperatorConfigureEvent({ - user: { - fullName, - shortName, - } - }).fire(); - }); + new GetLocalesRequest().sendAndParse().then((locales) => { + this.setLocales(locales); + }).catch(DefaultErrorHandler.handle); } static get(): AI { return AI.instance ?? (AI.instance = new AI()); } - setContentContext(content: Content): void { + setContent(content: Content): void { this.content = content; + new AiUpdateDataEvent({ + data: this.createContentData(), + language: this.createContentLanguage(), + }).fire(); this.checkAndNotifyReady(); } - setContentTypeContext(contentType: ContentType): void { + setContentType(contentType: ContentType): void { this.contentType = contentType; + new AiUpdateDataEvent({schema: this.createContentSchema()}).fire(); this.checkAndNotifyReady(); } setCurrentData(data: ContentData): void { this.currentData = data; - new AiUpdateDataEvent({data}).fire(); + new AiUpdateDataEvent({data: this.createContentData()}).fire(); + } + + setLocales(locales: Locale[]): void { + this.locales = locales; + new AiUpdateDataEvent({language: this.createContentLanguage()}).fire(); } updateInstructions(configs: ApplicationConfig[]): void { @@ -163,12 +172,12 @@ export class AI { this.getContentOperator()?.render(container); } - translate(language: string): Promise { - return this.getTranslator()?.translate(language) ?? Promise.resolve(false); + renderTranslator(container: HTMLElement): void { + this.getTranslator()?.render(container); } - canTranslate(): boolean { - return this.getTranslator()?.isAvailable() ?? false; + translate(language: string): Promise { + return this.getTranslator()?.translate(language) ?? Promise.resolve(false); } private translatorStartedEventListener = (event: AiTranslatorStartedEvent) => { @@ -181,23 +190,32 @@ export class AI { helper?.setState(AiHelperState.COMPLETED); }; - private showContentOperatorEventListener = () => { - new AiUpdateDataEvent({ - data: { - fields: this.content.getContentData().toJson(), - topic: this.content.getDisplayName(), - language: this.content.getLanguage(), - }, - schema: { - form: this.contentType.getForm().toJson(), - name: this.contentType.getDisplayName() - }, - }).fire(); + private createContentData(): ContentData | undefined { + // TODO: Add structuredClone, when target upgraded to ES2022 + return this.currentData || (this.content && { + fields: this.content.getContentData().toJson(), + topic: this.content.getDisplayName(), + }); + } - if (this.currentData) { - new AiUpdateDataEvent({data: this.currentData}).fire(); + private createContentLanguage(): ContentLanguage | undefined { + if (!this.content) { + return; } - }; + + const tag = this.content.getLanguage(); + const locale = this.locales?.find(l => l.getTag() === tag); + const name = locale ? locale.getDisplayName() : tag; + + return {tag, name}; + } + + private createContentSchema(): ContentSchema | undefined { + return this.contentType && { + form: this.contentType.getForm().toJson(), + name: this.contentType.getDisplayName(), + }; + } private applyContentOperatorEventListener = (event: AiContentOperatorResultAppliedEvent) => { const {topic} = event.result; @@ -226,7 +244,7 @@ export class AI { } private isReady(): boolean { - return this.content != null && this.contentType != null && this.instructions != null; + return this.content != null && this.contentType != null && this.instructions != null && this.locales != null; } private checkAndNotifyReady(): void { diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/data/AiData.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/data/AiData.ts new file mode 100644 index 0000000000..cc81a74bc8 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/ai/event/data/AiData.ts @@ -0,0 +1,23 @@ +import {PropertyArrayJson} from '@enonic/lib-admin-ui/data/PropertyArrayJson'; +import {FormJson} from '@enonic/lib-admin-ui/form/json/FormJson'; + +export interface AiData { + data?: ContentData; + schema?: ContentSchema; + language?: ContentLanguage; +} + +export interface ContentData { + fields: PropertyArrayJson[]; + topic: string; +} + +export interface ContentSchema { + form: FormJson; + name: string; +} + +export interface ContentLanguage { + tag: string; + name: string; +} diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/data/EnonicAiContentData.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/data/EnonicAiContentData.ts deleted file mode 100644 index e437feb02a..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/ai/event/data/EnonicAiContentData.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {PropertyArrayJson} from '@enonic/lib-admin-ui/data/PropertyArrayJson'; -import {FormJson} from '@enonic/lib-admin-ui/form/json/FormJson'; - -export interface EnonicAiContentData { - data: ContentData; - schema?: { - form: FormJson; - name: string; - }, -} - -export interface ContentData { - fields: PropertyArrayJson[]; - topic: string; - language: string; -} diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorDialogShownEvent.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorDialogShownEvent.ts deleted file mode 100644 index 8e3956acac..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorDialogShownEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {ClassHelper} from '@enonic/lib-admin-ui/ClassHelper'; -import {Event} from '@enonic/lib-admin-ui/event/Event'; - -export class AiContentOperatorDialogShownEvent - extends Event { - - private constructor() { - super(); - } - - static on(handler: (event: AiContentOperatorDialogShownEvent) => void) { - Event.bind(ClassHelper.getFullName(this), handler); - } - - static un(handler?: (event: AiContentOperatorDialogShownEvent) => void) { - Event.unbind(ClassHelper.getFullName(this), handler); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorRenderedEvent.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorRenderedEvent.ts deleted file mode 100644 index c855e05b69..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiContentOperatorRenderedEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {ClassHelper} from '@enonic/lib-admin-ui/ClassHelper'; -import {Event} from '@enonic/lib-admin-ui/event/Event'; - -export class AiContentOperatorRenderedEvent - extends Event { - - private constructor() { - super(); - } - - static on(handler: (event: AiContentOperatorRenderedEvent) => void) { - Event.bind(ClassHelper.getFullName(this), handler); - } - - static un(handler?: (event: AiContentOperatorRenderedEvent) => void) { - Event.unbind(ClassHelper.getFullName(this), handler); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiTranslatorDialogShownEvent.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiTranslatorOpenDialogEvent.ts similarity index 55% rename from modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiTranslatorDialogShownEvent.ts rename to modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiTranslatorOpenDialogEvent.ts index 208ec0eab4..6fb9f25ef3 100644 --- a/modules/lib/src/main/resources/assets/js/app/ai/event/incoming/AiTranslatorDialogShownEvent.ts +++ b/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiTranslatorOpenDialogEvent.ts @@ -1,18 +1,14 @@ import {ClassHelper} from '@enonic/lib-admin-ui/ClassHelper'; import {Event} from '@enonic/lib-admin-ui/event/Event'; -export class AiTranslatorDialogShownEvent +export class AiTranslatorOpenDialogEvent extends Event { - private constructor() { - super(); - } - - static on(handler: (event: AiTranslatorDialogShownEvent) => void) { + static on(handler: (event: AiTranslatorOpenDialogEvent) => void) { Event.bind(ClassHelper.getFullName(this), handler); } - static un(handler?: (event: AiTranslatorDialogShownEvent) => void) { + static un(handler?: (event: AiTranslatorOpenDialogEvent) => void) { Event.unbind(ClassHelper.getFullName(this), handler); } diff --git a/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiUpdateDataEvent.ts b/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiUpdateDataEvent.ts index c3f4d48284..c339cf9b8c 100644 --- a/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiUpdateDataEvent.ts +++ b/modules/lib/src/main/resources/assets/js/app/ai/event/outgoing/AiUpdateDataEvent.ts @@ -1,19 +1,19 @@ import {ClassHelper} from '@enonic/lib-admin-ui/ClassHelper'; import {Event} from '@enonic/lib-admin-ui/event/Event'; -import {EnonicAiContentData} from '../data/EnonicAiContentData'; +import {AiData} from '../data/AiData'; export class AiUpdateDataEvent extends Event { - private readonly payload: EnonicAiContentData; + private readonly payload: AiData; - constructor(data: EnonicAiContentData) { + constructor(data: AiData) { super(); this.payload = data; } - getData(): EnonicAiContentData { + getData(): AiData { return this.payload; } diff --git a/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardPanel.ts b/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardPanel.ts index 56e22729a0..cd04470b9f 100644 --- a/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardPanel.ts +++ b/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardPanel.ts @@ -11,6 +11,7 @@ import {Property} from '@enonic/lib-admin-ui/data/Property'; import {PropertyTree} from '@enonic/lib-admin-ui/data/PropertyTree'; import {PropertyTreeComparator} from '@enonic/lib-admin-ui/data/PropertyTreeComparator'; import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {Body} from '@enonic/lib-admin-ui/dom/Body'; import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; import {LangDirection} from '@enonic/lib-admin-ui/dom/Element'; import {Form, FormBuilder} from '@enonic/lib-admin-ui/form/Form'; @@ -51,6 +52,7 @@ import {LiveEditModel} from '../../page-editor/LiveEditModel'; import {Permission} from '../access/Permission'; import {AI} from '../ai/AI'; import {EnonicAiAppliedData} from '../ai/event/data/EnonicAiAppliedData'; +import {AiTranslatorOpenDialogEvent} from '../ai/event/outgoing/AiTranslatorOpenDialogEvent'; import {MovedContentItem} from '../browse/MovedContentItem'; import {CompareStatus} from '../content/CompareStatus'; import {Content, ContentBuilder} from '../content/Content'; @@ -96,7 +98,6 @@ import {GetApplicationsRequest} from '../resource/GetApplicationsRequest'; import {GetApplicationXDataRequest} from '../resource/GetApplicationXDataRequest'; import {GetContentByIdRequest} from '../resource/GetContentByIdRequest'; import {GetContentXDataRequest} from '../resource/GetContentXDataRequest'; -import {GetLocalesRequest} from '../resource/GetLocalesRequest'; import {GetPageTemplateByKeyRequest} from '../resource/GetPageTemplateByKeyRequest'; import {IsRenderableRequest} from '../resource/IsRenderableRequest'; import {Router} from '../Router'; @@ -316,7 +317,6 @@ export class ContentWizardPanel AI.get().setCurrentData({ fields: this.contentWizardStepForm.getData().toJson(), topic: this.getWizardHeader().getDisplayName(), - language: this.peristedLanguage, }); }, 300); @@ -587,7 +587,7 @@ export class ContentWizardPanel this.wizardHeader.setName(existing.getName().toString()); } - AI.get().setContentTypeContext(this.contentType); + AI.get().setContentType(this.contentType); AI.get().updateInstructions(this.getApplicationsConfigs()); return this.loadAndSetPageState(loader.content?.getPage()?.clone()); @@ -1929,10 +1929,7 @@ export class ContentWizardPanel if (this.params.localized) { this.onRendered(() => { NotifyManager.get().showFeedback(i18n('notify.content.localized')); - - if (this.isTranslatable()) { - this.openTranslateConfirmationDialog(); - } + this.renderAndOpenTranslatorDialog(); }); } @@ -2622,7 +2619,7 @@ export class ContentWizardPanel this.contentAfterLayout = this.getPersistedItem(); this.wizardHeader?.setPersistedPath(newPersistedItem); - AI.get().setContentContext(newPersistedItem); + AI.get().setContent(newPersistedItem); } isHeaderValidForSaving(): boolean { @@ -2698,10 +2695,7 @@ export class ContentWizardPanel } isTranslatable(): boolean { - const content = this.getContent(); - - return AI.get().canTranslate() && - (this.isContentExistsInParentProject() && content.hasOriginProject()) && + return this.isContentExistsInParentProject() && this.getContent().hasOriginProject() && !!ProjectContext.get().getProject().getLanguage(); } @@ -2852,19 +2846,17 @@ export class ContentWizardPanel this.formsContexts.set('live', liveFormContext); } - openTranslateConfirmationDialog(): void { - new GetLocalesRequest().sendAndParse().then((locales) => { - const layerLang = ProjectContext.get().getProject().getLanguage(); - const locale = locales.find(l => l.getTag() === layerLang); - const displayLang = locale ? StringHelper.format('{0} ({1})', locale.getDisplayName(), locale.getProcessedTag()) : layerLang; - const translateDialog = new ConfirmationDialog(); - translateDialog.setQuestion(i18n('dialog.translate.question', displayLang)); - translateDialog.setYesCallback(() => { - if (AI.get().canTranslate()) { - void AI.get().translate(layerLang); - } - }); - translateDialog.open(); - }).catch(DefaultErrorHandler.handle); + renderAndOpenTranslatorDialog(): void { + if (!this.isTranslatable()) { + return; + } + + const aiTranslatorContainer = new DivEl('ai-translator-container'); + Body.get().appendChild(aiTranslatorContainer); + AI.get().renderTranslator(aiTranslatorContainer.getHTMLElement()); + + AI.get().whenReady(() => { + new AiTranslatorOpenDialogEvent().fire(); + }); } } diff --git a/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardToolbar.ts b/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardToolbar.ts index d93ef9a31d..decb903d83 100644 --- a/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardToolbar.ts +++ b/modules/lib/src/main/resources/assets/js/app/wizard/ContentWizardToolbar.ts @@ -135,7 +135,7 @@ export class ContentWizardToolbar private addEnonicAiContentOperatorButton(): void { AI.get().whenReady(() => { if (AI.get().has('contentOperator')) { - this.aiContentOperatorContainer = new DivEl('ai-assistant-container'); + this.aiContentOperatorContainer = new DivEl('ai-content-operator-container'); this.addElement(this.aiContentOperatorContainer); this.addContentOperatorIntoCollaborationBlock(); AI.get().renderContentOperator(this.aiContentOperatorContainer.getHTMLElement()); diff --git a/modules/lib/src/main/resources/assets/js/app/wizard/action/LocalizeContentAction.ts b/modules/lib/src/main/resources/assets/js/app/wizard/action/LocalizeContentAction.ts index fc4135a253..8ce254f1a3 100644 --- a/modules/lib/src/main/resources/assets/js/app/wizard/action/LocalizeContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/wizard/action/LocalizeContentAction.ts @@ -22,9 +22,7 @@ export class LocalizeContentAction new LocalizeContentsRequest([contentId], language).sendAndParse().then(() => { NotifyManager.get().showFeedback(i18n('notify.content.localized')); wizardPanel.setEnabled(true); - if (wizardPanel.isTranslatable()) { - wizardPanel.openTranslateConfirmationDialog(); - } + wizardPanel.renderAndOpenTranslatorDialog(); }).catch(DefaultErrorHandler.handle); }); } diff --git a/modules/lib/src/main/resources/i18n/dialogs.properties b/modules/lib/src/main/resources/i18n/dialogs.properties index 9f2035c298..df0b7d741b 100644 --- a/modules/lib/src/main/resources/i18n/dialogs.properties +++ b/modules/lib/src/main/resources/i18n/dialogs.properties @@ -150,7 +150,6 @@ dialog.state.fail=Failed to check for errors... dialog.state.checking=Running checks... dialog.state.editing=Click apply when selection is completed dialog.state.resolved=Sweet, everything is good to go -dialog.translate.question=Translate all texts to "{0}"? # # HTML Area Dialogs #