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!")