[新增]AI: 思维导图
parent
c4ee9581d0
commit
33b0b5ceb3
|
@ -52,7 +52,12 @@
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
|
"markmap-common": "^0.16.0",
|
||||||
|
"markmap-lib": "^0.16.1",
|
||||||
|
"markmap-toolbar": "^0.17.0",
|
||||||
|
"markmap-view": "^0.16.0",
|
||||||
"min-dash": "^4.1.1",
|
"min-dash": "^4.1.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
@ -85,8 +90,8 @@
|
||||||
"@types/qs": "^6.9.12",
|
"@types/qs": "^6.9.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"@unocss/transformer-variant-group": "^0.58.5",
|
|
||||||
"@unocss/eslint-config": "^0.57.4",
|
"@unocss/eslint-config": "^0.57.4",
|
||||||
|
"@unocss/transformer-variant-group": "^0.58.5",
|
||||||
"@vitejs/plugin-legacy": "^5.3.1",
|
"@vitejs/plugin-legacy": "^5.3.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
|
12586
pnpm-lock.yaml
12586
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
||||||
|
import { getAccessToken } from '@/utils/auth'
|
||||||
|
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||||
|
import { config } from '@/config/axios/config'
|
||||||
|
|
||||||
|
export interface AiMindMapGenerateReqVO {
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
export const AiMindMapApi = {
|
||||||
|
generateMindMap: ({
|
||||||
|
data,
|
||||||
|
onClose,
|
||||||
|
onMessage,
|
||||||
|
onError,
|
||||||
|
ctrl
|
||||||
|
}: {
|
||||||
|
data: AiMindMapGenerateReqVO
|
||||||
|
onMessage?: (res: any) => void
|
||||||
|
onError?: (...args: any[]) => void
|
||||||
|
onClose?: (...args: any[]) => void
|
||||||
|
ctrl: AbortController
|
||||||
|
}) => {
|
||||||
|
const token = getAccessToken()
|
||||||
|
return fetchEventSource(`${config.base_url}/ai/mind-map/generate-stream`, {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
onmessage: onMessage,
|
||||||
|
onerror: onError,
|
||||||
|
onclose: onClose,
|
||||||
|
signal: ctrl.signal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-[350px] p-5 flex flex-col bg-[#f5f7f9]">
|
||||||
|
<h3 class="w-full h-full h-7 text-5 text-center leading-[28px] title">思维导图创作中心</h3>
|
||||||
|
<!--下面表单部分-->
|
||||||
|
<div class="flex-grow overflow-y-auto">
|
||||||
|
<div class="mt-[30ppx]">
|
||||||
|
<el-text tag="b">您的需求?</el-text>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.prompt"
|
||||||
|
maxlength="1024"
|
||||||
|
rows="5"
|
||||||
|
class="w-100% mt-15px"
|
||||||
|
input-style="border-radius: 7px;"
|
||||||
|
placeholder="请输入提示词,让AI帮你完善"
|
||||||
|
show-word-limit
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
class="!w-full mt-[15px]"
|
||||||
|
type="primary"
|
||||||
|
:loading="isGenerating"
|
||||||
|
@click="emits('submit', formData)"
|
||||||
|
>智能生成思维导图</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[30px]">
|
||||||
|
<el-text tag="b">使用已有内容生成?</el-text>
|
||||||
|
<el-input
|
||||||
|
v-model="existPrompt"
|
||||||
|
maxlength="1024"
|
||||||
|
rows="5"
|
||||||
|
class="w-100% mt-15px"
|
||||||
|
input-style="border-radius: 7px;"
|
||||||
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
show-word-limit
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
class="!w-full mt-[15px]"
|
||||||
|
type="primary"
|
||||||
|
@click="emits('directGenerate', existPrompt)"
|
||||||
|
:disabled="isGenerating"
|
||||||
|
>直接生成</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { MindmapExitExample } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
|
const emits = defineEmits(['submit', 'directGenerate'])
|
||||||
|
defineProps<{
|
||||||
|
isGenerating: boolean
|
||||||
|
}>()
|
||||||
|
// 提交的提示词字段
|
||||||
|
const formData = reactive({
|
||||||
|
prompt: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const existPrompt = ref(MindmapExitExample) // 已有的内容
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setExistPrompt(e: string){ // 设置已有的内容,在生成结束的时候将结果赋值给该值
|
||||||
|
existPrompt.value = e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.title {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,169 @@
|
||||||
|
<template>
|
||||||
|
<el-card class="my-card h-full flex-grow">
|
||||||
|
<template #header
|
||||||
|
><h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
|
||||||
|
<span>思维导图预览</span>
|
||||||
|
<!-- 展示在右上角 -->
|
||||||
|
<el-button type="primary" v-show="isEnd" @click="downloadImage" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="ph:copy-bold" />
|
||||||
|
</template>
|
||||||
|
下载图片
|
||||||
|
</el-button>
|
||||||
|
</h3></template
|
||||||
|
>
|
||||||
|
|
||||||
|
<div ref="contentRef" class="hide-scroll-bar h-full box-border">
|
||||||
|
<!--展示markdown的容器,最终生成的是html字符串,直接用v-html嵌入-->
|
||||||
|
<div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
|
||||||
|
<div class="flex flex-col items-center justify-center" v-html="html"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="mindmapRef" class="wh-full">
|
||||||
|
<svg ref="svgRef" class="w-full" :style="{ height: `${contentAreaHeight}px` }" />
|
||||||
|
<div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Markmap } from 'markmap-view'
|
||||||
|
import { Transformer } from 'markmap-lib'
|
||||||
|
import { Toolbar } from 'markmap-toolbar'
|
||||||
|
import markdownit from 'markdown-it'
|
||||||
|
|
||||||
|
const md = markdownit()
|
||||||
|
const props = defineProps<{
|
||||||
|
mindmapResult: string // 生成结果
|
||||||
|
isEnd: boolean // 是否结束
|
||||||
|
isGenerating: boolean // 是否正在生成
|
||||||
|
isStart: boolean // 开始状态,开始时需要清除html
|
||||||
|
}>()
|
||||||
|
const contentRef = ref<HTMLDivElement>() // 右侧出来header以下的区域
|
||||||
|
const mdContainerRef = ref<HTMLDivElement>() // markdown的容器,用来滚动到底下的
|
||||||
|
const mindmapRef = ref<HTMLDivElement>() // 思维导图的容器
|
||||||
|
const svgRef = ref<SVGElement>() // 思维导图的渲染svg
|
||||||
|
const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
|
||||||
|
const html = ref('') // 生成过程中的文本
|
||||||
|
const contentAreaHeight = ref(0) // 生成区域的高度,出去header部分
|
||||||
|
let markMap: Markmap | null = null
|
||||||
|
const transformer = new Transformer()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
|
||||||
|
/** 初始化思维导图 **/
|
||||||
|
try {
|
||||||
|
markMap = Markmap.create(svgRef.value!)
|
||||||
|
const { el } = Toolbar.create(markMap)
|
||||||
|
toolBarRef.value?.append(el)
|
||||||
|
nextTick(update)
|
||||||
|
} catch (e) {
|
||||||
|
message.error('思维导图初始化失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(props, ({ mindmapResult, isGenerating, isEnd, isStart }) => {
|
||||||
|
// 开始生成的时候清空一下markdown的内容
|
||||||
|
if (isStart) {
|
||||||
|
html.value = ''
|
||||||
|
}
|
||||||
|
// 生成内容的时候使用markdown来渲染
|
||||||
|
if (isGenerating) {
|
||||||
|
html.value = md.render(mindmapResult)
|
||||||
|
}
|
||||||
|
if (isEnd) {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
try {
|
||||||
|
const { root } = transformer.transform(processContent(props.mindmapResult))
|
||||||
|
markMap?.setData(root)
|
||||||
|
markMap?.fit()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processContent = (text) => {
|
||||||
|
const arr: string[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
for (let line of lines) {
|
||||||
|
if (line.indexOf('```') !== -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
|
||||||
|
arr.push(line)
|
||||||
|
}
|
||||||
|
return arr.join('\n')
|
||||||
|
}
|
||||||
|
// download SVG to png file
|
||||||
|
const downloadImage = () => {
|
||||||
|
const svgElement = mindmapRef.value
|
||||||
|
// 将 SVG 渲染到图片对象
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const source =
|
||||||
|
'<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value!)
|
||||||
|
const image = new Image()
|
||||||
|
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
|
||||||
|
|
||||||
|
// 将图片对象渲染
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = svgElement?.offsetWidth || 0
|
||||||
|
canvas.height = svgElement?.offsetHeight || 0
|
||||||
|
let context = canvas.getContext('2d')
|
||||||
|
context?.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
image.onload = function () {
|
||||||
|
context?.drawImage(image, 0, 0)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.download = 'ruoyi-mindmap.png'
|
||||||
|
a.href = canvas.toDataURL(`image/png`)
|
||||||
|
a.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
scrollBottom() {
|
||||||
|
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hide-scroll-bar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.my-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
@extend .hide-scroll-bar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// markmap的tool样式覆盖
|
||||||
|
:deep(.markmap) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
:deep(.mm-toolbar-brand) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:deep(.mm-toolbar) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="absolute top-0 left-0 right-0 bottom-0 flex">
|
||||||
|
<!--表单区域-->
|
||||||
|
<Left ref="leftRef" @submit="submit" @direct-generate="directGenerate" :is-generating="isGenerating" />
|
||||||
|
<!--右边生成思维导图区域-->
|
||||||
|
<Right
|
||||||
|
ref="rightRef"
|
||||||
|
:mindmapResult="mindmapResult"
|
||||||
|
:isEnd="isEnd"
|
||||||
|
:isGenerating="isGenerating"
|
||||||
|
:isStart="isStart"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Left from './components/Left.vue'
|
||||||
|
import Right from './components/Right.vue'
|
||||||
|
import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap'
|
||||||
|
import { MindmapExitExample } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AIMindMap'
|
||||||
|
})
|
||||||
|
const ctrl = ref<AbortController>() // 请求控制
|
||||||
|
const isGenerating = ref(false) // 是否正在生成思维导图
|
||||||
|
const isStart = ref(false) // 开始生成,用来清空思维导图
|
||||||
|
const isEnd = ref(true) // 用来判断结束的时候渲染思维导图
|
||||||
|
const message = useMessage() // 消息提示
|
||||||
|
|
||||||
|
const mindmapResult = ref('') // 生成思维导图结果
|
||||||
|
|
||||||
|
const leftRef = ref<InstanceType<typeof Left>>() // 左边组件
|
||||||
|
const rightRef = ref<InstanceType<typeof Right>>() // 右边组件
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mindmapResult.value = MindmapExitExample
|
||||||
|
})
|
||||||
|
/** 使用已有内容直接生成 **/
|
||||||
|
const directGenerate = (existPrompt: string) => {
|
||||||
|
isEnd.value = false // 先设置为false再设置为true,让子组建的watch能够监听到
|
||||||
|
mindmapResult.value = existPrompt
|
||||||
|
isEnd.value = true
|
||||||
|
}
|
||||||
|
/** 停止 stream 生成 */
|
||||||
|
const stopStream = () => {
|
||||||
|
isGenerating.value = false
|
||||||
|
isStart.value = false
|
||||||
|
ctrl.value?.abort()
|
||||||
|
}
|
||||||
|
const submit = (data: AiMindMapGenerateReqVO) => {
|
||||||
|
isGenerating.value = true
|
||||||
|
isStart.value = true
|
||||||
|
isEnd.value = false
|
||||||
|
ctrl.value = new AbortController() // 请求控制赋值
|
||||||
|
mindmapResult.value = '' // 清空生成数据
|
||||||
|
AiMindMapApi.generateMindMap({
|
||||||
|
data,
|
||||||
|
onMessage:async (res) => {
|
||||||
|
const { code, data, msg } = JSON.parse(res.data)
|
||||||
|
if (code !== 0) {
|
||||||
|
message.alert(`生成思维导图异常! ${msg}`)
|
||||||
|
stopStream()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mindmapResult.value = mindmapResult.value + data
|
||||||
|
await nextTick()
|
||||||
|
rightRef.value?.scrollBottom()
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
isEnd.value = true
|
||||||
|
leftRef.value?.setExistPrompt(mindmapResult.value)
|
||||||
|
stopStream()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error('生成思维导图失败', err)
|
||||||
|
stopStream()
|
||||||
|
},
|
||||||
|
ctrl: ctrl.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -414,3 +414,64 @@ export const WriteExample = {
|
||||||
data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务,请及时与我联系。\n\n祝工作顺利。\n\n谢谢。'
|
data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务,请及时与我联系。\n\n祝工作顺利。\n\n谢谢。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** 思维导图已有内容生成示例 **/
|
||||||
|
export const MindmapExitExample = `# Java 技术栈
|
||||||
|
|
||||||
|
## 核心技术
|
||||||
|
### Java SE
|
||||||
|
### Java EE
|
||||||
|
|
||||||
|
## 框架
|
||||||
|
### Spring
|
||||||
|
#### Spring Boot
|
||||||
|
#### Spring MVC
|
||||||
|
#### Spring Data
|
||||||
|
### Hibernate
|
||||||
|
### MyBatis
|
||||||
|
|
||||||
|
## 构建工具
|
||||||
|
### Maven
|
||||||
|
### Gradle
|
||||||
|
|
||||||
|
## 版本控制
|
||||||
|
### Git
|
||||||
|
### SVN
|
||||||
|
|
||||||
|
## 测试工具
|
||||||
|
### JUnit
|
||||||
|
### Mockito
|
||||||
|
### Selenium
|
||||||
|
|
||||||
|
## 应用服务器
|
||||||
|
### Tomcat
|
||||||
|
### Jetty
|
||||||
|
### WildFly
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
### MySQL
|
||||||
|
### PostgreSQL
|
||||||
|
### Oracle
|
||||||
|
### MongoDB
|
||||||
|
|
||||||
|
## 消息队列
|
||||||
|
### Kafka
|
||||||
|
### RabbitMQ
|
||||||
|
### ActiveMQ
|
||||||
|
|
||||||
|
## 微服务
|
||||||
|
### Spring Cloud
|
||||||
|
### Dubbo
|
||||||
|
|
||||||
|
## 容器化
|
||||||
|
### Docker
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
## 云服务
|
||||||
|
### AWS
|
||||||
|
### Azure
|
||||||
|
### Google Cloud
|
||||||
|
|
||||||
|
## 开发工具
|
||||||
|
### IntelliJ IDEA
|
||||||
|
### Eclipse
|
||||||
|
### Visual Studio Code`
|
||||||
|
|
Loading…
Reference in New Issue