Auto commit: 2026-03-13T11:47:52.842Z

This commit is contained in:
Flatlogic Bot 2026-03-13 11:47:52 +00:00
parent 027f4dd35b
commit ba1bd529a8
9 changed files with 425 additions and 6 deletions

View File

@ -22,6 +22,15 @@
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger me-2">Regenerate</button>
</form>
<select id="layout-select" class="form-select form-select-sm d-inline-block w-auto me-2" onchange="changeLayout(this.value)">
<option value="organic">Organic Layout</option>
<option value="hierarchical">Hierarchical Layout</option>
</select>
<div class="btn-group btn-group-sm me-2" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="zoomIn()" title="Zoom In">+</button>
<button type="button" class="btn btn-outline-secondary" onclick="zoomOut()" title="Zoom Out">-</button>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>
</div>
</div>
@ -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: "<strong>{{ node.title|escapejs }}</strong><br>{{ node.summary|escapejs }}<br><em>Category: {{ node.category|escapejs }}</em>",
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 = `
<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>
<div id="node-view-${nodeId}">
<h6 class="mb-2 fw-bold text-dark">${cleanLabel}</h6>
<span class="badge bg-secondary mb-2 bg-opacity-75">${node.category}</span>
<p class="small mb-2 text-dark"><strong>Summary:</strong><br>${node.summary}</p>
<button class="btn btn-sm btn-outline-primary" onclick="editNodeUI(${nodeId})">Edit</button>
</div>
<div id="node-edit-${nodeId}" class="d-none">
<input type="text" id="edit-node-title-${nodeId}" class="form-control form-control-sm mb-2" value="${cleanLabel}">
<input type="text" id="edit-node-category-${nodeId}" class="form-control form-control-sm mb-2" value="${node.category}">
<textarea id="edit-node-summary-${nodeId}" class="form-control form-control-sm mb-2" rows="3">${node.summary}</textarea>
<button class="btn btn-sm btn-primary me-1" onclick="saveNode(${nodeId}, event)">Save</button>
<button class="btn btn-sm btn-outline-secondary" onclick="cancelEditNode(${nodeId})">Cancel</button>
</div>
`;
} 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: `<strong>${node.title}</strong><br>${node.summary}<br><em>Category: ${node.category}</em>`,
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');

View File

@ -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/<int:pk>/", project_detail, name="project_detail"),
path("projects/<int:pk>/regenerate/", regenerate_mindmap, name="regenerate_mindmap"),
path("projects/<int:pk>/ai/", ai_chat, name="ai_chat"),
path('projects/<int:pk>/node/<int:node_id>/edit/', edit_node, name='edit_node'),
path("signup/", signup, name="signup"),
]

View File

@ -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)
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)

106
update_html.py Normal file
View File

@ -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 = """
<select id="layout-select" class="form-select form-select-sm d-inline-block w-auto me-2" onchange="changeLayout(this.value)">
<option value="organic">Organic Layout</option>
<option value="hierarchical">Hierarchical Layout</option>
</select>
<div class="btn-group btn-group-sm me-2" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="zoomIn()" title="Zoom In">+</button>
<button type="button" class="btn btn-outline-secondary" onclick="zoomOut()" title="Zoom Out">-</button>
</div>"""
html = html.replace('<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>',
buttons_to_add + '\n <button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>')
# 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!")

114
update_js.py Normal file
View File

@ -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 = `
<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>
`;
"""
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 = `
<div id="node-view-${nodeId}">
<h6 class="mb-2 fw-bold text-dark">${cleanLabel}</h6>
<span class="badge bg-secondary mb-2 bg-opacity-75">${node.category}</span>
<p class="small mb-2 text-dark"><strong>Summary:</strong><br>${node.summary}</p>
<button class="btn btn-sm btn-outline-primary" onclick="editNodeUI(${nodeId})">Edit</button>
</div>
<div id="node-edit-${nodeId}" class="d-none">
<input type="text" id="edit-node-title-${nodeId}" class="form-control form-control-sm mb-2" value="${cleanLabel}">
<input type="text" id="edit-node-category-${nodeId}" class="form-control form-control-sm mb-2" value="${node.category}">
<textarea id="edit-node-summary-${nodeId}" class="form-control form-control-sm mb-2" rows="3">${node.summary}</textarea>
<button class="btn btn-sm btn-primary me-1" onclick="saveNode(${nodeId}, event)">Save</button>
<button class="btn btn-sm btn-outline-secondary" onclick="cancelEditNode(${nodeId})">Cancel</button>
</div>
`;
"""
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: `<strong>${node.title}</strong><br>${node.summary}<br><em>Category: ${node.category}</em>`,
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)

19
update_urls.py Normal file
View File

@ -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/<int:pk>/node/<int:node_id>/edit/', edit_node, name='edit_node'),\n"
content = content.replace(
' path("projects/<int:pk>/ai/", ai_chat, name="ai_chat"),',
' path("projects/<int:pk>/ai/", ai_chat, name="ai_chat"),\n' + new_path
)
with open("core/urls.py", "w") as f:
f.write(content)

37
update_views.py Normal file
View File

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