Auto commit: 2026-03-12T14:15:33.094Z

This commit is contained in:
Flatlogic Bot 2026-03-12 14:15:33 +00:00
parent ffe8cee686
commit 33a9c38573
10 changed files with 538 additions and 67 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

60
core/ai_helpers.py Normal file
View File

@ -0,0 +1,60 @@
from ai.local_ai_api import LocalAIApi
from .models import MindMapNode, MindMapConnection
import json
def generate_initial_mindmap(project):
prompt = f"""
You are an expert business consultant. Create an initial mind map for a new project.
Project Title: {project.title}
Industry: {project.industry}
Goal: {project.goal}
Respond ONLY with a valid JSON object in the following format:
{{
"nodes": [
{{"id": "node_1", "title": "Main Goal", "summary": "Short description", "category": "Strategy"}},
{{"id": "node_2", "title": "Feature X", "summary": "Short description", "category": "Product"}}
],
"connections": [
{{"source_id": "node_1", "target_id": "node_2", "how": "Defines what to build", "why": "Feature X is critical to achieve the Main Goal"}}
]
}}
Create 6-10 interconnected nodes exploring key business areas like Target Audience, Core Features, Marketing Strategy, Revenue Streams, etc. Ensure the IDs in connections match the nodes.
"""
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": "You are a helpful business strategy AI. You must respond in valid JSON matching the exact requested format."},
{"role": "user", "content": prompt}
],
"response_format": {"type": "json_object"}
})
if response.get("success"):
data = LocalAIApi.decode_json_from_response(response)
if not data:
return False
# Parse and save to DB
node_map = {}
for n in data.get("nodes", []):
node = MindMapNode.objects.create(
project=project,
title=n.get("title", "Untitled"),
summary=n.get("summary", ""),
category=n.get("category", "General")
)
node_map[n.get("id")] = node
for c in data.get("connections", []):
source_id = c.get("source_id")
target_id = c.get("target_id")
if source_id in node_map and target_id in node_map:
MindMapConnection.objects.create(
project=project,
source=node_map[source_id],
target=node_map[target_id],
how=c.get("how", ""),
why=c.get("why", "")
)
return True
return False

View File

@ -1,18 +1,145 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from .models import Project, MindMapNode, MindMapConnection
from ai.local_ai_api import LocalAIApi
from django.shortcuts import get_object_or_404
import json import json
@csrf_exempt @csrf_exempt
@login_required @login_required
def ai_chat(request, pk): def ai_chat(request, pk):
if request.method == 'POST': if request.method == 'POST':
data = json.loads(request.body) project = get_object_or_404(Project, pk=pk, user=request.user)
user_message = data.get('message')
# In a real app, this would call your AI service (e.g., GPT, Gemini) try:
# Here we mock the AI response data = json.loads(request.body)
ai_response = f"AI thinking about: {user_message}. Based on my analysis, you should consider..." user_message = data.get('message')
return JsonResponse({'response': ai_response}) # Context builder for the project
return JsonResponse({'error': 'Invalid request'}, status=400) nodes = list(project.nodes.values('id', 'title', 'category'))
nodes_context = json.dumps(nodes)
system_prompt = f"""You are an AI business strategist helping a user build a mind map for their project.
Project Title: {project.title}
Industry: {project.industry}
Goal: {project.goal}
Current Nodes (with IDs):
{nodes_context}
Respond ONLY with a valid JSON object in the following format:
{{
"message": "Your text response to the user's prompt goes here.",
"new_nodes": [
{{"id": "temp_1", "title": "New Idea", "summary": "Short description", "category": "Strategy"}}
],
"new_connections": [
{{"source_id": 1, "target_id": "temp_1", "how": "Relates to existing node", "why": "Important reason"}}
]
}}
Instructions:
- If the user just asks a question, put your answer in "message" and leave "new_nodes" and "new_connections" empty.
- If the user asks to add nodes, brainstorm nodes, or expand the map, generate them and place them in "new_nodes". Use temporary string IDs for new nodes (e.g., "temp_1").
- For connections, "source_id" and "target_id" can be an existing integer ID from the Current Nodes list OR a temporary string ID from "new_nodes".
- Keep your answers concise and practical.
"""
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
],
"response_format": {"type": "json_object"}
})
if response.get("success"):
ai_data = LocalAIApi.decode_json_from_response(response)
if not ai_data:
# fallback if it's not valid JSON
ai_text = LocalAIApi.extract_text(response)
return JsonResponse({'response': ai_text})
# Extract parts
ai_message = ai_data.get("message", "I have updated the mind map.")
new_nodes_data = ai_data.get("new_nodes", [])
new_connections_data = ai_data.get("new_connections", [])
added_nodes = []
added_connections = []
if new_nodes_data or new_connections_data:
# Save nodes
temp_to_real_id = {}
for n in new_nodes_data:
node = MindMapNode.objects.create(
project=project,
title=n.get("title", "Untitled"),
summary=n.get("summary", ""),
category=n.get("category", "General")
)
temp_to_real_id[n.get("id")] = node
added_nodes.append({
"id": node.pk,
"title": node.title,
"summary": node.summary,
"category": node.category
})
# Create dictionary to fetch existing nodes quickly by int ID
existing_nodes_map = {n.id: n for n in project.nodes.all()}
# Save connections
for c in new_connections_data:
source_ref = c.get("source_id")
target_ref = c.get("target_id")
source_node = None
target_node = None
# Resolve source
if isinstance(source_ref, int) and source_ref in existing_nodes_map:
source_node = existing_nodes_map[source_ref]
elif isinstance(source_ref, str) and str(source_ref).isdigit() and int(source_ref) in existing_nodes_map:
source_node = existing_nodes_map[int(source_ref)]
elif source_ref in temp_to_real_id:
source_node = temp_to_real_id[source_ref]
# Resolve target
if isinstance(target_ref, int) and target_ref in existing_nodes_map:
target_node = existing_nodes_map[target_ref]
elif isinstance(target_ref, str) and str(target_ref).isdigit() and int(target_ref) in existing_nodes_map:
target_node = existing_nodes_map[int(target_ref)]
elif target_ref in temp_to_real_id:
target_node = temp_to_real_id[target_ref]
if source_node and target_node:
conn = MindMapConnection.objects.create(
project=project,
source=source_node,
target=target_node,
how=c.get("how", ""),
why=c.get("why", "")
)
added_connections.append({
"id": conn.pk,
"source_id": source_node.pk,
"target_id": target_node.pk,
"how": conn.how,
"why": conn.why
})
return JsonResponse({
'response': ai_message,
'added_nodes': added_nodes,
'added_connections': added_connections
})
else:
return JsonResponse({'response': "Sorry, I had trouble processing that request. Please try again."})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid request body'}, status=400)
return JsonResponse({'error': 'Invalid request method'}, status=405)

