39145-vm/core/templates/core/project_detail.html
2026-03-13 10:27:31 +00:00

357 lines
16 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid d-flex flex-column pt-4" style="height: 100vh; max-height: 100vh; overflow: hidden;">
<div class="row mb-3 flex-shrink-0">
<div class="col-md-8">
<h1 class="mb-0">{{ project.title }}</h1>
<p class="text-muted mb-0">{{ project.industry }} | <strong>Goal:</strong> {{ project.goal }}</p>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'project_list' %}" class="btn btn-outline-secondary">Back to Projects</a>
</div>
</div>
<div class="row flex-grow-1 pb-3" style="min-height: 0;">
<div class="col-12 h-100">
<div class="card shadow-sm h-100 w-100 d-flex flex-column" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08);">
<div class="card-header bg-white d-flex justify-content-between align-items-center flex-shrink-0" style="border-bottom: 1px solid rgba(0,0,0,0.05);">
<h5 class="mb-0 fw-bold">Interactive Mind Map</h5>
<div>
<button class="btn btn-sm btn-outline-success me-2" onclick="exportMap()">Export to Image</button>
<form method="POST" action="{% url 'regenerate_mindmap' project.pk %}" class="d-inline" onsubmit="return confirm('Are you sure you want to regenerate the entire map? This will delete all current nodes.');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger me-2">Regenerate</button>
</form>
<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>
</div>
</div>
<div class="card-body p-0 position-relative flex-grow-1" style="min-height: 0; overflow: hidden; background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%);">
{% if not nodes %}
<div class="position-absolute w-100 h-100 d-flex flex-column justify-content-center align-items-center" style="z-index: 10; background: rgba(255,255,255,0.9);">
<h4 class="text-muted mb-3">The mind map is empty</h4>
<p class="text-muted mb-4">Something went wrong during generation, or the map was cleared.</p>
<form method="POST" action="{% url 'regenerate_mindmap' project.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary px-4 py-2 shadow-sm rounded-pill">
Generate Map Now
</button>
</form>
</div>
{% endif %}
<!-- The vis-network container -->
<div id="mynetwork" style="width: 100%; height: 100%;"></div>
<!-- Floating Panels in Top Right -->
<div class="position-absolute d-flex flex-column" style="top: 15px; right: 15px; width: 350px; z-index: 1000; bottom: 15px; pointer-events: none;">
<!-- Node Details Panel -->
<div class="card shadow mb-3" style="pointer-events: auto; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.4); border-radius: 12px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);">
<div class="card-header py-2" style="background: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); border-radius: 12px 12px 0 0;">
<h6 class="mb-0 fw-bold text-primary">Node Details</h6>
</div>
<div class="card-body py-2" id="node-details" style="max-height: 200px; overflow-y: auto;">
<p class="text-muted small mb-0">Click on a node or edge in the map to see its details here.</p>
</div>
</div>
<!-- AI Chat Panel -->
<div class="card shadow d-flex flex-column" style="pointer-events: auto; flex: 1; min-height: 0; border: 1px solid rgba(255,255,255,0.4); border-radius: 12px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);">
<div class="card-header py-2" style="background: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); border-radius: 12px 12px 0 0;">
<h6 class="mb-0 fw-bold text-success">Ask AI Assistant</h6>
</div>
<div class="card-body d-flex flex-column p-2" style="min-height: 0;">
<div id="ai-response" class="p-2 rounded mb-2 flex-grow-1" style="overflow-y: auto; font-size: 0.9rem; background: rgba(255,255,255,0.6); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);">
<span class="text-muted">Hello! I'm your AI business strategist. Ask me to expand on a node, brainstorm new ideas, or analyze the map. If you ask me to add nodes to the map, I will!</span>
</div>
<div class="mt-auto flex-shrink-0">
<textarea id="ai-input" class="form-control form-control-sm mb-2" rows="2" style="resize: none; border-radius: 8px; background: rgba(255,255,255,0.9);" placeholder="e.g. Add 3 more nodes related to Marketing..."></textarea>
<button id="ai-submit" class="btn btn-primary btn-sm w-100 rounded-pill shadow-sm">Send to AI</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Load Vis Network library -->
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<!-- Add custom styles for vis-network tooltip -->
<style type="text/css">
.vis-tooltip {
position: absolute;
background-color: rgba(255, 255, 255, 0.95);
padding: 12px;
border: none;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
font-family: inherit;
font-size: 14px;
max-width: 320px;
pointer-events: none;
z-index: 10;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* Optional: custom scrollbar for details and chat */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.15);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.25);
}
</style>
<script>
// Colors for different categories
const categoryColors = {
'Strategy': '#e1bee7', // Purple
'Product': '#bbdefb', // Blue
'Market': '#c8e6c9', // Green
'Operations': '#ffcc80', // Orange
'Finance': '#ffcdd2', // Red
'General': '#f5f5f5' // Grey
};
function getNodeColor(category) {
let cat = category || 'General';
let color = '#e0e0e0'; // Default light grey
for (let key in categoryColors) {
if (cat.toLowerCase().includes(key.toLowerCase())) {
color = categoryColors[key];
break;
}
}
return {
background: color,
border: '#9e9e9e',
highlight: { background: '#ffffff', border: '#42a5f5' }
};
}
// Prepare data from Django context
const nodesArray = [
{% for node in nodes %}
{
id: {{ node.pk }},
label: "{{ node.title|escapejs }}",
title: "<strong>{{ node.title|escapejs }}</strong><br>{{ node.summary|escapejs }}<br><em>Category: {{ node.category|escapejs }}</em>",
summary: "{{ node.summary|escapejs }}",
category: "{{ node.category|escapejs }}",
color: getNodeColor("{{ node.category|escapejs }}"),
shape: 'box',
font: { size: 16, face: 'Arial', color: '#333' },
margin: 10
},
{% endfor %}
];
const edgesArray = [
{% for conn in connections %}
{
from: {{ conn.source.pk }},
to: {{ conn.target.pk }},
label: "{{ conn.how|escapejs }}",
title: "<strong>Why:</strong> {{ conn.why|escapejs }}",
arrows: "to",
why: "{{ conn.why|escapejs }}",
how: "{{ conn.how|escapejs }}"
},
{% endfor %}
];
const container = document.getElementById("mynetwork");
const data = {
nodes: new vis.DataSet(nodesArray),
edges: new vis.DataSet(edgesArray)
};
const options = {
layout: {
improvedLayout: true
},
physics: {
solver: 'forceAtlas2Based',
forceAtlas2Based: {
gravitationalConstant: -100,
centralGravity: 0.01,
springLength: 150,
springConstant: 0.08
},
maxVelocity: 50,
minVelocity: 0.1,
timestep: 0.5,
stabilization: { iterations: 150 }
},
interaction: {
tooltipDelay: 200,
hover: true
},
edges: {
font: { size: 12, align: 'top' },
color: { color: '#848484', highlight: '#42a5f5' },
smooth: { type: 'continuous' }
}
};
const network = new vis.Network(container, data, options);
function fitNetwork() {
network.fit({ animation: { duration: 1000, easingFunction: 'easeInOutQuad' } });
}
// Export Map function
function exportMap() {
const canvas = document.querySelector('canvas');
if (!canvas) return;
const image = canvas.toDataURL("image/png");
const link = document.createElement('a');
link.download = 'mindmap.png';
link.href = image;
link.click();
}
// Handle clicks to show details in the sidebar
network.on("click", function (params) {
const detailsPanel = document.getElementById('node-details');
if (params.nodes.length > 0) {
// Node clicked
const nodeId = params.nodes[0];
const node = data.nodes.get(nodeId);
detailsPanel.innerHTML = `
<h6 class="mb-2 fw-bold text-dark">${node.label}</h6>
<span class="badge bg-secondary mb-2 bg-opacity-75">${node.category}</span>
<p class="small mb-0 text-dark"><strong>Summary:</strong><br>${node.summary}</p>
`;
} else if (params.edges.length > 0) {
// Edge clicked
const edgeId = params.edges[0];
const edge = data.edges.get(edgeId);
const fromNode = data.nodes.get(edge.from);
const toNode = data.nodes.get(edge.to);
detailsPanel.innerHTML = `
<h6 class="mb-2 fw-bold text-dark">Connection Details</h6>
<p class="small mb-1 text-dark"><strong>From:</strong> ${fromNode.label}</p>
<p class="small mb-2 text-dark"><strong>To:</strong> ${toNode.label}</p>
<div class="small text-dark border-top pt-2">
<p class="mb-1"><strong>How:</strong> ${edge.how || 'N/A'}</p>
<p class="mb-0"><strong>Why:</strong> ${edge.why || 'N/A'}</p>
</div>
`;
} else {
// Clicked empty space
detailsPanel.innerHTML = '<p class="text-muted small mb-0">Click on a node or edge in the map to see its details here.</p>';
}
});
// AI Chat Handler
document.getElementById('ai-submit').addEventListener('click', function() {
const inputField = document.getElementById('ai-input');
const message = inputField.value.trim();
if (!message) return;
const responseDiv = document.getElementById('ai-response');
// Append user message
responseDiv.innerHTML += `<div class="mb-2 text-end"><span class="badge bg-primary rounded-pill px-3 py-2 text-wrap text-start d-inline-block shadow-sm" style="max-width: 85%; font-weight: normal; font-size: 0.9rem;">${message}</span></div>`;
inputField.value = '';
// Show loading state
const loadingId = 'loading-' + Date.now();
responseDiv.innerHTML += `<div id="${loadingId}" class="mb-2 text-start"><span class="badge bg-white text-primary border border-primary border-opacity-25 rounded-pill px-3 py-2 shadow-sm d-inline-block" style="font-weight: normal; font-size: 0.9rem;"><em><span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Thinking...</em></span></div>`;
responseDiv.scrollTop = responseDiv.scrollHeight;
fetch("{% url 'ai_chat' project.pk %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ message: message })
})
.then(response => response.json())
.then(apiData => {
document.getElementById(loadingId).remove();
// Handle chat response
if (apiData.response) {
responseDiv.innerHTML += `<div class="mb-2 text-start"><span class="badge bg-white text-dark rounded-4 px-3 py-2 text-wrap text-start d-inline-block shadow-sm" style="max-width: 90%; font-weight: normal; font-size: 0.9rem; border: 1px solid rgba(0,0,0,0.05);">${apiData.response}</span></div>`;
}
// Check for new nodes and add them to the map
let mapUpdated = false;
if (apiData.added_nodes && apiData.added_nodes.length > 0) {
apiData.added_nodes.forEach(node => {
data.nodes.add({
id: node.id,
label: node.title,
title: "<strong>" + node.title + "</strong><br>" + node.summary + "<br><em>Category: " + node.category + "</em>",
summary: node.summary,
category: node.category,
color: getNodeColor(node.category),
shape: 'box',
font: { size: 16, face: 'Arial', color: '#333' },
margin: 10
});
});
mapUpdated = true;
}
if (apiData.added_connections && apiData.added_connections.length > 0) {
apiData.added_connections.forEach(conn => {
data.edges.add({
id: conn.id,
from: conn.source_id,
to: conn.target_id,
label: conn.how,
title: "<strong>Why:</strong> " + conn.why,
arrows: "to",
why: conn.why,
how: conn.how
});
});
mapUpdated = true;
}
if (mapUpdated) {
// Stabilize and adjust view
network.stabilize();
setTimeout(fitNetwork, 500); // Fit network smoothly after a slight delay
responseDiv.innerHTML += `<div class="mb-2 text-center"><small class="text-success"><i class="bi bi-check-circle-fill"></i> New items added to map!</small></div>`;
}
responseDiv.scrollTop = responseDiv.scrollHeight;
})
.catch(err => {
document.getElementById(loadingId).remove();
responseDiv.innerHTML += `<div class="mb-2 text-start"><span class="badge bg-danger text-white rounded-pill px-3 py-2 shadow-sm d-inline-block" style="font-weight: normal;">Error: Failed to get response.</span></div>`;
console.error(err);
});
});
// Allow submitting AI input with Enter key
document.getElementById('ai-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.getElementById('ai-submit').click();
}
});
</script>
{% endblock %}