Compare commits

...

11 Commits

Author SHA1 Message Date
Flatlogic Bot
749383265f Auto commit: 2026-03-13T16:42:08.880Z 2026-03-13 16:42:08 +00:00
Flatlogic Bot
7b2fc4e3b2 Auto commit: 2026-03-13T16:35:19.045Z 2026-03-13 16:35:19 +00:00
Flatlogic Bot
bc8c102df7 Auto commit: 2026-03-13T16:18:43.826Z 2026-03-13 16:18:43 +00:00
Flatlogic Bot
ef8a8d4c27 Auto commit: 2026-03-13T12:26:02.181Z 2026-03-13 12:26:02 +00:00
Flatlogic Bot
ba1bd529a8 Auto commit: 2026-03-13T11:47:52.842Z 2026-03-13 11:47:52 +00:00
Flatlogic Bot
027f4dd35b Auto commit: 2026-03-13T10:27:31.944Z 2026-03-13 10:27:31 +00:00
Flatlogic Bot
4bea8583e5 Auto commit: 2026-03-13T09:39:35.644Z 2026-03-13 09:39:35 +00:00
Flatlogic Bot
134adfbc75 Auto commit: 2026-03-12T16:08:07.812Z 2026-03-12 16:08:07 +00:00
Flatlogic Bot
1829bd8f53 Auto commit: 2026-03-12T15:34:32.901Z 2026-03-12 15:34:32 +00:00
Flatlogic Bot
33a9c38573 Auto commit: 2026-03-12T14:15:33.094Z 2026-03-12 14:15:33 +00:00
Flatlogic Bot
ffe8cee686 Auto commit: 2026-03-12T13:34:18.743Z 2026-03-12 13:34:18 +00:00
53 changed files with 1881 additions and 153 deletions

Binary file not shown.

Binary file not shown.

View File

@ -345,6 +345,7 @@ 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -23,12 +23,12 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
os.getenv("HOST_FQDN", ""),
os.getenv("HOST_FQDN", ""), "aibmm.flatlogic.app",
]
CSRF_TRUSTED_ORIGINS = [
origin for origin in [
os.getenv("HOST_FQDN", ""),
os.getenv("HOST_FQDN", ""), "aibmm.flatlogic.app",
os.getenv("CSRF_TRUSTED_ORIGIN", "")
] if origin
]
@ -76,7 +76,7 @@ ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'core' / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -180,3 +180,6 @@ 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 = '/'

View File

@ -21,9 +21,10 @@ 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)

Binary file not shown.

Binary file not shown.

60
core/ai_helpers.py Normal file
View File

@ -0,0 +1,60 @@
from ai.local_ai_api import LocalAIApi
from .models import MindMapNode, MindMapConnection
import json
def generate_initial_mindmap(project):
prompt = f"""
You are an expert business consultant. Create an initial mind map for a new project.
Project Title: {project.title}
Industry: {project.industry}
Goal: {project.goal}
Respond ONLY with a valid JSON object in the following format:
{{
"nodes": [
{{"id": "node_1", "title": "Main Goal", "summary": "Short description", "category": "Strategy"}},
{{"id": "node_2", "title": "Feature X", "summary": "Short description", "category": "Product"}}
],
"connections": [
{{"source_id": "node_1", "target_id": "node_2", "how": "Defines what to build", "why": "Feature X is critical to achieve the Main Goal"}}
]
}}
Create 6-10 interconnected nodes exploring key business areas like Target Audience, Core Features, Marketing Strategy, Revenue Streams, etc. Ensure the IDs in connections match the nodes.
"""
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": "You are a helpful business strategy AI. You must respond in valid JSON matching the exact requested format."},
{"role": "user", "content": prompt}
],
"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 Normal file
View File

@ -0,0 +1,145 @@
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from .models import Project, MindMapNode, MindMapConnection
from ai.local_ai_api import LocalAIApi
from django.shortcuts import get_object_or_404
import json
@csrf_exempt
@login_required
def ai_chat(request, pk):
if request.method == 'POST':
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)

View File

@ -0,0 +1,52 @@
# 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')),
],
),
]

View File

@ -1,3 +1,34 @@
from django.db import models
from django.conf import settings
# Create your models here.
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}"

View File