View File

@ -1,22 +1,55 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-5">
<h1>Create New Project</h1> <div class="row justify-content-center">
<form method="post"> <div class="col-md-6">
{% csrf_token %} <div class="card shadow">
<div class="mb-3"> <div class="card-header bg-primary text-white">
<label for="title" class="form-label">Title</label> <h3 class="card-title mb-0">Start a New Project</h3>
<input type="text" class="form-control" id="title" name="title" required> </div>
<div class="card-body">
<form method="post" id="create-project-form">
{% csrf_token %}
<div class="mb-3">
<label for="title" class="form-label">Project Title</label>
<input type="text" class="form-control" id="title" name="title" required placeholder="e.g. NextGen CRM">
</div>
<div class="mb-3">
<label for="industry" class="form-label">Industry</label>
<input type="text" class="form-control" id="industry" name="industry" required placeholder="e.g. SaaS">
</div>
<div class="mb-4">
<label for="goal" class="form-label">Primary Goal</label>
<textarea class="form-control" id="goal" name="goal" rows="3" required placeholder="Describe what you want to achieve..."></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="submit-btn">
Generate Mind Map
<span class="spinner-border spinner-border-sm d-none ms-2" id="loading-spinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
<div class="card-footer text-muted text-center" id="loading-text" style="display: none;">
<small>Please wait while our AI analyzes your goal and builds the initial map (this may take up to 20 seconds)...</small>
</div>
</div>
</div> </div>
<div class="mb-3"> </div>
<label for="industry" class="form-label">Industry</label>
<input type="text" class="form-control" id="industry" name="industry" required>
</div>
<div class="mb-3">
<label for="goal" class="form-label">Goal</label>
<textarea class="form-control" id="goal" name="goal" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div> </div>
<script>
document.getElementById('create-project-form').addEventListener('submit', function() {
// Disable the button to prevent multiple submissions
const btn = document.getElementById('submit-btn');
setTimeout(() => { btn.disabled = true; }, 0);
// Show the spinner and loading text
document.getElementById('loading-spinner').classList.remove('d-none');
document.getElementById('loading-text').style.display = 'block';
// Let the form submit normally
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,49 +1,297 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container-fluid mt-4">
<h1>{{ project.title }}</h1> <div class="row mb-3">
<p>Industry: {{ project.industry }}</p> <div class="col-md-8">
<p>Goal: {{ project.goal }}</p> <h1 class="mb-0">{{ project.title }}</h1>
<p class="text-muted mb-0">{{ project.industry }} | <strong>Goal:</strong> {{ project.goal }}</p>
<h3>Mind Map Nodes</h3> </div>
<ul> <div class="col-md-4 text-end">
{% for node in nodes %} <a href="{% url 'project_list' %}" class="btn btn-outline-secondary">Back to Projects</a>
<li>{{ node.title }} - {{ node.category }}</li> </div>
{% endfor %}
</ul>
<h3>Connections</h3>
<ul>
{% for conn in connections %}
<li>{{ conn.source.title }} -> {{ conn.target.title }} (Why: {{ conn.why }})</li>
{% endfor %}
</ul>
<h3>Chat with AI</h3>
<div id="chat-container" class="mb-3">
<textarea id="ai-input" class="form-control" rows="2" placeholder="Ask AI..."></textarea>
<button id="ai-submit" class="btn btn-primary mt-2">Send</button>
</div> </div>
<div id="ai-response" class="card mt-2">
<div class="card-body">Waiting for input...</div> <div class="row">
<!-- Mind Map Visualization Column -->
<div class="col-md-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Interactive Mind Map</h5>
<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>
</div>
<div class="card-body p-0" style="height: 600px; position: relative;">
<!-- The vis-network container -->
<div id="mynetwork" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<!-- Sidebar for Node Details & AI Chat -->
<div class="col-md-4 d-flex flex-column">
<!-- Node Details Panel -->
<div class="card shadow-sm mb-3" style="flex: 1;">
<div class="card-header bg-white">
<h5 class="mb-0">Node Details</h5>
</div>
<div class="card-body" id="node-details">
<p class="text-muted">Click on a node or edge in the map to see its details here.</p>
</div>
</div>
<!-- AI Chat Panel -->
<div class="card shadow-sm" style="flex: 1;">
<div class="card-header bg-white">
<h5 class="mb-0">Ask AI Assistant</h5>
</div>
<div class="card-body d-flex flex-column">
<div id="ai-response" class="bg-light p-3 rounded mb-3 flex-grow-1" style="min-height: 100px; max-height: 200px; overflow-y: auto;">
<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">
<textarea id="ai-input" class="form-control mb-2" rows="2" placeholder="e.g. Add 3 more nodes related to Marketing..."></textarea>
<button id="ai-submit" class="btn btn-primary w-100">Send to AI</button>
</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: white;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
font-family: Arial, sans-serif;
font-size: 14px;
max-width: 300px;
pointer-events: none;
z-index: 10;
}
</style>
<script> <script>
document.getElementById('ai-submit').addEventListener('click', function() { // Colors for different categories
const message = document.getElementById('ai-input').value; const categoryColors = {
fetch("{% url 'ai_chat' project.pk %}", { 'Strategy': '#e1bee7', // Purple
method: 'POST', 'Product': '#bbdefb', // Blue
headers: { 'Market': '#c8e6c9', // Green
'Content-Type': 'application/json', 'Operations': '#ffcc80', // Orange
'X-CSRFToken': '{{ csrf_token }}' '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
}, },
body: JSON.stringify({ message: message }) {% endfor %}
}) ];
.then(response => response.json())
.then(data => { const edgesArray = [
document.getElementById('ai-response').querySelector('.card-body').innerText = data.response; {% 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' } });
}
// 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 = `
<h4 class="mb-2">${node.label}</h4>
<span class="badge bg-secondary mb-3">${node.category}</span>
<p><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 = `
<h5 class="mb-3">Connection Details</h5>
<p><strong>From:</strong> ${fromNode.label}</p>
<p><strong>To:</strong> ${toNode.label}</p>
<hr>
<p><strong>How:</strong><br>${edge.how || 'N/A'}</p>
<p><strong>Why:</strong><br>${edge.why || 'N/A'}</p>
`;
} else {
// Clicked empty space
detailsPanel.innerHTML = '<p class="text-muted">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"><strong>You:</strong> ${message}</div>`;
inputField.value = '';
// Show loading state
const loadingId = 'loading-' + Date.now();
responseDiv.innerHTML += `<div id="${loadingId}" class="mb-2 text-primary"><em>AI is thinking...</em></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"><strong>AI:</strong> ${apiData.response}</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-success small"><em><i class="bi bi-check-circle"></i> Successfully added new items to the map!</em></div>`;
}
responseDiv.scrollTop = responseDiv.scrollHeight;
})
.catch(err => {
document.getElementById(loadingId).remove();
responseDiv.innerHTML += `<div class="mb-2 text-danger"><strong>Error:</strong> Failed to get response from AI.</div>`;
console.error(err);
});
}); });
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login from django.contrib.auth import login
from .models import Project, MindMapNode, MindMapConnection from .models import Project, MindMapNode, MindMapConnection
from .ai_helpers import generate_initial_mindmap
def signup(request): def signup(request):
if request.method == 'POST': if request.method == 'POST':
@ -20,7 +21,7 @@ def home(request):
@login_required @login_required
def project_list(request): def project_list(request):
projects = Project.objects.filter(user=request.user) projects = Project.objects.filter(user=request.user).order_by('-created_at')
return render(request, 'core/project_list.html', {'projects': projects}) return render(request, 'core/project_list.html', {'projects': projects})
@login_required @login_required
@ -41,5 +42,7 @@ def create_project(request):
industry = request.POST.get('industry') industry = request.POST.get('industry')
goal = request.POST.get('goal') goal = request.POST.get('goal')
project = Project.objects.create(user=request.user, title=title, industry=industry, goal=goal) project = Project.objects.create(user=request.user, title=title, industry=industry, goal=goal)
# Automatically generate the first mind map based on the input
generate_initial_mindmap(project)
return redirect('project_detail', pk=project.pk) return redirect('project_detail', pk=project.pk)
return render(request, 'core/create_project.html') return render(request, 'core/create_project.html')