diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index 7c6ead7..f47932c 100644
Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index a1fb0eb..686e6cd 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/templates/core/project_detail.html b/core/templates/core/project_detail.html
index f7f3fa1..341b344 100644
--- a/core/templates/core/project_detail.html
+++ b/core/templates/core/project_detail.html
@@ -22,6 +22,15 @@
{% csrf_token %}
+
+
+
+
+
+
@@ -127,6 +136,26 @@
'General': '#f5f5f5' // Grey
};
+
+ const categoryEmojis = {
+ 'Strategy': '🎯',
+ 'Product': '📦',
+ 'Market': '🌍',
+ 'Operations': '⚙️',
+ 'Finance': '💰',
+ 'General': '💡'
+ };
+
+ function getNodeEmoji(category) {
+ let cat = category || 'General';
+ for (let key in categoryEmojis) {
+ if (cat.toLowerCase().includes(key.toLowerCase())) {
+ return categoryEmojis[key];
+ }
+ }
+ return '💡'; // default emoji
+ }
+
function getNodeColor(category) {
let cat = category || 'General';
let color = '#e0e0e0'; // Default light grey
@@ -148,7 +177,7 @@
{% for node in nodes %}
{
id: {{ node.pk }},
- label: "{{ node.title|escapejs }}",
+ label: getNodeEmoji("{{ node.category|escapejs }}") + " " + "{{ node.title|escapejs }}",
title: "{{ node.title|escapejs }}
{{ node.summary|escapejs }}
Category: {{ node.category|escapejs }}",
summary: "{{ node.summary|escapejs }}",
category: "{{ node.category|escapejs }}",
@@ -234,10 +263,22 @@
// Node clicked
const nodeId = params.nodes[0];
const node = data.nodes.get(nodeId);
+ // remove emoji from label for editing
+ const cleanLabel = node.label.replace(/^.*? /, '');
detailsPanel.innerHTML = `
- ${node.label}
- ${node.category}
- Summary:
${node.summary}
+
+
${cleanLabel}
+
${node.category}
+
Summary:
${node.summary}
+
+
+
+
+
+
+
+
+
`;
} else if (params.edges.length > 0) {
// Edge clicked
@@ -260,6 +301,75 @@
}
});
+
+ function editNodeUI(nodeId) {
+ document.getElementById(`node-view-${nodeId}`).classList.add('d-none');
+ document.getElementById(`node-edit-${nodeId}`).classList.remove('d-none');
+ }
+
+ function cancelEditNode(nodeId) {
+ document.getElementById(`node-edit-${nodeId}`).classList.add('d-none');
+ document.getElementById(`node-view-${nodeId}`).classList.remove('d-none');
+ }
+
+ function saveNode(nodeId, event) {
+ const title = document.getElementById(`edit-node-title-${nodeId}`).value;
+ const category = document.getElementById(`edit-node-category-${nodeId}`).value;
+ const summary = document.getElementById(`edit-node-summary-${nodeId}`).value;
+
+ const saveBtn = event.target;
+ saveBtn.disabled = true;
+ saveBtn.innerText = 'Saving...';
+
+ let csrfToken = '';
+ const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
+ if (csrfInput) {
+ csrfToken = csrfInput.value;
+ } else {
+ // fallback, get from form if present elsewhere
+ console.warn("CSRF token not found via input name. Ensure it exists on the page.");
+ }
+
+ const url = "{% url 'edit_node' project.pk 0 %}".replace('/0/', '/' + nodeId + '/');
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({ title: title, category: category, summary: summary })
+ })
+ .then(response => response.json())
+ .then(res => {
+ if (res.status === 'success') {
+ const node = res.node;
+ const emoji = getNodeEmoji(node.category);
+ const color = getNodeColor(node.category);
+ data.nodes.update({
+ id: node.id,
+ label: emoji + " " + node.title,
+ title: `${node.title}
${node.summary}
Category: ${node.category}`,
+ summary: node.summary,
+ category: node.category,
+ color: color
+ });
+
+ // re-render details
+ network.emit('click', { nodes: [nodeId], edges: [] });
+ } else {
+ alert('Error saving node: ' + res.message);
+ saveBtn.disabled = false;
+ saveBtn.innerText = 'Save';
+ }
+ })
+ .catch(err => {
+ alert('Error saving node');
+ saveBtn.disabled = false;
+ saveBtn.innerText = 'Save';
+ });
+ }
+
// AI Chat Handler
document.getElementById('ai-submit').addEventListener('click', function() {
const inputField = document.getElementById('ai-input');
diff --git a/core/urls.py b/core/urls.py
index 36fa0ba..3379124 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,5 +1,5 @@
from django.urls import path
-from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap
+from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap, edit_node
from .ai_views import ai_chat
urlpatterns = [
@@ -9,5 +9,7 @@ urlpatterns = [
path("projects//", project_detail, name="project_detail"),
path("projects//regenerate/", regenerate_mindmap, name="regenerate_mindmap"),
path("projects//ai/", ai_chat, name="ai_chat"),
+ path('projects//node//edit/', edit_node, name='edit_node'),
+
path("signup/", signup, name="signup"),
]
\ No newline at end of file
diff --git a/core/views.py b/core/views.py
index 4a2e209..5ae6070 100644
--- a/core/views.py
+++ b/core/views.py
@@ -54,4 +54,35 @@ def regenerate_mindmap(request, pk):
project.nodes.all().delete()
project.connections.all().delete()
generate_initial_mindmap(project)
- return redirect('project_detail', pk=project.pk)
\ No newline at end of file
+ return redirect('project_detail', pk=project.pk)
+import json
+from django.http import JsonResponse
+from django.views.decorators.http import require_POST
+
+@login_required
+@require_POST
+def edit_node(request, pk, node_id):
+ project = get_object_or_404(Project, pk=pk, user=request.user)
+ try:
+ node = project.nodes.get(pk=node_id)
+ data = json.loads(request.body)
+ title = data.get('title', '').strip()
+ summary = data.get('summary', '').strip()
+ category = data.get('category', '').strip()
+
+ if title:
+ node.title = title
+ if summary:
+ node.summary = summary
+ if category:
+ node.category = category
+
+ node.save()
+ return JsonResponse({'status': 'success', 'node': {
+ 'id': node.pk,
+ 'title': node.title,
+ 'summary': node.summary,
+ 'category': node.category
+ }})
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
diff --git a/update_html.py b/update_html.py
new file mode 100644
index 0000000..536c61a
--- /dev/null
+++ b/update_html.py
@@ -0,0 +1,106 @@
+import os
+
+path = "core/templates/core/project_detail.html"
+with open(path, "r") as f:
+ html = f.read()
+
+# 1. Add top bar buttons (Layout, Zoom, etc)
+buttons_to_add = """
+
+
+
+
+
"""
+
+html = html.replace('',
+ buttons_to_add + '\n ')
+
+# 2. Add emoji logic
+emoji_script = """
+ const categoryEmojis = {
+ 'Strategy': '🎯',
+ 'Product': '📦',
+ 'Market': '🌍',
+ 'Operations': '⚙️',
+ 'Finance': '💰',
+ 'General': '💡'
+ };
+
+ function getNodeEmoji(category) {
+ let cat = category || 'General';
+ for (let key in categoryEmojis) {
+ if (cat.toLowerCase().includes(key.toLowerCase())) {
+ return categoryEmojis[key];
+ }
+ }
+ return '💡'; // default emoji
+ }
+"""
+
+html = html.replace('function getNodeColor(category) {', emoji_script + '\n function getNodeColor(category) {')
+
+# update node array to include emoji
+html = html.replace('label: "{{ node.title|escapejs }}",', 'label: getNodeEmoji("{{ node.category|escapejs }}") + " " + "{{ node.title|escapejs }}",')
+
+# 3. Add script functions for zoom and layout
+scripts_to_add = """
+ function zoomIn() {
+ if (network) {
+ const scale = network.getScale() * 1.5;
+ network.moveTo({ scale: scale, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
+ }
+ }
+
+ function zoomOut() {
+ if (network) {
+ const scale = network.getScale() / 1.5;
+ network.moveTo({ scale: scale, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
+ }
+ }
+
+ function changeLayout(layoutType) {
+ if (layoutType === 'hierarchical') {
+ network.setOptions({
+ layout: {
+ hierarchical: {
+ enabled: true,
+ direction: 'UD',
+ sortMethod: 'directed',
+ nodeSpacing: 200,
+ levelSeparation: 150
+ }
+ },
+ physics: {
+ hierarchicalRepulsion: {
+ nodeDistance: 200
+ }
+ }
+ });
+ } else {
+ network.setOptions({
+ layout: { hierarchical: { enabled: false } },
+ 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 }
+ }
+ });
+ }
+ }
+"""
+hTml = html.replace('// Export Map function', scripts_to_add + '\n // Export Map function')
+
+with open(path, "w") as f:
+ f.write(html)
+print("Updated HTML!")
diff --git a/update_js.py b/update_js.py
new file mode 100644
index 0000000..3da7414
--- /dev/null
+++ b/update_js.py
@@ -0,0 +1,114 @@
+import os
+
+with open("core/templates/core/project_detail.html", "r") as f:
+ html = f.read()
+
+# Replace the node clicked block
+old_block = """
+ const nodeId = params.nodes[0];
+ const node = data.nodes.get(nodeId);
+ detailsPanel.innerHTML = `
+ ${node.label}
+ ${node.category}
+ Summary:
${node.summary}
+ `;
+"""
+
+new_block = """
+ const nodeId = params.nodes[0];
+ const node = data.nodes.get(nodeId);
+ // remove emoji from label for editing
+ const cleanLabel = node.label.replace(/^.*? /, '');
+ detailsPanel.innerHTML = `
+
+
${cleanLabel}
+
${node.category}
+
Summary:
${node.summary}
+
+
+
+
+
+
+
+
+
+ `;
+"""
+
+html = html.replace(old_block, new_block)
+
+new_scripts = """
+ function editNodeUI(nodeId) {
+ document.getElementById(`node-view-${nodeId}`).classList.add('d-none');
+ document.getElementById(`node-edit-${nodeId}`).classList.remove('d-none');
+ }
+
+ function cancelEditNode(nodeId) {
+ document.getElementById(`node-edit-${nodeId}`).classList.add('d-none');
+ document.getElementById(`node-view-${nodeId}`).classList.remove('d-none');
+ }
+
+ function saveNode(nodeId, event) {
+ const title = document.getElementById(`edit-node-title-${nodeId}`).value;
+ const category = document.getElementById(`edit-node-category-${nodeId}`).value;
+ const summary = document.getElementById(`edit-node-summary-${nodeId}`).value;
+
+ const saveBtn = event.target;
+ saveBtn.disabled = true;
+ saveBtn.innerText = 'Saving...';
+
+ let csrfToken = '';
+ const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
+ if (csrfInput) {
+ csrfToken = csrfInput.value;
+ } else {
+ // fallback, get from form if present elsewhere
+ console.warn("CSRF token not found via input name. Ensure it exists on the page.");
+ }
+
+ const url = "{% url 'edit_node' project.pk 0 %}".replace('/0/', '/' + nodeId + '/');
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({ title: title, category: category, summary: summary })
+ })
+ .then(response => response.json())
+ .then(res => {
+ if (res.status === 'success') {
+ const node = res.node;
+ const emoji = getNodeEmoji(node.category);
+ const color = getNodeColor(node.category);
+ data.nodes.update({
+ id: node.id,
+ label: emoji + " " + node.title,
+ title: `${node.title}
${node.summary}
Category: ${node.category}`,
+ summary: node.summary,
+ category: node.category,
+ color: color
+ });
+
+ // re-render details
+ network.emit('click', { nodes: [nodeId], edges: [] });
+ } else {
+ alert('Error saving node: ' + res.message);
+ saveBtn.disabled = false;
+ saveBtn.innerText = 'Save';
+ }
+ })
+ .catch(err => {
+ alert('Error saving node');
+ saveBtn.disabled = false;
+ saveBtn.innerText = 'Save';
+ });
+ }
+"""
+
+html = html.replace(' // AI Chat Handler', new_scripts + '\n // AI Chat Handler')
+
+with open("core/templates/core/project_detail.html", "w") as f:
+ f.write(html)
diff --git a/update_urls.py b/update_urls.py
new file mode 100644
index 0000000..6059b08
--- /dev/null
+++ b/update_urls.py
@@ -0,0 +1,19 @@
+import os
+
+with open("core/urls.py", "r") as f:
+ content = f.read()
+
+content = content.replace(
+ 'from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap',
+ 'from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap, edit_node'
+)
+
+new_path = " path('projects//node//edit/', edit_node, name='edit_node'),\n"
+content = content.replace(
+ ' path("projects//ai/", ai_chat, name="ai_chat"),',
+ ' path("projects//ai/", ai_chat, name="ai_chat"),\n' + new_path
+)
+
+with open("core/urls.py", "w") as f:
+ f.write(content)
+
diff --git a/update_views.py b/update_views.py
new file mode 100644
index 0000000..9198000
--- /dev/null
+++ b/update_views.py
@@ -0,0 +1,37 @@
+import os
+
+with open("core/views.py", "a") as f:
+ f.write("""
+import json
+from django.http import JsonResponse
+from django.views.decorators.http import require_POST
+
+@login_required
+@require_POST
+def edit_node(request, pk, node_id):
+ project = get_object_or_404(Project, pk=pk, user=request.user)
+ try:
+ node = project.nodes.get(pk=node_id)
+ data = json.loads(request.body)
+ title = data.get('title', '').strip()
+ summary = data.get('summary', '').strip()
+ category = data.get('category', '').strip()
+
+ if title:
+ node.title = title
+ if summary:
+ node.summary = summary
+ if category:
+ node.category = category
+
+ node.save()
+ return JsonResponse({'status': 'success', 'node': {
+ 'id': node.pk,
+ 'title': node.title,
+ 'summary': node.summary,
+ 'category': node.category
+ }})
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
+""")
+print("Updated views.py!")