Browse Source

add:word添加AI

master
prr 7 months ago
parent
commit
8e7c814fab
  1. 2
      frontend/src/stores/model.ts
  2. 3
      packages/word/index.html
  3. 1
      packages/word/src/assets/images/ai-edit.svg
  4. 2
      packages/word/src/editor/core/command/Command.ts
  5. 15
      packages/word/src/editor/core/command/CommandAdapt.ts
  6. 4
      packages/word/src/editor/core/worker/works/catalog.ts
  7. 3
      packages/word/src/editor/dataset/constant/Element.ts
  8. 1
      packages/word/src/editor/dataset/enum/Element.ts
  9. 11
      packages/word/src/main.ts
  10. 201
      packages/word/src/plugins/floatingToolbar/index.ts
  11. 18
      packages/word/src/plugins/floatingToolbar/style/index.scss
  12. 4
      packages/word/src/style.css

2
frontend/src/stores/model.ts

@ -153,6 +153,8 @@ export const useModelStore = defineStore('modelStore', () => {
return modelList.filter((d: any) => d.action == action)
}
async function addDownList(data: any) {
console.log(data);
//modelList.value.unshift(data)
// const has = modelList.value.find((d: any) => d.model == data.model)
// //console.log(has)

3
packages/word/index.html

@ -334,6 +334,9 @@
<div class="menu-item__print" data-menu="print">
<i></i>
</div>
<div class="menu-item__ai-edit" data-menu="save">
<i></i>
</div>
<div class="menu-item__save" data-menu="save">
<i></i>
</div>

1
packages/word/src/assets/images/ai-edit.svg

@ -0,0 +1 @@
<svg t="1731985324837" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2001" width="16" height="16"><path d="M683.7 922.7h-345c-73.5 0-133.3-59.8-133.3-133.3V459.8c0-73.5 59.8-133.3 133.3-133.3h345c73.5 0 133.3 59.8 133.3 133.3v329.6c0 73.5-59.8 133.3-133.3 133.3z m-345-506.9c-24.3 0-44.1 19.8-44.1 44.1v329.6c0 24.3 19.8 44.1 44.1 44.1h345c24.3 0 44.1-19.8 44.1-44.1V459.8c0-24.3-19.8-44.1-44.1-44.1h-345zM914.3 759.6c-24.6 0-44.6-20-44.6-44.6V534.3c0-24.6 20-44.6 44.6-44.6s44.6 20 44.6 44.6V715c0 24.7-20 44.6-44.6 44.6zM111.7 759.6c-24.6 0-44.6-20-44.6-44.6V534.3c0-24.6 20-44.6 44.6-44.6s44.6 20 44.6 44.6V715c0 24.7-19.9 44.6-44.6 44.6z" fill="#515151" p-id="2002"></path><path d="M511.2 415.8c-24.6 0-44.6-20-44.6-44.6V239.3c0-24.6 20-44.6 44.6-44.6s44.6 20 44.6 44.6v131.9c0 24.6-20 44.6-44.6 44.6z" fill="#515151" p-id="2003"></path><path d="M511.2 276.6c-49.2 0-89.2-40-89.2-89.2s40-89.2 89.2-89.2 89.2 40 89.2 89.2-40 89.2-89.2 89.2z m0-89.2h0.2-0.2z m0 0h0.2-0.2z m0 0h0.2-0.2z m0 0h0.2-0.2z m0 0z m0 0h0.2-0.2z m0 0h0.2-0.2z m0-0.1h0.2-0.2zM399 675.5c-28.1 0-50.9-22.8-50.9-50.9 0-28.1 22.8-50.9 50.9-50.9s50.9 22.8 50.9 50.9c0 28.1-22.8 50.9-50.9 50.9zM622.9 675.5c-28.1 0-50.9-22.8-50.9-50.9 0-28.1 22.8-50.9 50.9-50.9 28.1 0 50.9 22.8 50.9 50.9 0 28.1-22.8 50.9-50.9 50.9z" fill="#515151" p-id="2004"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

2
packages/word/src/editor/core/command/Command.ts

@ -28,6 +28,7 @@ export class Command {
public executeStrikeout: CommandAdapt['strikeout']
public executeSuperscript: CommandAdapt['superscript']
public executeSubscript: CommandAdapt['subscript']
public executeAiEdit: CommandAdapt['aiEdit']
public executeColor: CommandAdapt['color']
public executeHighlight: CommandAdapt['highlight']
public executeTitle: CommandAdapt['title']
@ -152,6 +153,7 @@ export class Command {
this.executeStrikeout = adapt.strikeout.bind(adapt)
this.executeSuperscript = adapt.superscript.bind(adapt)
this.executeSubscript = adapt.subscript.bind(adapt)
this.executeAiEdit = adapt.aiEdit.bind(adapt)
this.executeColor = adapt.color.bind(adapt)
this.executeHighlight = adapt.highlight.bind(adapt)
// 标题、对齐方式、列表

15
packages/word/src/editor/core/command/CommandAdapt.ts

@ -475,6 +475,7 @@ export class CommandAdapt {
const isDisabled = this.draw.isReadonly() || this.draw.isDisabled()
if (isDisabled) return
const selection = this.range.getSelectionElementList()
if (selection?.length) {
const noBoldIndex = selection.findIndex(s => !s.bold)
selection.forEach(el => {
@ -633,6 +634,20 @@ export class CommandAdapt {
this.draw.render({ isSetCursor: false })
}
public aiEdit(operate: string | null) {
console.log('ai edit:', operate)
const isDisabled = this.draw.isReadonly() || this.draw.isDisabled()
if (isDisabled) return
const selection = this.range.getSelectionElementList()
if (!selection) return
let content = ''
selection.forEach(el => {
content += el.value
})
//console.log('操作内容:', content);
return content
}
public color(payload: string | null) {
const isDisabled = this.draw.isReadonly() || this.draw.isDisabled()
if (isDisabled) return

4
packages/word/src/editor/core/worker/works/catalog.ts

@ -18,6 +18,7 @@ enum ElementType {
SUPERSCRIPT = 'superscript',
SUBSCRIPT = 'subscript',
SEPARATOR = 'separator',
AIEDIT = 'aiEdit',
PAGE_BREAK = 'pageBreak',
CONTROL = 'control',
CHECKBOX = 'checkbox',
@ -54,7 +55,8 @@ const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [
ElementType.SUBSCRIPT,
ElementType.SUPERSCRIPT,
ElementType.CONTROL,
ElementType.DATE
ElementType.DATE,
ElementType.AIEDIT
]
const ZERO = '\u200B'

3
packages/word/src/editor/dataset/constant/Element.ts

@ -131,7 +131,8 @@ export const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [
ElementType.SUBSCRIPT,
ElementType.SUPERSCRIPT,
ElementType.CONTROL,
ElementType.DATE
ElementType.DATE,
ElementType.AIEDIT
]
export const IMAGE_ELEMENT_TYPE: ElementType[] = [

1
packages/word/src/editor/dataset/enum/Element.ts

@ -6,6 +6,7 @@ export enum ElementType {
SUPERSCRIPT = 'superscript',
SUBSCRIPT = 'subscript',
SEPARATOR = 'separator',
AIEDIT = 'aiEdit',
PAGE_BREAK = 'pageBreak',
CONTROL = 'control',
CHECKBOX = 'checkbox',

11
packages/word/src/main.ts

@ -237,6 +237,14 @@ window.onload = function () {
instance.command.executeSubscript()
}
//ai
const aiEditDom = document.querySelector<HTMLDivElement>('.menu-item__ai-edit')!
aiEditDom.title = `ai帮助`
aiEditDom.onclick = function () {
console.log('ai-edit')
instance.command.executeAiEdit('')
}
const colorControlDom = document.querySelector<HTMLInputElement>('#color')!
colorControlDom.oninput = function () {
instance.command.executeColor(colorControlDom.value)
@ -1540,6 +1548,9 @@ window.onload = function () {
// 8. 内部事件监听
instance.listener.rangeStyleChange = function (payload) {
// 控件类型
payload.type === ElementType.AIEDIT
? aiEditDom.classList.add('active')
: aiEditDom.classList.remove('active')
payload.type === ElementType.SUBSCRIPT
? subscriptDom.classList.add('active')
: subscriptDom.classList.remove('active')

201
packages/word/src/plugins/floatingToolbar/index.ts

@ -6,7 +6,6 @@ import { ToolbarType } from './enum'
import { IToolbarRegister } from './interface'
import { PLUGIN_PREFIX } from './constant'
import { Svgs } from './icons/Svgs'
function createPickerToolbar(
container: HTMLDivElement,
toolbarType: ToolbarType,
@ -63,55 +62,54 @@ function createPickerToolbar(
}
export const defaultAiPanelMenus = [
{
prompt: `<content>{content}</content>\n请帮我优化一下这段内容,并直接返回优化后的结果。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
icon: Svgs.optimize,
title: '改进写作'
// title: '改进写作',
title: '优化',
key: 'creation_optimization'
},
{
prompt: `<content>{content}</content>\n请帮我检查一下这段内容,是否有拼写错误或者语法上的错误。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
icon: Svgs.checkGrammar,
title: '检查拼写和语法'
},
{
prompt: `<content>{content}</content>\n这句话的内容较长,帮我简化一下这个内容,并直接返回简化后的内容结果。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
icon: Svgs.simplification,
title: '简化内容'
// title: '检查拼写和语法'
title: '纠错',
key: 'creation_proofreading'
},
// {
// icon: Svgs.simplification,
// title: '简化内容'
// },
{
prompt: `<content>{content}</content>\n这句话的内容较简短,帮我简单的优化和丰富一下内容,并直接返回优化后的结果。注意:优化的内容不能超过原来内容的 2 倍。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
icon: Svgs.richContent,
title: '丰富内容'
// title: '丰富内容'
title: '续写',
key: 'creation_continuation'
},
'<hr/>',
{
prompt: `<content>{content}</content>\n请帮我翻译以上内容,在翻译之前,想先判断一下这个内容是不是中文,如果是中文,则翻译问英文,如果是其他语言,则需要翻译为中文,注意,你只需要返回翻译的结果,不需要对此进行任何解释,不需要除了翻译结果以外的其他任何内容。`,
icon: Svgs.translation,
title: '翻译'
title: '翻译',
key: 'creation_translation'
},
{
prompt: `<content>{content}</content>\n请帮我总结以上内容,并直接返回总结的结果\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
icon: Svgs.summary,
title: '总结'
title: '总结',
key: 'creation_summarize'
}
]
// 创建ai选择栏
function createAIToolbar(
container: HTMLDivElement,
toolbarType: ToolbarType,
changed: (color: string) => void
editor: Editor
) {
const toolbarItem = document.createElement('div')
toolbarItem.classList.add(`${PLUGIN_PREFIX}-picker-ai`)
toolbarItem.classList.add(`${PLUGIN_PREFIX}-${toolbarType}`)
const chooseBox = document.createElement('div')
chooseBox.classList.add('ce-picker-container')
toolbarItem.append(chooseBox)
const aiBubbleMenuItems = defaultAiPanelMenus
toolbarItem.innerHTML = `
<i></i>
<div class="aie-container">
<i id="${toolbarType}-btn"></i>
<div class="aie-container ce-picker-container ai-hide">
<div class="aie-ai-panel-body">
<div class="aie-ai-panel-body-content" style="display: none"><div class="loader">
<div class="aie-ai-panel-body-content ai-hide"><div class="loader">
${Svgs.refresh}
</div><textarea readonly></textarea></div>
<div class="aie-ai-panel-body-input"><input id="prompt" placeholder="告诉 AI 下一步应该如何?比如:帮我翻译成英语" type="text" />
@ -123,7 +121,7 @@ function createAIToolbar(
</div>
</div>
<div class="aie-ai-panel-footer" style="display: none">
<div class="aie-ai-panel-footer ai-hide" id="footer-one">
<div class="aie-ai-panel-footer-tips">
:
</div>
@ -134,32 +132,130 @@ function createAIToolbar(
</div>
<!--aie-ai-panel-actions-->
<div class="aie-ai-panel-footer aie-ai-panel-actions" >
<div class="aie-ai-panel-footer-tips">:</div>
${aiBubbleMenuItems
.map(menuItem => {
return typeof menuItem === 'string'
? menuItem
: `<p data-prompt="${menuItem.prompt}">${menuItem.icon} ${menuItem.title} </p>`
})
.join('')}
<div class="aie-ai-panel-footer aie-ai-panel-actions" id="footer-two">
<div class="aie-ai-panel-footer-tips">:</div>
${aiBubbleMenuItems
.map(menuItem => {
return typeof menuItem === 'string'
? menuItem
: `<p id="ai-operate" data-type="${menuItem.key}">${menuItem.icon} ${menuItem.title} </p>`
})
.join('')}
</div>
</div>
`
container.append(toolbarItem)
bindAiPanelEvent(container, editor)
}
//AI弹窗绑定点击事件
function bindAiPanelEvent(container: HTMLDivElement, editor: Editor) {
// 菜单栏AI选项
container.querySelector<HTMLDivElement>(`#ai-edit-btn`)?.addEventListener('click', () => {
// const target = e.currentTarget as HTMLDivElement
const target = container.querySelector<HTMLDivElement>(`.${PLUGIN_PREFIX}-ai-edit`)!
const isActive = target.classList.contains('ai-active')
isActive ? target.classList.remove('ai-active') : target.classList.add('ai-active')
const aiDialog = container.querySelector<HTMLDivElement>('.aie-container')
!isActive ? aiDialog?.classList.remove('ai-hide') : aiDialog?.classList.add('ai-hide')
})
//选中操作,进行处理
const aiOptions = Array.from(container.querySelectorAll('#ai-operate'))
aiOptions.forEach((item) => {
item.addEventListener('click', () => {
container.querySelector('#footer-one')?.classList.remove('ai-hide')
container.querySelector('#footer-two')?.classList.add('ai-hide')
container.querySelector('.aie-ai-panel-body-content')?.classList.remove('ai-hide')
//console.log('选项点击');
const aiContent = editor.command.executeAiEdit(item.getAttribute('data-type'))
const textarea = container.querySelector<HTMLTextAreaElement>('textarea')!
if (aiContent) {
textarea.textContent = aiContent
//console.log('textarea内容:', textarea, aiContent);
}
})
})
container.querySelector('#replace')!.addEventListener('click', () => {
console.log('替换');
// const textarea = container.querySelector('textarea')!
// if (textarea.value) {
// const {
// state: { selection, tr },
// view: { dispatch },
// schema
// } = holder.editor!
// const textNode = schema.text(textarea.value)
// dispatch(tr.replaceRangeWith(selection.from, selection.to, textNode))
// holder.aiPanelInstance?.hide()
// holder.tippyInstance?.show()
// }
})
container.querySelector('#insert')!.addEventListener('click', () => {
console.log('插入');
// const textarea = container.querySelector('textarea')!
// if (textarea.value) {
// const {
// state: { selection, tr },
// view: { dispatch }
// } = holder.editor!
// dispatch(tr.insertText(textarea.value, selection.to))
// holder.aiPanelInstance?.hide()
// holder.tippyInstance?.show()
// }
})
container.querySelector('#hide')!.addEventListener('click', () => {
console.log('舍弃');
// holder.aiPanelInstance?.hide()
// holder.tippyInstance?.show()
})
container.querySelector('#go')!.addEventListener('click', () => {
// const prompt = (container.querySelector('#prompt') as HTMLInputElement)
// .value
// startChat(holder, container, prompt)
})
container.querySelectorAll('.aie-ai-panel-actions p').forEach(element => {
// const prompt = element.getAttribute('data-prompt')!
// element.addEventListener('click', () => {
// startChat(holder, container, prompt)
// })
})
container.addEventListener('click', e => {
// if (e.target === container) {
// holder.aiPanelInstance?.hide()
// holder.tippyInstance?.show()
// }
})
}
//ai弹窗初始化
function initAiDialog(container: HTMLDivElement) {
container.querySelector<HTMLDivElement>('.aie-container')?.classList.add('ai-hide')
container.querySelector<HTMLDivElement>(`.${PLUGIN_PREFIX}-ai-edit`)?.classList.remove('ai-active')
container.querySelector<HTMLDivElement>('#footer-one')?.classList.add('ai-hide')
container.querySelector<HTMLDivElement>('#footer-two')?.classList.remove('ai-hide')
container.querySelector('.aie-ai-panel-body-content')?.classList.add('ai-hide')
const textarea = container.querySelector<HTMLTextAreaElement>('textarea')!
textarea.textContent = ""
}
// 工具栏列表
const toolbarRegisterList: IToolbarRegister[] = [
{
// key: ToolbarType.AI_EDIT,
// callback(editor) {
// console.log('editor:', editor)
// editor.command.executeAiEdit('')
// }
render(container, editor) {
createAIToolbar(container, ToolbarType.AI_EDIT, color => {
editor.command.executeColor(color)
})
console.log('是否会重复');
createAIToolbar(container, ToolbarType.AI_EDIT, editor)
}
},
{
@ -245,11 +341,21 @@ function createToolbar(editor: Editor): HTMLDivElement {
toolbarContainer.append(toolbarItem)
}
}
// let aiShow = false
// toolbarContainer.querySelector(`.${PLUGIN_PREFIX}-ai-edit`)?.addEventListener('click', () => {
// const aiDialog = toolbarContainer.querySelector<HTMLDivElement>('.aie-container')
// // aiDialog ? toggleToolbarItemActive(aiDialog, false) : ''
// // console.log('点击事件:', aiDialog);
// aiShow ? aiDialog?.classList.add('hide') : aiDialog?.classList.remove('hide')
// aiShow = !aiShow
// })
return toolbarContainer
}
function toggleToolbarVisible(toolbar: HTMLDivElement, visible: boolean) {
visible ? toolbar.classList.remove('hide') : toolbar.classList.add('hide')
// console.log('元素:', visible, toolbar);
}
function toggleToolbarItemActive(toolbarItem: HTMLDivElement, active: boolean) {
@ -273,6 +379,7 @@ export default function floatingToolbarPlugin(editor: Editor) {
const context = editor.command.getRangeContext()
if (!context || context.isCollapsed || !context.rangeRects[0]) {
toggleToolbarVisible(toolbarContainer, false)
initAiDialog(toolbarContainer)
return
}
// 定位
@ -280,12 +387,20 @@ export default function floatingToolbarPlugin(editor: Editor) {
toolbarContainer.style.left = `${position.x}px`
toolbarContainer.style.top = `${position.y + position.height}px`
// 样式回显
const aiDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-ai-edit`
)
if (aiDom) {
toggleToolbarItemActive(aiDom, rangeStyle.aiEdit)
}
// const aiDom = toolbarContainer.querySelector<HTMLDivElement>(
// `.${PLUGIN_PREFIX}-ai-edit`
// )
// // console.log('样式变化:', rangeStyle);
// if (aiDom) {
// toggleToolbarItemActive(aiDom, rangeStyle.aiEdit)
// // console.log('AI变化:', rangeStyle.aiEdit);
// // if (rangeStyle.aiEdit) {
// // const aiDialog = toolbarContainer.querySelector<HTMLDivElement>('.aie-container')
// // aiDialog ? toggleToolbarItemActive(aiDialog, rangeStyle.aiEdit) : ''
// // }
// }
const boldDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-bold`
)

18
packages/word/src/plugins/floatingToolbar/style/index.scss

@ -102,11 +102,20 @@
}
}
}
.ai-hide {
display: none !important;
}
.ai-active {
background-color: rgba(25, 55, 88, 0.07);
}
.aie-container {
position: absolute;
top: 40px;
left: 0;
height: 100%;
// visibility: hidden;
*, ::before, ::after {
box-sizing: content-box;
@ -141,14 +150,14 @@
&-content {
width: 100%;
// height: 160px;
max-height: 300px;
position: relative;
.loader {
position: absolute;
top: 10px;
right: 10px;
display: none;
// display: none;
svg {
width: 16px;
@ -168,10 +177,11 @@
textarea {
width: 100%;
height: 100%;
max-height: 300px;
// min-height: 100px;
// height: 300px;
padding: 15px;
box-sizing: border-box;
font-size: 12px;
font-size: 14px;
line-height: 1.6em;
font-family: Arial, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
border: none;

4
packages/word/src/style.css

@ -1069,3 +1069,7 @@ ul {
.menu-item__save i {
background-image: url('./assets/images/save.svg');
}
.menu-item__ai-edit i{
background-image: url('./assets/images/ai-edit.svg');
}

Loading…
Cancel
Save