Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', () => { | |
| // Get DOM elements | |
| const mapContainer = document.getElementById('knowledge-map-container'); | |
| const mapLayout = document.getElementById('map-layout'); | |
| const mapDepth = document.getElementById('map-depth'); | |
| const refreshBtn = document.getElementById('refresh-map'); | |
| const nodeTitle = document.getElementById('node-title'); | |
| const nodeDetails = document.getElementById('node-details'); | |
| // Add view toggle button reference | |
| const viewToggleBtn = document.createElement('button'); | |
| viewToggleBtn.id = 'view-toggle-btn'; | |
| viewToggleBtn.className = 'map-control-btn'; | |
| viewToggleBtn.innerHTML = '<i class="fas fa-exchange-alt"></i> Switch View'; | |
| // Add to control group | |
| const controlsContainer = document.querySelector('.map-controls'); | |
| if (controlsContainer) { | |
| const viewToggleGroup = document.createElement('div'); | |
| viewToggleGroup.className = 'control-group'; | |
| viewToggleGroup.appendChild(viewToggleBtn); | |
| controlsContainer.appendChild(viewToggleGroup); | |
| } | |
| // View mode: tag->doc (default) or doc->tag | |
| let viewMode = 'tag-doc'; | |
| // Initialize ECharts instance | |
| const myChart = echarts.init(mapContainer, null, { | |
| renderer: 'canvas' | |
| }); | |
| // Reset chart size on window resize | |
| window.addEventListener('resize', () => { | |
| myChart.resize(); | |
| }); | |
| // Chart configuration options | |
| let option = { | |
| tooltip: { | |
| trigger: 'item', | |
| formatter: '{b}: {c}' | |
| }, | |
| series: [{ | |
| type: 'tree', | |
| data: [], | |
| top: '10%', | |
| left: '5%', | |
| bottom: '10%', | |
| right: '15%', | |
| symbolSize: 10, | |
| label: { | |
| position: 'left', | |
| verticalAlign: 'middle', | |
| align: 'right', | |
| fontSize: 14 | |
| }, | |
| leaves: { | |
| label: { | |
| position: 'right', | |
| verticalAlign: 'middle', | |
| align: 'left' | |
| } | |
| }, | |
| emphasis: { | |
| focus: 'descendant' | |
| }, | |
| expandAndCollapse: true, | |
| animationDuration: 550, | |
| animationDurationUpdate: 750, | |
| // Add drag functionality | |
| roam: true, // Allow zooming and panning | |
| // Set styles for different level nodes | |
| itemStyle: { | |
| color: function(params) { | |
| // Set different colors based on node depth | |
| const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0; | |
| // Define different level colors | |
| const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f']; | |
| return colors[Math.min(depth, colors.length - 1)]; | |
| } | |
| }, | |
| // Set styles for different level labels | |
| levels: [ | |
| { // Root node | |
| itemStyle: { | |
| color: '#4e79a7', | |
| borderWidth: 0 | |
| }, | |
| label: { | |
| fontSize: 18, | |
| fontWeight: 'bold' | |
| } | |
| }, | |
| { // First level node | |
| itemStyle: { | |
| color: '#f28e2c', | |
| borderWidth: 1 | |
| }, | |
| label: { | |
| fontSize: 16, | |
| fontWeight: 'bold' | |
| } | |
| }, | |
| { // Second level node | |
| itemStyle: { | |
| color: '#e15759' | |
| }, | |
| label: { | |
| fontSize: 14 | |
| } | |
| }, | |
| { // Leaf node (document) | |
| itemStyle: { | |
| color: '#76b7b2' | |
| }, | |
| label: { | |
| fontSize: 12 | |
| } | |
| } | |
| ] | |
| }] | |
| }; | |
| // Set layout type | |
| function setLayout(layout) { | |
| if (layout === 'tree') { | |
| option.series[0].type = 'tree'; | |
| option.series[0].layout = 'orthogonal'; | |
| option.series[0].orient = 'LR'; | |
| // Restore default series settings | |
| option.series = [{ | |
| type: 'tree', | |
| data: [], | |
| top: '10%', | |
| left: '5%', | |
| bottom: '10%', | |
| right: '15%', | |
| symbolSize: 10, | |
| label: { | |
| position: 'left', | |
| verticalAlign: 'middle', | |
| align: 'right', | |
| fontSize: 14, | |
| color: '#000' // Default text color | |
| }, | |
| leaves: { | |
| label: { | |
| position: 'right', | |
| verticalAlign: 'middle', | |
| align: 'left', | |
| color: '#000' // Leaf node text color | |
| } | |
| }, | |
| emphasis: { | |
| focus: 'descendant' | |
| }, | |
| expandAndCollapse: true, | |
| animationDuration: 550, | |
| animationDurationUpdate: 750, | |
| roam: true, | |
| // Explicitly set node styles | |
| itemStyle: { | |
| color: function(params) { | |
| // Set different colors based on node depth | |
| const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0; | |
| const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f']; | |
| return colors[Math.min(depth, colors.length - 1)]; | |
| }, | |
| borderWidth: 1, | |
| borderColor: '#999' | |
| }, | |
| lineStyle: { | |
| color: '#999', | |
| width: 1 | |
| } | |
| }]; | |
| } else if (layout === 'force') { | |
| option.series[0].type = 'graph'; | |
| option.series[0].layout = 'force'; | |
| option.series[0].force = { | |
| repulsion: 100, | |
| edgeLength: 50 | |
| }; | |
| } else if (layout === 'radial') { | |
| option.series[0].type = 'tree'; | |
| option.series[0].layout = 'radial'; | |
| option.series[0].orient = undefined; | |
| } else if (layout === 'wordcloud') { | |
| // Real word cloud mode - Use wordcloud chart type | |
| console.log("Switch to word cloud mode"); | |
| // Completely replace series configuration with word cloud chart | |
| option.series = [{ | |
| type: 'wordCloud', | |
| // Word cloud shape can be rectangle or circle | |
| shape: 'circle', | |
| // Word cloud size | |
| left: 'center', | |
| top: 'center', | |
| width: '70%', | |
| height: '70%', | |
| // Word cloud background color, set to obvious white | |
| backgroundColor: '#ffffff', | |
| // Font size range, ensure words visible | |
| sizeRange: [24, 80], | |
| // Word rotation angle range - Reduce rotation for easier reading | |
| rotationRange: [0, 0], // Set to 0, no rotation | |
| // Grid size, for layout optimization, reduce this value for denser words | |
| gridSize: 6, | |
| // Word cloud text style | |
| textStyle: { | |
| fontFamily: 'Arial, sans-serif', | |
| fontWeight: 'bold', | |
| // Fixed text color, no random color | |
| color: '#333' | |
| }, | |
| // Global zoom scale | |
| layoutAnimation: false, | |
| // Mouse hover effect | |
| emphasis: { | |
| focus: 'self', | |
| textStyle: { | |
| shadowBlur: 10, | |
| shadowColor: '#333' | |
| } | |
| }, | |
| // Must attribute, set drawOutOfBound to false to prevent text from being clipped | |
| drawOutOfBound: false, | |
| // Whether to enable fuzzy antialiasing processing | |
| textShadowBlur: 2, | |
| textShadowColor: '#fff', | |
| // Data will be filled in buildWordcloudData function | |
| data: [] | |
| }]; | |
| } | |
| } | |
| // Set level depth | |
| function setDepth(depth) { | |
| if (depth === 'all') { | |
| option.series[0].expandAndCollapse = true; | |
| option.series[0].initialTreeDepth = -1; | |
| } else { | |
| option.series[0].expandAndCollapse = true; | |
| option.series[0].initialTreeDepth = parseInt(depth); | |
| } | |
| } | |
| // Get knowledge map data | |
| async function fetchKnowledgeMap() { | |
| try { | |
| const response = await fetch('/api/knowledge-map'); | |
| if (!response.ok) { | |
| throw new Error('Failed to get knowledge map data'); | |
| } | |
| const data = await response.json(); | |
| return data; | |
| } catch (error) { | |
| console.error('Failed to get knowledge map data:', error); | |
| return { | |
| nodes: [], | |
| links: [], | |
| error: error.message | |
| }; | |
| } | |
| } | |
| // Truncate long filename | |
| function truncateFilename(filename, maxLength = 25) { | |
| if (!filename || filename.length <= maxLength) { | |
| return filename; | |
| } | |
| const ext = filename.lastIndexOf('.') > 0 ? filename.slice(filename.lastIndexOf('.')) : ''; | |
| const nameWithoutExt = filename.slice(0, filename.lastIndexOf('.') > 0 ? filename.lastIndexOf('.') : filename.length); | |
| // Keep filename prefix, add ellipsis, then keep extension | |
| return nameWithoutExt.slice(0, maxLength - 3 - ext.length) + '...' + ext; | |
| } | |
| // Build knowledge map tree data - Tag to document structure (default view) | |
| function buildTagToDocTree(data) { | |
| // Process backend returned data, build into tree structure | |
| if (data.error) { | |
| showError(data.error); | |
| return []; | |
| } | |
| // If empty data, show prompt information | |
| if (!data.documents || data.documents.length === 0) { | |
| showError('No usable knowledge map data, please upload documents first'); | |
| return []; | |
| } | |
| // Build tree structure | |
| const rootNode = { | |
| name: data.centralTopic || 'Knowledge Center', | |
| value: data.documents.length, | |
| children: [] | |
| }; | |
| // Group processing | |
| const tagGroups = {}; | |
| // First find all tags common in all documents as the first layer | |
| const allTags = new Set(); | |
| data.documents.forEach(doc => { | |
| if (doc.tags && doc.tags.length > 0) { | |
| doc.tags.forEach(tag => allTags.add(tag)); | |
| } | |
| }); | |
| // Generate tag hierarchy structure (simulate mind map hierarchy) | |
| // Use tag co-occurrence frequency as hierarchy building basis | |
| const tagRelations = {}; | |
| // Calculate tag co-occurrence frequency | |
| data.documents.forEach(doc => { | |
| if (doc.tags && doc.tags.length > 0) { | |
| for (let i = 0; i < doc.tags.length; i++) { | |
| for (let j = i + 1; j < doc.tags.length; j++) { | |
| const tag1 = doc.tags[i]; | |
| const tag2 = doc.tags[j]; | |
| const key = `${tag1}:${tag2}`; | |
| const reverseKey = `${tag2}:${tag1}`; | |
| if (tagRelations[key]) { | |
| tagRelations[key]++; | |
| } else if (tagRelations[reverseKey]) { | |
| tagRelations[reverseKey]++; | |
| } else { | |
| tagRelations[key] = 1; | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Find main tags based on co-occurrence frequency | |
| let maxCoOccurrence = 0; | |
| let primaryTags = []; | |
| Object.keys(tagRelations).forEach(key => { | |
| if (tagRelations[key] > maxCoOccurrence) { | |
| maxCoOccurrence = tagRelations[key]; | |
| const [tag1, tag2] = key.split(':'); | |
| primaryTags = [tag1, tag2]; | |
| } | |
| }); | |
| // If no related tags are found, use the most common tag | |
| if (primaryTags.length === 0) { | |
| const tagFrequency = {}; | |
| data.documents.forEach(doc => { | |
| if (doc.tags && doc.tags.length > 0) { | |
| doc.tags.forEach(tag => { | |
| tagFrequency[tag] = (tagFrequency[tag] || 0) + 1; | |
| }); | |
| } | |
| }); | |
| // Find the tag with the highest frequency | |
| let maxFreq = 0; | |
| let mostFrequentTag = ''; | |
| Object.keys(tagFrequency).forEach(tag => { | |
| if (tagFrequency[tag] > maxFreq) { | |
| maxFreq = tagFrequency[tag]; | |
| mostFrequentTag = tag; | |
| } | |
| }); | |
| primaryTags = [mostFrequentTag]; | |
| } | |
| // Use main tags as first level nodes | |
| primaryTags.forEach(tag => { | |
| const tagNode = { | |
| name: tag, | |
| value: 0, | |
| children: [] | |
| }; | |
| // Find documents containing the tag | |
| const docsWithTag = data.documents.filter(doc => | |
| doc.tags && doc.tags.includes(tag) | |
| ); | |
| tagNode.value = docsWithTag.length; | |
| // Add child tag nodes to the tag node | |
| const secondaryTags = new Set(); | |
| docsWithTag.forEach(doc => { | |
| if (doc.tags) { | |
| doc.tags.forEach(t => { | |
| if (t !== tag && !primaryTags.includes(t)) { | |
| secondaryTags.add(t); | |
| } | |
| }); | |
| } | |
| }); | |
| // Add secondary tag nodes | |
| secondaryTags.forEach(secondaryTag => { | |
| const secondaryNode = { | |
| name: secondaryTag, | |
| value: 0, | |
| children: [] | |
| }; | |
| // Find documents containing both first and second level tags | |
| const docsWithBothTags = docsWithTag.filter(doc => | |
| doc.tags && doc.tags.includes(secondaryTag) | |
| ); | |
| secondaryNode.value = docsWithBothTags.length; | |
| // Add document nodes to secondary tag | |
| docsWithBothTags.forEach(doc => { | |
| secondaryNode.children.push({ | |
| name: truncateFilename(doc.filename), // Truncate long filename | |
| fullName: doc.filename, // Save full filename | |
| value: 1, | |
| document: doc, | |
| nodeType: 'document' | |
| }); | |
| }); | |
| if (secondaryNode.children.length > 0) { | |
| tagNode.children.push(secondaryNode); | |
| } | |
| }); | |
| // Directly add documents without secondary tags to first level tag | |
| const docsWithOnlyPrimaryTag = docsWithTag.filter(doc => | |
| doc.tags && doc.tags.filter(t => !primaryTags.includes(t) && secondaryTags.has(t)).length === 0 | |
| ); | |
| docsWithOnlyPrimaryTag.forEach(doc => { | |
| tagNode.children.push({ | |
| name: truncateFilename(doc.filename), // Truncate long filename | |
| fullName: doc.filename, // Save full filename | |
| value: 1, | |
| document: doc, | |
| nodeType: 'document' | |
| }); | |
| }); | |
| if (tagNode.children.length > 0) { | |
| rootNode.children.push(tagNode); | |
| } | |
| }); | |
| // Add documents without main tags | |
| const docsWithoutPrimaryTags = data.documents.filter(doc => | |
| !doc.tags || !doc.tags.some(tag => primaryTags.includes(tag)) | |
| ); | |
| if (docsWithoutPrimaryTags.length > 0) { | |
| const otherNode = { | |
| name: 'Other Documents', | |
| value: docsWithoutPrimaryTags.length, | |
| children: [] | |
| }; | |
| docsWithoutPrimaryTags.forEach(doc => { | |
| otherNode.children.push({ | |
| name: truncateFilename(doc.filename), // Truncate long filename | |
| fullName: doc.filename, // Save full filename | |
| value: 1, | |
| document: doc, | |
| nodeType: 'document' | |
| }); | |
| }); | |
| rootNode.children.push(otherNode); | |
| } | |
| return [rootNode]; | |
| } | |
| // Build document to tag tree structure (reverse view) | |
| function buildDocToTagTree(data) { | |
| // Process backend returned data, build into tree structure | |
| if (data.error) { | |
| showError(data.error); | |
| return []; | |
| } | |
| // If empty data, show prompt information | |
| if (!data.documents || data.documents.length === 0) { | |
| showError('No usable knowledge map data, please upload documents first'); | |
| return []; | |
| } | |
| // Build tree structure | |
| const rootNode = { | |
| name: data.centralTopic || 'Knowledge Center', | |
| value: data.documents.length, | |
| children: [] | |
| }; | |
| // Group documents by document type or topic | |
| const docCategories = {}; | |
| // Find all documents, extract possible classification information | |
| data.documents.forEach(doc => { | |
| // Try to infer category from filename | |
| let category = 'Document Set'; | |
| // Check file extension | |
| if (doc.filename) { | |
| const fileExt = doc.filename.split('.').pop().toLowerCase(); | |
| if (['pdf', 'docx', 'doc'].includes(fileExt)) { | |
| category = 'Text Document'; | |
| } else if (['ppt', 'pptx'].includes(fileExt)) { | |
| category = 'Presentation'; | |
| } else if (['xlsx', 'xls', 'csv'].includes(fileExt)) { | |
| category = 'Data File'; | |
| } | |
| // Also try to infer better classification from tags | |
| if (doc.tags && doc.tags.length > 0) { | |
| // Find some possible tags that might represent topic | |
| const possibleCategories = ['Education', 'Cognition', 'Learning', 'Media', 'Design', 'Evaluation']; | |
| for (const pc of possibleCategories) { | |
| const matchingTag = doc.tags.find(tag => tag.includes(pc)); | |
| if (matchingTag) { | |
| category = matchingTag; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // Ensure category exists | |
| if (!docCategories[category]) { | |
| docCategories[category] = []; | |
| } | |
| // Add document to category | |
| docCategories[category].push(doc); | |
| }); | |
| // Create a tree node for each category | |
| Object.keys(docCategories).forEach(category => { | |
| const categoryNode = { | |
| name: category, | |
| value: docCategories[category].length, | |
| children: [] | |
| }; | |
| // Create a node for each document in the category | |
| docCategories[category].forEach(doc => { | |
| const docNode = { | |
| name: truncateFilename(doc.filename), | |
| fullName: doc.filename, | |
| value: doc.tags ? doc.tags.length : 0, | |
| document: doc, | |
| nodeType: 'document', | |
| children: [] | |
| }; | |
| // Add tags as child nodes for each document | |
| if (doc.tags && doc.tags.length > 0) { | |
| doc.tags.forEach(tag => { | |
| docNode.children.push({ | |
| name: tag, | |
| value: 1, | |
| nodeType: 'tag', | |
| tagName: tag | |
| }); | |
| }); | |
| } else { | |
| // If no tags, add a "No Tags" node | |
| docNode.children.push({ | |
| name: 'No Tags', | |
| value: 1, | |
| nodeType: 'tag', | |
| tagName: 'No Tags' | |
| }); | |
| } | |
| categoryNode.children.push(docNode); | |
| }); | |
| rootNode.children.push(categoryNode); | |
| }); | |
| return [rootNode]; | |
| } | |
| // Choose suitable tree building function | |
| function buildKnowledgeTree(data) { | |
| // Return tree corresponding to current view mode | |
| if (viewMode === 'tag-doc') { | |
| return buildTagToDocTree(data); | |
| } else { | |
| return buildDocToTagTree(data); | |
| } | |
| } | |
| // Show error message | |
| function showError(message) { | |
| nodeTitle.textContent = 'Error'; | |
| nodeDetails.innerHTML = `<p class="error-message">${message}</p>`; | |
| } | |
| // Show node details | |
| function showNodeDetails(params) { | |
| const data = params.data; | |
| const isWordcloudMode = mapLayout.value === 'wordcloud'; | |
| if (data) { | |
| // Use saved full filename or display name | |
| nodeTitle.textContent = data.fullName || data.name; | |
| if (data.nodeType === 'document' || data.document) { | |
| // Show document information | |
| const doc = data.document; | |
| let html = `<p><strong>Filename:</strong> ${doc.filename}</p>`; | |
| if (doc.summary) { | |
| html += `<p><strong>Summary:</strong> ${doc.summary}</p>`; | |
| } | |
| if (doc.tags && doc.tags.length > 0) { | |
| html += `<p><strong>Tags:</strong></p><div class="tag-list">`; | |
| doc.tags.forEach(tag => { | |
| html += `<span class="tag">${tag}</span>`; | |
| }); | |
| html += `</div>`; | |
| } | |
| html += `<div class="document-action-buttons"> | |
| <a href="#" class="document-link" data-file="${doc.filename}"> | |
| <i class="fas fa-external-link-alt"></i> View Document | |
| </a> | |
| <a href="#" class="wordcloud-link" data-file="${doc.filename}"> | |
| <i class="fas fa-cloud"></i> View Word Cloud | |
| </a>`; | |
| // If in wordcloud mode, add direct wordcloud generation button | |
| if (isWordcloudMode) { | |
| html += `<a href="#" class="generate-cloud-link" data-file="${doc.filename}"> | |
| <i class="fas fa-magic"></i> Generate Word Cloud | |
| </a>`; | |
| } | |
| html += `</div>`; | |
| nodeDetails.innerHTML = html; | |
| } else if (data.nodeType === 'tag') { | |
| // Show tag information | |
| nodeDetails.innerHTML = ` | |
| <p>This is a tag node</p> | |
| <p><strong>Tag name:</strong> ${data.tagName || data.name}</p> | |
| <p>Click on parent node to view document details</p> | |
| `; | |
| } else { | |
| // Show tag node or category node information | |
| nodeDetails.innerHTML = ` | |
| <p>This is a ${data.children ? 'category node' : 'node'}</p> | |
| <p>Contains ${data.value} items</p> | |
| `; | |
| } | |
| } else { | |
| nodeTitle.textContent = 'No node selected'; | |
| nodeDetails.innerHTML = `<p class="placeholder-text">Click on a node in the map to view details</p>`; | |
| } | |
| } | |
| // Add chart controls | |
| function addChartControls() { | |
| // Add zoom controller | |
| const zoomController = document.createElement('div'); | |
| zoomController.className = 'zoom-controllers'; | |
| zoomController.innerHTML = ` | |
| <button class="zoom-btn zoom-in" title="Zoom In"><i class="fas fa-search-plus"></i></button> | |
| <button class="zoom-btn zoom-out" title="Zoom Out"><i class="fas fa-search-minus"></i></button> | |
| <button class="zoom-btn zoom-reset" title="Reset View"><i class="fas fa-redo-alt"></i></button> | |
| `; | |
| mapContainer.parentNode.insertBefore(zoomController, mapContainer); | |
| // Bind zoom events | |
| document.querySelector('.zoom-in').addEventListener('click', () => { | |
| myChart.dispatchAction({ | |
| type: 'dataZoom', | |
| start: 0, | |
| end: 50 | |
| }); | |
| }); | |
| document.querySelector('.zoom-out').addEventListener('click', () => { | |
| myChart.dispatchAction({ | |
| type: 'dataZoom', | |
| start: 0, | |
| end: 100 | |
| }); | |
| }); | |
| document.querySelector('.zoom-reset').addEventListener('click', () => { | |
| myChart.dispatchAction({ | |
| type: 'restore' | |
| }); | |
| }); | |
| } | |
| // Refresh knowledge map | |
| async function refreshKnowledgeMap() { | |
| const layout = mapLayout.value; | |
| const depth = mapDepth.value; | |
| console.log("Refreshing knowledge map, layout:", layout, "depth:", depth); | |
| // Set layout and depth | |
| setLayout(layout); | |
| setDepth(depth); | |
| // Show loading state | |
| myChart.showLoading({ | |
| text: 'Loading...', | |
| color: '#4e79a7', | |
| textColor: '#000', | |
| maskColor: 'rgba(255, 255, 255, 0.8)', | |
| zlevel: 0 | |
| }); | |
| try { | |
| // Get data | |
| const mapData = await fetchKnowledgeMap(); | |
| // Add or remove wordcloud mode CSS class | |
| const container = document.querySelector('.knowledge-map-container'); | |
| if (layout === 'wordcloud') { | |
| container.classList.add('wordcloud-mode'); | |
| // Clear node details, show wordcloud explanation | |
| nodeTitle.textContent = 'Word Cloud View'; | |
| nodeDetails.innerHTML = `<p>Word cloud shows high-frequency words and tags from documents. Word size represents its importance in the document.</p> | |
| <p>Click on any word to see related documents containing that word.</p>`; | |
| } else { | |
| container.classList.remove('wordcloud-mode'); | |
| nodeTitle.textContent = 'No node selected'; | |
| nodeDetails.innerHTML = '<p class="placeholder-text">Click on a node in the map to view details</p>'; | |
| } | |
| // First clear previous chart instance, completely re-render | |
| myChart.clear(); | |
| // Choose data building method based on different layout types | |
| let chartData; | |
| if (layout === 'wordcloud') { | |
| console.log("Building word cloud data..."); | |
| chartData = buildWordcloudData(mapData); | |
| console.log("Word cloud data preparation completed, data item count:", chartData.length); | |
| // For word cloud, set a simple configuration, focus on data | |
| myChart.setOption({ | |
| series: [{ | |
| type: 'wordCloud', | |
| shape: 'circle', | |
| left: 'center', | |
| top: 'center', | |
| width: '80%', | |
| height: '80%', | |
| right: null, | |
| bottom: null, | |
| sizeRange: [24, 80], | |
| rotationRange: [0, 0], | |
| rotationStep: 0, | |
| gridSize: 8, | |
| drawOutOfBound: false, | |
| layoutAnimation: false, | |
| textStyle: { | |
| fontFamily: 'Arial, 微软雅黑, sans-serif', | |
| fontWeight: 'bold', | |
| color: function(params) { | |
| // Use fixed color set, keep simple | |
| const colors = ['#000', '#333', '#666']; | |
| return colors[params.dataIndex % colors.length]; | |
| } | |
| }, | |
| emphasis: { | |
| textStyle: { | |
| color: '#f00' | |
| } | |
| }, | |
| data: chartData | |
| }] | |
| }, true); | |
| } else { | |
| // For tree chart and other chart types, use original data building method | |
| chartData = buildKnowledgeTree(mapData); | |
| option.series[0].data = chartData; | |
| // Apply configuration | |
| myChart.setOption(option, true); | |
| } | |
| console.log("Chart updated"); | |
| // For word cloud, ensure view is fully updated | |
| if (layout === 'wordcloud') { | |
| setTimeout(() => { | |
| console.log("Force redraw word cloud..."); | |
| myChart.resize(); | |
| }, 200); | |
| } | |
| } catch (error) { | |
| console.error('Failed to refresh knowledge map:', error); | |
| showError('Failed to refresh knowledge map: ' + error.message); | |
| } finally { | |
| // Hide loading state | |
| myChart.hideLoading(); | |
| } | |
| } | |
| // Toggle view mode | |
| function toggleViewMode() { | |
| // Toggle view mode | |
| viewMode = viewMode === 'tag-doc' ? 'doc-tag' : 'tag-doc'; | |
| // Update button text | |
| viewToggleBtn.innerHTML = viewMode === 'tag-doc' | |
| ? '<i class="fas fa-exchange-alt"></i> Switch to Document-Tag View' | |
| : '<i class="fas fa-exchange-alt"></i> Switch to Tag-Document View'; | |
| // Refresh knowledge map | |
| refreshKnowledgeMap(); | |
| } | |
| // Bind events | |
| mapLayout.addEventListener('change', refreshKnowledgeMap); | |
| mapDepth.addEventListener('change', refreshKnowledgeMap); | |
| refreshBtn.addEventListener('click', refreshKnowledgeMap); | |
| viewToggleBtn.addEventListener('click', toggleViewMode); | |
| // Click node event | |
| myChart.on('click', 'series', (params) => { | |
| if (mapLayout.value === 'wordcloud') { | |
| // Word cloud mode click processing | |
| handleWordCloudClick(params); | |
| } else { | |
| // Other chart mode | |
| showNodeDetails(params); | |
| } | |
| }); | |
| // Handle wordcloud click event | |
| function handleWordCloudClick(params) { | |
| console.log("Word cloud click event:", params); | |
| // Ensure there is data | |
| if (!params || !params.data) { | |
| console.error("Click event has no valid data"); | |
| return; | |
| } | |
| const word = params.data.name; | |
| const value = params.data.value; | |
| // Set node title to clicked word | |
| nodeTitle.textContent = word; | |
| // First show a temporary message indicating loading | |
| nodeDetails.innerHTML = ` | |
| <p><strong>Word:</strong> ${word}</p> | |
| <p><strong>Frequency:</strong> ${value}</p> | |
| <p><i class="fas fa-spinner fa-spin"></i> Searching for related documents...</p> | |
| `; | |
| // Find all documents containing the word | |
| fetchKnowledgeMap().then(mapData => { | |
| if (!mapData.documents || mapData.documents.length === 0) { | |
| nodeDetails.innerHTML = ` | |
| <p><strong>Word:</strong> ${word}</p> | |
| <p><strong>Frequency:</strong> ${value}</p> | |
| <p class="placeholder-text">No documents found</p> | |
| `; | |
| return; | |
| } | |
| // Find all documents containing the word (in tags or summary) | |
| const relatedDocs = mapData.documents.filter(doc => { | |
| // Check tags | |
| if (doc.tags && doc.tags.includes(word)) { | |
| return true; | |
| } | |
| // Check filename | |
| if (doc.filename && doc.filename.toLowerCase().includes(word.toLowerCase())) { | |
| return true; | |
| } | |
| // Check summary | |
| if (doc.summary && doc.summary.toLowerCase().includes(word.toLowerCase())) { | |
| return true; | |
| } | |
| return false; | |
| }); | |
| // Show related document list | |
| if (relatedDocs.length > 0) { | |
| let html = ` | |
| <p><strong>Word:</strong> ${word}</p> | |
| <p><strong>Frequency:</strong> ${value}</p> | |
| <p><strong>Related documents:</strong> ${relatedDocs.length}</p> | |
| <div class="related-docs"> | |
| `; | |
| relatedDocs.forEach(doc => { | |
| html += ` | |
| <div class="related-doc-item"> | |
| <span class="doc-title">${doc.filename || 'Unnamed Document'}</span> | |
| <div class="document-action-buttons"> | |
| <a href="/view-document?filename=${encodeURIComponent(doc.filename)}" class="document-link"> | |
| <i class="fas fa-external-link-alt"></i> View Document | |
| </a> | |
| <a href="/view-document?filename=${encodeURIComponent(doc.filename)}&show_wordcloud=true" class="wordcloud-link"> | |
| <i class="fas fa-cloud"></i> View Word Cloud | |
| </a> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| html += `</div>`; | |
| nodeDetails.innerHTML = html; | |
| } else { | |
| nodeDetails.innerHTML = ` | |
| <p><strong>Word:</strong> ${word}</p> | |
| <p><strong>Frequency:</strong> ${value}</p> | |
| <p class="placeholder-text">No documents containing "${word}" found</p> | |
| <p>This word may have been added by default, or comes from a deleted document.</p> | |
| `; | |
| } | |
| }).catch(error => { | |
| console.error('Failed to get document data:', error); | |
| nodeDetails.innerHTML = ` | |
| <p><strong>Word:</strong> ${word}</p> | |
| <p><strong>Frequency:</strong> ${value}</p> | |
| <p class="error-message">Failed to get data: ${error.message}</p> | |
| `; | |
| }); | |
| } | |
| // Document link click event | |
| nodeDetails.addEventListener('click', (e) => { | |
| if (e.target.closest('.document-link')) { | |
| e.preventDefault(); | |
| const filename = e.target.closest('.document-link').dataset.file; | |
| if (filename) { | |
| // Handle view document logic here, can send request or redirect | |
| console.log('View document:', filename); | |
| window.location.href = `/view-document?filename=${encodeURIComponent(filename)}`; | |
| } | |
| } | |
| else if (e.target.closest('.wordcloud-link')) { | |
| e.preventDefault(); | |
| const filename = e.target.closest('.wordcloud-link').dataset.file; | |
| if (filename) { | |
| // Directly redirect to view document page, adding show wordcloud parameter | |
| console.log('View word cloud:', filename); | |
| window.location.href = `/view-document?filename=${encodeURIComponent(filename)}&show_wordcloud=true`; | |
| } | |
| } | |
| else if (e.target.closest('.generate-cloud-link')) { | |
| e.preventDefault(); | |
| const filename = e.target.closest('.generate-cloud-link').dataset.file; | |
| if (filename) { | |
| // Generate word cloud directly on current page | |
| console.log('Directly generate word cloud:', filename); | |
| // Create a modal to display the word cloud | |
| const modal = document.createElement('div'); | |
| modal.className = 'wordcloud-modal'; | |
| modal.innerHTML = ` | |
| <div class="wordcloud-modal-content"> | |
| <div class="wordcloud-modal-header"> | |
| <h3>Word Cloud for ${filename}</h3> | |
| <button class="close-modal"><i class="fas fa-times"></i></button> | |
| </div> | |
| <div class="wordcloud-modal-body"> | |
| <div class="loading-indicator"> | |
| <i class="fas fa-spinner fa-spin"></i> Generating word cloud... | |
| </div> | |
| <div class="wordcloud-image-container" style="display: none;"> | |
| <img class="wordcloud-image" alt="Word Cloud"> | |
| </div> | |
| <div class="wordcloud-freq-container"> | |
| <h4>Word Frequency Statistics</h4> | |
| <div class="word-freq-list"> | |
| <p class="placeholder-text">Loading...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| // Add close modal event | |
| modal.querySelector('.close-modal').addEventListener('click', () => { | |
| document.body.removeChild(modal); | |
| }); | |
| // Call API to get word cloud data | |
| fetch(`/api/wordcloud/${encodeURIComponent(filename)}?top_n=50`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.wordcloud_image) { | |
| const imageContainer = modal.querySelector('.wordcloud-image-container'); | |
| const loadingIndicator = modal.querySelector('.loading-indicator'); | |
| const wordFreqList = modal.querySelector('.word-freq-list'); | |
| // Show word cloud | |
| imageContainer.style.display = 'block'; | |
| loadingIndicator.style.display = 'none'; | |
| const img = modal.querySelector('.wordcloud-image'); | |
| img.src = `data:image/png;base64,${data.wordcloud_image}`; | |
| // Show word frequency statistics | |
| wordFreqList.innerHTML = ''; | |
| data.word_frequency.forEach(([word, freq]) => { | |
| const wordItem = document.createElement('div'); | |
| wordItem.className = 'word-freq-item'; | |
| wordItem.textContent = `${word} (${freq})`; | |
| wordFreqList.appendChild(wordItem); | |
| }); | |
| } else { | |
| modal.querySelector('.loading-indicator').innerHTML = | |
| `<p class="error-message">Failed to generate word cloud: ${data.error || 'Unknown error'}</p>`; | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Failed to generate word cloud:', error); | |
| modal.querySelector('.loading-indicator').innerHTML = | |
| `<p class="error-message">Failed to generate word cloud: ${error.message}</p>`; | |
| }); | |
| } | |
| } | |
| }); | |
| // Initialize chart | |
| myChart.setOption(option); | |
| // Add chart controls | |
| addChartControls(); | |
| // Set initial view mode button text | |
| viewToggleBtn.innerHTML = '<i class="fas fa-exchange-alt"></i> Switch to Document-Tag View'; | |
| // First load knowledge map | |
| refreshKnowledgeMap(); | |
| // Build word cloud data - Extract tags and high-frequency words from documents | |
| function buildWordcloudData(data) { | |
| if (data.error) { | |
| showError(data.error); | |
| return [{name: 'Data Error', value: 30}]; // Return a simple word, ensure at least some content is displayed | |
| } | |
| // If empty data, show prompt information | |
| if (!data.documents || data.documents.length === 0) { | |
| showError('No usable knowledge map data, please upload documents first'); | |
| return [ | |
| {name: 'No Data', value: 50}, | |
| {name: 'Please Upload Documents', value: 40}, | |
| {name: 'Word Cloud', value: 30} | |
| ]; | |
| } | |
| console.log("Building word cloud data, document count:", data.documents.length); | |
| // Collect all tags and their frequencies | |
| const tagCount = {}; | |
| // Some common stop words | |
| const stopwords = ['the', 'and', 'for', 'with', 'this', 'that', 'in', 'on', 'at', 'to', 'of', 'a', 'an']; | |
| // Ensure some basic words | |
| const defaultWords = ['Document', 'Learning', 'Cognition', 'Knowledge', 'Material', 'Content']; | |
| let wordIndex = 0; | |
| // Loop through all documents, extract tags | |
| data.documents.forEach(doc => { | |
| // Process tags - Tag weight highest | |
| if (doc.tags && doc.tags.length > 0) { | |
| doc.tags.forEach(tag => { | |
| if (tag && tag.length > 1 && !stopwords.includes(tag.toLowerCase())) { | |
| tagCount[tag] = (tagCount[tag] || 0) + 15; | |
| } | |
| }); | |
| } else { | |
| // If no tags, use default word | |
| const word = defaultWords[wordIndex % defaultWords.length]; | |
| tagCount[word] = (tagCount[word] || 0) + 10; | |
| wordIndex++; | |
| } | |
| // Extract keywords from filename | |
| if (doc.filename) { | |
| const baseName = doc.filename.split('.')[0]; // Remove extension | |
| if (baseName && baseName.length > 1) { | |
| tagCount[baseName] = (tagCount[baseName] || 0) + 10; | |
| } | |
| } | |
| }); | |
| // Always add some default words, ensure enough content is displayed | |
| if (Object.keys(tagCount).length < 5) { | |
| defaultWords.forEach((word, index) => { | |
| tagCount[word] = 30 - index * 3; // Decreasing weight | |
| }); | |
| // Add a "new" word for testing | |
| tagCount["new"] = 10; | |
| } | |
| // Convert to word cloud data format | |
| const cloudData = Object.keys(tagCount).map(tag => ({ | |
| name: tag, | |
| value: tagCount[tag] | |
| })); | |
| // Sort by frequency | |
| cloudData.sort((a, b) => b.value - a.value); | |
| // Show some debugging information | |
| console.log("Word cloud data building completed, vocabulary count:", cloudData.length); | |
| console.log("Word cloud data:", JSON.stringify(cloudData.slice(0, 10))); | |
| // Return sorted word cloud data | |
| return cloudData.slice(0, 50); // Limit word count | |
| } | |
| }); |