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

chore: update

上级 9bd6f7eb
差异被折叠。
......@@ -26,11 +26,12 @@
"easy-formula-editor": "^0.0.2-alpha.1",
"echarts": "^5.4.3",
"evit-gm-crypt": "^1.0.1",
"highlight.js": "^11.9.0",
"highlight.js": "^11.11.1",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"js-md5": "^0.8.3",
"lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"qs": "^6.11.2",
"rc-slider-captcha": "^1.3.0",
"react": "^18.3.1",
......@@ -38,6 +39,7 @@
"react-error-boundary": "^4.0.13",
"react-redux": "^8.1.3",
"react-router-dom": "^6.23.1",
"react-use": "^17.6.0",
"redux-persist": "^6.0.0",
"sm4js": "^0.0.6",
"snabbdom": "^3.6.2",
......
import axios from '@/utils/wenku'
import axios, { fetchEventSourceFn } from '@/utils/wenku'
// AI搜索
export function aiSearch(options) {
return fetchEventSourceFn('/api/wenku/wenchain/partner/aisearch', options)
}
// 文件上传前置
export function uploadPrepare(data) {
return axios.post('/api/wenku/wenchain/partner/uploadprepare', data)
}
// 生成论文大纲
export function paperOutline(options) {
return fetchEventSourceFn('/api/wenku/wenchain/partner/paperoutline', options)
}
// 大纲生成论文
export function outlineToPaper(data) {
return axios.post('/api/wenku/wenchain/partner/outlinetopaper', data)
}
// ⽂档下载
export function download(data) {
return axios.post('/api/wenku/wenchain/partner/download', data)
}
......@@ -52,6 +52,8 @@ import AIQuestionGapFill from './menu/AIQuestionGapFill'
import AIQuestionOpenEnded from './menu/AIQuestionOpenEnded'
import AIDigitalHuman from './menu/AIDigitalHuman'
import AITranslate from './menu/AITranslate'
import AISearch from './menu/AISearch'
import AIWrite from './menu/AIWrite'
import ImageModal from './components/image'
import VideoModal from './components/video'
......@@ -116,8 +118,10 @@ const module = {
AIQuestionGapFill,
AIQuestionOpenEnded,
AIDigitalHuman,
AITranslate
]
AITranslate,
AISearch,
AIWrite,
],
}
Boot.registerModule(module)
// 注册节头内容
......@@ -147,7 +151,7 @@ Boot.registerModule(expandReadSimple)
const tabsMenu = [
{ key: 'text', title: '文本设置' },
{ key: 'style', title: '样式模版' }
{ key: 'style', title: '样式模版' },
]
storageChange()
......@@ -157,7 +161,7 @@ const WangEditorCustomer = (props, ref) => {
const dispatch = useDispatch()
// 自动保存时间
const { autosaveTime } = useSelector(state => state.editor)
const { autosaveTime } = useSelector((state) => state.editor)
const toolbarRef = useRef()
......@@ -245,11 +249,11 @@ const WangEditorCustomer = (props, ref) => {
useImperativeHandle(ref, () => {
return {
editor
editor,
}
})
const listenNodeStyle = path => {
const listenNodeStyle = (path) => {
const children = editor.children
let node = null
if (path[1] === 0) {
......@@ -360,8 +364,10 @@ const WangEditorCustomer = (props, ref) => {
'AIQuestionOpenEnded',
'|',
'AIDigitalHuman',
'AITranslate'
]
'AITranslate',
'AISearch',
'AIWrite',
],
}
const imageRef = useRef()
......@@ -380,8 +386,8 @@ const WangEditorCustomer = (props, ref) => {
{
type: 'paragraph',
lineHeight: '1.5',
children: [{ text: '', fontFamily: '黑体', fontSize: '18px' }]
}
children: [{ text: '', fontFamily: '黑体', fontSize: '18px' }],
},
]
// 编辑器配置
......@@ -404,15 +410,15 @@ const WangEditorCustomer = (props, ref) => {
'color',
'bgColor',
'clearStyle',
'AIChat'
]
'AIChat',
],
},
image: {
menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'ImageEditor', 'deleteImage']
menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'ImageEditor', 'deleteImage'],
},
ImageAuto: {
menuKeys: ['imageWidthChpater100', 'imageWidthChpater50', 'imageWidthChpater30', 'convertToLinkCard']
}
menuKeys: ['imageWidthChpater100', 'imageWidthChpater50', 'imageWidthChpater30', 'convertToLinkCard'],
},
},
MENU_CONF: {
fontSize: {
......@@ -432,12 +438,12 @@ const WangEditorCustomer = (props, ref) => {
{ name: '六号', value: '10px' },
{ name: '小六', value: '8px' },
{ name: '七号', value: '7px' },
{ name: '八号', value: '6px' }
]
{ name: '八号', value: '6px' },
],
},
fontFamily: { fontFamilyList },
lineHeight: { lineHeightList }
}
lineHeight: { lineHeightList },
},
}
// 编辑器按钮重排
......@@ -470,12 +476,12 @@ const WangEditorCustomer = (props, ref) => {
editorConfig.onFocus = () => {
clearTimeout(saveRef.current)
}
editorConfig.onBlur = editor => {
editorConfig.onBlur = (editor) => {
// 失焦保存
setHtml(editor.getHtml())
setContent(editor.getHtml())
}
editorConfig.onChange = editor => {
editorConfig.onChange = (editor) => {
setHtml(editor.getHtml())
setContent(editor.getHtml())
}
......@@ -551,7 +557,7 @@ const WangEditorCustomer = (props, ref) => {
console.log('ImageEditorClick', '----')
const nodeEntries = SlateEditor.nodes(editor, {
match: node => {
match: (node) => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
......@@ -560,13 +566,13 @@ const WangEditorCustomer = (props, ref) => {
}
return false
},
universal: true
universal: true,
})
let info = {}
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry
node.children.forEach(item => {
node.children.forEach((item) => {
if (item.type === 'image') {
info.image = item
}
......@@ -629,7 +635,7 @@ const WangEditorCustomer = (props, ref) => {
}
}, [gData, editor, disabled])
const tabKeyChange = key => {
const tabKeyChange = (key) => {
if (key === 'text') {
toolSettingReplace()
}
......@@ -654,17 +660,17 @@ const WangEditorCustomer = (props, ref) => {
{ color: '#ab1941', name: '默认' },
{ color: '#2970f6', name: '蓝色' },
{ color: '#2ad882', name: '绿色' },
{ color: '#eb3351', name: '红色' }
{ color: '#eb3351', name: '红色' },
]
const setColor = color => {
const setColor = (color) => {
const headers = document.querySelectorAll(`.w-e-scroll .chapter-item-header`)
const sections = document.querySelectorAll(`.w-e-scroll .chapter-item-section`)
headers.forEach(item => {
headers.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item)
const path = DomEditor.findPath(editor, node)
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: color }, { at: path })
})
sections.forEach(item => {
sections.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item)
const path = DomEditor.findPath(editor, node)
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: color }, { at: path })
......@@ -731,7 +737,7 @@ const WangEditorCustomer = (props, ref) => {
<div className="tabs">
{tabsMenu &&
tabsMenu.length &&
tabsMenu.map(item => {
tabsMenu.map((item) => {
return (
<div
className={`tabs-item ${item.key === tabKey ? 'active' : ''}`}
......@@ -757,7 +763,7 @@ const WangEditorCustomer = (props, ref) => {
<div className="styletem">
<p>样式模板</p>
<ul>
{colorList.map(item => {
{colorList.map((item) => {
return (
<li key={item.color}>
<div className="left">
......@@ -785,7 +791,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setImageVisible(false)}>
......@@ -808,7 +814,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setGalleryVisible(false)}
......@@ -835,7 +841,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setTitleVisible(false)}>
......@@ -857,7 +863,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setSectionVisible(false)}>
......@@ -880,7 +886,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setPracticeOpenVisible(false)}>
......@@ -903,7 +909,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setTooltipVisible(false)}>
......@@ -927,7 +933,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setLinkVisible(false)}>
......@@ -954,7 +960,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
wrapper: 'editor-wrapper-customer',
}}
maskClosable={false}
onCancel={() => setExpandVisible(false)}
......@@ -980,7 +986,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{
header: 'practice-topic-header',
body: 'practice-topic-content',
wrapper: 'practice-topic-modal'
wrapper: 'practice-topic-modal',
}}
maskClosable={false}
onCancel={closePanel}
......
import BaseModalMenu from './common/BaseModalMenu'
import AISearchModal from './common/AISearchModal'
class AISearch extends BaseModalMenu {
constructor() {
super()
this.title = '学术搜索'
this.iconSvg = `<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-file-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M12 21h-5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v4.5" /><path d="M16.5 17.5m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0" /><path d="M18.5 19.5l2.5 2.5" /></svg>`
}
getValue(editor) {
return <AISearchModal key={Date.now()} editor={editor}></AISearchModal>
}
}
export default {
key: 'AISearch',
factory() {
return new AISearch()
},
}
import BaseModalMenu from './common/BaseModalMenu'
import AIWriteModal from './common/AIWriteModal'
class AIWrite extends BaseModalMenu {
constructor() {
super()
this.title = '写作/大纲'
this.iconSvg = `<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pen"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>`
}
getValue(editor) {
return <AIWriteModal key={Date.now()} editor={editor}></AIWriteModal>
}
}
export default {
key: 'AIWrite',
factory() {
return new AIWrite()
},
}
import { useState, useEffect, useRef } from 'react'
import { SendOutlined, UndoOutlined, CopyOutlined, LinkOutlined } from '@ant-design/icons'
import { ConfigProvider, Modal, Input, Button } from 'antd'
const { TextArea } = Input
import './AISearchModal.less'
import { useSearch } from '@/hooks/useWenku'
import { useCopyToClipboard } from 'react-use'
export default function AIModal() {
const [isModalOpen, setIsModalOpen] = useState(true)
const [content, setContent] = useState('')
const [textIndent, setTextIndent] = useState(0)
const prePromptRef = useRef(null)
const messageScrollRef = useRef(null)
const { messages, isLoading, query } = useSearch()
useEffect(() => {
if (prePromptRef.current) {
const width = prePromptRef.current.offsetWidth + 10
setTextIndent(width)
}
}, [isModalOpen])
useEffect(() => {
if (messageScrollRef.current) {
const scrollContainer = messageScrollRef.current
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}, [messages])
const prePrompt = '帮我找一些文献资料,主题是:'
const handleEnterSearch = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSearch()
}
}
const handleSearch = () => {
query(prePrompt + content)
setContent('')
}
const [, copyToClipboard] = useCopyToClipboard()
const handleCopy = (content) => {
copyToClipboard(content)
}
const MessageRender = ({ msg }) => {
if (msg.role === 'ai') {
const copyMessageContent = msg.searchReferList
?.map((refer, index) => `${index + 1}.《${refer.title}》\n${refer.abstract}\n`)
.join('\n')
return (
msg.searchReferList?.length > 0 && (
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">
{msg.searchReferList.map((refer, index) => {
return (
<div key={index} className="result-item">
<p>
<strong>
{index + 1}.《{refer.title}
</strong>
</p>
<p>
{refer.abstract}
<a href={refer.orgUrl} target="_blank" rel="noreferrer">
<LinkOutlined />
</a>
</p>
</div>
)
})}
</div>
<div className="message-tools">
<Button
type="text"
size="small"
icon={<UndoOutlined />}
disabled={isLoading}
onClick={() => query(msg.userQuery)}>
重新生成
</Button>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(copyMessageContent)}>
复制内容
</Button>
</div>
</div>
</div>
)
)
} else {
return (
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">{msg.content}</div>
</div>
</div>
)
}
}
return (
<ConfigProvider theme={{ components: { Modal: { headerBg: '#f7f8fa', contentBg: '#f7f8fa' } } }}>
<Modal
title="学术/文献搜索:"
open={isModalOpen}
footer={null}
onCancel={() => setIsModalOpen(false)}
width={1000}>
<div className="message-scroll" ref={messageScrollRef}>
{messages.map((msg, index) => {
return <MessageRender msg={msg} key={index}></MessageRender>
})}
</div>
<div className="input-container">
<div className="input-box">
<div className="edit-area">
{prePrompt && (
<span className="pre-prompt" ref={prePromptRef}>
{prePrompt}
</span>
)}
<TextArea
className="content"
autoSize
value={content}
placeholder="请输入关键词"
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleEnterSearch}
style={{ textIndent }}
/>
</div>
<div className="input-tools">
<Button type="primary" size="large" icon={<SendOutlined />} onClick={handleSearch} loading={isLoading} />
</div>
</div>
</div>
</Modal>
</ConfigProvider>
)
}
.input-container {
fill: #fff;
border: 1px solid #e0e3e6;
border-radius: 16px;
stroke-width: 2px;
stroke: #d9d9d9;
filter: drop-shadow(0px 0px 11px rgba(0, 0, 0, 0.05));
transition: border-color 0.3s;
overflow: hidden;
}
.input-box {
display: flex;
align-items: center;
padding: 10px;
background-color: #fff;
.edit-area {
flex: 1;
position: relative;
max-height: 160px;
overflow: hidden auto;
}
.pre-prompt {
position: absolute;
top: 0;
left: 0;
z-index: 1;
line-height: 1.7;
background-color: #e6eaf0;
border-radius: 4px;
padding: 4px 8px 3px;
font-size: 14px;
font-weight: 500;
}
.content {
padding: 0;
border: 0;
background: none;
box-shadow: none;
font-size: 14px;
line-height: 30px;
}
.input-tools {
align-self: flex-end;
display: flex;
align-items: center;
gap: 10px;
}
}
.message-scroll {
margin: 10px 0;
height: 600px;
overflow-x: hidden;
overflow-y: auto;
}
.message-item {
display: flex;
margin: 10px 0;
&.user {
justify-content: flex-end;
.message-box {
background-color: #0000000d;
}
}
&.ai {
.message-box {
background-color: #fff;
}
}
.message-box {
max-width: 75%;
border-radius: 12px;
}
.message-content {
padding: 16px;
max-height: 300px;
overflow-y: auto;
}
.message-tools {
padding: 16px;
border-top: 1px solid #ececec;
}
.result-item {
margin-top: 12px;
p {
margin-bottom: 12px;
word-break: break-word !important;
}
}
}
.chapter-item {
display: flex;
}
.chapter-left {
display: flex;
position: relative;
color: #9ca3af;
text-align: right;
padding-right: 10px;
.chapter-left-title {
min-width: 60px;
margin-top: 10px;
}
.line-dot {
top: 18px;
height: 100%;
margin-left: 10px;
position: relative;
.dot {
position: absolute;
width: 7px;
height: 7px;
left: -3px;
background-color: #e5e7eb;
border-radius: 50%;
}
.line {
width: 1px;
height: 100%;
background-color: #e5e7eb;
}
}
}
.chapter-right {
flex: 1;
.chapter-right-content {
padding: 10px;
border-radius: 5px;
&:hover {
background-color: #e5e7eb;
}
h3 {
font-size: 14px;
font-weight: 600;
}
p {
font-size: 13px;
color: #999;
}
}
}
.message-file {
margin-top: 10px;
display: flex;
width: 256px;
border-radius: 12px;
background-color: #fff;
padding: 14px 16px;
box-shadow: 0 2px 15px #0000000a;
border: 0.5px solid #d9d9d9;
a {
color: currentColor;
}
.message-file-content {
flex: 1;
svg {
margin-right: 10px;
font-size: 20px;
color: #b83956;
}
}
.message-file-tools {
svg {
margin-left: 20px;
font-size: 20px;
}
}
&:hover {
border: 0.5px solid #b83956;
}
}
import { useState, useEffect, useRef } from 'react'
import {
SendOutlined,
UploadOutlined,
UndoOutlined,
CopyOutlined,
FileTextOutlined,
FileWordOutlined,
DownloadOutlined,
} from '@ant-design/icons'
import { ConfigProvider, Modal, Input, Button } from 'antd'
const { TextArea } = Input
import './AISearchModal.less'
import { usePaperOutline, useOutlineToPaper } from '@/hooks/useWenku'
import { useCopyToClipboard } from 'react-use'
export default function AIModal() {
const [isModalOpen, setIsModalOpen] = useState(true)
const [content, setContent] = useState('商业数据分析')
const [textIndent, setTextIndent] = useState(0)
const prePromptRef = useRef(null)
const messageScrollRef = useRef(null)
const { messages, setMessages, isLoading, query } = usePaperOutline()
useEffect(() => {
if (prePromptRef.current) {
const width = prePromptRef.current.offsetWidth + 10
setTextIndent(width)
}
}, [isModalOpen])
useEffect(() => {
if (messageScrollRef.current) {
const scrollContainer = messageScrollRef.current
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}, [messages])
const prePrompt = '帮我生成一篇课题报告,主题是:'
const handleEnterSearch = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSearch()
}
}
const handleSearch = () => {
query(prePrompt + content)
setContent('')
}
const [, copyToClipboard] = useCopyToClipboard()
// 复制内容
const handleCopy = (content) => {
copyToClipboard(content)
}
// 生成论文
const { query: outlineToPaper } = useOutlineToPaper()
const handleGeneratePaper = async (msg) => {
setMessages((prevMessages) => {
return [...prevMessages, { content: '正在生成长文...', role: 'ai', tips: '预计10分钟', queryID: msg.queryID }]
})
const paper = await outlineToPaper({ userQuery: msg.userQuery, queryID: msg.queryID, outline: msg.content })
setMessages((prevMessages) => {
prevMessages.pop()
return [...prevMessages, { content: '已为您生成初稿,请点击查看', role: 'ai', queryID: msg.queryID, paper }]
})
}
const MessageRender = ({ msg }) => {
if (msg.role === 'ai') {
if (msg.chapters && msg.chapters.length) {
return (
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">
{msg.chapters.map((item, index) => {
return (
<div className="chapter-item" key={index}>
<div className="chapter-left">
<div className="chapter-left-title">{item.chapter}</div>
<div className="line-dot">
<div className="dot"></div>
<div className="line"></div>
</div>
</div>
<div className="chapter-right">
<div className="chapter-right-content">
<h3>{item.title}</h3>
<p>{item.desc}</p>
</div>
</div>
</div>
)
})}
</div>
<div className="message-tools">
<Button
type="text"
size="small"
icon={<UndoOutlined />}
disabled={isLoading}
onClick={() => query(msg.userQuery)}>
换个大纲
</Button>
<Button
type="text"
size="small"
icon={<FileTextOutlined />}
disabled={isLoading}
onClick={() => handleGeneratePaper(msg)}>
生成长文
</Button>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(msg.content)}>
复制内容
</Button>
</div>
</div>
</div>
)
} else {
return (
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">
{msg.content}
{msg.paper?.downloadLink && (
<div className="message-file">
<div className="message-file-content">
<a href={msg.paper.downloadLink} target="_blank" rel="noreferrer">
<FileWordOutlined />
商业数据分析研究
</a>
</div>
<div className="message-file-tools">
<a href={msg.paper.downloadLink} target="_blank" rel="noreferrer">
<DownloadOutlined />
</a>
</div>
</div>
)}
</div>
</div>
</div>
)
}
} else {
return (
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">{msg.content}</div>
</div>
</div>
)
}
}
return (
<ConfigProvider theme={{ components: { Modal: { headerBg: '#f7f8fa', contentBg: '#f7f8fa' } } }}>
<Modal
title="长文写作/课程大纲:"
open={isModalOpen}
footer={null}
onCancel={() => setIsModalOpen(false)}
width={1000}>
<div className="message-scroll" ref={messageScrollRef}>
{messages.map((msg, index) => {
return <MessageRender msg={msg} key={index}></MessageRender>
})}
</div>
<div className="input-container">
<div className="input-box">
<div className="edit-area">
{prePrompt && (
<span className="pre-prompt" ref={prePromptRef}>
{prePrompt}
</span>
)}
<TextArea
className="content"
autoSize
value={content}
placeholder="今天需要我做些什么?shift+enter换行"
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleEnterSearch}
style={{ textIndent }}
/>
</div>
<div className="input-tools">
<Button type="text" icon={<UploadOutlined />} />
<Button type="primary" size="large" icon={<SendOutlined />} onClick={handleSearch} loading={isLoading} />
</div>
</div>
<div className="upload-list"></div>
</div>
</Modal>
</ConfigProvider>
)
}
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
export default function MarkdownRenderer({ children }) {
const md = new MarkdownIt({
html: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value
} catch (__) {
console.log(__)
}
}
return '' // use external default escaping
},
})
const renderedMarkdown = md.render(children)
return <div className="markdown-content" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
}
import { useState, useEffect, useRef } from 'react'
import { aiSearch, paperOutline, outlineToPaper, download } from '@/api/wenku'
// AI搜索
export function useSearch() {
const [messages, setMessages] = useState([])
const [isLoading, setIsLoading] = useState(false)
const query = async (userQuery) => {
try {
setIsLoading(true)
setMessages((prevMessages) => [...prevMessages, { content: userQuery, role: 'user' }])
const currentMessage = { content: '', searchReferList: [], role: 'ai', userQuery }
await aiSearch({
body: JSON.stringify({ userQuery }),
onmessage(message) {
try {
const data = JSON.parse(message.data)
const content = data.raw?.content || ''
const searchReferList = data.raw?.searchReferList || []
currentMessage.content += content
if (searchReferList.length) {
currentMessage.searchReferList.push(...searchReferList)
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1]
if (lastMessage?.role === 'ai') {
return [...prevMessages.slice(0, -1), currentMessage]
}
return [...prevMessages, currentMessage]
})
}
} catch (error) {
console.log(error)
}
},
})
} catch (error) {
console.log(error)
} finally {
setIsLoading(false)
}
}
return { messages, setMessages, isLoading, query }
}
function parseInput(inputStr) {
const result = []
const regex = /^(#+)\s*(.+?)\s*(?=\n|【描述】|$)/g
const descriptionRegex = /【描述】(.*)/
const sections = []
const lines = inputStr.split('\n')
let level = 0
let currentTitle = ''
let currentChapter = ''
for (let line of lines) {
const headerMatch = line.match(regex)
const descriptionMatch = line.match(descriptionRegex)
if (headerMatch) {
const header = headerMatch[0]
const newLevel = header.match(/^#+/)[0].length
const title = header.replace(/^#+\s*/, '').trim()
// Handle level change
if (newLevel > level) {
// Increase section number for sub-levels
if (level === 0) {
sections.push(1)
} else {
sections.push(sections[sections.length - 1] + 1)
}
} else if (newLevel === level) {
sections[sections.length - 1] = sections[sections.length - 1] + 1
} else {
sections.splice(newLevel, sections.length - newLevel)
}
// Generate chapter number
currentChapter = sections.join('.')
// Set level and title
level = newLevel
currentTitle = title
}
if (descriptionMatch) {
const desc = descriptionMatch[1].trim()
result.push({ title: currentTitle, level, desc, chapter: `第${currentChapter}章` })
}
}
return result
}
// 生成论文大纲
export function usePaperOutline() {}
export function usePaperOutline() {
const [messages, setMessages] = useState([])
const [isLoading, setIsLoading] = useState(false)
const query = async (userQuery) => {
try {
setIsLoading(true)
setMessages((prevMessages) => [...prevMessages, { content: userQuery, role: 'user' }])
const currentMessage = { content: '', role: 'ai', userQuery, chapters: [], logID: '', queryID: '' }
await paperOutline({
body: JSON.stringify({ userQuery }),
onmessage(message) {
try {
const data = JSON.parse(message.data)
const content = data.raw?.data || ''
if (data.logID) currentMessage.logID = data.logID
if (data.queryID) currentMessage.queryID = data.queryID
currentMessage.content += content
currentMessage.chapters = parseInput(currentMessage.content)
if (content) {
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1]
if (lastMessage?.role === 'ai') {
return [...prevMessages.slice(0, -1), currentMessage]
}
return [...prevMessages, currentMessage]
})
}
} catch (error) {
console.log(error)
}
},
})
} catch (error) {
console.log(error)
} finally {
setIsLoading(false)
}
}
return { messages, setMessages, isLoading, query }
}
// 大纲生成论文
export function useOutlineToPaper() {
const { queryWithPolling } = useDownload()
const [isLoading, setIsLoading] = useState(false)
const [paper, setPaper] = useState('')
const query = async (data) => {
try {
setIsLoading(true)
const { raw } = await outlineToPaper(data)
setPaper(raw)
const downloadLink = await queryWithPolling({
docID: '280fe43ee63a580216fc700abb68a98271feac83' || raw.docID,
})
setPaper({ ...raw, downloadLink })
return { ...raw, downloadLink }
} catch (error) {
console.log(error)
} finally {
setIsLoading(false)
}
}
return { paper, setPaper, isLoading, query }
}
// 文档下载
export function useDownload() {}
export function useDownload() {
const [downloadLink, setDownloadLink] = useState('')
const [isLoading, setIsLoading] = useState(false)
const timerRef = useRef(null)
const clearTimer = () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}
const query = async (data) => {
try {
setIsLoading(true)
const { raw } = await download(data)
if (raw) {
setDownloadLink(raw.download_link)
return raw.download_link
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}
const queryWithPolling = async (data, interval = 3000) => {
const poll = async (resolve, reject) => {
try {
const link = await query(data)
if (link) {
resolve(link)
return
}
} catch (error) {
reject(error)
return
}
timerRef.current = setTimeout(() => poll(resolve, reject), interval)
}
return new Promise(poll)
}
useEffect(() => {
// 清除定时器的逻辑
return () => {
clearTimer()
}
}, [])
// 文档上传
export function useUpload() {}
return { downloadLink, isLoading, query, queryWithPolling, clearTimer }
}
......@@ -26,6 +26,17 @@ httpRequest.interceptors.request.use((config) => {
return config
})
// 响应拦截器
httpRequest.interceptors.response.use(
(response) => {
const { data } = response
return data
},
(error) => {
return Promise.reject(error.response || error)
}
)
export default httpRequest
export async function fetchEventSourceFn(url, options) {
......@@ -36,6 +47,9 @@ export async function fetchEventSourceFn(url, options) {
onmessage(res) {
const message = JSON.parse(res.data)
messages.push(message)
if (options.onmessage) {
options.onmessage(message)
}
},
}
......
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import mkcert from 'vite-plugin-mkcert'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
export default defineConfig(() => {
return {
plugins: [react(), mkcert()],
server: {
open: true,
host: 'dev.ezijing.com',
proxy: {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), // 将请求路径中的“/api”前缀替换为空字符串
},
'/api/wenku': {
target: 'https://wenchain.baidu.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/wenku/, '/'), // 将请求路径中的“/api”前缀替换为空字符串
rewrite: (path) => path.replace(/^\/api\/wenku/, '/'),
},
'/api': {
target: 'https://zijingebook.ezijing.com',
changeOrigin: true,
},
},
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论