@ -0,0 +1,63 @@
{% 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">&larr; Back to Site</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,6 +3,7 @@
<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 }}">
@ -13,13 +14,33 @@
<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>
{% block content %}{% endblock %}
{% 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>
</body>
</html>
</html>

View File

@ -0,0 +1,55 @@
{% 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 %}

View File

@ -0,0 +1,22 @@
{% 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 %}

View File

@ -1,145 +1,235 @@
{% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %}
{% block title %}AI Business Planning & Mind Map{% 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;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Sora:wght@600;700;800&display=swap" rel="stylesheet">
<style>
: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);
}
* {
box-sizing: border-box;
--primary: #0F766E;
--primary-light: #38BDF8;
--secondary: #0B132B;
--accent: #F59E0B;
--bg-color: #F8FAFC;
--text-color: #334155;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
-webkit-font-smoothing: antialiased;
}
body::before {
.hero {
position: relative;
padding: 8rem 2rem 6rem;
text-align: center;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
overflow: hidden;
}
.hero::after {
content: '';
position: absolute;
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;
bottom: -50px;
left: 0;
right: 0;
height: 100px;
background: var(--bg-color);
transform: skewY(-2deg);
z-index: 1;
}
@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);
.hero-content {
position: relative;
z-index: 2;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
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;
}
p {
margin: 0.5rem 0;
.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;
font-size: 1.1rem;
opacity: 0.92;
text-decoration: none;
border-radius: 50px;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
.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);
border-radius: 50%;
animation: spin 1s linear infinite;
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;
}
@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;
@keyframes float {
0% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(10deg); }
100% { transform: translateY(0px) rotate(0deg); }
}
</style>
{% endblock %}
{% block content %}
<main>
<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>
<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>
<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>
</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>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}

View File

@ -0,0 +1,502 @@
{% 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 %}

View File

@ -0,0 +1,23 @@
{% 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 %}

View File

@ -0,0 +1,40 @@
{% 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 %}

View File

@ -0,0 +1,41 @@
{% 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 %}

View File

@ -1,7 +1,16 @@
from django.urls import path
from .views import home
from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap, edit_node, delete_project
from .ai_views import ai_chat
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"),
]

View File

