/* eslint-disable */ import { assertExists } from '@blockeditor/global/utils'; import type { InlineRange } from '@blockeditor/inline/types'; import type { BlockElement } from '@blockeditor/lit'; import { WithDisposable } from '@blockeditor/lit'; import { computePosition, inline, offset, shift } from '@floating-ui/dom'; import { html, LitElement, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import type { IconButton } from '../../../../../components/button.js'; import { BLOCK_ID_ATTR } from '../../../../../consts.js'; import { AIIcon, AISendIcon } from '../../../../../icons/ai.js'; import { createSend } from '../../../../../utils/sse/createSSE.js'; import type { AffineInlineEditor } from '../../../affine-inline-specs.js'; import { AIPopupStyle } from './styles.js'; import { menuGroups, nextMenuGroups } from './consts.js'; import type { AIMenuOptions, AIConfig } from './consts.js'; interface SendReturn { content : string; } @customElement('ai-popup') export class AIPopup extends WithDisposable(LitElement) { static override styles = AIPopupStyle; @property() type : 'select' | 'space' = 'select'; @property() inlineEditors! : AffineInlineEditor[]; @property() targetInlineRanges! : InlineRange[]; @query('.ai-popover') aiMenuElement ?: HTMLElement; @query('#text-input') textInput ?: HTMLInputElement; @query('#ai-input') AIInput ?: HTMLInputElement; @query('#ai-input-loading') AIInputLoading ?: HTMLDivElement; @query('#ai-input-box') AIInputBox ?: HTMLDivElement; @query('#ai-content') AIContent ?: HTMLDivElement; @query('#ai-menu') AIMenu ?: HTMLDivElement; @query('#next-ai-menu') NextAIMenu ?: HTMLDivElement; @query('.popup-container') popupContainer ?: HTMLDivElement; @query('.mock-selection-container') mockSelectionContainer ?: HTMLDivElement; @query('.affine-confirm-button') confirmButton ?: IconButton; private _bodyOverflowStyle = ''; private _aiResult : string = ''; private _status : string = 'init'; private aiOptions : any; override connectedCallback() { super.connectedCallback(); // if (this.targetInlineRange.length === 0) { // throw new Error('Cannot toggle AI popup on empty range'); // } if (this.type === 'select' || this.type === 'space') { // disable body scroll this._bodyOverflowStyle = document.body.style.overflow; document.body.style.overflow = 'hidden'; this.disposables.add({ dispose: () => { document.body.style.overflow = this._bodyOverflowStyle; }, }); } this.inlineEditors.forEach((inlineEditor, index) => { const blockElement = inlineEditor.rootElement.closest( `[${BLOCK_ID_ATTR}]` ); assertExists(blockElement); const parent = blockElement.host.page.getParent(blockElement.model); if (index === 0) { this.aiOptions = blockElement.host.page.aiOptions; // console.log(123, blockElement.host.page.aiOptions); } assertExists(parent); this.disposables.add( parent.childrenUpdated.on(() => { const children = parent.children; if (children.includes(blockElement.model)) return; this.remove(); }) ); }); } protected override firstUpdated() { if (!this.AIInput) return; this._disposables.addFromEvent(this.AIInput, 'copy', e => { e.stopPropagation(); }); this._disposables.addFromEvent(this.AIInput, 'cut', e => { e.stopPropagation(); }); this._disposables.addFromEvent(this.AIInput, 'paste', e => { e.stopPropagation(); }); } override updated() { assertExists(this.popupContainer); const docPageEle = document.querySelectorAll('.affine-doc-viewport')[0]; const scrollTop = docPageEle ? docPageEle.scrollTop : 0; const docPageEleRects = docPageEle.getBoundingClientRect(); this.inlineEditors.forEach((inlineEditor, index) => { const range = inlineEditor.toDomRange(this.targetInlineRanges[index]); assertExists(range); if (this.type === 'select') { const domRects = range.getClientRects(); Object.values(domRects).forEach(domRect => { const mockSelection = document.createElement('div'); mockSelection.classList.add('mock-selection', 'blockeditor-overlay'); mockSelection.style.left = `${domRect.left - docPageEleRects.left}px`; mockSelection.style.top = `${domRect.top + scrollTop - docPageEleRects.top }px`; mockSelection.style.width = `${domRect.width}px`; mockSelection.style.height = `${domRect.height}px`; assertExists(this.mockSelectionContainer); this.mockSelectionContainer.appendChild(mockSelection); }); } if (index === this.inlineEditors.length - 1 && this.popupContainer) { const visualElement = { getBoundingClientRect: () => range.getBoundingClientRect(), getClientRects: () => range.getClientRects(), }; computePosition(visualElement, this.popupContainer, { middleware: [ offset(10), inline(), shift({ padding: 6, }), ], }) .then(({ y }) => { if (!this.popupContainer) return; // this.popupContainer.style.left = `${x}px`; this.popupContainer.style.top = `${y}px`; }) .catch(console.error); } }); } get currentText() { let arr : any[] = []; this.inlineEditors.forEach((inlineEditor, index) => { const targetInlineRange = this.targetInlineRanges[index]; arr.push( inlineEditor.yTextString.slice( targetInlineRange.index, targetInlineRange.index + targetInlineRange.length ) ); }); return arr; } private async _onConfirm() { // if (!this.inlineEditors[0].isValidInlineRange(this.targetInlineRanges[0])) return; assertExists(this.AIInput); const AIInputValue = this.AIInput?.value; if (!AIInputValue) return; await this._send(AIInputValue); } private async _send(value : string | null) { this._statusChange('sseLoading'); try { let _currentText = this.currentText.join('\\n'); if (_currentText) _currentText = `\`\`\`${_currentText}\`\`\` `; const { result } = await createSend.$sendPrompt( { prompt: _currentText + value, aiOptions: this.aiOptions, }, ({ // start, content, // done }: SendReturn) => { // if (start) isStart.value = true; if (content) { if (this.AIContent) this.AIContent.innerText = content; } } ); this._aiResult = result; this._statusChange('receivedAIContent'); // setTimeout(() => { // console.log(777, _currentText + value) // const result = `秦始皇嬴政(前259年—前210年),嬴姓,赵氏(一说秦氏),名政(一说正) [3] [140] ,也有祖龙 [1]、吕政 [2]等别称(详见“人物争议-姓名之争”)。中国古代杰出的政治家、战略家、改革家,中国历史上第一个专制主义中央集权国家——秦朝的建立者,中国第一位称皇帝的君主。 [149] // 嬴政为秦庄襄王和赵姬之子,因父亲在赵国做人质,故生于赵都邯郸。 [3]秦庄襄王成为秦国太子后,嬴政被放回秦国。 [16]前247年,嬴政继承王位。前238年,平定长信侯嫪毐叛乱,并铲除权臣吕不韦,开始亲政,起用李斯、尉缭等客卿。自前230年起,先后灭韩、赵、魏、楚、燕、齐,完成了一统六国的大业。 [149] // 前221年,嬴政自诩“德兼三皇,功过五帝”,采用三皇之“皇”、五帝之“帝”构成“皇帝”称号,被称为“始皇帝”。 [6] [65]政治上,嬴政在中央设置三公九卿,地方上废除分封制,代以郡县制;经济上,统一货币和度量衡;社会文化上,实施书同文,车同轨的政策,以首都咸阳为中心修筑通往各地的道路,规定以法为教,以吏为师;军事上,北击匈奴,收取河南地,修筑万里长城;南征百越,修筑灵渠,沟通长江和珠江水系。 [149]前210年,嬴政驾崩于沙丘平台,享年五十岁,葬于骊山秦始皇陵。死后由胡亥继位,翌年就爆发了埋葬秦朝的秦末农民战争。 [150] // 嬴政结束了春秋战国诸侯纷争的局面 [149],奠定了中国两千余年政治制度基本格局 [151],被明代思想家李贽誉为“千古一帝”。 [152]同时,他推行严刑峻法、焚书坑儒、穷奢极欲、大兴土木、妄图成仙、滥征徭役等行为也引发后世争议。 [149]` // this.AIContent.innerText = result; // this._aiResult = result; // this._statusChange('receivedAIContent'); // }, 1000) } catch (e) { this._aiResult = this.AIContent?.innerText || ''; this._statusChange('init'); } } private _onKeydown(e : KeyboardEvent) { e.stopPropagation(); if (e.key === 'Enter' && !e.isComposing && !e.shiftKey) { e.preventDefault(); this._onConfirm(); } } private _inputChange() { assertExists(this.confirmButton); assertExists(this.AIMenu); assertExists(this.NextAIMenu); assertExists(this.AIInput); this.AIInput.style.height = 'inherit'; this.AIInput.style.height = `${this.AIInput.scrollHeight}px`; const value = this.AIInput?.value; this.confirmButton.disabled = !value; this.confirmButton.requestUpdate(); if (this._status === 'init') { this.AIMenu.style.display = value && value.trim() ? 'none' : 'block'; } if (this._status === 'receivedAIContent') { this.NextAIMenu.style.display = value && value.trim() ? 'none' : 'block'; } } private _statusChange(status ?: string) { if (status) this._status = status; switch (this._status) { case 'init': // 初始界面 if (this.AIContent) this.AIContent.innerText = ''; if (this.AIInput) { this.AIInput.value = ''; this.AIInput.style.height = '23px'; this.AIInput.placeholder = 'Ask AI to edit or generate...'; } if (this.AIInputBox) this.AIInputBox.style.display = 'flex'; if (this.AIInputLoading) this.AIInputLoading.style.display = 'none'; if (this.AIMenu) this.AIMenu.style.display = 'block'; if (this.NextAIMenu) this.NextAIMenu.style.display = 'none'; break; case 'sseLoading': // 正在发送sse请求 if (this.AIContent) this.AIContent.innerText = ''; if (this.AIInput) this.AIInput.value = ''; if (this.AIInputBox) this.AIInputBox.style.display = 'none'; if (this.AIInputLoading) this.AIInputLoading.style.display = 'flex'; if (this.AIMenu) this.AIMenu.style.display = 'none'; if (this.NextAIMenu) this.NextAIMenu.style.display = 'none'; break; case 'receivedAIContent': // 收到AI全部返回结果,等待user交互 if (this.AIInputBox) this.AIInputBox.style.display = 'flex'; if (this.AIInputLoading) this.AIInputLoading.style.display = 'none'; if (this.AIInput) { this.AIInput.placeholder = 'Tell AI what to do next...'; this.AIInput.style.height = '23px'; } if (this.AIMenu) this.AIMenu.style.display = 'none'; if (this.NextAIMenu) this.NextAIMenu.style.display = 'block'; break; default: break; } } private _confirmBtnTemplate() { return html`${AISendIcon}Send to AI`; } private _menuTemplate(status : string = 'init') { const group = status === 'receivedAIContent' ? nextMenuGroups : menuGroups; const _menuGroups = group.map(ele => { return { ...ele, items: ele.items.filter((item : AIConfig) => { return item.showWhen ? item.showWhen(this.type) : true; }) }; }).filter(ele => ele.items.length); return html`
${_menuGroups.map(({ name, items } : AIMenuOptions) => { let title : any = ''; if (name) title = html``; return html`${title}${items.map( ({ name, icon, action, sendText } : AIConfig) => html` { this._handleClickItem(action, sendText); }} > ${icon} ` )}`; })}
`; } private _handleClickItem(action : any, sendText : string | null) { if (sendText) { this._send(sendText); } else { action({ inlineEditors: this.inlineEditors, targetInlineRanges: this.targetInlineRanges, content: this._aiResult, })?.catch(console.error); this.remove(); } } private _createTemplate() { this.updateComplete .then(() => { this.AIInput?.focus(); setTimeout(() => { assertExists(this.AIInput); this.AIInput.value = ''; this._inputChange(); }, 0); }) .catch(console.error); return html`
${AIIcon}
${this._confirmBtnTemplate()}
${AIIcon}
AI is writing
${this._menuTemplate()}
${this._menuTemplate('receivedAIContent')}
`; } // override render() { const ele : any = document.querySelectorAll('.doc-editor-container')[0]; let height : number = 0; if (ele) { height = (ele?.offsetTop || 0) + (ele?.offsetHeight || 0); } const mask = this.type === 'select' || this.type === 'space' ? html`
this.remove()} >
` : nothing; const popover = this._createTemplate(); return html`
${mask}
`; } } declare global { interface HTMLElementTagNameMap { 'ai-popup' : AIPopup; } }