smart-thinking-mcp
Version:
Un serveur MCP avancé pour le raisonnement multi-dimensionnel, adaptatif et collaboratif
1,209 lines • 56.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Visualizer = void 0;
/**
* Classe qui génère des visualisations du graphe de pensées
*/
class Visualizer {
// Couleurs associées aux types de pensées
thoughtTypeColors = {
'regular': '#4285F4', // Bleu
'revision': '#EA4335', // Rouge
'meta': '#FBBC05', // Jaune
'hypothesis': '#34A853', // Vert
'conclusion': '#9C27B0' // Violet
};
// Couleurs associées aux types de connexions
connectionTypeColors = {
// Types existants
'supports': '#34A853', // Vert
'contradicts': '#EA4335', // Rouge
'refines': '#4285F4', // Bleu
'branches': '#FBBC05', // Jaune
'derives': '#9C27B0', // Violet
'associates': '#757575', // Gris
// Nouveaux types
'exemplifies': '#00897B', // Teal
'generalizes': '#43A047', // Vert clair
'compares': '#1E88E5', // Bleu clair
'contrasts': '#D81B60', // Rose
'questions': '#8E24AA', // Violet clair
'extends': '#3949AB', // Indigo
'analyzes': '#00ACC1', // Cyan
'synthesizes': '#7CB342', // Vert-jaune
'applies': '#039BE5', // Bleu ciel
'evaluates': '#F4511E', // Orange
'cites': '#6D4C41', // Marron
// Types réciproques
'extended-by': '#3949AB', // Indigo (même que extends)
'analyzed-by': '#00ACC1', // Cyan (même que analyzes)
'component-of': '#7CB342', // Vert-jaune (même que synthesizes)
'applied-by': '#039BE5', // Bleu ciel (même que applies)
'evaluated-by': '#F4511E', // Orange (même que evaluates)
'cited-by': '#6D4C41' // Marron (même que cites)
};
/**
* Génère une visualisation du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @param centerThoughtId Optionnel: l'ID de la pensée centrale (si non spécifié, utilise la plus récente)
* @returns Une visualisation du graphe
*/
generateVisualization(thoughtGraph, centerThoughtId) {
const thoughts = thoughtGraph.getAllThoughts();
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Si aucun ID central n'est spécifié, utiliser la pensée la plus récente
if (!centerThoughtId) {
const recentThoughts = thoughtGraph.getRecentThoughts(1);
centerThoughtId = recentThoughts[0]?.id;
}
// Créer les nœuds de visualisation
const nodes = thoughts.map(thought => {
// Calculer la taille du nœud en fonction du nombre de connexions
const connectionCount = thought.connections.length;
const size = 10 + Math.min(connectionCount * 2, 15);
// Obtenir la couleur en fonction du type de pensée
const color = this.thoughtTypeColors[thought.type] || '#757575';
return {
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size,
color,
tooltip: thought.content,
highlighted: thought.id === centerThoughtId
};
});
// Créer les liens de visualisation
const links = [];
// Pour chaque pensée, ajouter ses connexions comme liens
for (const thought of thoughts) {
for (const connection of thought.connections) {
// Éviter les doublons (chaque lien ne doit apparaître qu'une fois)
const linkExists = links.some(link => (link.source === thought.id && link.target === connection.targetId) ||
(link.source === connection.targetId && link.target === thought.id));
if (!linkExists) {
// Calculer l'épaisseur du lien en fonction de la force de la connexion
const width = 1 + connection.strength * 4;
// Obtenir la couleur en fonction du type de connexion
const color = this.connectionTypeColors[connection.type] || '#757575';
links.push({
source: thought.id,
target: connection.targetId,
type: connection.type,
strength: connection.strength,
width,
color,
tooltip: connection.description
});
}
}
}
// Génération de métadonnées pour la visualisation
const metadata = this.generateMetadata(thoughts, links, centerThoughtId);
// Options d'interactivité par défaut
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.5, 2],
highlightOnHover: true
};
// Options de mise en page par défaut
const layout = {
type: 'force',
forceStrength: 0.5,
spacing: 100
};
return {
nodes,
links,
interactivity,
layout,
metadata
};
}
/**
* Génère une visualisation chronologique du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @returns Une visualisation chronologique du graphe
*/
generateChronologicalVisualization(thoughtGraph) {
const thoughts = thoughtGraph.getAllThoughts()
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Créer les nœuds de visualisation
const nodes = thoughts.map((thought, index) => {
// Calculer la taille du nœud en fonction de l'ordre chronologique
// Les pensées plus récentes sont légèrement plus grandes
const size = 10 + Math.min(index * 0.5, 10);
// Obtenir la couleur en fonction du type de pensée
const color = this.thoughtTypeColors[thought.type] || '#757575';
return {
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size,
color,
tooltip: thought.content,
level: index
};
});
// Créer les liens chronologiques
const links = [];
// Connecter chaque pensée à la suivante chronologiquement
for (let i = 0; i < thoughts.length - 1; i++) {
links.push({
source: thoughts[i].id,
target: thoughts[i + 1].id,
type: 'associates',
strength: 0.5,
width: 1,
color: '#757575',
tooltip: 'Progression chronologique'
});
}
// Ajouter également les connexions explicites
for (const thought of thoughts) {
for (const connection of thought.connections) {
// Éviter les doublons (chaque lien ne doit apparaître qu'une fois)
const linkExists = links.some(link => (link.source === thought.id && link.target === connection.targetId) ||
(link.source === connection.targetId && link.target === thought.id));
if (!linkExists) {
// Calculer l'épaisseur du lien en fonction de la force de la connexion
const width = 1 + connection.strength * 3;
// Obtenir la couleur en fonction du type de connexion
const color = this.connectionTypeColors[connection.type] || '#757575';
links.push({
source: thought.id,
target: connection.targetId,
type: connection.type,
strength: connection.strength,
width,
color,
tooltip: connection.description || `Connexion de type ${connection.type}`
});
}
}
}
// Options d'interactivité par défaut
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.5, 2],
highlightOnHover: true
};
// Options de mise en page
const layout = {
type: 'chronological',
spacing: 50
};
// Génération de métadonnées pour la visualisation
const metadata = {
type: 'chronological',
thoughtCount: thoughts.length,
timeline: thoughts.map(thought => ({
id: thought.id,
timestamp: thought.timestamp.toISOString()
}))
};
return {
nodes,
links,
interactivity,
layout,
metadata
};
}
/**
* Génère une visualisation thématique du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @returns Une visualisation thématique du graphe
*/
generateThematicVisualization(thoughtGraph) {
const thoughts = thoughtGraph.getAllThoughts();
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Extraire les thèmes (mots-clés) des pensées
const themes = this.extractThemes(thoughts);
// Associer chaque pensée à ses thèmes
const thoughtThemes = {};
for (const thought of thoughts) {
thoughtThemes[thought.id] = themes.filter(theme => thought.content.toLowerCase().includes(theme.toLowerCase()));
}
// Créer les nœuds de visualisation
const nodes = thoughts.map(thought => {
// Obtenir la couleur en fonction du type de pensée
const color = this.thoughtTypeColors[thought.type] || '#757575';
// La taille dépend du nombre de thèmes associés
const themeCount = thoughtThemes[thought.id].length;
const size = 10 + Math.min(themeCount * 2, 15);
return {
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size,
color,
tooltip: thought.content,
metadata: {
themes: thoughtThemes[thought.id]
}
};
});
// Créer les liens thématiques
const links = [];
// Connecter les pensées qui partagent des thèmes
for (let i = 0; i < thoughts.length; i++) {
for (let j = i + 1; j < thoughts.length; j++) {
const thoughtA = thoughts[i];
const thoughtB = thoughts[j];
// Trouver les thèmes communs
const themesA = thoughtThemes[thoughtA.id];
const themesB = thoughtThemes[thoughtB.id];
const commonThemes = themesA.filter(theme => themesB.includes(theme));
// S'il y a des thèmes communs, créer un lien
if (commonThemes.length > 0) {
// La force dépend du nombre de thèmes communs
const strength = Math.min(0.3 + commonThemes.length * 0.1, 0.9);
links.push({
source: thoughtA.id,
target: thoughtB.id,
type: 'associates',
strength,
width: 1 + commonThemes.length,
color: '#757575',
tooltip: `Thèmes partagés: ${commonThemes.join(', ')}`
});
}
}
}
// Générer des clusters thématiques
const clusters = this.generateClusters(thoughts, 'theme');
// Options d'interactivité
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.5, 2],
highlightOnHover: true
};
// Options de mise en page
const layout = {
type: 'thematic',
spacing: 80
};
// Génération de métadonnées pour la visualisation
const metadata = {
type: 'thematic',
thoughtCount: thoughts.length,
themes,
themeAssociations: Object.entries(thoughtThemes).map(([id, themeList]) => ({
id,
themes: themeList
}))
};
return {
nodes,
links,
clusters,
interactivity,
layout,
metadata
};
}
/**
* Génère une couleur distincte pour un index donné
*/
getDistinctColor(index) {
const colors = [
'#4285F4', '#EA4335', '#FBBC05', '#34A853', '#9C27B0',
'#00ACC1', '#FF9800', '#795548', '#607D8B', '#3949AB'
];
return colors[index % colors.length];
}
/**
* Génère des clusters à partir des nœuds du graphe
*
* @param thoughts Les pensées à regrouper en clusters
* @param clusterBy Critère de regroupement ('type', 'theme', 'metric', 'connectivity')
* @returns Les clusters générés
*/
generateClusters(thoughts, clusterBy = 'type') {
const clusters = [];
switch (clusterBy) {
case 'type':
// Regrouper par type de pensée
const typeGroups = {};
thoughts.forEach(thought => {
if (!typeGroups[thought.type]) {
typeGroups[thought.type] = [];
}
typeGroups[thought.type].push(thought.id);
});
// Créer un cluster pour chaque type
Object.entries(typeGroups).forEach(([type, nodeIds], index) => {
clusters.push({
id: `cluster-${type}`,
label: `Pensées de type ${type}`,
nodeIds,
color: this.thoughtTypeColors[type] || '#757575',
expanded: true,
level: 1 // Premier niveau de hiérarchie
});
});
break;
case 'theme':
// Extraire les thèmes puis regrouper par thème principal
const themes = this.extractThemes(thoughts);
const themeGroups = {};
// Associer chaque pensée à son thème dominant
thoughts.forEach(thought => {
const content = thought.content.toLowerCase();
const dominantTheme = themes.find(theme => content.includes(theme)) || 'other';
if (!themeGroups[dominantTheme]) {
themeGroups[dominantTheme] = [];
}
themeGroups[dominantTheme].push(thought.id);
});
// Créer un cluster pour chaque thème
Object.entries(themeGroups).forEach(([theme, nodeIds], index) => {
clusters.push({
id: `cluster-theme-${index}`,
label: `Thème: ${theme}`,
nodeIds,
color: this.getDistinctColor(index),
expanded: true,
level: 1
});
});
break;
case 'metric':
// Regrouper par niveau de qualité
const qualityGroups = {
'high': [], // Qualité > 0.7
'medium': [], // Qualité entre 0.4 et 0.7
'low': [] // Qualité < 0.4
};
thoughts.forEach(thought => {
if (thought.metrics.quality > 0.7) {
qualityGroups['high'].push(thought.id);
}
else if (thought.metrics.quality > 0.4) {
qualityGroups['medium'].push(thought.id);
}
else {
qualityGroups['low'].push(thought.id);
}
});
// Créer un cluster pour chaque niveau de qualité
clusters.push({
id: 'cluster-quality-high',
label: 'Qualité élevée',
nodeIds: qualityGroups['high'],
color: '#34A853', // Vert
expanded: true,
level: 1
});
clusters.push({
id: 'cluster-quality-medium',
label: 'Qualité moyenne',
nodeIds: qualityGroups['medium'],
color: '#FBBC05', // Jaune
expanded: true,
level: 1
});
clusters.push({
id: 'cluster-quality-low',
label: 'Qualité basse',
nodeIds: qualityGroups['low'],
color: '#EA4335', // Rouge
expanded: true,
level: 1
});
break;
case 'connectivity':
// Regrouper par densité de connexions
const connectivityGroups = {
'high': [], // Plus de 3 connexions
'medium': [], // 1-3 connexions
'isolated': [] // Aucune connexion
};
thoughts.forEach(thought => {
if (thought.connections.length > 3) {
connectivityGroups['high'].push(thought.id);
}
else if (thought.connections.length >= 1) {
connectivityGroups['medium'].push(thought.id);
}
else {
connectivityGroups['isolated'].push(thought.id);
}
});
// Créer un cluster pour chaque niveau de connectivité
clusters.push({
id: 'cluster-connectivity-high',
label: 'Forte connectivité',
nodeIds: connectivityGroups['high'],
color: '#4285F4', // Bleu
expanded: true,
level: 1
});
clusters.push({
id: 'cluster-connectivity-medium',
label: 'Connectivité moyenne',
nodeIds: connectivityGroups['medium'],
color: '#9C27B0', // Violet
expanded: true,
level: 1
});
clusters.push({
id: 'cluster-connectivity-isolated',
label: 'Nœuds isolés',
nodeIds: connectivityGroups['isolated'],
color: '#757575', // Gris
expanded: true,
level: 1
});
break;
}
return clusters;
}
/**
* Génère une visualisation hiérarchique du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @param rootId Optionnel: l'ID du nœud racine
* @param options Options de visualisation
* @returns Une visualisation hiérarchique du graphe
*/
generateHierarchicalVisualization(thoughtGraph, rootId, options = {}) {
const thoughts = thoughtGraph.getAllThoughts();
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Déterminer la racine
let rootThought;
if (rootId) {
rootThought = thoughts.find(t => t.id === rootId);
}
if (!rootThought) {
// Si pas de racine spécifiée, utiliser la pensée avec le plus de connexions sortantes
rootThought = thoughts.reduce((max, current) => (current.connections.length > max.connections.length) ? current : max, thoughts[0]);
}
// Créer les nœuds avec des niveaux hiérarchiques
const nodes = [];
const visited = new Set();
const queue = [{ thought: rootThought, level: 0 }];
while (queue.length > 0) {
const { thought, level } = queue.shift();
if (visited.has(thought.id))
continue;
visited.add(thought.id);
// Ajouter le nœud avec son niveau hiérarchique
nodes.push({
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size: 10 + Math.min(thought.connections.length * 2, 15),
color: this.thoughtTypeColors[thought.type] || '#757575',
level,
tooltip: thought.content,
collapsed: level > 2 // Replier automatiquement les niveaux profonds
});
// Ajouter les pensées connectées à la file
const connectedThoughts = thoughtGraph.getConnectedThoughts(thought.id);
for (const connectedThought of connectedThoughts) {
if (!visited.has(connectedThought.id)) {
queue.push({ thought: connectedThought, level: level + 1 });
}
}
}
// Créer les liens
const links = [];
for (const node of nodes) {
const thought = thoughts.find(t => t.id === node.id);
if (!thought)
continue;
for (const connection of thought.connections) {
// Ne montrer que les liens entre nœuds visibles
if (nodes.some(n => n.id === connection.targetId)) {
// Éviter les doublons
const linkExists = links.some(link => (link.source === thought.id && link.target === connection.targetId) ||
(link.source === connection.targetId && link.target === thought.id));
if (!linkExists) {
links.push({
source: thought.id,
target: connection.targetId,
type: connection.type,
strength: connection.strength,
width: 1 + connection.strength * 3,
color: this.connectionTypeColors[connection.type] || '#757575',
dashed: connection.type === 'associates', // Ligne pointillée pour les liens faibles
tooltip: connection.description
});
}
}
}
}
// Générer des clusters si demandé
let clusters;
if (options.clusterBy) {
clusters = this.generateClusters(thoughts, options.clusterBy);
}
// Paramètres d'interactivité
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.5, 2],
highlightOnHover: true
};
// Options de mise en page
const layout = {
type: 'hierarchical',
direction: options.direction || 'TB',
levelSeparation: options.levelSeparation || 100,
spacing: 80
};
// Métadonnées supplémentaires
const metadata = {
type: 'hierarchical',
thoughtCount: thoughts.length,
nodeCount: nodes.length,
linkCount: links.length,
maxLevel: Math.max(...nodes.map(n => n.level || 0)),
rootId: rootThought.id
};
return {
nodes,
links,
clusters,
interactivity,
layout,
metadata
};
}
/**
* Génère une visualisation force-directed du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @param options Options de visualisation
* @returns Une visualisation force-directed du graphe
*/
generateForceDirectedVisualization(thoughtGraph, options = {}) {
const thoughts = thoughtGraph.getAllThoughts();
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Créer les nœuds
const nodes = thoughts.map(thought => {
// Calculer la taille du nœud en fonction du nombre de connexions
const connectionCount = thought.connections.length;
const size = 10 + Math.min(connectionCount * 2, 15);
// Obtenir la couleur en fonction du type de pensée
const color = this.thoughtTypeColors[thought.type] || '#757575';
// Assigner une importance basée sur la métrique de qualité
const importance = 0.5 + (thought.metrics.quality * 0.5);
return {
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size,
color,
tooltip: thought.content,
highlighted: thought.id === options.centerNode,
metadata: {
importance,
connectionCount
}
};
});
// Créer les liens avec des poids pour l'algorithme force-directed
const links = [];
for (const thought of thoughts) {
for (const connection of thought.connections) {
// Éviter les doublons
const linkExists = links.some(link => (link.source === thought.id && link.target === connection.targetId) ||
(link.source === connection.targetId && link.target === thought.id));
if (!linkExists) {
// Calculer le poids pour l'algorithme force-directed
// Les connexions fortes ont un poids plus élevé (plus d'attraction)
const weight = connection.strength * 2;
// Calculer l'épaisseur du lien en fonction de la force de la connexion
const width = 1 + connection.strength * 4;
// Obtenir la couleur en fonction du type de connexion
const color = this.connectionTypeColors[connection.type] || '#757575';
links.push({
source: thought.id,
target: connection.targetId,
type: connection.type,
strength: connection.strength,
width,
color,
weight,
dashed: connection.type === 'associates',
bidirectional: connection.type === 'contradicts',
tooltip: connection.description || `Connexion de type ${connection.type}`
});
}
}
}
// Générer des clusters si demandé
let clusters;
if (options.clusterBy) {
clusters = this.generateClusters(thoughts, options.clusterBy);
}
// Paramètres d'interactivité
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.2, 3],
highlightOnHover: true
};
// Options de mise en page
const layout = {
type: 'force',
forceStrength: options.forceStrength || 0.5,
spacing: 100,
centerNode: options.centerNode
};
// Métadonnées
const metadata = {
type: 'force-directed',
thoughtCount: thoughts.length,
nodeCount: nodes.length,
linkCount: links.length,
averageConnections: thoughts.reduce((sum, t) => sum + t.connections.length, 0) / thoughts.length,
centerNode: options.centerNode
};
return {
nodes,
links,
clusters,
interactivity,
layout,
metadata
};
}
/**
* Génère une visualisation radiale du graphe de pensées
*
* @param thoughtGraph Le graphe de pensées à visualiser
* @param centerNodeId Optionnel: l'ID du nœud central (si non spécifié, utilise le nœud avec le plus de connexions)
* @param options Options de visualisation
* @returns Une visualisation radiale du graphe
*/
generateRadialVisualization(thoughtGraph, centerNodeId, options = {}) {
const thoughts = thoughtGraph.getAllThoughts();
if (thoughts.length === 0) {
return {
nodes: [],
links: [],
metadata: {
isEmpty: true
}
};
}
// Déterminer le nœud central
let centerThought;
if (centerNodeId) {
centerThought = thoughts.find(t => t.id === centerNodeId);
}
if (!centerThought) {
// Si pas de nœud central spécifié, utiliser celui avec le plus de connexions
centerThought = thoughts.reduce((max, current) => current.connections.length > max.connections.length ? current : max, thoughts[0]);
}
// Configuration des cercles concentriques
const maxDepth = options.maxDepth || 3;
const radialDistance = options.radialDistance || 120;
// Map pour stocker le niveau radial de chaque nœud
const radialLevels = new Map();
radialLevels.set(centerThought.id, 0);
// Calculer les niveaux radiaux par BFS
const queue = [
{ id: centerThought.id, level: 0 }
];
const visited = new Set([centerThought.id]);
while (queue.length > 0) {
const { id, level } = queue.shift();
if (level >= maxDepth)
continue;
const thought = thoughts.find(t => t.id === id);
if (!thought)
continue;
for (const connection of thought.connections) {
if (!visited.has(connection.targetId)) {
visited.add(connection.targetId);
radialLevels.set(connection.targetId, level + 1);
queue.push({ id: connection.targetId, level: level + 1 });
}
}
}
// Créer les nœuds
const nodes = [];
// Compter le nombre de nœuds à chaque niveau
const levelCounts = {};
for (const level of radialLevels.values()) {
levelCounts[level] = (levelCounts[level] || 0) + 1;
}
// Positions angulaires à chaque niveau
const levelAngles = {};
// Ajouter le nœud central
nodes.push({
id: centerThought.id,
label: this.truncateText(centerThought.content, 40),
type: centerThought.type,
metrics: centerThought.metrics,
size: 15, // Nœud central plus grand
color: this.thoughtTypeColors[centerThought.type] || '#757575',
position: { x: 0, y: 0 }, // Au centre
tooltip: centerThought.content,
highlighted: true
});
// Ajouter les nœuds aux différents niveaux radiaux
for (const thought of thoughts) {
if (thought.id === centerThought.id)
continue; // Déjà ajouté
const level = radialLevels.get(thought.id);
if (level === undefined || level > maxDepth)
continue; // Hors de la profondeur maximale
// Calculer l'angle pour ce nœud
const angleKey = `${thought.id}-${level}`;
if (!levelAngles[angleKey]) {
levelAngles[angleKey] = 2 * Math.PI * (Object.keys(levelAngles).filter(key => key.endsWith(`-${level}`)).length) / levelCounts[level];
}
const angle = levelAngles[angleKey];
// Calculer la position radiale
const radius = level * radialDistance;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
nodes.push({
id: thought.id,
label: this.truncateText(thought.content, 40),
type: thought.type,
metrics: thought.metrics,
size: 10 + Math.min(thought.connections.length, 8),
color: this.thoughtTypeColors[thought.type] || '#757575',
position: { x, y },
tooltip: thought.content,
level
});
}
// Créer les liens
const links = [];
// Ajouter uniquement les liens entre les nœuds visibles
const visibleNodeIds = nodes.map(n => n.id);
for (const thought of thoughts) {
if (!visibleNodeIds.includes(thought.id))
continue;
for (const connection of thought.connections) {
if (visibleNodeIds.includes(connection.targetId)) {
// Éviter les doublons
const linkExists = links.some(link => (link.source === thought.id && link.target === connection.targetId) ||
(link.source === connection.targetId && link.target === thought.id));
if (!linkExists) {
// Obtenir les niveaux radiaux des nœuds
const sourceLevel = radialLevels.get(thought.id) || 0;
const targetLevel = radialLevels.get(connection.targetId) || 0;
// Les liens entre niveaux adjacents sont plus courts et plus épais
const levelDifference = Math.abs(sourceLevel - targetLevel);
const width = 1 + (3 / (levelDifference || 1)) * connection.strength;
links.push({
source: thought.id,
target: connection.targetId,
type: connection.type,
strength: connection.strength,
width,
color: this.connectionTypeColors[connection.type] || '#757575',
dashed: levelDifference > 1, // Ligne pointillée pour les liens traversant plusieurs niveaux
animated: thought.id === centerThought.id || connection.targetId === centerThought.id,
tooltip: connection.description || `Connexion de type ${connection.type}`
});
}
}
}
}
// Paramètres d'interactivité
const interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true,
initialZoom: 1,
zoomRange: [0.5, 2],
highlightOnHover: true
};
// Options de mise en page
const layout = {
type: 'radial',
centerNode: centerThought.id,
spacing: radialDistance
};
// Métadonnées
const metadata = {
type: 'radial',
thoughtCount: thoughts.length,
visibleNodeCount: nodes.length,
linkCount: links.length,
maxDepth,
centerNodeId: centerThought.id,
radialLevelDistribution: Object.entries(levelCounts).reduce((acc, [level, count]) => ({ ...acc, [level]: count }), {})
};
return {
nodes,
links,
interactivity,
layout,
metadata
};
}
/**
* Applique des filtres à une visualisation
*
* @param visualization La visualisation à filtrer
* @param filters Les options de filtrage à appliquer
* @returns La visualisation filtrée
*/
applyFilters(visualization, filters) {
// Créer des copies profondes pour ne pas modifier l'original
const nodes = [...visualization.nodes];
const links = [...visualization.links];
// Filtrage par type de nœud
if (filters.nodeTypes && filters.nodeTypes.length > 0) {
const filteredNodeIds = nodes
.filter(node => !filters.nodeTypes.includes(node.type))
.map(node => node.id);
// Supprimer les nœuds qui ne correspondent pas aux types demandés
for (let i = nodes.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(nodes[i].id)) {
nodes.splice(i, 1);
}
}
// Supprimer les liens qui pointent vers des nœuds supprimés
for (let i = links.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(links[i].source) ||
filteredNodeIds.includes(links[i].target)) {
links.splice(i, 1);
}
}
}
// Filtrage par type de connexion
if (filters.connectionTypes && filters.connectionTypes.length > 0) {
for (let i = links.length - 1; i >= 0; i--) {
if (!filters.connectionTypes.includes(links[i].type)) {
links.splice(i, 1);
}
}
}
// Filtrage par seuils de métriques
if (filters.metricThresholds) {
// Pour chaque métrique spécifiée
const metricsToCheck = [
{ name: 'confidence', thresholds: filters.metricThresholds.confidence },
{ name: 'relevance', thresholds: filters.metricThresholds.relevance },
{ name: 'quality', thresholds: filters.metricThresholds.quality }
];
const filteredNodeIds = [];
for (const node of nodes) {
let shouldFilter = false;
for (const metric of metricsToCheck) {
if (metric.thresholds) {
const [min, max] = metric.thresholds;
const value = node.metrics[metric.name];
if (value < min || value > max) {
shouldFilter = true;
break;
}
}
}
if (shouldFilter) {
filteredNodeIds.push(node.id);
}
}
// Supprimer les nœuds qui ne correspondent pas aux seuils
for (let i = nodes.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(nodes[i].id)) {
nodes.splice(i, 1);
}
}
// Supprimer les liens qui pointent vers des nœuds supprimés
for (let i = links.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(links[i].source) ||
filteredNodeIds.includes(links[i].target)) {
links.splice(i, 1);
}
}
}
// Filtrage par recherche textuelle
if (filters.textSearch && filters.textSearch.trim() !== '') {
const searchTerm = filters.textSearch.toLowerCase().trim();
const filteredNodeIds = [];
for (const node of nodes) {
if (!node.label.toLowerCase().includes(searchTerm) &&
!node.tooltip?.toLowerCase().includes(searchTerm)) {
filteredNodeIds.push(node.id);
}
}
// Supprimer les nœuds qui ne correspondent pas à la recherche
for (let i = nodes.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(nodes[i].id)) {
nodes.splice(i, 1);
}
}
// Supprimer les liens qui pointent vers des nœuds supprimés
for (let i = links.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(links[i].source) ||
filteredNodeIds.includes(links[i].target)) {
links.splice(i, 1);
}
}
}
// Filtrage par plage de dates
if (filters.dateRange) {
const [startDate, endDate] = filters.dateRange;
// Nous supposons que l'info de date est stockée dans les métadonnées des nœuds
const filteredNodeIds = [];
for (const node of nodes) {
const timestamp = node.metadata?.timestamp;
if (timestamp) {
const nodeDate = new Date(timestamp);
if (nodeDate < startDate || nodeDate > endDate) {
filteredNodeIds.push(node.id);
}
}
}
// Supprimer les nœuds hors de la plage de dates
for (let i = nodes.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(nodes[i].id)) {
nodes.splice(i, 1);
}
}
// Supprimer les liens qui pointent vers des nœuds supprimés
for (let i = links.length - 1; i >= 0; i--) {
if (filteredNodeIds.includes(links[i].source) ||
filteredNodeIds.includes(links[i].target)) {
links.splice(i, 1);
}
}
}
// Mettre à jour les métadonnées
const metadata = {
...visualization.metadata,
filteredNodeCount: nodes.length,
filteredLinkCount: links.length,
appliedFilters: { ...filters },
originalNodeCount: visualization.nodes.length,
originalLinkCount: visualization.links.length
};
return {
...visualization,
nodes,
links,
metadata,
filters
};
}
/**
* Applique des interactions à une visualisation
*
* @param visualization La visualisation
* @param interaction L'interaction à appliquer
* @returns La visualisation mise à jour avec l'interaction
*/
applyInteraction(visualization, interaction) {
// Créer des copies profondes pour ne pas modifier l'original
const nodes = [...visualization.nodes];
const links = [...visualization.links];
switch (interaction.type) {
case 'highlight':
// Réinitialiser tous les surlignages
nodes.forEach(node => {
node.highlighted = false;
});
links.forEach(link => {
link.highlighted = false;
});
// Surligner les nœuds spécifiés
for (const nodeId of interaction.nodeIds) {
const nodeIndex = nodes.findIndex(n => n.id === nodeId);
if (nodeIndex !== -1) {
nodes[nodeIndex].highlighted = true;
}
// Surligner également les liens connectés
for (let i = 0; i < links.length; i++) {
if (links[i].source === nodeId || links[i].target === nodeId) {
links[i].highlighted = true;
}
}
}
break;
case 'select':
// Réinitialiser toutes les sélections
nodes.forEach(node => {
node.selected = false;
});
// Sélectionner les nœuds spécifiés
for (const nodeId of interaction.nodeIds) {
const nodeIndex = nodes.findIndex(n => n.id === nodeId);
if (nodeIndex !== -1) {
nodes[nodeIndex].selected = true;
}
}
break;
case 'expand':
// Développer les nœuds spécifiés
for (const nodeId of interaction.nodeIds) {
const nodeIndex = nodes.findIndex(n => n.id === nodeId);
if (nodeIndex !== -1) {
nodes[nodeIndex].collapsed = false;
}
}
break;
case 'collapse':
// Replier les nœuds spécifiés
for (const nodeId of interaction.nodeIds) {
const nodeIndex = nodes.findIndex(n => n.id === nodeId);
if (nodeIndex !== -1) {
nodes[nodeIndex].collapsed = true;
}
}
break;
case 'focus':
// Cela pourrait impliquer un recentrage de la visualisation
// et potentiellement un zoom sur les nœuds spécifiés
// Mettre à jour les métadonnées pour indiquer les nœuds focalisés
if (!visualization.metadata) {
visualization.metadata = {};
}
visualization.metadata.focusedNodeIds = interaction.nodeIds;
// Mettre à jour l'interactivité
if (!visualization.interactivity) {
visualization.interactivity = {
zoomable: true,
draggable: true,
selectable: true,
tooltips: true,
expandableNodes: true
};
}
// Réajuster le zoom pour se concentrer sur les nœuds
visualization.interactivity.initialZoom = 1.5;
break;
}
// Mettre à jour les métadonnées
const metadata = {
...visualization.metadata,
lastInteraction: interaction
};
return {
...visualization,
nodes,
links,
metadata
};
}
/**
* Crée une version simplifiée d'une visualisation pour améliorer les performances
*
* @param visualization La visualisation à simplifier
* @param options Options de simplification
* @returns La visualisation simplifiée
*/
simplifyVisualization(visualization, options = {}) {
const maxNodes = options.maxNodes || 100;
const minNodeImportance = options.minNodeImportance || 0.3;
if (visualization.nodes.length <= maxNodes) {
return visualization; // Pas besoin de simplifier
}
// Calculer l'importance de chaque nœud
const nodeImportance = new Map();
for (const node of visualization.nodes) {
let importance = 0;
// Importance basée sur les métriques
if (node.metrics) {
importance += (node.metrics.quality || 0.5) * 0.4;
importance += (node.metrics.relevance || 0.5) * 0.3;
importance += (node.metrics.confidence || 0.5) * 0.3;
}
// Importance basée sur les connexions
const connectionCount = visualization.links.filter(link => link.source === node.id || link.target === node.id).length;
importance += Math.min(connectionCount / 10, 1) * 0.5;
// Importance basée sur le type
if (node.type === 'conclusion' || node.type === 'hypothesis') {
importance += 0.2;
}
// Importance basée sur la mise en évidence
if (node.highlighted || node.selected) {
importance += 0.3;
}
nodeImportance.set(node.id, importance);
}
// Trier les nœuds par importance
const nodesSorted = [...visualization.nodes].sort((a, b) => (nodeImportance.get(b.id) || 0) - (nodeImportance.get(a.id) || 0));
// Prendre les nœuds les plus importants
const nodesTop = nodesSorted.slice(0, maxNodes);
const topNodeIds = new Set(nodesTop.map(n => n.id));
// Filtrer les liens qui concernent uniquement les nœuds conservés
const filteredLinks = visualization.links.filter(link => topNodeIds.has(link.source) && topNodeIds.has(link.target));
// Ajouter des indicateurs de nœuds agrégés
// Compter les nœuds cachés par type