@ -1,25 +1,97 @@
import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
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
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):
"""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()
return render(request, 'core/index.html')
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)
@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})

View File

@ -1,4 +1,123 @@
/* 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;
}

View File

@ -1,21 +1,123 @@
: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);
}
/* Custom styles for the application */
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;
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

106
update_html.py Normal file
View File

@ -0,0 +1,106 @@
import os
path = "core/templates/core/project_detail.html"
with open(path, "r") as f:
html = f.read()
# 1. Add top bar buttons (Layout, Zoom, etc)
buttons_to_add = """
<select id="layout-select" class="form-select form-select-sm d-inline-block w-auto me-2" onchange="changeLayout(this.value)">
<option value="organic">Organic Layout</option>
<option value="hierarchical">Hierarchical Layout</option>
</select>
<div class="btn-group btn-group-sm me-2" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="zoomIn()" title="Zoom In">+</button>
<button type="button" class="btn btn-outline-secondary" onclick="zoomOut()" title="Zoom Out">-</button>
</div>"""
html = html.replace('<button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>',
buttons_to_add + '\n <button class="btn btn-sm btn-outline-primary" onclick="fitNetwork()">Reset View</button>')
# 2. Add emoji logic
emoji_script = """
const categoryEmojis = {
'Strategy': '🎯',
'Product': '📦',
'Market': '🌍',
'Operations': '⚙️',
'Finance': '💰',
'General': '💡'
};
function getNodeEmoji(category) {
let cat = category || 'General';
for (let key in categoryEmojis) {
if (cat.toLowerCase().includes(key.toLowerCase())) {
return categoryEmojis[key];
}
}
return '💡'; // default emoji
}
"""
html = html.replace('function getNodeColor(category) {', emoji_script + '\n function getNodeColor(category) {')
# update node array to include emoji
html = html.replace('label: "{{ node.title|escapejs }}",', 'label: getNodeEmoji("{{ node.category|escapejs }}") + " " + "{{ node.title|escapejs }}",')
# 3. Add script functions for zoom and layout
scripts_to_add = """
function zoomIn() {
if (network) {
const scale = network.getScale() * 1.5;
network.moveTo({ scale: scale, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
}
}
function zoomOut() {
if (network) {
const scale = network.getScale() / 1.5;
network.moveTo({ scale: scale, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
}
}
function changeLayout(layoutType) {
if (layoutType === 'hierarchical') {
network.setOptions({
layout: {
hierarchical: {
enabled: true,
direction: 'UD',
sortMethod: 'directed',
nodeSpacing: 200,
levelSeparation: 150
}
},
physics: {
hierarchicalRepulsion: {
nodeDistance: 200
}
}
});
} else {
network.setOptions({
layout: { hierarchical: { enabled: false } },
physics: {
solver: 'forceAtlas2Based',
forceAtlas2Based: {
gravitationalConstant: -100,
centralGravity: 0.01,
springLength: 150,
springConstant: 0.08
},
maxVelocity: 50,
minVelocity: 0.1,
timestep: 0.5,
stabilization: { iterations: 150 }
}
});
}
}
"""
hTml = html.replace('// Export Map function', scripts_to_add + '\n // Export Map function')
with open(path, "w") as f:
f.write(html)
print("Updated HTML!")

114
update_js.py Normal file
View File

@ -0,0 +1,114 @@
import os
with open("core/templates/core/project_detail.html", "r") as f:
html = f.read()
# Replace the node clicked block
old_block = """
const nodeId = params.nodes[0];
const node = data.nodes.get(nodeId);
detailsPanel.innerHTML = `
<h6 class="mb-2 fw-bold text-dark">${node.label}</h6>
<span class="badge bg-secondary mb-2 bg-opacity-75">${node.category}</span>
<p class="small mb-0 text-dark"><strong>Summary:</strong><br>${node.summary}</p>
`;
"""
new_block = """
const nodeId = params.nodes[0];
const node = data.nodes.get(nodeId);
// remove emoji from label for editing
const cleanLabel = node.label.replace(/^.*? /, '');
detailsPanel.innerHTML = `
<div id="node-view-${nodeId}">
<h6 class="mb-2 fw-bold text-dark">${cleanLabel}</h6>
<span class="badge bg-secondary mb-2 bg-opacity-75">${node.category}</span>
<p class="small mb-2 text-dark"><strong>Summary:</strong><br>${node.summary}</p>
<button class="btn btn-sm btn-outline-primary" onclick="editNodeUI(${nodeId})">Edit</button>
</div>
<div id="node-edit-${nodeId}" class="d-none">
<input type="text" id="edit-node-title-${nodeId}" class="form-control form-control-sm mb-2" value="${cleanLabel}">
<input type="text" id="edit-node-category-${nodeId}" class="form-control form-control-sm mb-2" value="${node.category}">
<textarea id="edit-node-summary-${nodeId}" class="form-control form-control-sm mb-2" rows="3">${node.summary}</textarea>
<button class="btn btn-sm btn-primary me-1" onclick="saveNode(${nodeId}, event)">Save</button>
<button class="btn btn-sm btn-outline-secondary" onclick="cancelEditNode(${nodeId})">Cancel</button>
</div>
`;
"""
html = html.replace(old_block, new_block)
new_scripts = """
function editNodeUI(nodeId) {
document.getElementById(`node-view-${nodeId}`).classList.add('d-none');
document.getElementById(`node-edit-${nodeId}`).classList.remove('d-none');
}
function cancelEditNode(nodeId) {
document.getElementById(`node-edit-${nodeId}`).classList.add('d-none');
document.getElementById(`node-view-${nodeId}`).classList.remove('d-none');
}
function saveNode(nodeId, event) {
const title = document.getElementById(`edit-node-title-${nodeId}`).value;
const category = document.getElementById(`edit-node-category-${nodeId}`).value;
const summary = document.getElementById(`edit-node-summary-${nodeId}`).value;
const saveBtn = event.target;
saveBtn.disabled = true;
saveBtn.innerText = 'Saving...';
let csrfToken = '';
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (csrfInput) {
csrfToken = csrfInput.value;
} else {
// fallback, get from form if present elsewhere
console.warn("CSRF token not found via input name. Ensure it exists on the page.");
}
const url = "{% url 'edit_node' project.pk 0 %}".replace('/0/', '/' + nodeId + '/');
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ title: title, category: category, summary: summary })
})
.then(response => response.json())
.then(res => {
if (res.status === 'success') {
const node = res.node;
const emoji = getNodeEmoji(node.category);
const color = getNodeColor(node.category);
data.nodes.update({
id: node.id,
label: emoji + " " + node.title,
title: `<strong>${node.title}</strong><br>${node.summary}<br><em>Category: ${node.category}</em>`,
summary: node.summary,
category: node.category,
color: color
});
// re-render details
network.emit('click', { nodes: [nodeId], edges: [] });
} else {
alert('Error saving node: ' + res.message);
saveBtn.disabled = false;
saveBtn.innerText = 'Save';
}
})
.catch(err => {
alert('Error saving node');
saveBtn.disabled = false;
saveBtn.innerText = 'Save';
});
}
"""
html = html.replace(' // AI Chat Handler', new_scripts + '\n // AI Chat Handler')
with open("core/templates/core/project_detail.html", "w") as f:
f.write(html)

19
update_urls.py Normal file
View File

@ -0,0 +1,19 @@
import os
with open("core/urls.py", "r") as f:
content = f.read()
content = content.replace(
'from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap',
'from .views import home, project_list, project_detail, create_project, signup, regenerate_mindmap, edit_node'
)
new_path = " path('projects/<int:pk>/node/<int:node_id>/edit/', edit_node, name='edit_node'),\n"
content = content.replace(
' path("projects/<int:pk>/ai/", ai_chat, name="ai_chat"),',
' path("projects/<int:pk>/ai/", ai_chat, name="ai_chat"),\n' + new_path
)
with open("core/urls.py", "w") as f:
f.write(content)

37
update_views.py Normal file
View File

@ -0,0 +1,37 @@
import os
with open("core/views.py", "a") as f:
f.write("""
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
@login_required
@require_POST
def edit_node(request, pk, node_id):
project = get_object_or_404(Project, pk=pk, user=request.user)
try:
node = project.nodes.get(pk=node_id)
data = json.loads(request.body)
title = data.get('title', '').strip()
summary = data.get('summary', '').strip()
category = data.get('category', '').strip()
if title:
node.title = title
if summary:
node.summary = summary
if category:
node.category = category
node.save()
return JsonResponse({'status': 'success', 'node': {
'id': node.pk,
'title': node.title,
'summary': node.summary,
'category': node.category
}})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
""")
print("Updated views.py!")