浏览代码

从markdown中提取表格和echarts数据

kagg886 1 月之前
父节点
当前提交
4beb5dfed9

+ 6 - 2
src/components/assistant/ComponentLibrary.vue

@@ -3,6 +3,10 @@ import { computed, ref } from 'vue'
 import { Plus } from '@element-plus/icons-vue'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import type { ComponentLibraryItem, Content } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
 
 const props = defineProps<{
 	library?: ComponentLibraryItem[]
@@ -78,7 +82,7 @@ const openPreview = (component: any) => {
 					<div class="component-preview">
 						<Markdown
 							:content="component.preview"
-							:plugins="[]"
+							:plugins="plugin"
 							class="preview-content"
 						/>
 					</div>
@@ -128,7 +132,7 @@ const openPreview = (component: any) => {
 			<div class="preview-dialog-content">
 				<Markdown
 					:content="previewComponent.data"
-					:plugins="[]"
+					:plugins="plugin"
 					class="full-preview-content"
 				/>
 			</div>

+ 6 - 2
src/components/assistant/DraggableCard.vue

@@ -3,6 +3,10 @@ import { ref, computed, onUnmounted } from 'vue'
 import { Delete, MoreFilled, Edit } from '@element-plus/icons-vue'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import type { MarkdownDashBoard, Position, Size, Content, ResizeType } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
 
 const props = defineProps<{
 	card: MarkdownDashBoard
@@ -225,7 +229,7 @@ onUnmounted(() => {
 			<div class="card-body">
 				<Markdown
 					:content="card.data"
-					:plugins="[]"
+					:plugins="plugin"
 					class="markdown-content"
 				/>
 			</div>
@@ -287,7 +291,7 @@ onUnmounted(() => {
 					<div class="preview-container">
 						<Markdown
 							:content="editData || '暂无内容'"
-							:plugins="[]"
+							:plugins="plugin"
 							class="preview-content"
 						/>
 					</div>

+ 232 - 9
src/views/assistant/dashboard/edit.vue

@@ -5,8 +5,10 @@ import DashboardDesigner from '/@/components/assistant/DashboardDesigner.vue'
 import ComponentLibrary from '/@/components/assistant/ComponentLibrary.vue'
 import { ElMessage } from 'element-plus'
 import type { MarkdownDashBoard, Position, Size, Content, AddCardData, ComponentLibraryItem } from '/@/components/assistant/types'
-import { LmSession } from '/@/api/assist/type'
+import { LmSession, Message } from '/@/api/assist/type'
 import assist from '/@/api/assist'
+import MarkdownIt from 'markdown-it'
+import {PieChart } from '@element-plus/icons-vue'
 
 // 预留props,暂时不使用
 const props = defineProps<{
@@ -123,13 +125,234 @@ const { loading: loadingChat, doLoading: doLoadingChat } = useLoading(async () =
 // 组件库数据
 const library = ref<ComponentLibraryItem[]>([])
 
-const { loading: loadingLibrary, doLoading: doLoadingLibrary } = useLoading(async (id: number) => {})
+const { loading: loadingLibrary, doLoading: doLoadingLibrary } = useLoading(async (id: number) => {
+	const result: {
+		messages: Message[]
+		total: number
+	} = await assist.session.message.list({ sessionId: id }).catch(() => {
+		return {
+			list: [],
+			total: 0,
+		}
+	})
+
+	const messages = result.messages.filter((it) => it.role === 'assistant')
+
+	// 解析消息内容,提取echarts和表格
+	library.value = parseMessagesForLibrary(messages)
+})
+
+// 解析消息内容,提取echarts代码块和表格
+const parseMessagesForLibrary = (messages: Message[]): ComponentLibraryItem[] => {
+	const libraryItems: ComponentLibraryItem[] = []
+	const md = new MarkdownIt({
+		html: true,
+		linkify: true,
+		typographer: true
+	})
+
+	messages.forEach((message, messageIndex) => {
+		if (!message.render_content) return
+
+		// 解析markdown内容为tokens
+		const tokens = md.parse(message.render_content, {})
+
+		// 提取echarts代码块
+		extractEchartsFromTokens(tokens, messageIndex, libraryItems)
+
+		// 提取表格
+		extractTablesFromTokens(tokens, messageIndex, libraryItems)
+	})
+
+	return libraryItems
+}
+
+// 验证JSON格式(echarts)
+const isValidEchartsJSON = (str: string): boolean => {
+	if (str.startsWith('[')) {
+		return false
+	}
+	try {
+		const expr = JSON.parse(str)
+		return expr["series"] !== undefined
+	} catch {
+		return false
+	}
+}
+
+// 从tokens中提取echarts代码块
+const extractEchartsFromTokens = (tokens: any[], messageIndex: number, libraryItems: ComponentLibraryItem[]) => {
+	tokens.forEach((token, tokenIndex) => {
+		if (token.type === 'fence') {
+			const info = token.info ? token.info.trim() : ''
+			const content = token.content.trim()
+
+			if ((info === 'echarts' || info === 'json') && isValidEchartsJSON(content)) {
+				try {
+					const config = JSON.parse(content)
+					const title = config.title?.text || `图表 ${libraryItems.length + 1}`
+
+					libraryItems.push({
+						id: `echarts-${messageIndex}-${tokenIndex}`,
+						title: title,
+						icon: PieChart,
+						description: `来自消息的ECharts图表`,
+						data: `\`\`\`echarts
+${content}
+\`\`\``,
+						preview: `\`\`\`echarts
+${content}
+\`\`\``
+					})
+				} catch (error) {
+					console.warn('解析echarts配置失败:', error)
+				}
+			}
+		}
+	})
+}
+
+// 解析表格数据的接口
+interface TableData {
+	headers: string[]
+	rows: string[][]
+}
+
+// 从tokens中提取表格
+const extractTablesFromTokens = (tokens: any[], messageIndex: number, libraryItems: ComponentLibraryItem[]) => {
+	tokens.forEach((token, tokenIndex) => {
+		if (token.type === 'table_open') {
+			const tableData = parseTableDataFromTokens(tokens, tokenIndex + 1)
+
+			if (tableData.headers.length > 0 || tableData.rows.length > 0) {
+				const title = `表格 ${libraryItems.length + 1}`
+				const previewData = {
+					headers: tableData.headers.slice(0, 3), // 只显示前3列
+					rows: tableData.rows.slice(0, 3) // 只显示前3行
+				}
+
+				libraryItems.push({
+					id: `table-${messageIndex}-${tokenIndex}`,
+					title: title,
+					icon: 'ele-Table',
+					description: `来自消息的数据表格 (${tableData.rows.length}行 x ${tableData.headers.length}列)`,
+					data: generateMarkdownTable(tableData),
+					preview: generateMarkdownTable(previewData)
+				})
+			}
+		}
+	})
+}
+
+// 解析表格tokens获取数据
+const parseTableDataFromTokens = (tokens: any[], startIdx: number): TableData => {
+	const headers: string[] = []
+	const rows: string[][] = []
+	let currentRow: string[] = []
+	let inHeader = false
+	let inBody = false
+	let cellContent = ''
+
+	for (let i = startIdx; i < tokens.length; i++) {
+		const token = tokens[i]
+
+		if (token.type === 'table_close') {
+			break
+		}
+
+		if (token.type === 'thead_open') {
+			inHeader = true
+			continue
+		}
+
+		if (token.type === 'thead_close') {
+			inHeader = false
+			continue
+		}
+
+		if (token.type === 'tbody_open') {
+			inBody = true
+			continue
+		}
+
+		if (token.type === 'tbody_close') {
+			inBody = false
+			continue
+		}
+
+		if (token.type === 'tr_open') {
+			currentRow = []
+			continue
+		}
+
+		if (token.type === 'tr_close') {
+			if (inHeader && currentRow.length > 0) {
+				headers.push(...currentRow)
+			} else if (inBody && currentRow.length > 0) {
+				rows.push([...currentRow])
+			}
+			currentRow = []
+			continue
+		}
+
+		if (token.type === 'th_open' || token.type === 'td_open') {
+			cellContent = ''
+			continue
+		}
+
+		if (token.type === 'th_close' || token.type === 'td_close') {
+			currentRow.push(cellContent.trim())
+			cellContent = ''
+			continue
+		}
+
+		if (token.type === 'inline' && token.children) {
+			// 处理内联内容
+			for (const child of token.children) {
+				if (child.type === 'text') {
+					cellContent += child.content
+				} else if (child.type === 'code_inline') {
+					cellContent += `\`${child.content}\``
+				} else if (child.type === 'strong_open') {
+					cellContent += '**'
+				} else if (child.type === 'strong_close') {
+					cellContent += '**'
+				} else if (child.type === 'em_open') {
+					cellContent += '*'
+				} else if (child.type === 'em_close') {
+					cellContent += '*'
+				}
+			}
+		}
+	}
+
+	return { headers, rows }
+}
+
+// 生成markdown表格
+const generateMarkdownTable = (tableData: TableData): string => {
+	if (tableData.headers.length === 0 && tableData.rows.length === 0) {
+		return ''
+	}
+
+	let markdown = ''
+
+	// 生成表头
+	if (tableData.headers.length > 0) {
+		markdown += '| ' + tableData.headers.join(' | ') + ' |\n'
+		markdown += '| ' + tableData.headers.map(() => '---').join(' | ') + ' |\n'
+	}
+
+	// 生成表格行
+	tableData.rows.forEach(row => {
+		markdown += '| ' + row.join(' | ') + ' |\n'
+	})
+
+	return markdown
+}
 
-onMounted(async ()=> {
-	await Promise.all([
-		doLoadingChat(),
-		doLoadingDashBoard(),
-	])
+onMounted(async () => {
+	await Promise.all([doLoadingChat(), doLoadingDashBoard()])
 })
 </script>
 
@@ -164,9 +387,9 @@ onMounted(async ()=> {
 			<!-- 右侧组件库面板 -->
 			<div class="library-panel">
 				<el-select class="library-panel-header" v-model="currentSelectedChat" v-loading="loadingChat">
-					<el-option v-for="(i,index) in chat" :key="i.session_id" :value="index" :label="i.title"></el-option>
+					<el-option v-for="(i, index) in chat" :key="i.session_id" :value="index" :label="i.title"></el-option>
 				</el-select>
-				<ComponentLibrary class="library-panel-content" :library="library" @add-card="addCard" />
+				<ComponentLibrary class="library-panel-content" v-loading="loadingLibrary" :library="library" @add-card="addCard" />
 			</div>
 		</div>
 	</div>