ai-teaching-assistant / static /js /knowledge-map.js
roxqtang's picture
Initial commit
8fe50ee
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
}
});