使用模型分析文章并生成大纲、根据目标拆解成思维导图,这些需求都离不开思维导图展示、编辑和 AIGC 的结合。这篇文章就来讲一下前端方面如何实现这个需求。首先来看一下效果:
需求也很简单,用户输入文本或者文件,模型根据输入生成对应的思维导图。这个做之前很多人都觉得会很难,其实不然。wanglin2/mind-map: 一个还算强大的Web思维导图。 直接当一个纯粹的调包侠就完事了。
具体实现
模型生成思维导图时,一般都是会返回一个 markdown,其中按照不同级别的标题来区分节点。例如上图的思维导图就是如下格式的 markdown:
# 黑神话悟空
## 游戏背景
### 以《西游记》为基础
### 融合中国传统文化元素
## 开发团队
### Game Science
### 经验丰富的开发者
## 发行信息
### 预计发行日期
### 支持多平台
## 玩家期待
### 高度自由的探索
### 深入的剧情体验
## 市场反响
### 备受期待的动作角色扮演游戏
### 吸引全球玩家关注
而 simple-mind-map(就是上面提到的 wanglin2/mind-map
)就恰好支持 markdown 格式的导入,所以我们可以直接封装好一个组件,接受一个字符串类型的 value 然后将其渲染成思维导图。完整组件代码在文末,需要自取。
1、依赖导入
除了本体之外需要三个插件:markdown 解析器、导出插件、xmind 导出插件。然后简单注册一下就行:
import MindMap from 'simple-mind-map';
import markdown from 'simple-mind-map/src/parse/markdown';
import Export from 'simple-mind-map/src/plugins/Export';
import ExportXMind from 'simple-mind-map/src/plugins/ExportXMind';
MindMap.usePlugin(Export);
MindMap.usePlugin(ExportXMind);
2、MindMap 初始化
const mindMapRef = useRef<MindMap | null>(null);
const [activeNodes, setActiveNodes] = useState<any[]>([]);
useEffect(() => {
const mindMap = new MindMap({
el: document.getElementById('mindMapContainer'),
mousewheelAction: 'zoom',
themeConfig: {
lineStyle: 'curve',
lineWidth: 2,
rootLineKeepSameInCurve: true,
rootLineStartPositionKeepSameInCurve: true,
},
data: { data: {}, children: [] },
} as any);
mindMap.on('node_active', (node, activeNodeList) => {
setActiveNodes(activeNodeList);
});
mindMapRef.current = mindMap;
updateMdContent(props.value);
return () => mindMap.destroy();
}, []);
初始化的时候记得绑定事件回调,因为这个组件是没有外围 UI 的,也就是说那种工具栏需要我们自己来实现,所以要通过 node_active
事件通知 react 要渲染对应的 UI 了。
其中 updateMdContent
就是调下解析器:
const updateMdContent = async (mdContent: string) => {
const data = await markdown.transformMarkdownTo(mdContent);
mindMapRef.current.setData(data);
};
3、更新思维导图
当 props.value 更新的时候我们需要用新的 value 重新触发一次渲染:
import { useDebounceEffect } from 'ahooks';
useDebounceEffect(
() => {
if (!mindMapRef.current) return;
updateMdContent(props.value);
},
[props.value],
{ wait: 500 },
);
这里注意要加一个防抖,我本地测试下来发现过于频繁的更新会导致渲染异常,就像下面这样:
是的你没有眼花,就是渲染了两边叠在一起了。
4、节点编辑操作
想要增删节点也是直接调 api 即可:
// 增加同级
mindMapRef.current?.execCommand('INSERT_NODE')
// 增加子级
mindMapRef.current?.execCommand('INSERT_CHILD_NODE')
// 删除节点
mindMapRef.current?.execCommand('REMOVE_NODE')
完整代码
下面这个是基于 react + antd,能直接拿来使用的组件。注意里边还包含了三种导出,因为也很简单就不再重复讲了。
import {
DeleteOutlined,
DownloadOutlined,
PartitionOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { useDebounceEffect } from 'ahooks';
import { Button, Dropdown, Space } from 'antd';
import { useRef, useEffect, FC, useState } from 'react';
import MindMap from 'simple-mind-map';
import markdown from 'simple-mind-map/src/parse/markdown';
import Export from 'simple-mind-map/src/plugins/Export';
import ExportXMind from 'simple-mind-map/src/plugins/ExportXMind';
MindMap.usePlugin(Export);
MindMap.usePlugin(ExportXMind);
interface SimpleMindMapRendererProps {
value: string;
}
export function downloadDataUrl(url: string, fileName: string) {
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export const SimpleMindMapRenderer: FC<SimpleMindMapRendererProps> = (props) => {
const mindMapRef = useRef<MindMap | null>(null);
const [activeNodes, setActiveNodes] = useState<any[]>([]);
const updateMdContent = async (mdContent: string) => {
const data = await markdown.transformMarkdownTo(mdContent);
mindMapRef.current.setData(data);
};
useEffect(() => {
const mindMap = new MindMap({
el: document.getElementById('mindMapContainer'),
mousewheelAction: 'zoom',
themeConfig: {
lineStyle: 'curve',
lineWidth: 2,
rootLineKeepSameInCurve: true,
rootLineStartPositionKeepSameInCurve: true,
},
data: {
data: {},
children: [],
},
} as any);
mindMap.on('node_active', (node, activeNodeList) => {
setActiveNodes(activeNodeList);
});
// mindMap.setMode('readonly');
mindMapRef.current = mindMap;
updateMdContent(props.value);
return () => {
mindMap.destroy();
};
}, []);
useDebounceEffect(
() => {
if (!mindMapRef.current) return;
updateMdContent(props.value);
},
[props.value],
{ wait: 500 },
);
const exportBtns = [
{
key: 'exportPng',
label: '导出为 PNG',
onClick: async () => {
const url = await (mindMapRef.current as any).doExport.png('思维导图');
downloadDataUrl(url, `思维导图.png`);
},
},
{
key: 'exportMd',
label: '导出为 Markdown',
onClick: async () => {
const url = await (mindMapRef.current as any).doExport.md();
downloadDataUrl(url, `思维导图.md`);
},
},
{
key: 'exportXMind',
label: '导出为 XMind',
onClick: async () => {
const url = await (mindMapRef.current as any).doExport.xmind('思维导图');
downloadDataUrl(url, `思维导图.xmind`);
},
},
];
return (
<>
<div className='absolute top-2 left-2'>
<Space>
<Dropdown menu={{ items: exportBtns }} placement='bottom'>
<Button size='small' icon={<DownloadOutlined />}>
导出
</Button>
</Dropdown>
{activeNodes.length > 0 && (
<>
<Button
size='small'
icon={<PlusOutlined />}
onClick={() => mindMapRef.current?.execCommand('INSERT_NODE')}
>
新增同级
</Button>
<Button
size='small'
icon={<PartitionOutlined />}
onClick={() => mindMapRef.current?.execCommand('INSERT_CHILD_NODE')}
>
新增子级
</Button>
<Button
size='small'
danger
icon={<DeleteOutlined />}
onClick={() => mindMapRef.current?.execCommand('REMOVE_NODE')}
>
删除节点
</Button>
</>
)}
</Space>
</div>
<div id='mindMapContainer' className='h-full w-full'></div>
</>
);
};
至此功能的核心实现就基本完成了,如果你对前端 AI 领域的其他开发技能感兴趣,可以来看一下我写的相关系列文章 AI 应用前端开发技能库。