Auto commit: 2026-03-13T11:47:52.842Z
This commit is contained in:
parent
027f4dd35b
commit
ba1bd529a8
Binary file not shown.
Binary file not shown.
@ -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');
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
@ -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
106
update_html.py
Normal 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
114
update_js.py
Normal 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
19
update_urls.py
Normal 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
37
update_views.py
Normal 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!")
|
||||
Loading…
x
Reference in New Issue
Block a user