|
@@ -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>
|