Auto commit: 2026-03-12T14:15:33.094Z
This commit is contained in:
parent
ffe8cee686
commit
33a9c38573
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/ai_helpers.cpython-311.pyc
Normal file
BIN
core/__pycache__/ai_helpers.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
60
core/ai_helpers.py
Normal file
60
core/ai_helpers.py
Normal 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
|
||||||
137
core/ai_views.py
137
core/ai_views.py
@ -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':
|
||||||
|
project = get_object_or_404(Project, pk=pk, user=request.user)
|
||||||
|
|
||||||
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
user_message = data.get('message')
|
user_message = data.get('message')
|
||||||
|
|
||||||
# In a real app, this would call your AI service (e.g., GPT, Gemini)
|
# Context builder for the project
|
||||||
# Here we mock the AI response
|
nodes = list(project.nodes.values('id', 'title', 'category'))
|
||||||
ai_response = f"AI thinking about: {user_message}. Based on my analysis, you should consider..."
|
nodes_context = json.dumps(nodes)
|
||||||
|
|
||||||
return JsonResponse({'response': ai_response})
|
system_prompt = f"""You are an AI business strategist helping a user build a mind map for their project.
|
||||||
return JsonResponse({'error': 'Invalid request'}, status=400)
|
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)
|
||||||
@ -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">
|
||||||
|
<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 %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="title" class="form-label">Title</label>
|
<label for="title" class="form-label">Project Title</label>
|
||||||
<input type="text" class="form-control" id="title" name="title" required>
|
<input type="text" class="form-control" id="title" name="title" required placeholder="e.g. NextGen CRM">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="industry" class="form-label">Industry</label>
|
<label for="industry" class="form-label">Industry</label>
|
||||||
<input type="text" class="form-control" id="industry" name="industry" required>
|
<input type="text" class="form-control" id="industry" name="industry" required placeholder="e.g. SaaS">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<label for="goal" class="form-label">Goal</label>
|
<label for="goal" class="form-label">Primary Goal</label>
|
||||||
<textarea class="form-control" id="goal" name="goal" rows="3" required></textarea>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Create</button>
|
|
||||||
</form>
|
</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>
|
</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 %}
|
||||||
@ -1,37 +1,230 @@
|
|||||||
{% 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 %}
|
</div>
|
||||||
</ul>
|
|
||||||
|
<div class="row">
|
||||||
<h3>Connections</h3>
|
<!-- Mind Map Visualization Column -->
|
||||||
<ul>
|
<div class="col-md-8">
|
||||||
{% for conn in connections %}
|
<div class="card shadow-sm h-100">
|
||||||
<li>{{ conn.source.title }} -> {{ conn.target.title }} (Why: {{ conn.why }})</li>
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
{% endfor %}
|
<h5 class="mb-0">Interactive Mind Map</h5>
|
||||||
</ul>
|
<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>
|
||||||
|
</div>
|
||||||
<h3>Chat with AI</h3>
|
<div class="card-body p-0" style="height: 600px; position: relative;">
|
||||||
<div id="chat-container" class="mb-3">
|
<!-- The vis-network container -->
|
||||||
<textarea id="ai-input" class="form-control" rows="2" placeholder="Ask AI..."></textarea>
|
<div id="mynetwork" style="width: 100%; height: 100%;"></div>
|
||||||
<button id="ai-submit" class="btn btn-primary mt-2">Send</button>
|
</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 id="ai-response" class="card mt-2">
|
|
||||||
<div class="card-body">Waiting for input...</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 = {
|
||||||
|
'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
|
||||||
|
},
|
||||||
|
{% 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 %}", {
|
fetch("{% url 'ai_chat' project.pk %}", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -41,9 +234,64 @@ document.getElementById('ai-submit').addEventListener('click', function() {
|
|||||||
body: JSON.stringify({ message: message })
|
body: JSON.stringify({ message: message })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(apiData => {
|
||||||
document.getElementById('ai-response').querySelector('.card-body').innerText = data.response;
|
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 %}
|
||||||
@ -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')
|
||||||
Loading…
x
Reference in New Issue
Block a user