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.views.decorators.csrf import csrf_exempt
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
@csrf_exempt
@login_required
def ai_chat(request, pk):
if request.method == 'POST':
data = json.loads(request.body)
user_message = data.get('message')
project = get_object_or_404(Project, pk=pk, user=request.user)
# In a real app, this would call your AI service (e.g., GPT, Gemini)
# Here we mock the AI response
ai_response = f"AI thinking about: {user_message}. Based on my analysis, you should consider..."
return JsonResponse({'response': ai_response})
return JsonResponse({'error': 'Invalid request'}, status=400)
try:
data = json.loads(request.body)
user_message = data.get('message')
# Context builder for the project
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' %}
{% block content %}
<div class="container mt-4">
<h1>Create New Project</h1>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="card-title mb-0">Start a New Project</h3>
</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 class="mb-3">
<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>
{% endblock %}
<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 %}

View File

@ -1,49 +1,297 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
<h1>{{ project.title }}</h1>
<p>Industry: {{ project.industry }}</p>
<p>Goal: {{ project.goal }}</p>
<h3>Mind Map Nodes</h3>
<ul>
{% for node in nodes %}
<li>{{ node.title }} - {{ node.category }}</li>
{% 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 class="container-fluid mt-4">
<div class="row mb-3">
<div class="col-md-8">
<h1 class="mb-0">{{ project.title }}</h1>
<p class="text-muted mb-0">{{ project.industry }} | <strong>Goal:</strong> {{ project.goal }}</p>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'project_list' %}" class="btn btn-outline-secondary">Back to Projects</a>
</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>
<!-- 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>
document.getElementById('ai-submit').addEventListener('click', function() {
const message = document.getElementById('ai-input').value;
fetch("{% url 'ai_chat' project.pk %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
// Colors for different categories
const categoryColors = {
'Strategy': '#e1bee7', // Purple
'Product': '#bbdefb', // Blue
'Market': '#c8e6c9', // Green
'Operations': '#ffcc80', // Orange
'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 })
})
.then(response => response.json())
.then(data => {
document.getElementById('ai-response').querySelector('.card-body').innerText = data.response;
{% endfor %}
];
const edgesArray = [
{% 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>
{% 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 import login
from .models import Project, MindMapNode, MindMapConnection
from .ai_helpers import generate_initial_mindmap
def signup(request):
if request.method == 'POST':
@ -20,7 +21,7 @@ def home(request):
@login_required
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})
@login_required
@ -41,5 +42,7 @@ def create_project(request):
industry = request.POST.get('industry')
goal = request.POST.get('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 render(request, 'core/create_project.html')
return render(request, 'core/create_project.html')