Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/ui/text-editor/src/plugins/copilot/ |
Upload File : |
import { Runtime, Type, Dom, Tag, Loc, Event } from 'main.core'; import type { BaseEvent } from 'main.core.events'; import type { Copilot, CopilotOptions } from 'ai.copilot'; import { DIALOG_VISIBILITY_COMMAND, HIDE_DIALOG_COMMAND } from '../../commands'; import { $createNodesFromText } from '../../helpers/create-nodes-from-text'; import { $getSelectionPosition } from '../../helpers/get-selection-position'; import { $getSelection, $isRangeSelection, $isRootNode, $isTextNode, createCommand, $getRoot, $setSelection, $isParagraphNode, $createParagraphNode, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_LOW, type LexicalCommand, type PointType, type TextNode, type ElementNode, type RangeSelection, } from 'ui.lexical.core'; import Button from '../../toolbar/button'; import BasePlugin from '../base-plugin'; import { type TextEditor } from '../../text-editor'; import './copilot.css'; import { CustomParagraphNode } from '../paragraph/custom-paragraph-node'; export const INSERT_COPILOT_DIALOG_COMMAND: LexicalCommand = createCommand('INSERT_COPILOT_DIALOG_COMMAND'); const CopilotStatus = { INIT: 'init', LOADING: 'loading', LOADED: 'loaded', }; export class CopilotPlugin extends BasePlugin { #copilot: Copilot = null; #copilotStatus: boolean = CopilotStatus.INIT; #copilotOptions: CopilotOptions = null; #targetParagraph: HTMLParagraphElement = null; #lastSelection: RangeSelection = null; #onEditorScroll: Function = this.#handleEditorScroll.bind(this); #triggerBySpace: boolean = false; constructor(editor: TextEditor) { super(editor); this.#copilotOptions = editor.getOption('copilot.copilotOptions'); if (Type.isPlainObject(this.#copilotOptions)) { this.#registerListeners(); this.#registerComponents(); } } static getName(): string { return 'Copilot'; } #registerListeners(): void { this.#triggerBySpace = this.getEditor().getOption('copilot.triggerBySpace', false); this.cleanUpRegister( this.getEditor().registerCommand( INSERT_COPILOT_DIALOG_COMMAND, (payload): boolean => { const options = Type.isPlainObject(payload) ? payload : {}; this.show(options); return true; }, COMMAND_PRIORITY_EDITOR, ), this.getEditor().registerCommand( HIDE_DIALOG_COMMAND, (): boolean => { this.#hide(); return false; }, COMMAND_PRIORITY_LOW, ), this.getEditor().registerCommand( DIALOG_VISIBILITY_COMMAND, (): boolean => { return this.isCopilotShown(); }, COMMAND_PRIORITY_LOW, ), this.#triggerBySpace ? this.#registerParagraphNodeTransform() : () => {}, ); } #registerParagraphNodeTransform(): () => void { return this.getEditor().registerNodeTransform(CustomParagraphNode, (node: CustomParagraphNode) => { if (node.getChildrenSize() !== 1 || !$isRootNode(node.getParent())) { return; } if (!$isTextNode(node.getFirstChild()) || node.getFirstChild().getTextContent() !== ' ') { this.#resetLoader(); return; } const selection: RangeSelection = $getSelection(); if (!$isRangeSelection(selection) || !selection.isCollapsed()) { return; } const anchorNode = selection.anchor.getNode(); if (anchorNode !== node.getFirstChild()) { return; } if (!this.isCopilotLoaded() && !this.isCopilotLoading()) { this.#resetLoader(); this.#targetParagraph = this.getEditor().getElementByKey(node.getKey()); if (this.#targetParagraph) { Dom.addClass(this.#targetParagraph, 'ui-text-editor-loading-ellipsis'); } } node.getFirstChild().remove(); node.select(); this.show({ onShow: () => this.#resetLoader(), onError: () => this.#resetLoader(), }); }); } #registerComponents(): void { this.getEditor().getComponentRegistry().register('copilot', (): Button => { const button: Button = new Button(); const copilotIconClass = '--copilot-ai'; const refreshIconClass = '--refresh-5 ui-text-editor-copilot-loading'; const icon = Tag.render` <span class="ui-icon-set ${copilotIconClass}" style="--ui-icon-set__icon-color: #8e52ec"></span> `; button.setContent(icon); button.setTooltip(Loc.getMessage('TEXT_EDITOR_BTN_COPILOT')); button.subscribe('onClick', (): void => { this.getEditor().focus(); if (this.isCopilotLoading()) { return; } const resetRefresh = () => { if (!Dom.hasClass(icon, copilotIconClass)) { Dom.removeClass(icon, refreshIconClass); Dom.addClass(icon, copilotIconClass); } }; this.getEditor().dispatchCommand( INSERT_COPILOT_DIALOG_COMMAND, { onShow: resetRefresh, onError: resetRefresh, }, ); if (!this.isCopilotLoaded()) { setTimeout(() => { if (!this.isCopilotLoaded()) { Dom.removeClass(icon, copilotIconClass); Dom.addClass(icon, refreshIconClass); } }, 500); } }); return button; }); } shouldTriggerBySpace(): boolean { return this.#triggerBySpace; } isCopilotLoaded(): boolean { return this.#copilotStatus === CopilotStatus.LOADED; } isCopilotLoading(): boolean { return this.#copilotStatus === CopilotStatus.LOADING; } isCopilotShown(): boolean { return this.#copilot !== null && this.#copilot.isShown(); } show({ onShow, onError } = {}) { if (this.isCopilotLoaded()) { this.#show({ onShow }); } else if (!this.isCopilotLoading()) { this.#createCopilot() .then(() => { this.#show({ onShow }); }).catch(() => { if (Type.isFunction(onError)) { onError(); } }) ; } } #show({ onShow } = {}) { this.getEditor().update(() => { const selection: RangeSelection = $getSelection(); if (!$isRangeSelection(selection) || !this.getEditor().isEditable()) { return; } this.getEditor().dispatchCommand(HIDE_DIALOG_COMMAND); const selectionText = selection.getTextContent(); const editorPosition = Dom.getPosition(this.getEditor().getScrollerContainer()); const width = Math.min(editorPosition.width, 600); this.#lastSelection = selection.clone(); const selectedText = selectionText.trim(); if (selectedText.length > 0) { this.#copilot.setSelectedText(selectedText); } else { const wholeText = $getRoot().getTextContent().trim(); if (wholeText.length > 0) { this.#copilot.setContext(wholeText); } } this.#copilot.show({ width }); this.#adjustDialogPosition(); Event.bind(this.getEditor().getScrollerContainer(), 'scroll', this.#onEditorScroll); if (!selection.isCollapsed()) { this.getEditor().highlightSelection(); } if (Type.isFunction(onShow)) { onShow(); } }); } #hide() { if (this.isCopilotLoaded() && this.#copilot.isShown()) { this.#copilot.hide(); } } #createCopilot(): Promise { if (this.isDestroyed()) { return Promise.reject(new Error('Copilot plugin was destroyed.')); } this.#copilotStatus = CopilotStatus.LOADING; return new Promise((resolve, reject) => { Runtime.loadExtension('ai.copilot') .then(({ Copilot, CopilotEvents }) => { if (this.isDestroyed()) { reject(new Error('Copilot plugin was destroyed.')); return; } this.#copilot = new Copilot({ showResultInCopilot: true, ...this.#copilotOptions, autoHide: true, }); this.#copilot.subscribe(CopilotEvents.FINISH_INIT, () => { if (this.isDestroyed()) { reject(new Error('Copilot plugin was destroyed.')); return; } this.#copilotStatus = CopilotStatus.LOADED; resolve(); }); this.#copilot.subscribe(CopilotEvents.TEXT_SAVE, this.#handleCopilotSave.bind(this)); this.#copilot.subscribe(CopilotEvents.TEXT_PLACE_BELOW, this.#handleCopilotAddBelow.bind(this)); this.#copilot.subscribe(CopilotEvents.HIDE, this.#handleCopilotHide.bind(this)); this.#copilot.init(); }) .catch(() => { reject(); }) ; }); } #resetLoader(): void { if (this.#targetParagraph) { Dom.removeClass(this.#targetParagraph, 'ui-text-editor-loading-ellipsis'); } } // #handleCopilotResult(event: BaseEvent): void // { // console.log('#handleCopilotResult', event.getData()); // const { result } = event.getData(); // this.getEditor().update( // () => { // this.#targetParagraph.clear(); // this.#targetParagraph.append($createTextNode(result)); // }, // { // onUpdate: () => { // const targetNode: HTMLElement = this.getEditor().getElementByKey(this.#targetParagraph.getKey()); // this.#copilot.adjustPosition(targetNode); // }, // }, // ); // } #adjustDialogPosition(): void { this.getEditor().update(() => { this.#restoreSelection(); const selectionPosition = $getSelectionPosition(this.getEditor(), $getSelection(), document.body); if (selectionPosition === null) { return; } const { top, left, bottom } = selectionPosition; const scrollerRect: DOMRect = Dom.getPosition(this.getEditor().getScrollerContainer()); const popupWidth = Math.min(scrollerRect.width, 600); let offsetLeft = popupWidth / 2; if (left - offsetLeft < scrollerRect.left) { // Left boundary const overflow = scrollerRect.left - (left - offsetLeft); offsetLeft -= overflow + 16; } else if (scrollerRect.right < (left + popupWidth - offsetLeft)) { // Right boundary offsetLeft += (left + popupWidth - offsetLeft) - scrollerRect.right + 16; } if (bottom < scrollerRect.top || top > scrollerRect.bottom) { this.#copilot.adjust({ hide: true }); } else { this.#copilot.adjust({ hide: false, position: { left: left - offsetLeft, top: bottom }, }); } }); } #handleEditorScroll(): void { this.#adjustDialogPosition(); } #restoreSelection(): boolean { const selection = $getSelection(); if (!$isRangeSelection(selection) && this.#lastSelection !== null) { $setSelection(this.#lastSelection); this.#lastSelection = null; return true; } return false; } #handleCopilotSave(event: BaseEvent): void { const { result } = event.getData(); this.getEditor().update(() => { this.#restoreSelection(); const selection: RangeSelection = $getSelection(); if ($isRangeSelection(selection)) { selection.insertRawText(result); } this.#hide(); }); } #handleCopilotAddBelow(event: BaseEvent): void { const { result } = event.getData(); this.getEditor().update(() => { this.#restoreSelection(); const selection: RangeSelection = $getSelection(); if ($isRangeSelection(selection)) { const focus: PointType = selection.focus; const focusNode: TextNode | ElementNode = focus.getNode(); if (!selection.isCollapsed()) { focusNode.selectEnd(); } const parentNode: ElementNode = focusNode.getParent(); if ($isParagraphNode(parentNode)) { const paragraph = $createParagraphNode(); paragraph.append(...$createNodesFromText(result)); parentNode.insertAfter(paragraph); } else { selection.insertLineBreak(); selection.insertRawText(result); } } this.#hide(); }); } #handleCopilotHide(): void { Event.unbind(this.getEditor().getScrollerContainer(), 'scroll', this.#onEditorScroll); this.getEditor().resetHighlightSelection(); this.getEditor().update(() => { if (!this.#restoreSelection()) { this.getEditor().focus(); } }); } destroy(): void { super.destroy(); if (this.#copilot !== null) { this.#copilot.hide(); this.#copilot = null; } } }