“shubhamdhamal”
Deploy Flask app with Docker
7644eac
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ path.title }} | Learning Path</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/glassmorphic.css') }}">
</head>
<body class="grid-background min-h-screen" data-is-authenticated="{{ 'true' if current_user.is_authenticated else 'false' }}">
<nav class="glass-nav py-4 px-6">
<div class="container mx-auto flex justify-between items-center">
<a href="/" class="text-2xl font-bold text-white">
Learning<span class="text-neon-pink">Path</span>
</a>
<div class="flex items-center gap-6">
{% if current_user.is_authenticated %}
<a href="/dashboard" class="text-gray-700 dark:text-secondary hover:text-neon-cyan transition">Dashboard</a>
<a href="/" class="neon-btn-sm">New Path</a>
<a href="{{ url_for('auth.logout') }}" class="text-gray-700 dark:text-secondary hover:text-neon-cyan transition">Logout</a>
{% else %}
<a href="{{ url_for('auth.login') }}" class="text-gray-700 dark:text-secondary hover:text-neon-cyan transition">Login</a>
<a href="{{ url_for('auth.register') }}" class="neon-btn-sm">Register</a>
{% endif %}
<!-- Theme toggle -->
<button id="theme-toggle" class="ml-2 inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 text-secondary dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-magenta" aria-label="Toggle dark mode">
<svg id="theme-toggle-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M10 15a5 5 0 100-10 5 5 0 000 10zM10 1a1 1 0 011 1v1a1 1 0 11-2 0V2a1 1 0 011-1zm0 14a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm9-5a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM3 10a1 1 0 01-1 1H1a1 1 0 110-2h1a1 1 0 011 1zm12.364-6.364a1 1 0 010 1.414L14.95 6.464a1 1 0 01-1.414-1.414l1.414-1.414a1 1 0 011.414 0zM5.05 14.95a1 1 0 011.414 0l1.414-1.414a1 1 0 10-1.414-1.414L5.05 13.536a1 1 0 010 1.414zm9.9 0a1 1 0 10-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414l1.414-1.414zM5.05 5.05a1 1 0 011.414 0L7.878 6.464A1 1 0 116.464 7.878L5.05 6.464A1 1 0 015.05 5.05z" clip-rule="evenodd"></path>
</svg>
<svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8 8 0 1010.586 10.586z"></path>
</svg>
</button>
</div>
</div>
</nav>
<!-- Unified Sticky Header Bar -->
<div class="sticky top-0 z-40 glass-nav border-b border-gray-200 dark:border-gray-700 shadow-lg">
<div class="container mx-auto px-6 py-4">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<!-- Left: Path Title & Key Stats -->
<div class="flex-1">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">{{ path.title }}</h1>
<div class="flex flex-wrap items-center gap-3 text-sm">
<span class="flex items-center gap-1 text-gray-700 dark:text-secondary">
<svg class="w-4 h-4 text-neon-cyan" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
</svg>
<strong>{{ path.total_hours }}</strong> Hours
</span>
<span class="text-gray-400"></span>
<span class="flex items-center gap-1 text-gray-700 dark:text-secondary">
<svg class="w-4 h-4 text-neon-purple" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/>
</svg>
<strong>{{ path.milestones|length }}</strong> Milestones
</span>
<span class="text-gray-400"></span>
<span class="flex items-center gap-1 text-gray-700 dark:text-secondary">
<svg class="w-4 h-4 text-sunshine" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/>
</svg>
<strong>{{ path.duration_weeks }}</strong> Weeks
</span>
<span class="text-gray-400"></span>
<span class="px-2 py-1 bg-neon-purple bg-opacity-20 border border-neon-purple text-neon-purple rounded-full text-xs font-medium">
{{ path.expertise_level|title }}
</span>
</div>
</div>
<!-- Right: Action Buttons -->
<div class="flex items-center gap-3">
<a href="/download/{{ path.id }}" class="inline-flex items-center gap-2 px-4 py-2 bg-magenta text-white rounded-full font-medium text-sm hover:bg-magentaLight transition-colors duration-300">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
Download
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.save_path') }}" class="inline-flex items-center gap-2 px-4 py-2 bg-sunshine text-white rounded-full font-medium text-sm hover:bg-sunnyYellow transition-colors duration-300">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
</svg>
Save
</a>
{% else %}
<a href="{{ url_for('auth.login') }}" class="inline-flex items-center gap-2 px-4 py-2 bg-sunshine text-white rounded-full font-medium text-sm hover:bg-sunnyYellow transition-colors duration-300">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
</svg>
Login to Save
</a>
{% endif %}
<button class="share-button inline-flex items-center gap-2 px-4 py-2 border-2 border-magenta text-magenta rounded-full font-medium text-sm hover:bg-magenta hover:text-white transition-colors duration-300">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M15 8a3 3 0 10-2.977-2.63l-4.94 2.47a3 3 0 100 4.319l4.94 2.47a3 3 0 10.895-1.789l-4.94-2.47a3.027 3.027 0 000-.74l4.94-2.47C13.456 7.68 14.19 8 15 8z"/>
</svg>
Share
</button>
</div>
</div>
</div>
</div>
<main class="container mx-auto px-6 py-8">
{% set total_milestones = progress_total %}
{% set completed_milestones = progress_completed %}
{% set progress_map = progress or {} %}
{% set progress_percentage = progress_percentage_value %}
<!-- Hidden Path ID for JavaScript -->
<input type="hidden" id="path-id" value="{{ path.id }}">
<!-- Learning Path Overview -->
<div class="max-w-5xl mx-auto">
<div class="glass-card p-8 my-8">
<div class="grid md:grid-cols-3 gap-6">
<!-- Left Column: Description -->
<div class="md:col-span-2">
<div class="mb-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">About This Path</h3>
<p class="text-gray-700 dark:text-secondary leading-relaxed">{{ path.description }}</p>
</div>
<div class="mb-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">What You'll Learn</h3>
<ul class="space-y-3">
{% for goal in path.goals %}
<li class="flex items-start">
<span class="text-neon-cyan mr-2"></span>
<span class="text-gray-700 dark:text-secondary">{{ goal }}</span>
</li>
{% endfor %}
</ul>
</div>
{% if path.prerequisites %}
<div class="mb-8">
<h3 class="text-2xl font-bold text-white mb-4">Before You Start</h3>
<ul class="space-y-3">
{% for prereq in path.prerequisites %}
<li class="flex items-start">
<span class="text-neon-purple mr-2"></span>
<span class="text-secondary">{{ prereq }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- Right Column: Progress -->
<div class="glass-card p-6">
<h3 class="text-xl font-bold text-white mb-4">Your Progress</h3>
<div class="mb-6">
<div class="flex justify-between mb-2">
<span class="text-secondary text-sm">Learning Journey</span>
<span class="text-white text-sm font-medium" id="progress-percentage-text">{{ progress_percentage|round(0) }}%</span>
</div>
<div class="progress-bar-container" data-progress="{{ progress_percentage }}">
<div class="progress-bar" id="progress-bar" style="width: 0%;"></div>
</div>
<p id="progress-text" class="text-xs text-secondary mt-2">{{ completed_milestones }} of {{ total_milestones }} milestones completed ({{ progress_percentage|round(0) }}%)</p>
</div>
</div>
</div>
</div>
<!-- Progress Visualization -->
<div class="glass-card p-8 my-12">
<h3 class="text-2xl font-bold text-white mb-6">Your Learning <span class="text-neon-cyan">Journey</span></h3>
<div class="h-72">
<canvas id="progressChart" data-milestones='{{ path.milestones | tojson }}'></canvas>
</div>
</div>
<!-- Milestones Section -->
<div class="my-12">
<h3 class="text-3xl font-bold text-white mb-8 text-center">Your Learning <span class="text-neon-purple">Milestones</span></h3>
<!-- Milestone Timeline -->
<div class="relative py-8">
<!-- Timeline Line -->
<div class="space-y-12">
{% for milestone in path.milestones %}
<div class="relative milestone-card">
<!-- Milestone Card -->
<div class="w-full flex justify-center mb-12">
<div class="glass-card p-8 w-full max-w-3xl border-t-4 border-magenta" id="milestone-{{ loop.index0 }}">
<div class="mb-6 text-center">
<h3 class="text-2xl font-bold text-neon-purple">Milestone {{ loop.index }}</h3>
</div>
<h4 class="text-2xl font-bold text-white mb-3">{{ milestone.title }}</h4>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 {% if loop.index % 2 == 0 %}bg-sunshine text-white{% else %}bg-magentaLight text-white{% endif %} rounded-full text-sm font-medium">{{ milestone.estimated_hours }} hours</span>
</div>
<p class="text-secondary mb-6">{{ milestone.description }}</p>
<!-- Progress Status and Actions -->
{% if current_user.is_authenticated %}
<div class="mb-6">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-secondary">Progress Status:</span>
<div class="flex items-center gap-2">
{% set milestone_status = progress_map.get(loop.index0|string) %}
{% if milestone_status == 'completed' %}
<span class="px-3 py-1 bg-green-500 text-white text-xs rounded-full">Completed</span>
<button class="mark-incomplete-btn px-3 py-1 bg-gray-500 text-white text-xs rounded-full hover:bg-gray-600 transition-colors"
data-milestone="{{ loop.index0 }}">
Mark Incomplete
</button>
{% else %}
<span class="px-3 py-1 bg-yellow-500 text-white text-xs rounded-full">Not Started</span>
<button class="mark-complete-btn px-3 py-1 bg-green-500 text-white text-xs rounded-full hover:bg-green-600 transition-colors"
data-milestone="{{ loop.index0 }}">
Mark as Complete
</button>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="mb-8">
<h5 class="font-bold text-white mb-3">Skills you'll gain:</h5>
<div class="flex flex-wrap gap-2">
{% for skill in milestone.skills_gained %}
<span class="glass-pill">{{ skill }}</span>
{% endfor %}
</div>
</div>
<!-- Job Market Data removed from milestones - shown once at top -->
<!-- Resources -->
<div class="mt-8">
<h4 class="text-xl font-bold text-white mb-4">Recommended Resources:</h4>
{% if milestone.resources %}
{% set milestone_idx = loop.index0 %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for resource in milestone.resources %}
<div class="glass-card-dark p-5 transition-all duration-300 hover:border-neon-cyan hover:border relative">
<input type="checkbox" class="resource-checkbox absolute top-3 right-3 w-4 h-4 text-neon-cyan bg-gray-700 border-gray-600 rounded focus:ring-neon-cyan focus:ring-2 cursor-pointer" data-milestone="{{ milestone_idx }}" data-resource="{{ loop.index0 }}">
<div class="font-semibold text-white mb-2">
<a href="{% if resource.url %}{{ resource.url }}{% else %}https://www.google.com/search?q={{ resource.description | urlencode }}{% endif %}" target="_blank" class="text-neon-cyan hover:text-neon-purpleLight transition-colors duration-200">
{{ resource.description }}
</a>
</div>
<div class="text-sm text-secondary mb-3 flex items-center gap-1">
{% if resource.type == 'Video' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-sunshine" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z" />
</svg>
{% elif resource.type == 'Online Course' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-neon-purple" viewBox="0 0 20 20" fill="currentColor">
<path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
</svg>
{% elif resource.type == 'Book' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-white" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z" />
</svg>
{% elif resource.type == 'Article' or resource.type == 'Tutorial' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-sunnyYellow" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" />
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-muted" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2a1 1 0 00-1-1H8a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z" clip-rule="evenodd" />
</svg>
{% endif %}
<span>{{ resource.type }}</span>
</div>
<div class="flex justify-between items-center">
<a href="{% if resource.url %}{{ resource.url }}{% else %}https://www.google.com/search?q={{ resource.description | urlencode }}{% endif %}" target="_blank" class="inline-flex items-center text-neon-cyan text-sm font-medium hover:text-neon-purple transition-colors duration-200">
<span>View Resource</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</a>
<span class="text-xs text-muted">{{ resource.format|default('') }}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted italic">No specific resources recommended for this milestone.</p>
{% endif %} </div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Job Market Snapshot - Career Outcome Section -->
{% if path.job_market_data and not path.job_market_data.error %}
<div class="bg-gradient-to-r from-magenta to-magentaLight rounded-xl shadow-xl p-8 my-12 text-white">
<h3 class="text-3xl font-bold mb-3 text-center"> Your Career Outlook</h3>
<p class="text-center text-white opacity-90 mb-8">See the real-world impact of mastering this skill</p>
<div class="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
<div class="bg-white bg-opacity-20 rounded-lg p-6 text-center backdrop-blur-sm">
<p class="text-4xl font-bold mb-2">{{ path.job_market_data.open_positions }}</p>
<p class="text-sm opacity-90">Open Positions</p>
</div>
<div class="bg-white bg-opacity-20 rounded-lg p-6 text-center backdrop-blur-sm">
<p class="text-2xl font-bold mb-2">{{ path.job_market_data.average_salary }}</p>
<p class="text-sm opacity-90">Average Salary</p>
</div>
<div class="bg-white bg-opacity-20 rounded-lg p-6 backdrop-blur-sm">
<p class="text-lg font-semibold mb-2">Trending Employers:</p>
<div class="flex flex-wrap gap-2 justify-center">
{% for employer in path.job_market_data.trending_employers[:3] %}
<span class="bg-white text-neon-purple px-3 py-1 rounded-full text-sm font-medium">{{ employer }}</span>
{% endfor %}
</div>
</div>
</div>
{% if path.job_market_data.related_roles %}
<div class="mt-6 text-center">
<p class="text-sm opacity-90 mb-2">Related Roles:</p>
<div class="flex flex-wrap gap-2 justify-center">
{% for role in path.job_market_data.related_roles[:5] %}
<span class="bg-white bg-opacity-30 px-3 py-1 rounded-full text-sm">{{ role }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</main>
<!-- Floating Chat Button -->
<button id="chat-toggle-btn" class="fixed bottom-8 right-8 bg-magenta text-white w-16 h-16 rounded-full shadow-lg flex items-center justify-center transform hover:scale-110 transition-transform z-50">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
</button>
<!-- Chat Panel -->
<div id="chat-panel" class="hidden fixed bottom-28 right-8 w-full max-w-md h-[60vh] bg-white dark:bg-gray-800 rounded-lg shadow-2xl flex flex-col glass-card z-50">
<!-- Header -->
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center flex-shrink-0">
<h3 class="font-bold text-lg text-gray-900 dark:text-white">Ask a follow-up question</h3>
<button id="chat-close-btn" class="text-gray-500 hover:text-gray-900 dark:hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<!-- Messages -->
<div id="chat-messages" class="flex-1 p-4 space-y-4 overflow-y-auto">
<!-- Initial AI message -->
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-magenta flex-shrink-0 flex items-center justify-center text-white font-bold">AI</div>
<div class="glass-card-dark p-3 rounded-lg max-w-xs">
<p class="text-sm text-gray-200">Ask me anything about {{ path.topic }}!</p>
</div>
</div>
</div>
<!-- Input Form -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<form id="chat-form" class="flex items-center gap-2">
<input type="text" id="chat-input" class="glass-input w-full" placeholder="Type your question..." autocomplete="off">
<button type="submit" class="neon-btn-sm">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clip-rule="evenodd"></path></svg>
</button>
</form>
</div>
</div>
<!-- Theme Toggle Script -->
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Chat Panel Logic
const chatToggleBtn = document.getElementById('chat-toggle-btn');
const chatPanel = document.getElementById('chat-panel');
const chatCloseBtn = document.getElementById('chat-close-btn');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
chatToggleBtn.addEventListener('click', () => {
chatPanel.classList.toggle('hidden');
});
chatCloseBtn.addEventListener('click', () => {
chatPanel.classList.add('hidden');
});
chatForm.addEventListener('submit', function(e) {
e.preventDefault();
const question = chatInput.value.trim();
if (!question) return;
// Display user message
const userMessageHtml = `
<div class="flex items-start gap-3 justify-end">
<div class="glass-card-dark p-3 rounded-lg max-w-xs bg-neon-cyan bg-opacity-20">
<p class="text-sm text-white">${question}</p>
</div>
</div>
`;
chatMessages.innerHTML += userMessageHtml;
chatMessages.scrollTop = chatMessages.scrollHeight;
chatInput.value = '';
// Show typing indicator
const typingIndicatorHtml = `
<div id="typing-indicator" class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-magenta flex-shrink-0 flex items-center justify-center text-white font-bold">AI</div>
<div class="glass-card-dark p-3 rounded-lg max-w-xs">
<p class="text-sm text-gray-200">Typing...</p>
</div>
</div>
`;
chatMessages.innerHTML += typingIndicatorHtml;
chatMessages.scrollTop = chatMessages.scrollHeight;
// Send to backend
fetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: question,
path_id: pathId
})
})
.then(response => response.json())
.then(data => {
document.getElementById('typing-indicator').remove();
if (data.success) {
const aiMessageHtml = `
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-magenta flex-shrink-0 flex items-center justify-center text-white font-bold">AI</div>
<div class="glass-card-dark p-3 rounded-lg max-w-xs">
<p class="text-sm text-gray-200">${data.data.answer}</p>
</div>
</div>
`;
chatMessages.innerHTML += aiMessageHtml;
} else {
const errorMessageHtml = `
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-red-500 flex-shrink-0 flex items-center justify-center text-white font-bold">!</div>
<div class="glass-card-dark p-3 rounded-lg max-w-xs">
<p class="text-sm text-red-400">Error: ${data.message}</p>
</div>
</div>
`;
chatMessages.innerHTML += errorMessageHtml;
}
chatMessages.scrollTop = chatMessages.scrollHeight;
})
.catch(error => {
document.getElementById('typing-indicator').remove();
const errorMessageHtml = `
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-red-500 flex-shrink-0 flex items-center justify-center text-white font-bold">!</div>
<div class="glass-card-dark p-3 rounded-lg max-w-xs">
<p class="text-sm text-red-400">A network error occurred.</p>
</div>
</div>
`;
chatMessages.innerHTML += errorMessageHtml;
chatMessages.scrollTop = chatMessages.scrollHeight;
});
});
// FAQ Accordion
const faqQuestions = document.querySelectorAll('.faq-question');
faqQuestions.forEach(question => {
question.addEventListener('click', function() {
const answer = this.nextElementSibling;
const icon = this.querySelector('.faq-icon');
// Toggle answer visibility
answer.classList.toggle('hidden');
// Rotate icon
if (answer.classList.contains('hidden')) {
icon.style.transform = 'rotate(0deg)';
} else {
icon.style.transform = 'rotate(180deg)';
}
});
});
// FAQ Search
const faqSearch = document.getElementById('faqSearch');
if (faqSearch) {
faqSearch.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const faqItems = document.querySelectorAll('.faq-item');
faqItems.forEach(item => {
const question = item.querySelector('.faq-question span').textContent.toLowerCase();
const answer = item.querySelector('.faq-answer').textContent.toLowerCase();
if (question.includes(searchTerm) || answer.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
}
// Share Button
const shareButton = document.querySelector('.share-button');
if (shareButton) {
shareButton.addEventListener('click', function() {
if (navigator.share) {
navigator.share({
title: document.title,
url: window.location.href
}).catch(err => console.log('Error sharing:', err));
} else {
// Fallback: copy to clipboard
navigator.clipboard.writeText(window.location.href).then(() => {
alert('Link copied to clipboard!');
});
}
});
}
// Progress Chart
const ctx = document.getElementById('progressChart');
if (ctx) {
const milestones = JSON.parse(ctx.dataset.milestones);
const labels = milestones.map((m, i) => `M${i + 1}`);
const hours = milestones.map(m => m.estimated_hours);
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Estimated Hours',
data: hours,
borderColor: '#00D9FF',
backgroundColor: 'rgba(0, 217, 255, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#fff'
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#9CA3AF'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
},
x: {
ticks: {
color: '#9CA3AF'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
}
}
}
});
}
// Milestone Progress Tracking
const pathId = document.getElementById('path-id').value;
const progressBar = document.getElementById('progress-bar');
const progressBarContainer = document.querySelector('.progress-bar-container');
if (progressBar && progressBarContainer) {
const progressPercentage = parseFloat(progressBarContainer.dataset.progress) || 0;
progressBar.style.width = `${progressPercentage}%`;
}
const isAuthenticated = document.body.dataset.isAuthenticated === 'true';
// Mark Complete/Incomplete buttons
document.querySelectorAll('.mark-complete-btn, .mark-incomplete-btn').forEach(button => {
button.addEventListener('click', function() {
const milestoneIndex = parseInt(this.dataset.milestone);
const isComplete = this.classList.contains('mark-complete-btn');
fetch('/api/track-milestone', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path_id: pathId,
milestone_index: milestoneIndex,
completed: isComplete
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => console.error('Error:', error));
});
});
// Resource Checkbox Tracking - Persistent Database Storage
const resourceCheckboxes = document.querySelectorAll('.resource-checkbox');
// Load saved checkbox states from database (for logged-in users)
if (isAuthenticated) {
fetch('/api/get-resource-progress/' + pathId)
.then(response => response.json())
.then(data => {
if (data.success) {
resourceCheckboxes.forEach(checkbox => {
const key = 'm' + checkbox.dataset.milestone + '_r' + checkbox.dataset.resource;
if (data.progress[key] && data.progress[key].completed) {
checkbox.checked = true;
checkbox.closest('.glass-card-dark').classList.add('opacity-60');
}
});
}
})
.catch(error => console.error('Error loading progress:', error));
} else {
// Fallback to localStorage for non-authenticated users
resourceCheckboxes.forEach(checkbox => {
const key = 'path_' + pathId + '_resource_' + checkbox.dataset.milestone + '_' + checkbox.dataset.resource;
const saved = localStorage.getItem(key);
if (saved === 'true') {
checkbox.checked = true;
checkbox.closest('.glass-card-dark').classList.add('opacity-60');
}
});
}
// Save checkbox state on change
resourceCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const milestoneIndex = parseInt(this.dataset.milestone);
const resourceIndex = parseInt(this.dataset.resource);
const completed = this.checked;
// Visual feedback
if (completed) {
this.closest('.glass-card-dark').classList.add('opacity-60');
} else {
this.closest('.glass-card-dark').classList.remove('opacity-60');
}
// Save to database (for logged-in users)
if (isAuthenticated) {
const resourceUrl = this.closest('.glass-card-dark').querySelector('a').href;
fetch('/api/track-resource', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path_id: pathId,
milestone_index: milestoneIndex,
resource_index: resourceIndex,
completed: completed,
resource_url: resourceUrl
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Progress saved to database');
} else {
console.error('Failed to save progress:', data.error);
}
})
.catch(error => {
console.error('Error saving progress:', error);
});
} else {
// Fallback to localStorage for non-authenticated users
const key = 'path_' + pathId + '_resource_' + milestoneIndex + '_' + resourceIndex;
localStorage.setItem(key, completed.toString());
}
});
});
});
</script>
</body>
</html>