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

chore: update

上级 e0539045
......@@ -28,7 +28,9 @@
"js-cookie": "^3.0.5",
"js-md5": "^0.8.3",
"lodash-es": "^4.17.21",
"lucide-react": "^0.474.0",
"markdown-it": "^14.1.0",
"nanoid": "^3.3.8",
"qs": "^6.11.2",
"rc-slider-captcha": "^1.3.0",
"react": "^18.3.1",
......@@ -1402,7 +1404,8 @@
"node_modules/@types/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@types/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ=="
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
"license": "MIT"
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
......@@ -1525,6 +1528,7 @@
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"license": "MIT",
"dependencies": {
"is-url": "^1.2.4"
},
......@@ -1541,6 +1545,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
"license": "MIT",
"dependencies": {
"prismjs": "^1.23.0"
},
......@@ -1555,6 +1560,7 @@
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"license": "MIT",
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
......@@ -1623,6 +1629,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@wangeditor/list-module/-/list-module-1.0.5.tgz",
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
......@@ -1643,6 +1650,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@wangeditor/table-module/-/table-module-1.1.4.tgz",
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
......@@ -1657,6 +1665,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.0.3",
"@uppy/xhr-upload": "^2.0.3",
......@@ -1672,6 +1681,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@wangeditor/video-module/-/video-module-1.1.4.tgz",
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.1.4",
"@uppy/xhr-upload": "^2.0.7",
......@@ -3029,7 +3039,8 @@
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
......@@ -3250,6 +3261,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
......@@ -3798,6 +3810,7 @@
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
......@@ -3812,6 +3825,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
......@@ -3822,6 +3836,7 @@
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
......@@ -4029,6 +4044,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
......@@ -4100,6 +4116,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
......@@ -4195,6 +4212,7 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
......@@ -4844,6 +4862,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
......@@ -4873,6 +4892,7 @@
"version": "20.6.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz",
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0"
}
......@@ -5438,7 +5458,8 @@
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": {
"version": "2.0.2",
......@@ -5830,6 +5851,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.474.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz",
"integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
......@@ -6221,15 +6251,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
......@@ -6328,7 +6359,8 @@
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/node-libs-browser": {
"version": "2.2.1",
......@@ -6960,6 +6992,7 @@
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
......@@ -8377,6 +8410,7 @@
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
......@@ -8541,6 +8575,7 @@
"version": "0.66.0",
"resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.66.0.tgz",
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
"license": "MIT",
"dependencies": {
"is-plain-object": "^5.0.0"
},
......@@ -9361,9 +9396,10 @@
"peer": true
},
"node_modules/type": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
......
......@@ -31,7 +31,9 @@
"js-cookie": "^3.0.5",
"js-md5": "^0.8.3",
"lodash-es": "^4.17.21",
"lucide-react": "^0.474.0",
"markdown-it": "^14.1.0",
"nanoid": "^3.3.8",
"qs": "^6.11.2",
"rc-slider-captcha": "^1.3.0",
"react": "^18.3.1",
......
......@@ -122,22 +122,35 @@
}
.chapter-right {
flex: 1;
display: flex;
align-items: center;
.chapter-right-content {
flex: 1;
padding: 10px;
border-radius: 5px;
&:hover {
background-color: #e5e7eb;
}
h3 {
h3,
.chapter-title {
font-size: 14px;
font-weight: 600;
}
p {
p,
.chapter-desc {
font-size: 13px;
color: #999;
}
}
&:hover {
.chapter-right-tools {
opacity: 1;
}
}
.chapter-right-tools {
opacity: 0;
}
}
.message-file {
......
......@@ -7,33 +7,47 @@ import {
FileTextOutlined,
FileWordOutlined,
DownloadOutlined,
PlusCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons'
import { ConfigProvider, Modal, Input, Button } from 'antd'
import { ConfigProvider, Modal, Input, Button, Dropdown } from 'antd'
const { TextArea } = Input
import './AISearchModal.less'
import { usePaper } from '@/hooks/useWenku'
import { useCopyToClipboard } from 'react-use'
import { CircleEllipsis } from 'lucide-react'
export default function AIModal() {
const [isModalOpen, setIsModalOpen] = useState(true)
const [content, setContent] = useState('商业数据分析')
const [content, setContent] = useState('')
const [textIndent, setTextIndent] = useState(0)
const prePromptRef = useRef(null)
const messageScrollRef = useRef(null)
const { messages, setMessages, isLoading, generateOutline, generatePaper } = usePaper()
const {
messages,
setMessages,
isLoading,
generateOutline,
generatePaper,
chapterTagRender,
getChaptersMarkdown,
addChapter,
updateChapter,
removeChapter,
} = usePaper()
useEffect(() => {
if (prePromptRef.current) {
const width = prePromptRef.current.offsetWidth + 10
setTextIndent(width)
}
}, [isModalOpen])
const [isEditMessage, setIsEditMessage] = useState(false)
useEffect(() => {
if (messageScrollRef.current) {
if (messageScrollRef.current && !isEditMessage) {
const scrollContainer = messageScrollRef.current
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}, [messages])
}, [messages, isEditMessage])
const prePrompt = '帮我生成一篇课题报告,主题是:'
const handleEnterSearch = (e) => {
......@@ -45,13 +59,14 @@ export default function AIModal() {
const handleSearch = () => {
generateOutline(prePrompt + content)
setContent('')
setIsEditMessage(false)
}
const [, copyToClipboard] = useCopyToClipboard()
// 复制内容
const handleCopy = (content) => {
copyToClipboard(content)
const handleCopy = (msg) => {
copyToClipboard(getChaptersMarkdown(msg.chapters))
}
// 生成论文
......@@ -59,13 +74,26 @@ export default function AIModal() {
setMessages((prevMessages) => {
return [...prevMessages, { content: '正在生成长文...', role: 'ai', tips: '预计10分钟', queryID: msg.queryID }]
})
const paper = await generatePaper({ userQuery: msg.userQuery, queryID: msg.queryID, outline: msg.content })
const paper = await generatePaper({
userQuery: msg.userQuery,
queryID: msg.queryID,
outline: getChaptersMarkdown(msg.chapters),
})
setMessages((prevMessages) => {
prevMessages.pop()
return [...prevMessages, { content: '已为您生成初稿,请点击下载', role: 'ai', queryID: msg.queryID, paper }]
})
}
let chapterMenuItems = [
{ key: 'add', label: '新增', icon: <PlusCircleOutlined style={{ fontSize: 14 }} /> },
{ key: 'remove', label: '删除', icon: <DeleteOutlined style={{ fontSize: 14 }} /> },
]
const handleMenuClick = async (e, chapter, message) => {
setIsEditMessage(true)
if (e.key === 'add') addChapter(chapter, message)
if (e.key === 'remove') removeChapter(chapter, message)
}
const MessageRender = ({ msg }) => {
if (msg.role === 'ai') {
if (msg.chapters && msg.chapters.length) {
......@@ -73,11 +101,11 @@ export default function AIModal() {
<div className={`message-item ${msg.role}`}>
<div className="message-box">
<div className="message-content">
{msg.chapters.map((item, index) => {
{msg.chapters.map((item) => {
return (
<div className="chapter-item" key={index}>
<div className="chapter-item" key={item.id}>
<div className="chapter-left">
<div className="chapter-left-title">{item.tag}</div>
<div className="chapter-left-title">{chapterTagRender(item, msg.chapters)}</div>
<div className="line-dot">
<div className="dot"></div>
<div className="line"></div>
......@@ -85,8 +113,33 @@ export default function AIModal() {
</div>
<div className="chapter-right">
<div className="chapter-right-content">
<h3>{item.title}</h3>
<p>{item.desc}</p>
<Input
className="chapter-title"
placeholder="请输入标题"
defaultValue={item.title}
variant="borderless"
onBlur={(e) => updateChapter(item, msg, { ...item, title: e.target.value })}
/>
{item.level !== 1 && (
<Input
className="chapter-desc"
placeholder="请输入你的备注,如这个章节必须包含正反观点等。"
defaultValue={item.desc}
variant="borderless"
onBlur={(e) => updateChapter(item, msg, { ...item, desc: e.target.value })}
/>
)}
{/* <h3>{item.title}</h3> */}
{/* <p>{item.desc}</p> */}
</div>
<div className="chapter-right-tools">
{item.level !== 1 && (
<Dropdown
menu={{ items: chapterMenuItems, onClick: (e) => handleMenuClick(e, item, msg) }}
getPopupContainer={(triggerNode) => triggerNode.parentNode}>
<Button type="text" icon={<CircleEllipsis color="#222" size={20} />}></Button>
</Dropdown>
)}
</div>
</div>
</div>
......@@ -110,7 +163,7 @@ export default function AIModal() {
onClick={() => handleGeneratePaper(msg)}>
生成长文
</Button>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(msg.content)}>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(msg)}>
复制内容
</Button>
</div>
......@@ -163,8 +216,8 @@ export default function AIModal() {
onCancel={() => setIsModalOpen(false)}
width={1000}>
<div className="message-scroll" ref={messageScrollRef}>
{messages.map((msg, index) => {
return <MessageRender msg={msg} key={index}></MessageRender>
{messages.map((msg) => {
return <MessageRender msg={msg} key={msg.id}></MessageRender>
})}
</div>
<div className="input-container">
......
import { useState, useEffect, useRef } from 'react'
import { aiSearch, paperOutline, outlineToPaper, download } from '@/api/wenku'
import { useAsyncTask } from './useAsyncTask'
/**
* @typedef {Object} Message
* @property {string} content
* @property {'user' | 'ai'} role
* @property {Array<{title: string, url: string}>} [searchReferList]
* @property {string} [userQuery]
* @property {Chapter[]} [chapters]
* @property {string} [logID]
* @property {string} [queryID]
*/
/**
* @typedef {Object} Chapter
* @property {string} title
* @property {number} level
* @property {string} desc
* @property {string} chapter
*/
/**
* @typedef {Object} Paper
* @property {string} content
* @property {string} [downloadLink]
* @property {string} [docID]
*/
/**
* @typedef {Object} DownloadResult
* @property {string} download_link
*/
import { nanoid } from 'nanoid'
// AI搜索
export function useSearch() {
......@@ -68,80 +38,9 @@ export function useSearch() {
return { messages, setMessages, isLoading, query: execute }
}
function parseInput(markdown) {
// Split content into lines
const lines = markdown.split('\n').filter((line) => line.trim())
const result = []
let chapterCount = 0
let subChapterCount = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
let level = 0
let title = ''
let desc = ''
// Determine level and title
if (line.startsWith('# ')) {
level = 1
title = line.replace('# ', '')
} else if (line.startsWith('## ')) {
level = 2
title = line.replace('## ', '')
chapterCount++
subChapterCount = 0
} else if (line.startsWith('### ')) {
level = 3
title = line.replace('### ', '')
subChapterCount++
}
// If this is a header line
if (level > 0) {
// Get description from next line if it exists and contains 【描述】
const nextLine = lines[i + 1]
if (nextLine && nextLine.includes('【描述】')) {
desc = nextLine.replace('【描述】', '')
i++ // Skip the description line in next iteration
}
// Determine tag based on level
let tag
if (level === 1) {
tag = '标题'
} else if (level === 2) {
tag = `第${chapterCount}章`
} else if (level === 3) {
tag = `${chapterCount}.${subChapterCount}`
}
// Create content string
const content = desc ? `${'#'.repeat(level)} ${title}\n【描述】${desc}` : `${'#'.repeat(level)} ${title}`
// Add to result array
result.push({
title,
level,
tag,
desc: desc || '',
content,
})
}
}
return result
}
/**
* 论文生成全流程管理
* @returns {{
* messages: Message[],
* paper: Paper,
* isLoading: boolean,
* generateOutline: (userQuery: string) => Promise<void>,
* generatePaper: (data: any) => Promise<Paper>,
* downloadPaper: (docID: string) => Promise<string>
* }}
*/
export function usePaper() {
const [messages, setMessages] = useState([])
......@@ -156,9 +55,202 @@ export function usePaper() {
}
}
const parseMarkdown = (text) => {
const lines = text.split('\n')
const result = []
let currentSection = null
lines.forEach((line) => {
const match = line.match(/^(#+)\s+(.*)/)
if (match) {
const level = match[1].length
const title = match[2]
currentSection = {
id: nanoid(),
title,
level,
content: line,
}
result.push(currentSection)
} else if (line.startsWith('【描述】')) {
if (currentSection) {
currentSection.desc = line.replace('【描述】', '').trim()
currentSection.content += '\n' + line
}
} else if (currentSection) {
currentSection.content += '\n' + line
}
})
return result
}
const getChaptersMarkdown = (chapters) => {
return chapters
.map((chapter) => {
const level = '#'.repeat(chapter.level)
return `${level} ${chapter.title}\n${chapter.desc ?? ''}`
})
.join('\n')
}
const chapterTagRender = (chapter, chapters) => {
let chapterCount = 0 // 用于记录章节编号
let sectionCount = 0 // 用于记录当前章节下的小节编号
for (let i = 0; i < chapters.length; i++) {
const ch = chapters[i]
// 一级章节
if (ch.level === 1) {
if (ch === chapter) {
return '标题' // 一级章节显示“标题”
}
}
// 二级章节
else if (ch.level === 2) {
chapterCount++ // 一级章节增加
sectionCount = 0 // 重置二级章节计数
if (ch === chapter) {
return `${chapterCount}` // 返回“第X章”
}
}
// 三级章节
else if (ch.level === 3) {
sectionCount++ // 二级章节增加
if (ch === chapter) {
return `${chapterCount}.${sectionCount}` // 返回“小节编号 X.Y”
}
}
}
return ''
}
// 新增章节
const addChapter = (chapter, message) => {
const newChapter = {
title: '',
level: chapter.level,
desc: '',
}
setMessages((prevMessages) => {
const messageIndex = prevMessages.findIndex((msg) => msg === message)
// 找到目标章节在数组中的索引
const chapterIndex = message.chapters.findIndex((item) => item === chapter)
// 如果找不到章节,直接添加到末尾
if (chapterIndex === -1) {
const updatedMessages = [...prevMessages]
updatedMessages[messageIndex] = {
...prevMessages[messageIndex],
chapters: [...message.chapters, newChapter],
}
return updatedMessages
}
// 在当前章节后直接插入新章节
const updatedChapters = [...message.chapters]
updatedChapters.splice(chapterIndex + 1, 0, newChapter)
// 更新消息数组
const updatedMessages = [...prevMessages]
updatedMessages[messageIndex] = {
...prevMessages[messageIndex],
chapters: updatedChapters,
}
return updatedMessages
})
}
// 更新章节
const updateChapter = (chapter, message, updatedData) => {
console.log(updatedData)
setMessages((prevMessages) => {
const messageIndex = prevMessages.findIndex((msg) => msg === message)
// 找到要更新的章节在数组中的索引
const chapterIndex = message.chapters.findIndex((item) => item === chapter)
// 如果找不到章节,返回原数组
if (chapterIndex === -1) {
return prevMessages
}
// 创建更新后的章节数组
const updatedChapters = [...message.chapters]
// 更新章节数据,保持原有属性不变
updatedChapters[chapterIndex] = {
...chapter, // 保留原有属性
...updatedData, // 使用新的数据覆盖
level: chapter.level, // 确保level不被意外修改
}
// 更新消息数组
const updatedMessages = [...prevMessages]
updatedMessages[messageIndex] = {
...prevMessages[messageIndex],
chapters: updatedChapters,
}
return updatedMessages
})
}
// 删除章节
const removeChapter = (chapter, message) => {
setMessages((prevMessages) => {
const messageIndex = prevMessages.findIndex((msg) => msg === message)
// 找到要删除的章节在数组中的索引
const chapterIndex = message.chapters.findIndex((item) => item === chapter)
// 如果找不到章节,返回原数组
if (chapterIndex === -1) {
return prevMessages
}
// 获取当前章节的层级
const targetLevel = chapter.level
// 找到下一个同级或更高级章节的索引
const nextSameLevelIndex = message.chapters.findIndex((item, index) => {
return index > chapterIndex && item.level <= targetLevel
})
// 过滤章节:
// 1. 保留目标章节之前的所有章节
// 2. 删除目标章节
// 3. 删除到下一个同级章节之前的所有子章节
const filteredChapters = message.chapters.filter((item, index) => {
if (index < chapterIndex) {
// 保留目标章节之前的章节
return true
} else if (index === chapterIndex) {
// 删除目标章节
return false
} else if (nextSameLevelIndex === -1) {
// 如果没有下一个同级章节,只保留层级小于等于目标章节的
return item.level <= targetLevel
} else {
// 如果有下一个同级章节,删除其之前的所有子章节
return index >= nextSameLevelIndex
}
})
// 更新消息数组
const updatedMessages = [...prevMessages]
updatedMessages[messageIndex] = {
...prevMessages[messageIndex],
chapters: filteredChapters,
}
return updatedMessages
})
}
const { isLoading: isOutlineLoading, execute: generateOutline } = useAsyncTask(async (userQuery) => {
setMessages((prevMessages) => [...prevMessages, { content: userQuery, role: 'user' }])
const currentMessage = { content: '', role: 'ai', userQuery, chapters: [], logID: '', queryID: '' }
setMessages((prevMessages) => [...prevMessages, { id: nanoid(), content: userQuery, role: 'user' }])
const currentMessage = { id: nanoid(), content: '', role: 'ai', userQuery, chapters: [], logID: '', queryID: '' }
await paperOutline({
body: JSON.stringify({ userQuery }),
......@@ -169,7 +261,7 @@ export function usePaper() {
if (data.logID) currentMessage.logID = data.logID
if (data.queryID) currentMessage.queryID = data.queryID
currentMessage.content += content
currentMessage.chapters = parseInput(currentMessage.content)
currentMessage.chapters = parseMarkdown(currentMessage.content)
if (content) {
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1]
......@@ -236,5 +328,10 @@ export function usePaper() {
generateOutline,
generatePaper,
downloadPaper,
chapterTagRender,
getChaptersMarkdown,
addChapter,
updateChapter,
removeChapter,
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论