Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -345,7 +345,6 @@ def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[st
|
||||
Shared HTTP helper for GET/POST requests.
|
||||
"""
|
||||
req = urlrequest.Request(url, data=body, method=method.upper())
|
||||
req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Flatlogic/1.0")
|
||||
for name, value in headers.items():
|
||||
req.add_header(name, value)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 503 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 936 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@ -23,12 +23,12 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||
ALLOWED_HOSTS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
os.getenv("HOST_FQDN", ""), "aibmm.flatlogic.app",
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
origin for origin in [
|
||||
os.getenv("HOST_FQDN", ""), "aibmm.flatlogic.app",
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||
] if origin
|
||||
]
|
||||
@ -76,7 +76,7 @@ ROOT_URLCONF = 'config.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'core' / 'templates'],
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -180,6 +180,3 @@ if EMAIL_USE_SSL:
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
LOGIN_REDIRECT_URL = '/projects/'
|
||||
LOGOUT_REDIRECT_URL = '/'
|
||||
@ -21,10 +21,9 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
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}
|
||||
],
|
||||
"text": {"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
|
||||
145
core/ai_views.py
@ -1,145 +0,0 @@
|
||||
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':
|
||||
project = get_object_or_404(Project, pk=pk, user=request.user)
|
||||
|
||||
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}
|
||||
],
|
||||
"text": {"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,52 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2026-03-12 07:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('industry', models.CharField(max_length=100)),
|
||||
('goal', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MindMapNode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('summary', models.TextField()),
|
||||
('category', models.CharField(max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='nodes', to='core.project')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MindMapConnection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('how', models.TextField(help_text='How they connect')),
|
||||
('why', models.TextField(help_text='Why they connect')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outgoing_connections', to='core.mindmapnode')),
|
||||
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_connections', to='core.mindmapnode')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections', to='core.project')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,34 +1,3 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class Project(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=200)
|
||||
industry = models.CharField(max_length=100)
|
||||
goal = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class MindMapNode(models.Model):
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='nodes')
|
||||
title = models.CharField(max_length=200)
|
||||
summary = models.TextField()
|
||||
category = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class MindMapConnection(models.Model):
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='connections')
|
||||
source = models.ForeignKey(MindMapNode, on_delete=models.CASCADE, related_name='outgoing_connections')
|
||||
target = models.ForeignKey(MindMapNode, on_delete=models.CASCADE, related_name='incoming_connections')
|
||||
how = models.TextField(help_text="How they connect")
|
||||
why = models.TextField(help_text="Why they connect")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.source.title} -> {self.target.title}"
|
||||
# Create your models here.
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page d-flex align-items-center justify-content-center min-vh-100 position-relative overflow-hidden">
|
||||
<!-- Decorative 3D-like shapes -->
|
||||
<div class="shape shape-sphere top-left"></div>
|
||||
<div class="shape shape-cube bottom-right"></div>
|
||||
|
||||
<div class="container py-5 z-1">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card glass-card border-0 text-center p-4 p-md-5">
|
||||
<!-- Admin specific title -->
|
||||
<h2 class="mb-4 fw-bold text-dark">
|
||||
{% if site_header %}{{ site_header }}{% else %}Admin Login{% endif %}
|
||||
</h2>
|
||||
|
||||
{% if form.errors and not form.non_field_errors %}
|
||||
<p class="errornote text-danger small mb-3">
|
||||
{% if form.errors.items|length == 1 %}Please correct the error below.{% else %}Please correct the errors below.{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="errornote text-danger small mb-3">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<p class="errornote text-danger small mb-3">
|
||||
You are authenticated as {{ username }}, but are not authorized to access this page. Would you like to login to a different account?
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="login-form" class="auth-form text-start">
|
||||
{% csrf_token %}
|
||||
<div class="form-row mb-3">
|
||||
{{ form.username.errors }}
|
||||
<label for="id_username" class="form-label text-muted small fw-bold text-uppercase">{{ form.username.label }}</label>
|
||||
{{ form.username }}
|
||||
</div>
|
||||
<div class="form-row mb-3">
|
||||
{{ form.password.errors }}
|
||||
<label for="id_password" class="form-label text-muted small fw-bold text-uppercase">{{ form.password.label }}</label>
|
||||
{{ form.password }}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</div>
|
||||
|
||||
<div class="submit-row mt-4">
|
||||
<button type="submit" class="btn btn-dark w-100 py-3 btn-glass fw-bold shadow-sm">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<p class="mb-0 text-muted small"><a href="{% url 'home' %}" class="fw-bold text-secondary text-decoration-none">← Back to Site</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
@ -14,33 +13,13 @@
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% if user.is_authenticated %}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'project_list' %}">AI Business Planning</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="navbar-text me-3">Welcome, {{ user.username }}</span>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
</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 %}
|
||||
@ -1,22 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-danger">
|
||||
<div class="card-body text-center">
|
||||
<h2 class="card-title text-danger mb-4">Delete Project?</h2>
|
||||
<p class="lead mb-4">Are you sure you want to delete the project <strong>{{ project.title }}</strong>? This action cannot be undone.</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="{% url 'project_list' %}" class="btn btn-secondary px-4">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger px-4">Yes, Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,235 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AI Business Planning & Mind Map{% endblock %}
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Sora:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #0F766E;
|
||||
--primary-light: #38BDF8;
|
||||
--secondary: #0B132B;
|
||||
--accent: #F59E0B;
|
||||
--bg-color: #F8FAFC;
|
||||
--text-color: #334155;
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
padding: 8rem 2rem 6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100px;
|
||||
background: var(--bg-color);
|
||||
transform: skewY(-2deg);
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 1.5rem;
|
||||
letter-spacing: -0.03em;
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2.5rem;
|
||||
opacity: 0.9;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
font-weight: 600;
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.6);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid white;
|
||||
color: white;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: white;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.features {
|
||||
padding: 6rem 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 3rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
padding: 2.5rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
color: var(--primary);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-family: 'Sora', sans-serif;
|
||||
color: var(--secondary);
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 3D decorative shapes */
|
||||
.shape {
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
bottom: 20%;
|
||||
right: 15%;
|
||||
border-radius: 20px;
|
||||
transform: rotate(45deg);
|
||||
animation-delay: 2s;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(10deg); }
|
||||
100% { transform: translateY(0px) rotate(0deg); }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
|
||||
<div class="hero-content">
|
||||
<h1>Build Businesses from Scratch with AI</h1>
|
||||
<p>Your intelligent workspace for planning, validating, and pitching business ideas. Create dynamic mind maps that research, reason, and write for you.</p>
|
||||
|
||||
<div class="cta-group">
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'project_list' %}" class="btn btn-primary">Go to My Projects</a>
|
||||
{% else %}
|
||||
<a href="{% url 'project_list' %}" class="btn btn-primary">Start a Project</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🧠</div>
|
||||
<h3>Living Mind Maps</h3>
|
||||
<p>Connect your ideas with AI-powered 'how' and 'why' reasoning. Watch your initial concepts grow into comprehensive business models.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📚</div>
|
||||
<h3>Expert Research Layer</h3>
|
||||
<p>Upload articles, URLs, and notes. The AI learns from your specific sources to become an expert in your precise field and niche.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">💼</div>
|
||||
<h3>Investor-Ready Docs</h3>
|
||||
<p>Automatically generate polished business plans, VC pitch decks, and vendor-finance proposals directly from your expanded mind map.</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
@ -1,502 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="container-fluid d-flex flex-column pt-4" style="height: 100vh; max-height: 100vh; overflow: hidden;">
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<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 class="row flex-grow-1 pb-3" style="min-height: 0;">
|
||||
<div class="col-12 h-100">
|
||||
<div class="card shadow-sm h-100 w-100 d-flex flex-column" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08);">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center flex-shrink-0" style="border-bottom: 1px solid rgba(0,0,0,0.05);">
|
||||
<h5 class="mb-0 fw-bold">Interactive Mind Map</h5>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-success me-2" onclick="exportMap()">Export to Image</button>
|
||||
<form method="POST" action="{% url 'regenerate_mindmap' project.pk %}" class="d-inline" onsubmit="return confirm('Are you sure you want to regenerate the entire map? This will delete all current nodes.');">
|
||||
{% 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>
|
||||
|
||||
<div class="card-body p-0 position-relative flex-grow-1" style="min-height: 0; overflow: hidden; background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%);">
|
||||
{% if not nodes %}
|
||||
<div class="position-absolute w-100 h-100 d-flex flex-column justify-content-center align-items-center" style="z-index: 10; background: rgba(255,255,255,0.9);">
|
||||
<h4 class="text-muted mb-3">The mind map is empty</h4>
|
||||
<p class="text-muted mb-4">Something went wrong during generation, or the map was cleared.</p>
|
||||
<form method="POST" action="{% url 'regenerate_mindmap' project.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary px-4 py-2 shadow-sm rounded-pill">
|
||||
Generate Map Now
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- The vis-network container -->
|
||||
<div id="mynetwork" style="width: 100%; height: 100%;"></div>
|
||||
|
||||
<!-- Floating Panels in Top Right -->
|
||||
<div class="position-absolute d-flex flex-column" style="top: 15px; right: 15px; width: 350px; z-index: 1000; bottom: 15px; pointer-events: none;">
|
||||
|
||||
<!-- Node Details Panel -->
|
||||
<div class="card shadow mb-3" style="pointer-events: auto; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.4); border-radius: 12px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);">
|
||||
<div class="card-header py-2" style="background: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); border-radius: 12px 12px 0 0;">
|
||||
<h6 class="mb-0 fw-bold text-primary">Node Details</h6>
|
||||
</div>
|
||||
<div class="card-body py-2" id="node-details" style="max-height: 200px; overflow-y: auto;">
|
||||
<p class="text-muted small mb-0">Click on a node or edge in the map to see its details here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Chat Panel -->
|
||||
<div class="card shadow d-flex flex-column" style="pointer-events: auto; flex: 1; min-height: 0; border: 1px solid rgba(255,255,255,0.4); border-radius: 12px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);">
|
||||
<div class="card-header py-2" style="background: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); border-radius: 12px 12px 0 0;">
|
||||
<h6 class="mb-0 fw-bold text-success">Ask AI Assistant</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column p-2" style="min-height: 0;">
|
||||
<div id="ai-response" class="p-2 rounded mb-2 flex-grow-1" style="overflow-y: auto; font-size: 0.9rem; background: rgba(255,255,255,0.6); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);">
|
||||
<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 flex-shrink-0">
|
||||
<textarea id="ai-input" class="form-control form-control-sm mb-2" rows="2" style="resize: none; border-radius: 8px; background: rgba(255,255,255,0.9);" placeholder="e.g. Add 3 more nodes related to Marketing..."></textarea>
|
||||
<button id="ai-submit" class="btn btn-primary btn-sm w-100 rounded-pill shadow-sm">Send to AI</button>
|
||||
</div>
|
||||
</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: rgba(255, 255, 255, 0.95);
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
max-width: 320px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Optional: custom scrollbar for details and chat */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0,0,0,0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Colors for different categories
|
||||
const categoryColors = {
|
||||
'Strategy': '#e1bee7', // Purple
|
||||
'Product': '#bbdefb', // Blue
|
||||
'Market': '#c8e6c9', // Green
|
||||
'Operations': '#ffcc80', // Orange
|
||||
'Finance': '#ffcdd2', // Red
|
||||
'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
|
||||
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: 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 }}",
|
||||
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);
|
||||
|
||||
|
||||
// Change Layout function
|
||||
function changeLayout(value) {
|
||||
if (value === 'hierarchical') {
|
||||
network.setOptions({
|
||||
layout: {
|
||||
improvedLayout: true,
|
||||
hierarchical: {
|
||||
direction: 'UD',
|
||||
sortMethod: 'directed'
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
network.setOptions({
|
||||
layout: {
|
||||
improvedLayout: true,
|
||||
hierarchical: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom In function
|
||||
function zoomIn() {
|
||||
var scale = network.getScale();
|
||||
network.moveTo({ scale: scale * 1.5 });
|
||||
}
|
||||
|
||||
// Zoom Out function
|
||||
function zoomOut() {
|
||||
var scale = network.getScale();
|
||||
network.moveTo({ scale: scale / 1.5 });
|
||||
}
|
||||
|
||||
function fitNetwork() {
|
||||
network.fit({ animation: { duration: 1000, easingFunction: 'easeInOutQuad' } });
|
||||
}
|
||||
|
||||
// Export Map function
|
||||
function exportMap() {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const image = canvas.toDataURL("image/png");
|
||||
const link = document.createElement('a');
|
||||
link.download = 'mindmap.png';
|
||||
link.href = image;
|
||||
link.click();
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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>
|
||||
`;
|
||||
} 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 = `
|
||||
<h6 class="mb-2 fw-bold text-dark">Connection Details</h6>
|
||||
<p class="small mb-1 text-dark"><strong>From:</strong> ${fromNode.label}</p>
|
||||
<p class="small mb-2 text-dark"><strong>To:</strong> ${toNode.label}</p>
|
||||
<div class="small text-dark border-top pt-2">
|
||||
<p class="mb-1"><strong>How:</strong> ${edge.how || 'N/A'}</p>
|
||||
<p class="mb-0"><strong>Why:</strong> ${edge.why || 'N/A'}</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Clicked empty space
|
||||
detailsPanel.innerHTML = '<p class="text-muted small mb-0">Click on a node or edge in the map to see its details here.</p>';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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');
|
||||
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"><span class="badge bg-primary rounded-pill px-3 py-2 text-wrap text-start d-inline-block shadow-sm" style="max-width: 85%; font-weight: normal; font-size: 0.9rem;">${message}</span></div>`;
|
||||
inputField.value = '';
|
||||
|
||||
// Show loading state
|
||||
const loadingId = 'loading-' + Date.now();
|
||||
responseDiv.innerHTML += `<div id="${loadingId}" class="mb-2 text-start"><span class="badge bg-white text-primary border border-primary border-opacity-25 rounded-pill px-3 py-2 shadow-sm d-inline-block" style="font-weight: normal; font-size: 0.9rem;"><em><span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Thinking...</em></span></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 text-start"><span class="badge bg-white text-dark rounded-4 px-3 py-2 text-wrap text-start d-inline-block shadow-sm" style="max-width: 90%; font-weight: normal; font-size: 0.9rem; border: 1px solid rgba(0,0,0,0.05);">${apiData.response}</span></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-center"><small class="text-success"><i class="bi bi-check-circle-fill"></i> New items added to map!</small></div>`;
|
||||
}
|
||||
|
||||
responseDiv.scrollTop = responseDiv.scrollHeight;
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById(loadingId).remove();
|
||||
responseDiv.innerHTML += `<div class="mb-2 text-start"><span class="badge bg-danger text-white rounded-pill px-3 py-2 shadow-sm d-inline-block" style="font-weight: normal;">Error: Failed to get response.</span></div>`;
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Allow submitting AI input with Enter key
|
||||
document.getElementById('ai-input').addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
document.getElementById('ai-submit').click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,23 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>My Projects</h1>
|
||||
<a href="{% url 'create_project' %}" class="btn btn-primary mb-3">New Project</a>
|
||||
<div class="row">
|
||||
{% for project in projects %}
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ project.title }}</h5>
|
||||
<p class="card-text">{{ project.industry }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'project_detail' project.pk %}" class="btn btn-secondary">Open</a>
|
||||
<a href="{% url 'delete_project' project.pk %}" class="btn btn-outline-danger btn-sm">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,40 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page d-flex align-items-center justify-content-center min-vh-100 position-relative overflow-hidden">
|
||||
<!-- Decorative 3D-like shapes -->
|
||||
<div class="shape shape-sphere top-left"></div>
|
||||
<div class="shape shape-cube bottom-right"></div>
|
||||
|
||||
<div class="container py-5 z-1">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card glass-card border-0 text-center p-4 p-md-5">
|
||||
<h2 class="mb-4 fw-bold text-dark">Welcome Back</h2>
|
||||
<form method="post" class="auth-form text-start">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label text-muted small fw-bold text-uppercase">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text small text-muted">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 py-3 mt-4 btn-glass fw-bold shadow-sm">Log In</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<p class="mb-0 text-muted small">New here? <a href="{% url 'signup' %}" class="fw-bold text-primary text-decoration-none">Create an account</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,41 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page d-flex align-items-center justify-content-center min-vh-100 position-relative overflow-hidden">
|
||||
<!-- Decorative 3D-like shapes -->
|
||||
<div class="shape shape-cylinder top-right"></div>
|
||||
<div class="shape shape-sphere bottom-left"></div>
|
||||
|
||||
<div class="container py-5 z-1">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card glass-card border-0 text-center p-4 p-md-5">
|
||||
<h2 class="mb-4 fw-bold text-dark">Join Us</h2>
|
||||
<p class="text-muted small mb-4">Start your journey today and explore the possibilities.</p>
|
||||
<form method="post" class="auth-form text-start">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label text-muted small fw-bold text-uppercase">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text small text-muted">{{ field.help_text|safe }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 py-3 mt-4 btn-glass fw-bold shadow-sm">Sign Up</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<p class="mb-0 text-muted small">Already registered? <a href="{% url 'login' %}" class="fw-bold text-primary text-decoration-none">Log in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
15
core/urls.py
@ -1,16 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap, edit_node, delete_project
|
||||
from .ai_views import ai_chat
|
||||
|
||||
from .views import home
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("projects/", project_list, name="project_list"),
|
||||
path("projects/new/", create_project, name="create_project"),
|
||||
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('projects/<int:pk>/delete/', delete_project, name='delete_project'),
|
||||
|
||||
path("signup/", signup, name="signup"),
|
||||
]
|
||||
]
|
||||
|
||||
114
core/views.py
@ -1,97 +1,25 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
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
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
def signup(request):
|
||||
if request.method == 'POST':
|
||||
form = UserCreationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect('project_list')
|
||||
else:
|
||||
form = UserCreationForm()
|
||||
return render(request, 'registration/signup.html', {'form': form})
|
||||
|
||||
def home(request):
|
||||
return render(request, 'core/index.html')
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
|
||||
@login_required
|
||||
def project_list(request):
|
||||
projects = Project.objects.filter(user=request.user).order_by('-created_at')
|
||||
return render(request, 'core/project_list.html', {'projects': projects})
|
||||
|
||||
@login_required
|
||||
def project_detail(request, pk):
|
||||
project = get_object_or_404(Project, pk=pk, user=request.user)
|
||||
nodes = project.nodes.all()
|
||||
connections = project.connections.all()
|
||||
return render(request, 'core/project_detail.html', {
|
||||
'project': project,
|
||||
'nodes': nodes,
|
||||
'connections': connections
|
||||
})
|
||||
|
||||
@login_required
|
||||
def create_project(request):
|
||||
if request.method == 'POST':
|
||||
title = request.POST.get('title')
|
||||
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')
|
||||
|
||||
@login_required
|
||||
def regenerate_mindmap(request, pk):
|
||||
project = get_object_or_404(Project, pk=pk, user=request.user)
|
||||
if request.method == 'POST':
|
||||
project.nodes.all().delete()
|
||||
project.connections.all().delete()
|
||||
generate_initial_mindmap(project)
|
||||
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)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_project(request, pk):
|
||||
project = get_object_or_404(Project, pk=pk, user=request.user)
|
||||
if request.method == 'POST':
|
||||
project.delete()
|
||||
return redirect('project_list')
|
||||
return render(request, 'core/delete_project_confirm.html', {'project': project})
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@ -1,123 +1,4 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: #f7f9fc;
|
||||
}
|
||||
|
||||
/* Glassmorphism Auth Page Styles */
|
||||
.auth-page {
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #ffffff 100%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.7) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5) !important;
|
||||
border-radius: 20px !important;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.05) !important;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.btn-glass {
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-glass:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(13, 110, 253, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Form Styling */
|
||||
.auth-form input[type="text"],
|
||||
.auth-form input[type="password"],
|
||||
.auth-form input[type="email"] {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.auth-form input[type="text"]:focus,
|
||||
.auth-form input[type="password"]:focus,
|
||||
.auth-form input[type="email"]:focus {
|
||||
background-color: #fff;
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.auth-form .form-label {
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Decorative 3D Shapes using CSS */
|
||||
.shape {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
opacity: 0.6;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-sphere {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, #a8c0ff, #3f2b96);
|
||||
filter: blur(4px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.shape-cube {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: linear-gradient(135deg, #fbc2eb 0%, #a6c1ee 100%);
|
||||
border-radius: 20px;
|
||||
transform: rotate(45deg);
|
||||
filter: blur(3px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.shape-cylinder {
|
||||
width: 100px;
|
||||
height: 300px;
|
||||
border-radius: 50px;
|
||||
background: linear-gradient(to right, #84fab0 0%, #8fd3f4 100%);
|
||||
transform: rotate(30deg);
|
||||
filter: blur(5px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Positioning */
|
||||
.top-left { top: -50px; left: -50px; animation-delay: 0s; }
|
||||
.bottom-right { bottom: -50px; right: -50px; animation-delay: 2s; }
|
||||
.top-right { top: 10%; right: -20px; animation-delay: 1s; }
|
||||
.bottom-left { bottom: 10%; left: -30px; animation-delay: 3s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(10deg); }
|
||||
100% { transform: translateY(0px) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes float-rotate {
|
||||
0% { transform: translateY(0px) rotate(45deg); }
|
||||
50% { transform: translateY(-25px) rotate(55deg); }
|
||||
100% { transform: translateY(0px) rotate(45deg); }
|
||||
}
|
||||
|
||||
.shape-cube.bottom-right {
|
||||
animation: float-rotate 8s ease-in-out infinite;
|
||||
}
|
||||
@ -1,123 +1,21 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: #f7f9fc;
|
||||
}
|
||||
|
||||
/* Glassmorphism Auth Page Styles */
|
||||
.auth-page {
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #ffffff 100%);
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.7) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5) !important;
|
||||
border-radius: 20px !important;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.05) !important;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.btn-glass {
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-glass:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(13, 110, 253, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Form Styling */
|
||||
.auth-form input[type="text"],
|
||||
.auth-form input[type="password"],
|
||||
.auth-form input[type="email"] {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.auth-form input[type="text"]:focus,
|
||||
.auth-form input[type="password"]:focus,
|
||||
.auth-form input[type="email"]:focus {
|
||||
background-color: #fff;
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.auth-form .form-label {
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Decorative 3D Shapes using CSS */
|
||||
.shape {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
opacity: 0.6;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-sphere {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, #a8c0ff, #3f2b96);
|
||||
filter: blur(4px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.shape-cube {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: linear-gradient(135deg, #fbc2eb 0%, #a6c1ee 100%);
|
||||
border-radius: 20px;
|
||||
transform: rotate(45deg);
|
||||
filter: blur(3px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.shape-cylinder {
|
||||
width: 100px;
|
||||
height: 300px;
|
||||
border-radius: 50px;
|
||||
background: linear-gradient(to right, #84fab0 0%, #8fd3f4 100%);
|
||||
transform: rotate(30deg);
|
||||
filter: blur(5px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Positioning */
|
||||
.top-left { top: -50px; left: -50px; animation-delay: 0s; }
|
||||
.bottom-right { bottom: -50px; right: -50px; animation-delay: 2s; }
|
||||
.top-right { top: 10%; right: -20px; animation-delay: 1s; }
|
||||
.bottom-left { bottom: 10%; left: -30px; animation-delay: 3s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(10deg); }
|
||||
100% { transform: translateY(0px) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes float-rotate {
|
||||
0% { transform: translateY(0px) rotate(45deg); }
|
||||
50% { transform: translateY(-25px) rotate(55deg); }
|
||||
100% { transform: translateY(0px) rotate(45deg); }
|
||||
}
|
||||
|
||||
.shape-cube.bottom-right {
|
||||
animation: float-rotate 8s ease-in-out infinite;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 34 KiB |
106
update_html.py
@ -1,106 +0,0 @@
|
||||
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
@ -1,114 +0,0 @@
|
||||
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)
|
||||
@ -1,19 +0,0 @@
|
||||
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)
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
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!")
|
||||