提交 f77be006 authored 作者: 王鹏飞's avatar 王鹏飞

chore: 修改编辑器AI相关内容

上级 1cc62424
......@@ -584,6 +584,7 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@fortaine/fetch-event-source/-/fetch-event-source-3.0.6.tgz",
"integrity": "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==",
"license": "MIT",
"engines": {
"node": ">=16.15"
}
......
import { useState, useEffect } from 'react'
import { SendOutlined } from '@ant-design/icons'
import { Input, Button, message } from 'antd'
import md5 from 'js-md5'
import dayjs from 'dayjs'
import { sleep } from '@/utils/common'
import './index.less'
import { useSelector, useDispatch } from 'react-redux'
import { setEditorAi } from '@/store/modules/editor'
import normalAvatar from '@/assets/images/icon-normal-avatar.png'
const UUID = 'f3846153ba784b6d86bdcd5533259c88'
const auth_key = 'f3846153ba784b6d86bdcd5533259c88'
const auth_secret = 'HO4IyLEwEOHpeOXBxaLQUOqWslJRGs1M'
const AIDrawerComponent = (props) => {
const { selectText } = props
const dispatch = useDispatch()
const { userInfo } = useSelector((state) => state.user) // 用户状态信息
const { editorAi } = useSelector((state) => state.editor) // ai信息设置
const [value, setValue] = useState(selectText) // 输入值
const [loading, setLoading] = useState(false) // loading
const [chatId, setChatId] = useState(null) // 对话的id
const [chatContentList, setChatContentList] = useState([]) // 对话数据
const element = document.querySelector('#chat-content')
let str = ''
useEffect(() => {
if (editorAi && Object.entries(editorAi).length > 0) {
let tempJSON = JSON.parse(JSON.stringify(chatContentList))
let conversationId = editorAi.conversationId
const item = tempJSON.filter((item) => item.conversationId === conversationId)
if (item && item.length > 0) {
tempJSON[tempJSON.length - 1] = editorAi
} else {
tempJSON.push(editorAi)
}
setChatId(editorAi.chatId)
setChatContentList(tempJSON)
element.scrollTo(0, 999999)
} else {
setLoading(false)
}
}, [editorAi])
useEffect(() => {
if (selectText) {
targetChat()
}
return () => {
setValue('')
setChatContentList([])
setChatId(null)
}
}, [])
let buffer = ''
const readBuffer = (reader, decoder) => {
return reader.read().then(async ({ done, value }) => {
if (done) {
// 处理最后的缓冲数据
setLoading(false)
setValue('')
dispatch(setEditorAi({}))
str = ''
processBuffer(buffer)
return
}
await sleep(80)
buffer += decoder.decode(value, { stream: true })
// 检查缓冲区中是否有完整的消息
const messages = buffer.split('\n\n') // 假设每个消息以换行符结束
messages.forEach((message, index) => {
if (index < messages.length - 1) {
processBuffer(message)
}
})
buffer = messages[messages.length - 1] // 更新缓冲区
readBuffer(reader, decoder)
})
}
const processBuffer = (text) => {
let aiContent = {}
try {
const content = text.replace(/\n+/g, '').split('data:')
if (content instanceof Array) {
const itemContent = JSON.parse(content[1])
if (!(itemContent.finish && itemContent.finish === true)) {
str += itemContent.content
aiContent = {
type: 'ai',
content: str,
chatId: itemContent.chatId,
avatar: itemContent.role.avatar,
conversationId: itemContent.conversationId
}
dispatch(setEditorAi(aiContent))
}
}
} catch {}
}
// 提交对话
const targetChat = async () => {
if (value) {
setLoading(true)
const timestamp = Date.now()
fetch('/api/tiangong/openapi/agent/chat/stream/v1', {
method: 'POST',
responseType: 'stream', // 设置响应类型为 'stream'
headers: {
'Content-Type': 'application/json',
authKey: auth_key,
timestamp: timestamp,
sign: md5(`${auth_key}${auth_secret}${timestamp}`),
stream: true // or change to "false" 不处理流式返回内容
},
body: JSON.stringify({
agentId: UUID,
chatId: chatId,
userChatInput: value
}),
mode: 'cors'
})
.then((response) => {
// response.body 是一个 ReadableStream 对象
const stream = response.body
// 使用流式数据
const reader = stream.getReader()
const decoder = new TextDecoder() // 用于解码二进制数据流
let userContent = {
type: 'user',
content: value,
time: Date.now()
}
let tempJSON = JSON.parse(JSON.stringify(chatContentList))
tempJSON.push(userContent)
setChatContentList(tempJSON)
// 开始读取流数据
readBuffer(reader, decoder)
})
.catch((error) => {
// 处理请求错误
console.error('Request failed:', error)
})
} else {
message.error('请输入对话内容!')
}
}
return (
<div className="ai-drawer-content">
<div className="ai-chat-container">
<div className="chat-content" id="chat-content">
<div className="chat-content-padd">
{chatContentList &&
chatContentList.length > 0 &&
chatContentList.map((item, index) => {
return (
<div className={`chat-content-item`} key={index}>
{item.type === 'user' && <div className="time">{dayjs(item.time).format('YYYY-MM-DD HH:mm')}</div>}
{item.type === 'ai' && (
<div className="inside">
<div className="ai-in-content">
<div className="img">
<img src={item.avatar} alt="" />
</div>
<div className="ask-content">{item.content}</div>
</div>
</div>
)}
{item.type === 'user' && (
<div className="inside inside-user">
<div className="user-in-content">
<div className="ask-content">{item.content}</div>
<div className="img">
<img src={userInfo.pic ? userInfo.pic : normalAvatar} alt="用户头像" />
</div>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</div>
<div className="ai-chat-ask">
<div className="text">
<Input.TextArea
value={value}
defaultValue={value}
allowClear
disabled={loading}
onChange={(e) => setValue(e.target.value)}
palceholder="请输入内容"
style={{ height: '80px', resize: 'none' }}></Input.TextArea>
</div>
<div className="button">
<Button type="primary" icon={<SendOutlined />} loading={loading} size="large" onClick={targetChat}></Button>
</div>
</div>
</div>
)
}
export default AIDrawerComponent
......@@ -4,11 +4,11 @@ import './index.less'
import { addGallery } from '../utils/request'
import { SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor'
import { findNodeWithParent, fontFizeList } from '../utils/setting'
import { findNodeWithParent, fontSizeList } from '../utils/setting'
import GalleryFormItem from './galleryItem'
const randomOne = Math.random().toString(16).substring(2, 10)
const GalleryModal = (props) => {
const GalleryModal = props => {
const { editor, ossClient, galleryInfo, bookId, chapterId, setGalleryVisible, setGalleryInfo, selectionSize = 18, isOnline = false } = props
const [form] = Form.useForm()
......@@ -57,7 +57,7 @@ const GalleryModal = (props) => {
const [activeKey, setActiveKey] = useState(initialItems[0].key)
const [items, setItems] = useState(initialItems)
const onChange = (newActiveKey) => {
const onChange = newActiveKey => {
setActiveKey(newActiveKey)
}
const add = () => {
......@@ -81,8 +81,8 @@ const GalleryModal = (props) => {
setItems(newPanes)
setActiveKey(newActiveKey)
}
const remove = async (targetKey) => {
const tempGallery = picList.filter((item) => item.key !== targetKey)
const remove = async targetKey => {
const tempGallery = picList.filter(item => item.key !== targetKey)
await setPicList(tempGallery)
console.log(tempGallery)
form.setFieldsValue({ gallery: tempGallery })
......@@ -94,7 +94,7 @@ const GalleryModal = (props) => {
lastIndex = i - 1
}
})
let newPanes = items.filter((item) => item.key !== targetKey)
let newPanes = items.filter(item => item.key !== targetKey)
if (newPanes.length && newActiveKey === targetKey) {
if (lastIndex >= 0) {
newActiveKey = newPanes[lastIndex].key
......@@ -184,7 +184,7 @@ const GalleryModal = (props) => {
}
}, [galleryInfo])
const onFinish = async (values) => {
const onFinish = async values => {
editor.restoreSelection()
// setLoading(true);
const { galleryTitle, flex, gallery, fontSize, theme = '' } = values
......@@ -193,7 +193,7 @@ const GalleryModal = (props) => {
if (parseInt(flex) !== 2) {
// 删除空白的p标签
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
......@@ -310,7 +310,7 @@ const GalleryModal = (props) => {
<Col span={12}>
<Form.Item label="字号" name="fontSize">
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
......
import { DomEditor, SlateTransforms, SlateRange } from '@wangeditor/editor';
import { DomEditor, SlateTransforms, SlateRange } from '@wangeditor/editor'
class ChapterItem {
constructor() {
this.title = '章节'
this.title = '节头'
this.iconSvg = `<svg width="22px" height="23px" viewBox="0 0 22 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
......@@ -14,14 +14,14 @@ class ChapterItem {
</g>
</g>
</g>
</svg>`;
</svg>`
this.tag = 'button'
}
getValue(editor) {
return 'hello, 图片, , , 图片'
}
isActive(editor) {
return false;
return false
}
isDisabled(editor) {
const { selection } = editor
......@@ -35,7 +35,7 @@ class ChapterItem {
// eslint-disable-next-line array-callback-return
const hasPreElem = selectedElems.some(elem => {
const type = DomEditor.getNodeType(elem);
const type = DomEditor.getNodeType(elem)
// 代码块 引用 表格 禁用
if (type === 'pre' || type === 'blockquote' || type === 'table' || type === 'table-row' || type === 'table-cell') return true
})
......@@ -45,10 +45,10 @@ class ChapterItem {
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if(this.isDisabled(editor)){
return;
if (this.isDisabled(editor)) {
return
}
editor.emit('ChapterItemMenuClick');
editor.emit('ChapterItemMenuClick')
}
}
......@@ -56,9 +56,7 @@ export default {
key: 'ChapterItem', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new ChapterItem() // 把 `YourMenuClass` 替换为你菜单的 class
},
}
}
export {
ChapterItem
}
\ No newline at end of file
export { ChapterItem }
......@@ -239,174 +239,42 @@
}
}
.editor-toolbar-container {
flex: 0 0 306px;
border: none !important;
margin: 10px;
.w-e-bar {
background-color: transparent;
padding: 5px 15px;
}
.w-e-bar-boxitem {
background-color: #fff;
width: 100%;
display: flex;
justify-content: space-around;
&.you {
flex-flow: row wrap;
.w-e-bar-item {
width: 16.6%;
flex: 0 0 16.6%;
}
}
&.line-height {
justify-content: flex-start;
}
}
.custom-bar-box {
background-color: #fff;
width: 100%;
display: flex;
justify-content: flex-start;
flex-flow: row wrap;
.w-e-menu-tooltip-v5 {
height: 50px;
}
&.two {
.w-e-bar-item {
width: 33%;
flex: 0 0 33%;
justify-content: flex-start;
text-align: left;
display: block;
p {
color: #333;
font-weight: bold;
}
button {
padding-left: 0;
}
}
}
&.input {
justify-content: space-around;
padding: 10px 0;
height: 62px;
.input-item {
flex: 1;
padding: 0 10px;
text-align: center;
input {
width: 100%;
color: #333;
text-align: center;
border: 1px solid #d9d9d9;
border-radius: 5px;
height: 30px;
line-height: 30px;
&:hover {
border-color: #b83956;
}
}
.text {
color: #999;
font-size: 12px;
margin-top: 8px;
}
}
}
&.media {
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
p {
font-size: 10px;
-webkit-transform: scale(1.2);
-moz-transform: scale(1.2);
-o-transform: scale(1.2);
-ms-transform: scale(1.2);
transform: scale(1.2);
}
}
}
&.hight {
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
p {
font-size: 10px;
-webkit-transform: scale(1.2);
-moz-transform: scale(1.2);
-o-transform: scale(1.2);
-ms-transform: scale(1.2);
transform: scale(1.2);
}
}
}
&.media,
&.hight {
.w-e-bar-item {
button {
display: block;
flex-direction: column;
padding: 10px;
height: 50px;
svg {
font-size: 22px;
}
}
p {
display: block;
width: 100%;
margin: 0px;
}
}
}
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
display: flex;
flex-direction: column;
height: 60px;
padding: 0;
svg {
width: 18px;
height: 18px;
}
}
.w-e-bar-divider {
display: block;
height: 1px;
margin: 0;
padding-top: 10px;
width: 100%;
margin: 8px 0;
&:nth-of-type(16) {
height: 0;
}
// &:nth-child(odd) {
// height: 0;
// }
}
.w-auto {
display: block;
width: 100%;
height: 40px;
height: auto;
background: #fafafa;
font-size: 14px;
font-weight: 600;
line-height: 40px;
&.type-heading {
font-weight: 600;
}
}
.w-e-bar {
svg {
width: 18px;
height: 18px;
&:nth-child(2) {
.w-e-bar-item {
height: auto;
button {
svg:nth-child(2) {
display: none;
}
&.has-title {
width: 100%;
height: 100%;
padding: 10px;
flex-direction: column;
align-items: center;
.title {
margin: 5px 0 0 0;
}
}
}
}
.w-e-select-list {
z-index: 10001;
width: 180px !important;
}
.w-e-menu-tooltip-v5:before {
z-index: 10001;
}
}
.ant-spin-nested-loading,
.ant-spin-container {
......@@ -417,7 +285,7 @@
}
.menu-tabs-key {
flex: 1;
flex: 0 0 306px;
background: #fafafa;
border: 1px solid #e5e5e5;
min-width: 300px;
......
// Extend menu
class AISelectTextAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIChatDrawer from './common/AIChatDrawer'
class AIChat extends BaseModalMenu {
constructor() {
this.title = 'AI';
this.iconSvg = `<svg t="1713246032755" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12352" width="16" height="16"><path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#cdcdcd" p-id="12353"></path></svg>`;
this.tag = 'button';
super()
this.title = 'AI'
this.iconSvg = `<svg t="1713246032755" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12352" width="16" height="16"><path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#cdcdcd" p-id="12353"></path></svg>`
this.tag = 'button'
}
getValue(editor) {
return 'hello, AI';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('AISelectTextClick');
return <AIChatDrawer key={Date.now()} editor={editor}></AIChatDrawer>
}
}
export default {
key: 'AISelectTextAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIChat', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new AISelectTextAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { AISelectTextAuto };
return new AIChat() // 把 `YourMenuClass` 替换为你菜单的 class
}
}
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIContentInspect extends BaseModalMenu {
constructor() {
super()
this.title = '内容检查'
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11.5 13.5q.525 0 .988-.137t.912-.413l1.525 1.575q.2.225.513.213t.537-.213q.225-.225.238-.537T16 13.45l-1.55-1.55q.275-.425.413-.9T15 10q0-1.475-1.038-2.488T11.5 6.5T9.037 7.513T8 10t1.038 2.488T11.5 13.5m0-1.5q-.825 0-1.412-.587T9.5 10t.588-1.412T11.5 8q.8 0 1.4.588T13.5 10t-.587 1.413T11.5 12M2 21q-.425 0-.712-.288T1 20t.288-.712T2 19h20q.425 0 .713.288T23 20t-.288.713T22 21zm2-3q-.825 0-1.412-.587T2 16V5q0-.825.588-1.412T4 3h16q.825 0 1.413.588T22 5v11q0 .825-.587 1.413T20 18zm0-2h16V5H4zm0 0V5z"/></svg>`
}
getValue(editor) {
return <AIModal key={Date.now()} editor={editor} docAction="contentInspect"></AIModal>
}
}
export default {
key: 'AIContentInspect',
factory() {
return new AIContentInspect()
}
}
// Extend menu
class AIDigitalHuman {
constructor() {
this.title = '数字人'
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 28 28"><path fill="currentColor" d="M12 5.5a2 2 0 0 0 1.491 1.935c.337.053.68.053 1.018 0A2 2 0 1 0 12 5.5m-1.337 1.058a3.5 3.5 0 1 1 6.675 0l4.419-1.436a2.477 2.477 0 1 1 1.53 4.712L18 11.552v3.822c0 .16.03.32.091.468l2.728 6.752a2.477 2.477 0 0 1-4.594 1.856l-2.243-5.553l-2.232 5.56a2.46 2.46 0 0 1-3.21 1.362a2.477 2.477 0 0 1-1.364-3.215l2.734-6.812q.09-.224.09-.466v-3.774L4.712 9.834a2.477 2.477 0 0 1 1.531-4.712zm2.518 2.346a5 5 0 0 1-.649-.162L5.78 6.548a.977.977 0 0 0-.604 1.859l5.46 1.774c.515.168.864.648.864 1.189v3.957c0 .35-.067.698-.198 1.024l-2.734 6.811a.977.977 0 0 0 .538 1.267a.96.96 0 0 0 1.252-.531l2.463-6.136c.42-1.045 1.897-1.047 2.319-.003l2.476 6.129a.977.977 0 1 0 1.812-.732L16.7 16.404a2.8 2.8 0 0 1-.2-1.03V11.37c0-.541.349-1.021.864-1.189l5.46-1.774a.977.977 0 1 0-.604-1.859l-6.752 2.194q-.32.104-.649.162a3.5 3.5 0 0 1-1.639 0"/></svg>`
this.tag = 'button'
}
getValue() {
return 'hello, 音频'
}
isActive() {
return false
}
isDisabled() {
return true
}
exec() {
return
}
}
export default {
key: 'AIDigitalHuman',
factory() {
return new AIDigitalHuman()
}
}
// Extend menu
class ExpandArticleAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIExpand extends BaseModalMenu {
constructor() {
this.title = '扩写';
super()
this.title = '扩写'
this.iconSvg = `<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-622, -216)" fill="#333333" fill-rule="nonzero">
<g id="文章扩写" transform="translate(622, 216)">
......@@ -11,32 +12,16 @@ class ExpandArticleAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('ExpandArticleMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="expand"></AIModal>
}
}
export default {
key: 'ExpandArticleAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIExpand',
factory() {
return new ExpandArticleAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { ExpandArticleAuto };
return new AIExpand()
}
}
// Extend menu
class PolishingAuto {
constructor() {
this.title = '缩写';
this.iconSvg = `<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-536, -221)" fill="#333333" fill-rule="nonzero">
<g id="缩写-(1)" transform="translate(536, 221)">
<path d="M1.95471575,0 L44.9584623,0 C46.2616061,0 46.913178,0.678713837 46.913178,2.03614151 L46.913178,2.03614151 C46.913178,3.39356919 46.2616061,4.07228302 44.9584623,4.07228302 L1.95471575,4.07228302 C0.651571917,4.07228302 0,3.39356919 0,2.03614151 L0,2.03614151 C0,0.678713837 0.651571917,0 1.95471575,0 Z" id="路径"></path>
<path d="M1.95471575,12.2168491 L29.3207363,12.2168491 C30.6238801,12.2168491 31.275452,12.8955629 31.275452,14.2529906 L31.275452,14.2529906 C31.275452,15.6104183 30.6238801,16.2891321 29.3207363,16.2891321 L1.95471575,16.2891321 C0.651571917,16.2891321 0,15.6104183 0,14.2529906 L0,14.2529906 C0,12.8955629 0.651571917,12.2168491 1.95471575,12.2168491 Z" id="路径"></path>
<path d="M1.95471575,24.4336981 L17.5924418,24.4336981 C18.8955856,24.4336981 19.5471575,25.112412 19.5471575,26.4698397 L19.5471575,26.4698397 C19.5471575,27.8272673 18.8955856,28.5059812 17.5924418,28.5059812 L1.95471575,28.5059812 C0.651571917,28.5059812 0,27.8272673 0,26.4698397 L0,26.4698397 C0,25.112412 0.651571917,24.4336981 1.95471575,24.4336981 Z" id="路径"></path>
<path d="M44.5362437,34.9727666 L36.4437205,39.5744464 L32.0221534,48 L27.6005864,39.5744464 L19.5158821,34.9727666 L27.6005864,30.3670145 L32.0221534,21.9373886 L36.439811,30.3670145 L44.5323343,34.9686943 L44.5362437,34.9727666 Z M44.7238964,23.712904 L48,22.7151947 L47.0421893,26.1277679 L48,29.5484856 L44.7238964,28.5385594 L41.4438834,29.5444133 L42.4056035,26.1318402 L41.4438834,22.7151947 L44.719987,23.712904 L44.7238964,23.712904 Z M20.1453005,41.4762026 L22.5965141,40.7228302 L21.8771787,43.280224 L22.5926047,45.8376177 L20.1374817,45.0842454 L17.6862681,45.8376177 L18.4056035,43.280224 L17.6901776,40.7228302 L20.1413911,41.4721303 L20.1413911,41.4762026 L20.1453005,41.4762026 Z" id="形状"></path>
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
}
getValue(editor) {
return 'hello, 缩写';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('PolishingMenuClick');
}
}
export default {
key: 'PolishingAuto', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new PolishingAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { PolishingAuto };
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIPolishing extends BaseModalMenu {
constructor() {
super()
this.title = '润色'
this.iconSvg = `<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-536, -221)" fill="#333333" fill-rule="nonzero">
<g id="润色-(1)" transform="translate(536, 221)">
<path d="M1.95471575,0 L44.9584623,0 C46.2616061,0 46.913178,0.678713837 46.913178,2.03614151 L46.913178,2.03614151 C46.913178,3.39356919 46.2616061,4.07228302 44.9584623,4.07228302 L1.95471575,4.07228302 C0.651571917,4.07228302 0,3.39356919 0,2.03614151 L0,2.03614151 C0,0.678713837 0.651571917,0 1.95471575,0 Z" id="路径"></path>
<path d="M1.95471575,12.2168491 L29.3207363,12.2168491 C30.6238801,12.2168491 31.275452,12.8955629 31.275452,14.2529906 L31.275452,14.2529906 C31.275452,15.6104183 30.6238801,16.2891321 29.3207363,16.2891321 L1.95471575,16.2891321 C0.651571917,16.2891321 0,15.6104183 0,14.2529906 L0,14.2529906 C0,12.8955629 0.651571917,12.2168491 1.95471575,12.2168491 Z" id="路径"></path>
<path d="M1.95471575,24.4336981 L17.5924418,24.4336981 C18.8955856,24.4336981 19.5471575,25.112412 19.5471575,26.4698397 L19.5471575,26.4698397 C19.5471575,27.8272673 18.8955856,28.5059812 17.5924418,28.5059812 L1.95471575,28.5059812 C0.651571917,28.5059812 0,27.8272673 0,26.4698397 L0,26.4698397 C0,25.112412 0.651571917,24.4336981 1.95471575,24.4336981 Z" id="路径"></path>
<path d="M44.5362437,34.9727666 L36.4437205,39.5744464 L32.0221534,48 L27.6005864,39.5744464 L19.5158821,34.9727666 L27.6005864,30.3670145 L32.0221534,21.9373886 L36.439811,30.3670145 L44.5323343,34.9686943 L44.5362437,34.9727666 Z M44.7238964,23.712904 L48,22.7151947 L47.0421893,26.1277679 L48,29.5484856 L44.7238964,28.5385594 L41.4438834,29.5444133 L42.4056035,26.1318402 L41.4438834,22.7151947 L44.719987,23.712904 L44.7238964,23.712904 Z M20.1453005,41.4762026 L22.5965141,40.7228302 L21.8771787,43.280224 L22.5926047,45.8376177 L20.1374817,45.0842454 L17.6862681,45.8376177 L18.4056035,43.280224 L17.6901776,40.7228302 L20.1413911,41.4721303 L20.1413911,41.4762026 L20.1453005,41.4762026 Z" id="形状"></path>
</g>
</g>
</g>
</svg>`
}
getValue(editor) {
return <AIModal key={Date.now()} editor={editor} docAction="abbreviate"></AIModal>
}
}
export default {
key: 'AIPolishing', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new AIPolishing() // 把 `YourMenuClass` 替换为你菜单的 class
}
}
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIPunctuation extends BaseModalMenu {
constructor() {
super()
this.title = '标点校对'
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M920 416H616c-4.4 0-8 3.6-8 8v112c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-56h60v320h-46c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h164c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8h-46V480h60v56c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V424c0-4.4-3.6-8-8-8M656 296V168c0-4.4-3.6-8-8-8H104c-4.4 0-8 3.6-8 8v128c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-64h168v560h-92c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-92V232h168v64c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8"/></svg>`
}
getValue(editor) {
return <AIModal key={Date.now()} editor={editor} docAction="punctuation"></AIModal>
}
}
export default {
key: 'AIPunctuation',
factory() {
return new AIPunctuation()
}
}
// Extend menu
class RewriteAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIRewrite extends BaseModalMenu {
constructor() {
this.title = '改写';
super()
this.title = '改写'
this.iconSvg = `<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-731, -218)" fill="#333333" fill-rule="nonzero">
......@@ -11,32 +15,17 @@ class RewriteAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
this.tag = 'button'
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('RewriteMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="rewrite"></AIModal>
}
}
export default {
key: 'RewriteAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIRewrite',
factory() {
return new RewriteAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { RewriteAuto };
return new AIRewrite()
}
}
// Extend menu
class SummaryAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AISummary extends BaseModalMenu {
constructor() {
this.title = '总结';
super()
this.title = '总结'
this.iconSvg = `<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-821, -216)" fill="#333333" fill-rule="nonzero">
<g id="总结" transform="translate(821, 216)">
......@@ -13,32 +16,16 @@ class SummaryAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('SummaryMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="summary"></AIModal>
}
}
export default {
key: 'SummaryAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AISummary',
factory() {
return new SummaryAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { SummaryAuto };
return new AISummary()
}
}
import { useState, useEffect } from 'react'
import { SendOutlined } from '@ant-design/icons'
import { Drawer, Input, Button, message } from 'antd'
import dayjs from 'dayjs'
import { useAIChat } from './useAIChat'
import normalAvatar from '@/assets/images/icon-normal-avatar.png'
import './AIChatDrawer.less'
const AIChatDrawer = props => {
const selectText = props.editor.getSelectionText()
const [open, setOpen] = useState(true)
const { messages, post, isLoading } = useAIChat()
const [value, setValue] = useState(selectText) // 输入值
useEffect(() => {
if (selectText) {
targetChat()
}
return () => {
setValue('')
}
}, [selectText])
const element = document.querySelector('#chat-content')
useEffect(() => {
element?.scrollTo(0, 999999)
}, [messages, element])
// 提交对话
const targetChat = async () => {
if (value) {
post({ userChatInput: value })
setValue('')
} else {
message.error('请输入对话内容!')
}
}
return (
<Drawer
open={open}
width="600px"
title="AI对话"
destroyOnClose
onClose={() => setOpen(false)}
rootClassName="ai-drawer-wrapper"
className="ai-drawer-container">
<div className="ai-drawer-content">
<div className="ai-chat-container">
<div className="chat-content" id="chat-content">
<div className="chat-content-padd">
{messages.map((item, index) => {
return (
<div className={`chat-content-item`} key={index}>
{item.role_type === 'user' && <div className="time">{dayjs(item.time).format('YYYY-MM-DD HH:mm')}</div>}
{item.role_type === 'ai' && (
<div className="inside">
<div className="ai-in-content">
<div className="img">
<img src={item.role?.avatar} />
</div>
<div className="ask-content">{item.content}</div>
</div>
</div>
)}
{item.role_type === 'user' && (
<div className="inside inside-user">
<div className="user-in-content">
<div className="ask-content">{item.content}</div>
<div className="img">
<img src={normalAvatar} />
</div>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</div>
<div className="ai-chat-ask">
<div className="text">
<Input.TextArea
value={value}
defaultValue={value}
allowClear
disabled={isLoading}
onChange={e => setValue(e.target.value)}
placeholder="请输入内容"
style={{ height: '80px', resize: 'none' }}></Input.TextArea>
</div>
<div className="button">
<Button type="primary" icon={<SendOutlined />} loading={isLoading} size="large" onClick={targetChat}></Button>
</div>
</div>
</div>
</Drawer>
)
}
export default AIChatDrawer
......@@ -58,7 +58,7 @@
border-radius: 5px;
}
}
.inside-user {
display: flex;
justify-content: flex-end;
......@@ -102,7 +102,7 @@
flex: 1;
}
.button {
flex: 0 0 60px;
flex: 0 0 40px;
padding-left: 10px;
.ant-btn {
height: 80px;
......
......@@ -6,25 +6,27 @@ const { TextArea } = Input
const actionMap = {
rewrite: { name: '改写', prompt: '帮我改写以下文字内容:' },
expand: { name: '扩写', prompt: '帮我在以下文字内容基础上进行扩写:' },
abbreviate: { name: '缩写', prompt: '帮我缩写以下文字内容:' },
summary: { name: '总结', prompt: '帮我总结以下文字内容:' }
abbreviate: { name: '润色', prompt: '帮我润色以下文字内容:' },
summary: { name: '总结', prompt: '帮我总结以下文字内容:' },
punctuation: { name: '标点校对', prompt: '帮我校对以下文字内容中的标点符号:' },
contentInspect: { name: '内容检查', prompt: '帮我检查以下文字内容中的敏感词和错别字:' }
}
export default function AIWrite({ editor, docAction, ...rest }) {
export default function AIModal({ editor, docAction }) {
const [isModalOpen, setIsModalOpen] = useState(true)
const [content, setContent] = useState('')
const { text, fetch, isLoading } = useAIEdit()
const actionText = actionMap[docAction]?.name
const [selectionText, setSelectionText] = useState('')
useEffect(() => {
if (rest.open) {
const selection = editor.getSelectionText()
if (selection) {
setSelectionText(selection)
setContent(selection)
fetch({ messages: [{ role: 'user', content: actionMap[docAction].prompt + selection }] })
}
const selection = editor.getSelectionText()
if (selection) {
setSelectionText(selection)
setContent(selection)
fetch({ messages: [{ role: 'user', content: actionMap[docAction].prompt + selection }] })
}
}, [rest.open])
}, [docAction, editor, fetch])
useEffect(() => {
setContent(text)
......@@ -37,21 +39,19 @@ export default function AIWrite({ editor, docAction, ...rest }) {
const handlePrimary = () => {
editor.restoreSelection()
editor.insertText(text)
rest.onCancel()
setIsModalOpen(false)
}
return (
<Modal
title={`以下是AI${actionText}结果:`}
{...rest}
open={isModalOpen}
footer={null}
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
}}>
classNames={{ header: 'editor-header-customer', body: 'editor-body-customer', wrapper: 'editor-wrapper-customer' }}
onOk={handlePrimary}
onCancel={() => setIsModalOpen(false)}>
<Spin spinning={isLoading}>
<TextArea autoSize={{ minRows: 4 }} value={content} onChange={(e) => setContent(e.target.value)} />
<TextArea autoSize={{ minRows: 4 }} value={content} onChange={e => setContent(e.target.value)} />
</Spin>
<br />
<Flex gap="small" justify="center">
......@@ -61,7 +61,7 @@ export default function AIWrite({ editor, docAction, ...rest }) {
<Button type="primary" onClick={handleFetch}>
重新{actionText}
</Button>
<Button type="primary" onClick={rest.onCancel}>
<Button type="primary" onClick={() => setIsModalOpen(false)}>
取消
</Button>
</Flex>
......
import ReactDOM from 'react-dom/client'
import { ConfigProvider, App } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export default class BaseModalMenu {
constructor() {
this.tag = 'button'
this.$el = document.createElement('div')
document.body.appendChild(this.$el)
this.$root = ReactDOM.createRoot(this.$el)
}
isActive() {
return false // or true
}
isDisabled() {
return false // or true
}
exec(editor, value) {
this.$root.render(
<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#ab1941' } }}>
<App>{value}</App>
</ConfigProvider>
)
}
}
import md5 from 'js-md5'
import { fetchEventSource } from '@fortaine/fetch-event-source'
import { useState, useCallback } from 'react'
export function useAIChat() {
const authKey = 'f3846153ba784b6d86bdcd5533259c88'
const authSecret = 'HO4IyLEwEOHpeOXBxaLQUOqWslJRGs1M'
const [messages, setMessages] = useState([])
const [chatId, setChatId] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const addMessage = useCallback(message => {
setMessages(prevMessages => [...prevMessages, message])
}, [])
const updateMessages = useCallback(newMessage => {
setMessages(prevMessages => {
const existingMessage = prevMessages.find(msg => msg.conversationId === newMessage.conversationId)
const content = newMessage.content === '\n' ? '<br/>' : newMessage.content || ''
if (existingMessage) {
// 更新现有消息
return prevMessages.map(msg => (msg.conversationId === newMessage.conversationId ? { ...msg, content: msg.content + content } : msg))
} else {
// 新增消息
return [...prevMessages, { ...newMessage, content, role_type: 'ai', time: Date.now() }]
}
})
}, [])
const post = useCallback(
async data => {
const timestamp = Date.now()
const sign = md5(`${authKey}${authSecret}${timestamp}`)
// 插入用户消息
addMessage({
role_type: 'user',
content: data.userChatInput,
conversationId: `user-${timestamp}`, // 生成一个唯一的 conversationId
time: Date.now()
})
setIsLoading(true)
try {
await fetchEventSource('/api/tiangong/openapi/agent/chat/stream/v1', {
method: 'POST',
headers: {
authKey,
timestamp,
sign,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...data, chatId, agentId: authKey }),
onopen(response) {
if (!response.ok) {
throw new Error('Network response was not ok')
}
},
onmessage(event) {
const message = JSON.parse(event.data)
setChatId(message.chatId)
updateMessages(message)
},
onerror(error) {
console.error('Fetch error:', error)
},
onclose() {
setIsLoading(false)
}
})
} catch (error) {
console.error('Post error:', error)
setIsLoading(false)
}
},
[authKey, authSecret, chatId, addMessage, updateMessages]
)
return { messages, isLoading, post }
}
......@@ -4,10 +4,10 @@ export const fontFamilyList = [
{ name: '黑体', value: '黑体' },
{ name: '宋体', value: '宋体' },
{ name: '楷体', value: '楷体' },
{ name: '仿宋', value: '仿宋' },
];
{ name: '仿宋', value: '仿宋' }
]
export const fontFizeList = [
export const fontSizeList = [
{ name: '初号', value: 56 },
{ name: '小初', value: 48 },
{ name: '一号', value: 34 },
......@@ -23,55 +23,58 @@ export const fontFizeList = [
{ name: '六号', value: 10 },
{ name: '小六', value: 8 },
{ name: '七号', value: 7 },
{ name: '八号', value: 6 },
];
{ name: '八号', value: 6 }
]
export const lineHeightList = ['1', '1.25', '1.5', '2', '2.5', '3']
export const findElementPath = (editor, elementType) => {
const { children } = editor;
const { children } = editor
// 递归函数来遍历节点
function traverseNodes(node) {
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
const child = node[i];
traverseNodes(child);
const child = node[i]
traverseNodes(child)
if (child.type === elementType) {
// 当找到匹配的元素类型时,返回其路径
return [i, ...traverseNodes(child)];
return [i, ...traverseNodes(child)]
}
}
} else if (node.type === elementType) {
// 如果当前节点是元素节点且类型匹配,则返回其路径
return [traverseNodes(node.parent)];
return [traverseNodes(node.parent)]
}
return null;
return null
}
// 从文档的根节点开始遍历
const path = traverseNodes(children);
return path;
};
const path = traverseNodes(children)
return path
}
export const findNodeWithParent = (node, type, key = 'random', value) => {
const path = []; // 用于存储当前节点的路径
const path = [] // 用于存储当前节点的路径
// 递归遍历节点的辅助函数
function traverse(nodes, type, key, value) {
for (let i = 0; i < nodes.length; i++) {
const current = nodes[i];
const current = nodes[i]
if (current.type === type && String(current[key]) === String(value)) {
// 找到匹配的节点,将索引添加到路径数组中
path.push(i);
return true;
path.push(i)
return true
}
if (Array.isArray(current.children) && traverse(current.children, type, key, value)) {
// 如果子节点中找到匹配的节点,添加当前节点的索引到路径
path.push(i);
return true;
path.push(i)
return true
}
}
return false; // 未找到匹配的节点
return false // 未找到匹配的节点
}
// 从根节点开始遍历
traverse(node, type, key, value);
return path; // 返回找到的路径,如果没有找到则返回空数组
};
traverse(node, type, key, value)
return path // 返回找到的路径,如果没有找到则返回空数组
}
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论