unknown commited on
Commit
ef3e3ef
·
1 Parent(s): 19b79ca

Improved UI

Browse files
Files changed (2) hide show
  1. chatbot/templates/base.html +285 -174
  2. chatbot/templates/index.html +537 -520
chatbot/templates/base.html CHANGED
@@ -3,187 +3,298 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}Constellation Predictor{% endblock %}</title>
7
 
8
- <!-- Tailwind CSS -->
9
  <script src="https://cdn.tailwindcss.com"></script>
 
 
10
 
11
- <!-- Custom Fonts -->
12
- <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
13
-
14
- <!-- Custom Tailwind Configuration -->
15
- <script>
16
- tailwind.config = {
17
- theme: {
18
- extend: {
19
- fontFamily: {
20
- 'orbitron': ['Orbitron', 'monospace'],
21
- 'space': ['Space Mono', 'monospace'],
22
- },
23
- animation: {
24
- 'twinkle': 'twinkle 2s infinite',
25
- 'twinkle-star': 'twinkle-star 3s infinite',
26
- 'shoot': 'shoot 1.5s ease-out forwards',
27
- },
28
- keyframes: {
29
- twinkle: {
30
- '0%, 100%': { opacity: '1' },
31
- '50%': { opacity: '0.3' }
32
- },
33
- 'twinkle-star': {
34
- '0%, 100%': { opacity: '1', transform: 'scale(1)' },
35
- '50%': { opacity: '0.3', transform: 'scale(0.8)' }
36
- },
37
- shoot: {
38
- '0%': { opacity: '0', transform: 'translateX(0) translateY(0)' },
39
- '10%': { opacity: '1' },
40
- '100%': { opacity: '0', transform: 'translateX(300px) translateY(150px)' }
41
- }
42
- }
43
- }
44
- }
45
- }
46
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  {% block extra_css %}{% endblock %}
49
  </head>
50
- <body class="font-space bg-gradient-to-b from-blue-950 via-black to-black text-white min-h-screen relative">
51
- <!-- Animated Star Background -->
52
- <div class="fixed top-0 left-0 w-full h-full pointer-events-none -z-10" id="starBackground"></div>
53
-
54
- <!-- Navigation -->
55
- <header class="border-b border-white/10 sticky top-0 z-50 backdrop-blur-sm bg-black/20">
56
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
57
- <nav class="flex justify-between items-center py-4">
58
- <!-- Logo -->
59
- <div class="font-orbitron text-2xl font-bold text-blue-400">
60
- <a href="{{ PREDICTOR_BASE_URL }}">Constellation Predictor</a>
61
- </div>
62
-
63
- <!-- Navigation Links -->
64
- <ul class="hidden md:flex space-x-8">
65
- <li><a href="{{ PREDICTOR_BASE_URL }}" class="text-white hover:text-blue-400 transition-colors duration-300">Home</a></li>
66
- <li><a href="{{ PREDICTOR_BASE_URL }}predict/" class="text-white hover:text-blue-400 transition-colors duration-300">Predict</a></li>
67
- <li><a href="{{ PREDICTOR_BASE_URL }}database/" class="text-white hover:text-blue-400 transition-colors duration-300">Database</a></li>
68
- <li><a href="{{ CHATBOT_BASE_URL }}" class="text-white hover:text-blue-400 transition-colors duration-300">Chatbot</a></li>
69
- <li><a href="{{ LOCATOR_BASE_URL }}" class="text-white hover:text-blue-400 transition-colors duration-300">Locator</a></li>
70
- </ul>
71
-
72
- <!-- Navigation Buttons -->
73
- <div class="flex space-x-4">
74
- <a href="{{ PREDICTOR_BASE_URL }}upload/" class="hidden sm:inline-block px-5 py-2 border border-white/30 rounded-full text-white hover:bg-white/10 transition-all duration-300">
75
- Upload Image
76
- </a>
77
- <a href="{{ PREDICTOR_BASE_URL }}detect/" class="px-5 py-2 bg-gradient-to-r from-indigo-800 to-stone-900 rounded-full text-white hover:-translate-y-0.5 hover:shadow-lg hover:shadow-blue-500/40 transition-all duration-300">
78
- Start Detection
79
- </a>
80
- </div>
81
-
82
- <button class="md:hidden text-white" id="mobileMenuBtn">
83
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
85
- </svg>
86
- </button>
87
- </nav>
88
-
89
- <!-- Mobile Menu -->
90
- <div class="md:hidden hidden" id="mobileMenu">
91
- <div class="px-2 pt-2 pb-3 space-y-1 bg-black/40 backdrop-blur-sm rounded-lg mt-2">
92
- <a href="{{ PREDICTOR_BASE_URL }}" class="block px-3 py-2 text-white hover:text-blue-400">Home</a>
93
- <a href="{{ PREDICTOR_BASE_URL }}predict/" class="block px-3 py-2 text-white hover:text-blue-400">Predict</a>
94
- <a href="{{ PREDICTOR_BASE_URL }}database/" class="block px-3 py-2 text-white hover:text-blue-400">Database</a>
95
- <a href="{{ CHATBOT_BASE_URL }}" class="block px-3 py-2 text-white hover:text-blue-400">Chatbot</a>
96
- <a href="{{ LOCATOR_BASE_URL }}" class="block px-3 py-2 text-white hover:text-blue-400">Locator</a>
97
- </div>
98
- </div>
99
  </div>
100
- </header>
101
-
102
- <!-- Main Content -->
103
- <main>
104
- {% block content %}
105
- {% endblock %}
106
- </main>
107
-
108
- <!-- Footer -->
109
- <footer class="border-t border-white/10 mt-16">
110
- <div class="max-w-7xl mx-auto px-4 py-10">
111
- <div class="flex flex-col md:flex-row justify-between items-center mb-6">
112
- <div class="flex items-center space-x-6 mb-4 md:mb-0">
113
- <a href="#" class="text-white hover:text-blue-400 transition-colors">Privacy Policy</a>
114
- <span class="text-white/50">|</span>
115
- <a href="#" class="text-white hover:text-blue-400 transition-colors">Terms of Service</a>
116
- </div>
117
- <div class="flex space-x-5">
118
- <a href="#" class="text-white hover:text-blue-400 transition-colors text-xl">🌐</a>
119
- <a href="#" class="text-white hover:text-blue-400 transition-colors text-xl">🚀</a>
120
- <a href="#" class="text-white hover:text-blue-400 transition-colors text-xl">🌌</a>
121
- </div>
122
- </div>
123
  </div>
124
- </footer>
125
-
126
- <!-- Base JavaScript -->
127
- <script>
128
- const mobileMenuBtn = document.getElementById('mobileMenuBtn');
129
- const mobileMenu = document.getElementById('mobileMenu');
130
-
131
- mobileMenuBtn.addEventListener('click', () => {
132
- mobileMenu.classList.toggle('hidden');
133
- });
134
-
135
- function createAnimatedStarBackground() {
136
- const starBackground = document.getElementById('starBackground');
137
- const numberOfStars = 200;
138
-
139
- for (let i = 0; i < numberOfStars; i++) {
140
- const star = document.createElement('div');
141
- star.className = 'absolute bg-white rounded-full animate-twinkle-star';
142
- star.style.left = Math.random() * 100 + '%';
143
- star.style.top = Math.random() * 100 + '%';
144
- const size = 0.5 + Math.random() * 2.5;
145
- star.style.width = size + 'px';
146
- star.style.height = size + 'px';
147
- star.style.animationDuration = (2 + Math.random() * 6) + 's';
148
- star.style.animationDelay = Math.random() * 4 + 's';
149
- if (Math.random() < 0.1) star.style.background = '#FFD700';
150
- else if (Math.random() < 0.05) star.style.background = '#87CEEB';
151
- starBackground.appendChild(star);
152
- }
153
- createShootingStars();
154
- }
155
-
156
- function createShootingStars() {
157
- const starBackground = document.getElementById('starBackground');
158
- setInterval(() => {
159
- if (Math.random() < 0.3) {
160
- const shootingStar = document.createElement('div');
161
- shootingStar.className = 'absolute w-0.5 h-0.5 rounded-full opacity-0';
162
- shootingStar.style.background = 'linear-gradient(45deg, white, transparent)';
163
- shootingStar.style.left = Math.random() * 100 + '%';
164
- shootingStar.style.top = Math.random() * 50 + '%';
165
- shootingStar.style.animation = 'shoot 1.5s ease-out forwards';
166
- starBackground.appendChild(shootingStar);
167
- setTimeout(() => {
168
- if (starBackground.contains(shootingStar)) starBackground.removeChild(shootingStar);
169
- }, 1500);
170
- }
171
- }, 3000);
172
- }
173
-
174
- document.querySelectorAll('a[href^="#"]').forEach(anchor => {
175
- anchor.addEventListener('click', function (e) {
176
- e.preventDefault();
177
- const target = document.querySelector(this.getAttribute('href'));
178
- if (target) target.scrollIntoView({ behavior: 'smooth' });
179
- });
180
- });
181
-
182
- document.addEventListener('DOMContentLoaded', function() {
183
- createAnimatedStarBackground();
184
- });
185
- </script>
186
-
187
- {% block extra_js %}{% endblock %}
188
  </body>
189
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Constellar Chatbot{% endblock %}</title>
7
 
 
8
  <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>tailwind.config = { theme: { extend: { fontFamily: { 'orbitron': ['Orbitron', 'monospace'] } } } }</script>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@700;900&display=swap" rel="stylesheet">
11
 
12
+ <style>
13
+ :root {
14
+ --bg: #060d1f;
15
+ --surface: rgba(255,255,255,0.03);
16
+ --border: rgba(255,255,255,0.07);
17
+ --accent: #6366f1;
18
+ --accent-dim: rgba(99,102,241,0.15);
19
+ --text: #dde4f0;
20
+ --muted: rgba(221,228,240,0.45);
21
+ }
22
+
23
+ *, *::before, *::after { box-sizing: border-box; }
24
+
25
+ body {
26
+ margin: 0;
27
+ font-family: 'Inter', system-ui, sans-serif;
28
+ background-color: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ }
32
+
33
+ body::before {
34
+ content: '';
35
+ position: fixed;
36
+ inset: 0;
37
+ z-index: 0;
38
+ pointer-events: none;
39
+ background-image:
40
+ radial-gradient(1.5px 1.5px at 8% 12%, rgba(255,255,255,.55) 0%, transparent 100%),
41
+ radial-gradient(1px 1px at 22% 5%, rgba(255,255,255,.35) 0%, transparent 100%),
42
+ radial-gradient(1.5px 1.5px at 38% 18%, rgba(200,210,255,.40) 0%, transparent 100%),
43
+ radial-gradient(1px 1px at 55% 8%, rgba(255,255,255,.45) 0%, transparent 100%),
44
+ radial-gradient(2px 2px at 72% 22%, rgba(200,210,255,.35) 0%, transparent 100%),
45
+ radial-gradient(1px 1px at 88% 6%, rgba(255,255,255,.50) 0%, transparent 100%),
46
+ radial-gradient(1.5px 1.5px at 5% 42%, rgba(255,255,255,.30) 0%, transparent 100%),
47
+ radial-gradient(1px 1px at 18% 58%, rgba(255,255,255,.40) 0%, transparent 100%),
48
+ radial-gradient(1px 1px at 33% 48%, rgba(200,210,255,.30) 0%, transparent 100%),
49
+ radial-gradient(1.5px 1.5px at 50% 65%, rgba(255,255,255,.35) 0%, transparent 100%),
50
+ radial-gradient(1px 1px at 65% 52%, rgba(255,255,255,.45) 0%, transparent 100%),
51
+ radial-gradient(1px 1px at 80% 70%, rgba(255,255,255,.30) 0%, transparent 100%),
52
+ radial-gradient(1.5px 1.5px at 92% 45%, rgba(200,210,255,.40) 0%, transparent 100%),
53
+ radial-gradient(1px 1px at 12% 80%, rgba(255,255,255,.40) 0%, transparent 100%),
54
+ radial-gradient(1px 1px at 27% 88%, rgba(255,255,255,.30) 0%, transparent 100%),
55
+ radial-gradient(1.5px 1.5px at 45% 78%, rgba(200,210,255,.35) 0%, transparent 100%),
56
+ radial-gradient(1px 1px at 60% 92%, rgba(255,255,255,.40) 0%, transparent 100%),
57
+ radial-gradient(1px 1px at 75% 82%, rgba(255,255,255,.35) 0%, transparent 100%),
58
+ radial-gradient(1.5px 1.5px at 90% 88%, rgba(255,255,255,.30) 0%, transparent 100%),
59
+ radial-gradient(1px 1px at 3% 95%, rgba(255,255,255,.45) 0%, transparent 100%);
60
+ }
61
+
62
+ .page-wrap { position: relative; z-index: 1; }
63
+
64
+ .site-header {
65
+ position: sticky;
66
+ top: 0;
67
+ z-index: 50;
68
+ border-bottom: 1px solid var(--border);
69
+ background: rgba(6,13,31,.88);
70
+ backdrop-filter: blur(20px);
71
+ }
72
+ .nav-inner {
73
+ max-width: 1280px;
74
+ margin: 0 auto;
75
+ padding: 0 1.5rem;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: space-between;
79
+ height: 60px;
80
+ }
81
+ .nav-logo {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 10px;
85
+ text-decoration: none;
86
+ }
87
+ .nav-logo-text {
88
+ font-family: 'Orbitron', monospace;
89
+ font-size: .75rem;
90
+ font-weight: 900;
91
+ letter-spacing: .12em;
92
+ color: #fff;
93
+ }
94
+ .nav-links {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 2rem;
98
+ list-style: none;
99
+ margin: 0; padding: 0;
100
+ }
101
+ .nav-links a {
102
+ font-size: .8125rem;
103
+ font-weight: 500;
104
+ color: var(--muted);
105
+ text-decoration: none;
106
+ letter-spacing: .01em;
107
+ transition: color .18s;
108
+ position: relative;
109
+ padding-bottom: 2px;
110
+ }
111
+ .nav-links a::after {
112
+ content: '';
113
+ position: absolute;
114
+ bottom: -1px; left: 0;
115
+ width: 0; height: 1px;
116
+ background: var(--accent);
117
+ transition: width .18s;
118
+ }
119
+ .nav-links a:hover { color: #fff; }
120
+ .nav-links a:hover::after { width: 100%; }
121
+
122
+ .nav-actions { display: flex; align-items: center; gap: .625rem; }
123
+
124
+ .btn-ghost {
125
+ font-size: .8125rem;
126
+ font-weight: 500;
127
+ color: var(--muted);
128
+ padding: .45rem 1rem;
129
+ border-radius: 6px;
130
+ border: 1px solid var(--border);
131
+ background: transparent;
132
+ text-decoration: none;
133
+ transition: all .18s;
134
+ }
135
+ .btn-ghost:hover {
136
+ color: #fff;
137
+ border-color: rgba(255,255,255,.18);
138
+ background: rgba(255,255,255,.05);
139
+ }
140
+ .btn-accent {
141
+ font-size: .8125rem;
142
+ font-weight: 600;
143
+ color: #fff;
144
+ padding: .45rem 1.125rem;
145
+ border-radius: 6px;
146
+ background: var(--accent);
147
+ text-decoration: none;
148
+ transition: all .18s;
149
+ }
150
+ .btn-accent:hover {
151
+ background: #818cf8;
152
+ box-shadow: 0 0 20px rgba(99,102,241,.4);
153
+ transform: translateY(-1px);
154
+ }
155
+
156
+ .nav-mobile-btn {
157
+ display: none;
158
+ background: none;
159
+ border: none;
160
+ cursor: pointer;
161
+ color: var(--muted);
162
+ padding: .25rem;
163
+ }
164
+ .nav-mobile-btn:hover { color: #fff; }
165
+
166
+ .nav-mobile-menu {
167
+ display: none;
168
+ flex-direction: column;
169
+ gap: .25rem;
170
+ padding: .75rem 1.5rem 1rem;
171
+ border-top: 1px solid var(--border);
172
+ }
173
+ .nav-mobile-menu a {
174
+ font-size: .875rem;
175
+ color: var(--muted);
176
+ text-decoration: none;
177
+ padding: .625rem .75rem;
178
+ border-radius: 6px;
179
+ transition: all .15s;
180
+ }
181
+ .nav-mobile-menu a:hover {
182
+ color: #fff;
183
+ background: rgba(255,255,255,.05);
184
+ }
185
+ .nav-mobile-menu .mobile-actions {
186
+ display: flex;
187
+ gap: .5rem;
188
+ margin-top: .5rem;
189
+ }
190
+ .nav-mobile-menu .mobile-actions a {
191
+ flex: 1;
192
+ text-align: center;
193
+ padding: .6rem;
194
+ }
195
+
196
+ @media (max-width: 768px) {
197
+ .nav-links, .nav-actions { display: none; }
198
+ .nav-mobile-btn { display: block; }
199
+ .nav-mobile-menu.open { display: flex; }
200
+ }
201
+
202
+ .site-footer {
203
+ margin-top: 4rem;
204
+ border-top: 1px solid var(--border);
205
+ }
206
+ .footer-inner {
207
+ max-width: 1280px;
208
+ margin: 0 auto;
209
+ padding: 2.5rem 1.5rem 1.5rem;
210
+ }
211
+ .footer-bottom {
212
+ border-top: 1px solid var(--border);
213
+ padding-top: 1rem;
214
+ display: flex;
215
+ justify-content: space-between;
216
+ align-items: center;
217
+ flex-wrap: wrap;
218
+ gap: .5rem;
219
+ }
220
+ .footer-bottom span { font-size: .75rem; color: rgba(255,255,255,.2); }
221
+ </style>
222
 
223
  {% block extra_css %}{% endblock %}
224
  </head>
225
+ <body>
226
+ <div class="page-wrap">
227
+
228
+ <header class="site-header">
229
+ <div class="nav-inner">
230
+ <a href="{{ PREDICTOR_BASE_URL }}" class="nav-logo">
231
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
232
+ <circle cx="12" cy="12" r="2.5" fill="#6366f1"/>
233
+ <circle cx="4" cy="5" r="1.5" fill="rgba(99,102,241,.7)"/>
234
+ <circle cx="21" cy="3" r="1" fill="rgba(255,255,255,.5)"/>
235
+ <circle cx="19" cy="20" r="1.5" fill="rgba(99,102,241,.6)"/>
236
+ <circle cx="5" cy="21" r="1" fill="rgba(255,255,255,.4)"/>
237
+ <circle cx="22" cy="13" r="1" fill="rgba(255,255,255,.35)"/>
238
+ <line x1="4" y1="5" x2="12" y2="12" stroke="rgba(99,102,241,.35)" stroke-width=".8"/>
239
+ <line x1="12" y1="12" x2="21" y2="3" stroke="rgba(99,102,241,.25)" stroke-width=".8"/>
240
+ <line x1="12" y1="12" x2="19" y2="20" stroke="rgba(99,102,241,.35)" stroke-width=".8"/>
241
+ <line x1="12" y1="12" x2="5" y2="21" stroke="rgba(99,102,241,.25)" stroke-width=".8"/>
242
+ <line x1="12" y1="12" x2="22" y2="13" stroke="rgba(99,102,241,.2)" stroke-width=".8"/>
243
+ </svg>
244
+ <span class="nav-logo-text">CONSTELLAR</span>
245
+ </a>
246
+
247
+ <ul class="nav-links">
248
+ <li><a href="{{ PREDICTOR_BASE_URL }}">Home</a></li>
249
+ <li><a href="{{ PREDICTOR_BASE_URL }}predict/">Predict</a></li>
250
+ <li><a href="{{ PREDICTOR_BASE_URL }}database/">Database</a></li>
251
+ <li><a href="{{ CHATBOT_BASE_URL }}">Chatbot</a></li>
252
+ <li><a href="{{ LOCATOR_BASE_URL }}">Locator</a></li>
253
+ </ul>
254
+
255
+ <div class="nav-actions">
256
+ <a href="{{ PREDICTOR_BASE_URL }}upload/" class="btn-ghost">Upload Image</a>
257
+ <a href="{{ PREDICTOR_BASE_URL }}detect/" class="btn-accent">Live Detection →</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  </div>
259
+
260
+ <button class="nav-mobile-btn" id="mobileMenuBtn" aria-label="Menu">
261
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
262
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
263
+ </svg>
264
+ </button>
265
+ </div>
266
+
267
+ <nav class="nav-mobile-menu" id="mobileMenu">
268
+ <a href="{{ PREDICTOR_BASE_URL }}">Home</a>
269
+ <a href="{{ PREDICTOR_BASE_URL }}predict/">Predict</a>
270
+ <a href="{{ PREDICTOR_BASE_URL }}database/">Database</a>
271
+ <a href="{{ CHATBOT_BASE_URL }}">Chatbot</a>
272
+ <a href="{{ LOCATOR_BASE_URL }}">Locator</a>
273
+ <div class="mobile-actions">
274
+ <a href="{{ PREDICTOR_BASE_URL }}upload/" class="btn-ghost">Upload</a>
275
+ <a href="{{ PREDICTOR_BASE_URL }}detect/" class="btn-accent">Detect</a>
 
 
 
 
 
 
276
  </div>
277
+ </nav>
278
+ </header>
279
+
280
+ <main>{% block content %}{% endblock %}</main>
281
+
282
+ <footer class="site-footer">
283
+ <div class="footer-inner">
284
+ <div class="footer-bottom">
285
+ <span>© 2026 Constellar · Constellation Predictor</span>
286
+ <span>Powered by Gemini AI + OpenAI Whisper</span>
287
+ </div>
288
+ </div>
289
+ </footer>
290
+
291
+ </div>
292
+
293
+ <script>
294
+ const btn = document.getElementById('mobileMenuBtn');
295
+ const menu = document.getElementById('mobileMenu');
296
+ btn.addEventListener('click', () => menu.classList.toggle('open'));
297
+ </script>
298
+ {% block extra_js %}{% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  </body>
300
  </html>
chatbot/templates/index.html CHANGED
@@ -1,552 +1,569 @@
1
  {% extends 'base.html' %}
2
- {% block content%}
3
- <style>
4
- @keyframes twinkle {
5
- 0%, 100% { opacity: 0.3; transform: scale(1); }
6
- 50% { opacity: 1; transform: scale(1.2); }
7
- }
8
-
9
- @keyframes float {
10
- 0%, 100% { transform: translateY(0px); }
11
- 50% { transform: translateY(-10px); }
12
- }
13
-
14
- @keyframes pulse {
15
- 0%, 100% { opacity: 1; }
16
- 50% { opacity: 0.5; }
17
- }
18
-
19
- .star {
20
- animation: twinkle 3s infinite;
21
- }
22
-
23
- .star:nth-child(odd) {
24
- animation-delay: 1s;
25
- }
26
-
27
- .star:nth-child(3n) {
28
- animation-delay: 2s;
29
- }
30
-
31
- .floating {
32
- animation: float 6s ease-in-out infinite;
33
- }
34
-
35
- .listening {
36
- animation: pulse 1.5s ease-in-out infinite;
37
- }
38
-
39
- .gradient-text {
40
- background: linear-gradient(45deg, #60a5fa, #a78bfa, #c084fc);
41
- background-clip: text;
42
- -webkit-background-clip: text;
43
- -webkit-text-fill-color: transparent;
44
- }
45
-
46
- .glass {
47
- backdrop-filter: blur(16px);
48
- -webkit-backdrop-filter: blur(16px);
49
- }
50
-
51
- .chat-scroll::-webkit-scrollbar {
52
- width: 6px;
53
- }
54
-
55
- .chat-scroll::-webkit-scrollbar-track {
56
- background: rgba(255, 255, 255, 0.1);
57
- border-radius: 3px;
58
- }
59
-
60
- .chat-scroll::-webkit-scrollbar-thumb {
61
- background: rgba(147, 112, 219, 0.5);
62
- border-radius: 3px;
63
- }
64
-
65
- .chat-scroll::-webkit-scrollbar-thumb:hover {
66
- background: rgba(147, 112, 219, 0.7);
67
- }
68
- </style>
69
- </head>
70
- <body class="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-purple-900 text-white overflow-x-hidden">
71
- <!-- Starfield Background -->
72
- <div class="fixed inset-0 z-0" id="starfield"></div>
73
-
74
- <!-- Main Container -->
75
- <div class="relative z-10 min-h-screen">
76
- <!-- Header -->
77
- <header class="text-center py-12 px-6">
78
- <div class="glass bg-white bg-opacity-5 rounded-3xl p-8 max-w-4xl mx-auto border border-white border-opacity-10">
79
- <h1 class="text-6xl font-bold mb-4 gradient-text floating">
80
- 🌟 Constellation AI Assistant
81
- </h1>
82
- <p class="text-xl text-blue-200 opacity-90">
83
- Your guide to the mysteries of the universe ✨
84
- </p>
85
- </div>
86
- </header>
87
-
88
- <!-- Chat Interface -->
89
- <main class="max-w-4xl mx-auto px-6 pb-12">
90
- <div class="glass bg-black bg-opacity-20 rounded-3xl p-8 border border-white border-opacity-10">
91
- <!-- Chat Messages Area -->
92
- <div id="chatMessages" class="h-96 overflow-y-auto mb-6 space-y-4 chat-scroll rounded-2xl p-4 bg-black bg-opacity-20">
93
- <!-- Welcome Message -->
94
- <div class="flex items-start space-x-3">
95
- <div class="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-lg">
96
- 🤖
97
- </div>
98
- <div class="glass bg-blue-900 bg-opacity-30 rounded-2xl rounded-tl-none p-4 max-w-md border border-blue-400 border-opacity-30">
99
- <p class="text-blue-100">
100
- Welcome to the Constellation AI Assistant! 🌌 I'm here to help you explore the wonders of astronomy, constellations, and space. Ask me anything about the night sky! ⭐
101
- </p>
102
- </div>
103
- </div>
104
- </div>
105
-
106
- <!-- Input Area -->
107
- <div class="space-y-4">
108
- <div class="flex space-x-3">
109
- <input
110
- type="text"
111
- id="messageInput"
112
- placeholder="Ask me about constellations, stars, planets..."
113
- class="flex-1 p-4 rounded-2xl glass bg-white bg-opacity-10 border border-white border-opacity-20 text-white placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent"
114
- >
115
- <button
116
- id="micBtn"
117
- class="px-4 py-4 glass bg-red-600 bg-opacity-30 rounded-2xl hover:bg-opacity-50 transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-red-400 border border-red-400 border-opacity-30"
118
- title="Voice Input (English/Hindi)"
119
- >
120
- 🎤
121
- </button>
122
- <button
123
- id="sendBtn"
124
- class="px-6 py-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-2xl hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-400"
125
- >
126
- 🚀 Send
127
- </button>
128
- </div>
129
-
130
- <!-- Controls -->
131
- <div class="flex flex-wrap gap-3 justify-center">
132
- <button
133
- id="speechToggle"
134
- class="px-4 py-2 glass bg-blue-600 bg-opacity-30 rounded-xl hover:bg-opacity-50 transition-all duration-300 border border-blue-400 border-opacity-30"
135
- >
136
- 🔇 Speech: OFF
137
- </button>
138
-
139
- <button
140
- id="speakBtn"
141
- class="px-4 py-2 glass bg-green-600 bg-opacity-30 rounded-xl hover:bg-opacity-50 transition-all duration-300 border border-green-400 border-opacity-30 disabled:opacity-50 disabled:cursor-not-allowed"
142
- disabled
143
- >
144
- 🔊 Speak Last Response
145
- </button>
146
-
147
- <button
148
- id="clearBtn"
149
- class="px-4 py-2 glass bg-red-600 bg-opacity-30 rounded-xl hover:bg-opacity-50 transition-all duration-300 border border-red-400 border-opacity-30"
150
- >
151
- 🗑️ Clear Chat
152
- </button>
153
-
154
- <select
155
- id="languageSelect"
156
- class="px-4 py-2 glass bg-white bg-opacity-10 rounded-xl border border-white border-opacity-20 text-white focus:outline-none focus:ring-2 focus:ring-purple-400"
157
- >
158
- <option value="en" class="bg-gray-800">English 🇺🇸</option>
159
- <option value="hi" class="bg-gray-800">Hindi 🇮🇳</option>
160
- </select>
161
- </div>
162
- </div>
163
-
164
- <!-- Loading Indicator -->
165
- <div id="loadingIndicator" class="hidden mt-4 text-center">
166
- <div class="inline-flex items-center space-x-2 glass bg-white bg-opacity-10 rounded-xl px-4 py-2">
167
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-400"></div>
168
- <span class="text-purple-200">Consulting the stars...</span>
169
- </div>
170
- </div>
171
-
172
- <!-- Voice Status -->
173
- <div id="voiceStatus" class="hidden mt-4 text-center">
174
- <div class="inline-flex items-center space-x-2 glass bg-red-600 bg-opacity-20 rounded-xl px-4 py-2 border border-red-400 border-opacity-30">
175
- <div class="w-3 h-3 bg-red-400 rounded-full listening"></div>
176
- <span class="text-red-200">Recording... Speak now!</span>
177
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  </div>
179
-
180
- <!-- Processing Status -->
181
- <div id="processingStatus" class="hidden mt-4 text-center">
182
- <div class="inline-flex items-center space-x-2 glass bg-blue-600 bg-opacity-20 rounded-xl px-4 py-2 border border-blue-400 border-opacity-30">
183
- <div class="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-400"></div>
184
- <span class="text-blue-200">Processing your voice...</span>
185
- </div>
186
  </div>
187
  </div>
188
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  </div>
190
 
191
- <script>
192
- // Create starfield background
193
- function createStarfield() {
194
- const starfield = document.getElementById('starfield');
195
- const numberOfStars = 100;
196
-
197
- for (let i = 0; i < numberOfStars; i++) {
198
- const star = document.createElement('div');
199
- star.className = 'star absolute bg-white rounded-full';
200
-
201
- const size = Math.random() * 3 + 1;
202
- star.style.width = size + 'px';
203
- star.style.height = size + 'px';
204
- star.style.left = Math.random() * 100 + '%';
205
- star.style.top = Math.random() * 100 + '%';
206
-
207
- starfield.appendChild(star);
208
- }
209
- }
210
 
211
- // Global variables
212
- let lastAIResponse = '';
213
- let speechEnabled = false;
214
- let mediaRecorder = null;
215
- let audioChunks = [];
216
- let isRecording = false;
217
-
218
- // DOM elements
219
- const messagesContainer = document.getElementById('chatMessages');
220
- const messageInput = document.getElementById('messageInput');
221
- const sendBtn = document.getElementById('sendBtn');
222
- const micBtn = document.getElementById('micBtn');
223
- const speechToggle = document.getElementById('speechToggle');
224
- const speakBtn = document.getElementById('speakBtn');
225
- const clearBtn = document.getElementById('clearBtn');
226
- const languageSelect = document.getElementById('languageSelect');
227
- const loadingIndicator = document.getElementById('loadingIndicator');
228
- const voiceStatus = document.getElementById('voiceStatus');
229
- const processingStatus = document.getElementById('processingStatus');
230
-
231
- // Initialize audio recording
232
- async function initAudioRecording() {
233
- try {
234
- const stream = await navigator.mediaDevices.getUserMedia({
235
- audio: {
236
- sampleRate: 16000,
237
- channelCount: 1,
238
- echoCancellation: true,
239
- noiseSuppression: true
240
- }
241
- });
242
-
243
- mediaRecorder = new MediaRecorder(stream, {
244
- mimeType: 'audio/webm;codecs=opus'
245
- });
246
-
247
- mediaRecorder.ondataavailable = (event) => {
248
- if (event.data.size > 0) {
249
- audioChunks.push(event.data);
250
- }
251
- };
252
-
253
- mediaRecorder.onstop = async () => {
254
- const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
255
- audioChunks = [];
256
- await processAudioInput(audioBlob);
257
- };
258
-
259
- return true;
260
- } catch (error) {
261
- console.error('Failed to initialize audio recording:', error);
262
- micBtn.disabled = true;
263
- micBtn.title = 'Microphone access denied or not available';
264
- return false;
265
- }
266
- }
267
 
268
- async function processAudioInput(audioBlob) {
269
- try {
270
- // Show processing status
271
- voiceStatus.classList.add('hidden');
272
- processingStatus.classList.remove('hidden');
273
- micBtn.disabled = true;
274
-
275
- // Prepare form data
276
- const formData = new FormData();
277
- formData.append('audio', audioBlob, 'recording.webm');
278
- formData.append('language', languageSelect.value);
279
-
280
- // Get CSRF token
281
- const csrfToken = getCookie('csrftoken');
282
-
283
- // Send to speech-to-text endpoint
284
- const response = await fetch("{% url 'speech_to_text'%}", {
285
- method: 'POST',
286
- headers: {
287
- 'X-CSRFToken': csrfToken,
288
- },
289
- body: formData
290
- });
291
-
292
- const data = await response.json();
293
-
294
- if (data.success && data.transcript) {
295
- // Add voice message to chat
296
- addMessage(`🎤 "${data.transcript}"`, true, true);
297
-
298
- // Set the transcript in the input field
299
- messageInput.value = data.transcript;
300
-
301
- // Automatically send the message
302
- setTimeout(() => sendMessage(), 500);
303
- } else {
304
- addMessage('Sorry, I couldn\'t understand your speech. Please try again.');
305
- }
306
-
307
- } catch (error) {
308
- console.error('Error processing audio:', error);
309
- addMessage('Error processing your voice. Please try again.');
310
- } finally {
311
- // Hide processing status and re-enable microphone
312
- processingStatus.classList.add('hidden');
313
- micBtn.disabled = false;
314
- }
315
- }
316
 
317
- function toggleSpeech() {
318
- speechEnabled = !speechEnabled;
319
- if (speechEnabled) {
320
- speechToggle.innerHTML = '🔊 Speech: ON';
321
- speechToggle.classList.remove('bg-blue-600');
322
- speechToggle.classList.add('bg-green-600');
323
- speechToggle.classList.remove('border-blue-400');
324
- speechToggle.classList.add('border-green-400');
325
- } else {
326
- speechToggle.innerHTML = '🔇 Speech: OFF';
327
- speechToggle.classList.remove('bg-green-600');
328
- speechToggle.classList.add('bg-blue-600');
329
- speechToggle.classList.remove('border-green-400');
330
- speechToggle.classList.add('border-blue-400');
331
- }
332
- }
333
 
334
- async function startRecording() {
335
- if (!mediaRecorder) {
336
- const initialized = await initAudioRecording();
337
- if (!initialized) return;
338
- }
339
-
340
- if (isRecording) {
341
- // Stop recording
342
- mediaRecorder.stop();
343
- isRecording = false;
344
- voiceStatus.classList.add('hidden');
345
- micBtn.innerHTML = '🎤';
346
- micBtn.classList.remove('listening');
347
- } else {
348
- // Start recording
349
  audioChunks = [];
350
- mediaRecorder.start();
351
- isRecording = true;
352
- voiceStatus.classList.remove('hidden');
353
- micBtn.innerHTML = '🔴';
354
- micBtn.classList.add('listening');
355
-
356
- // Auto-stop after 30 seconds
357
- setTimeout(() => {
358
- if (isRecording) {
359
- mediaRecorder.stop();
360
- isRecording = false;
361
- voiceStatus.classList.add('hidden');
362
- micBtn.innerHTML = '🎤';
363
- micBtn.classList.remove('listening');
364
- }
365
- }, 30000);
366
- }
367
  }
 
 
 
 
 
 
 
 
 
 
 
368
 
369
- function addMessage(content, isUser = false, isVoice = false) {
370
- const messageDiv = document.createElement('div');
371
- messageDiv.className = 'flex items-start space-x-3';
372
-
373
- if (isUser) {
374
- messageDiv.className += ' justify-end';
375
- const iconClass = isVoice ? '🎤' : '👤';
376
- const bgClass = isVoice ? 'bg-gradient-to-r from-red-500 to-pink-500' : 'bg-gradient-to-r from-purple-500 to-pink-500';
377
- messageDiv.innerHTML = `
378
- <div class="glass bg-purple-900 bg-opacity-40 rounded-2xl rounded-tr-none p-4 max-w-md border border-purple-400 border-opacity-30">
379
- <p class="text-purple-100">${content}</p>
380
- </div>
381
- <div class="w-10 h-10 rounded-full ${bgClass} flex items-center justify-center text-lg">
382
- ${iconClass}
383
- </div>
384
- `;
385
  } else {
386
- messageDiv.innerHTML = `
387
- <div class="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-lg">
388
- 🤖
389
- </div>
390
- <div class="glass bg-blue-900 bg-opacity-30 rounded-2xl rounded-tl-none p-4 max-w-md border border-blue-400 border-opacity-30">
391
- <p class="text-blue-100">${content}</p>
392
- </div>
393
- `;
394
  }
395
-
396
- messagesContainer.appendChild(messageDiv);
397
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
 
 
398
  }
 
399
 
400
- function showLoading(show) {
401
- loadingIndicator.classList.toggle('hidden', !show);
402
- sendBtn.disabled = show;
403
- messageInput.disabled = show;
404
- if (!isRecording) {
405
- micBtn.disabled = show;
406
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
 
408
 
409
- async function sendMessage() {
410
- const message = messageInput.value.trim();
411
- if (!message) return;
412
 
413
- // Only add text message if it wasn't already added by voice input
414
- if (!message.startsWith('🎤')) {
415
- addMessage(message, true);
416
- }
417
- messageInput.value = '';
418
- showLoading(true);
419
-
420
- try {
421
- // Get CSRF token
422
- const csrfToken = getCookie('csrftoken');
423
-
424
- const response = await fetch("{% url 'ask_ai'%}", {
425
- method: 'POST',
426
- headers: {
427
- 'Content-Type': 'application/json',
428
- 'X-CSRFToken': csrfToken,
429
- },
430
- body: JSON.stringify({
431
- message: message.replace('🎤 "', '').replace('"', '') // Clean voice input formatting
432
- })
433
- });
434
-
435
- const data = await response.json();
436
-
437
- if (data.success) {
438
- lastAIResponse = data.reply;
439
- addMessage(data.reply);
440
- speakBtn.disabled = false;
441
-
442
- // Auto-speak if speech is enabled
443
- if (speechEnabled) {
444
- setTimeout(() => speakText(), 500);
445
- }
446
- } else {
447
- addMessage('Sorry, I encountered an error: ' + (data.error || 'Unknown error'));
448
- }
449
- } catch (error) {
450
- addMessage('Connection error. Please try again.');
451
- console.error('Error:', error);
452
- } finally {
453
- showLoading(false);
454
- }
455
  }
456
 
457
- async function speakText() {
458
- if (!lastAIResponse || !speechEnabled) return;
459
-
460
- speakBtn.disabled = true;
461
- speakBtn.innerHTML = '🔄 Speaking...';
462
-
463
- try {
464
- // Get CSRF token
465
- const csrfToken = getCookie('csrftoken');
466
-
467
- const response = await fetch("{% url 'text_to_speech'%}", {
468
- method: 'POST',
469
- headers: {
470
- 'Content-Type': 'application/json',
471
- 'X-CSRFToken': csrfToken,
472
- },
473
- body: JSON.stringify({
474
- text: lastAIResponse,
475
- language: languageSelect.value
476
- })
477
- });
478
-
479
- const data = await response.json();
480
-
481
- if (data.success) {
482
- const audio = new Audio('data:audio/mp3;base64,' + data.audio_data);
483
- audio.play();
484
-
485
- audio.onended = () => {
486
- speakBtn.disabled = false;
487
- speakBtn.innerHTML = '🔊 Speak Last Response';
488
- };
489
- } else {
490
- throw new Error(data.error || 'Failed to generate speech');
491
- }
492
- } catch (error) {
493
- console.error('Speech synthesis failed:', error);
494
- addMessage('Sorry, speech synthesis failed.');
 
495
  speakBtn.disabled = false;
496
- speakBtn.innerHTML = '🔊 Speak Last Response';
 
 
497
  }
 
 
 
 
498
  }
 
499
 
500
- // Helper function to get CSRF token
501
- function getCookie(name) {
502
- let cookieValue = null;
503
- if (document.cookie && document.cookie !== '') {
504
- const cookies = document.cookie.split(';');
505
- for (let i = 0; i < cookies.length; i++) {
506
- const cookie = cookies[i].trim();
507
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
508
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
509
- break;
510
- }
511
- }
 
 
 
 
 
512
  }
513
- return cookieValue;
 
 
514
  }
 
515
 
516
- function clearChat() {
517
- messagesContainer.innerHTML = `
518
- <div class="flex items-start space-x-3">
519
- <div class="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-lg">
520
- 🤖
521
- </div>
522
- <div class="glass bg-blue-900 bg-opacity-30 rounded-2xl rounded-tl-none p-4 max-w-md border border-blue-400 border-opacity-30">
523
- <p class="text-blue-100">
524
- Welcome back! 🌌 I'm ready to help you explore the cosmos again. What would you like to know about the stars? ⭐
525
- </p>
526
- </div>
527
- </div>
528
- `;
529
- lastAIResponse = '';
530
- speakBtn.disabled = true;
531
  }
 
 
532
 
533
- // Event listeners
534
- sendBtn.addEventListener('click', sendMessage);
535
- micBtn.addEventListener('click', startRecording);
536
- speechToggle.addEventListener('click', toggleSpeech);
537
- speakBtn.addEventListener('click', speakText);
538
- clearBtn.addEventListener('click', clearChat);
 
 
 
 
 
 
539
 
540
- messageInput.addEventListener('keypress', (e) => {
541
- if (e.key === 'Enter' && !sendBtn.disabled) {
542
- sendMessage();
543
- }
544
- });
545
-
546
- // Initialize
547
- createStarfield();
548
-
549
- // Auto-focus input
550
- messageInput.focus();
551
- </script>
552
- {% endblock %}
 
1
  {% extends 'base.html' %}
2
+
3
+ {% block title %}Constellar — AI Astronomy Chatbot{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <style>
7
+ :root {
8
+ --bg: #060d1f;
9
+ --surface: rgba(255,255,255,0.03);
10
+ --border: rgba(255,255,255,0.07);
11
+ --accent: #6366f1;
12
+ --accent-dim: rgba(99,102,241,0.15);
13
+ --text: #dde4f0;
14
+ --muted: rgba(221,228,240,0.45);
15
+ }
16
+
17
+ .chat-wrap {
18
+ max-width: 780px;
19
+ margin: 0 auto;
20
+ padding: 2rem 1.5rem 3rem;
21
+ }
22
+
23
+ /* page header */
24
+ .chat-header {
25
+ text-align: center;
26
+ margin-bottom: 1.75rem;
27
+ }
28
+ .chat-header h1 {
29
+ font-family: 'Orbitron', monospace;
30
+ font-size: clamp(1.25rem, 3vw, 1.75rem);
31
+ font-weight: 900;
32
+ letter-spacing: -.01em;
33
+ margin: 0 0 .4rem;
34
+ color: #fff;
35
+ }
36
+ .chat-header p {
37
+ font-size: .8125rem;
38
+ color: var(--muted);
39
+ margin: 0;
40
+ }
41
+
42
+ /* chat container */
43
+ .chat-container {
44
+ background: rgba(255,255,255,.025);
45
+ border: 1px solid var(--border);
46
+ border-radius: 14px;
47
+ overflow: hidden;
48
+ }
49
+
50
+ /* message list */
51
+ #chatMessages {
52
+ height: 420px;
53
+ overflow-y: auto;
54
+ padding: 1.25rem;
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: .875rem;
58
+ scroll-behavior: smooth;
59
+ }
60
+ #chatMessages::-webkit-scrollbar { width: 5px; }
61
+ #chatMessages::-webkit-scrollbar-track { background: transparent; }
62
+ #chatMessages::-webkit-scrollbar-thumb {
63
+ background: rgba(99,102,241,.3);
64
+ border-radius: 3px;
65
+ }
66
+
67
+ /* bubbles */
68
+ .msg-row { display: flex; gap: .625rem; align-items: flex-end; }
69
+ .msg-row.user { flex-direction: row-reverse; }
70
+
71
+ .msg-avatar {
72
+ width: 30px; height: 30px;
73
+ border-radius: 8px;
74
+ flex-shrink: 0;
75
+ display: flex; align-items: center; justify-content: center;
76
+ }
77
+ .msg-avatar.ai {
78
+ background: rgba(99,102,241,.2);
79
+ border: 1px solid rgba(99,102,241,.3);
80
+ }
81
+ .msg-avatar.human {
82
+ background: rgba(255,255,255,.06);
83
+ border: 1px solid rgba(255,255,255,.1);
84
+ }
85
+
86
+ .bubble {
87
+ max-width: 72%;
88
+ padding: .7rem 1rem;
89
+ border-radius: 10px;
90
+ font-size: .875rem;
91
+ line-height: 1.65;
92
+ }
93
+ .bubble.ai {
94
+ background: rgba(99,102,241,.1);
95
+ border: 1px solid rgba(99,102,241,.18);
96
+ color: var(--text);
97
+ border-bottom-left-radius: 3px;
98
+ }
99
+ .bubble.user {
100
+ background: rgba(255,255,255,.06);
101
+ border: 1px solid rgba(255,255,255,.1);
102
+ color: var(--text);
103
+ border-bottom-right-radius: 3px;
104
+ }
105
+
106
+ /* input area */
107
+ .chat-input-area {
108
+ border-top: 1px solid var(--border);
109
+ padding: 1rem 1.25rem;
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: .75rem;
113
+ }
114
+
115
+ .input-row {
116
+ display: flex;
117
+ gap: .625rem;
118
+ align-items: center;
119
+ }
120
+
121
+ #messageInput {
122
+ flex: 1;
123
+ background: rgba(255,255,255,.04);
124
+ border: 1px solid var(--border);
125
+ border-radius: 8px;
126
+ padding: .65rem 1rem;
127
+ font-size: .875rem;
128
+ font-family: inherit;
129
+ color: var(--text);
130
+ outline: none;
131
+ transition: border-color .15s;
132
+ }
133
+ #messageInput:focus { border-color: rgba(99,102,241,.4); }
134
+ #messageInput::placeholder { color: var(--muted); }
135
+
136
+ .icon-btn {
137
+ width: 38px; height: 38px;
138
+ border-radius: 8px;
139
+ border: 1px solid var(--border);
140
+ background: rgba(255,255,255,.04);
141
+ color: var(--muted);
142
+ cursor: pointer;
143
+ display: flex; align-items: center; justify-content: center;
144
+ transition: all .15s;
145
+ flex-shrink: 0;
146
+ }
147
+ .icon-btn:hover { border-color: rgba(255,255,255,.18); color: #fff; background: rgba(255,255,255,.07); }
148
+ .icon-btn:disabled { opacity: .4; cursor: not-allowed; }
149
+ .icon-btn.recording {
150
+ border-color: rgba(239,68,68,.4);
151
+ background: rgba(239,68,68,.12);
152
+ color: #f87171;
153
+ }
154
+
155
+ .send-btn {
156
+ padding: .6rem 1.25rem;
157
+ background: var(--accent);
158
+ border: none;
159
+ border-radius: 8px;
160
+ color: #fff;
161
+ font-size: .8125rem;
162
+ font-weight: 600;
163
+ font-family: inherit;
164
+ cursor: pointer;
165
+ transition: all .15s;
166
+ flex-shrink: 0;
167
+ }
168
+ .send-btn:hover { background: #818cf8; }
169
+ .send-btn:disabled { opacity: .5; cursor: not-allowed; }
170
+
171
+ /* control row */
172
+ .control-row {
173
+ display: flex;
174
+ gap: .5rem;
175
+ flex-wrap: wrap;
176
+ align-items: center;
177
+ }
178
+
179
+ .ctrl-btn {
180
+ font-size: .75rem;
181
+ font-weight: 500;
182
+ padding: .35rem .75rem;
183
+ border-radius: 6px;
184
+ border: 1px solid var(--border);
185
+ background: rgba(255,255,255,.03);
186
+ color: var(--muted);
187
+ cursor: pointer;
188
+ transition: all .15s;
189
+ font-family: inherit;
190
+ }
191
+ .ctrl-btn:hover { border-color: rgba(255,255,255,.15); color: #fff; }
192
+ .ctrl-btn.active { border-color: rgba(99,102,241,.4); background: var(--accent-dim); color: #a5b4fc; }
193
+ .ctrl-btn:disabled { opacity: .4; cursor: not-allowed; }
194
+
195
+ .lang-select {
196
+ font-size: .75rem;
197
+ padding: .35rem .625rem;
198
+ border-radius: 6px;
199
+ border: 1px solid var(--border);
200
+ background: rgba(255,255,255,.03);
201
+ color: var(--muted);
202
+ cursor: pointer;
203
+ font-family: inherit;
204
+ outline: none;
205
+ }
206
+ .lang-select:focus { border-color: rgba(99,102,241,.4); }
207
+
208
+ /* status bars */
209
+ .status-bar {
210
+ display: none;
211
+ align-items: center;
212
+ gap: .5rem;
213
+ padding: .45rem .875rem;
214
+ border-radius: 7px;
215
+ font-size: .75rem;
216
+ margin: 0 1.25rem .75rem;
217
+ }
218
+ .status-bar.visible { display: flex; }
219
+ .status-bar.loading {
220
+ background: rgba(99,102,241,.1);
221
+ border: 1px solid rgba(99,102,241,.2);
222
+ color: #a5b4fc;
223
+ }
224
+ .status-bar.recording-bar {
225
+ background: rgba(239,68,68,.1);
226
+ border: 1px solid rgba(239,68,68,.2);
227
+ color: #fca5a5;
228
+ }
229
+ .status-bar.processing-bar {
230
+ background: rgba(59,130,246,.1);
231
+ border: 1px solid rgba(59,130,246,.2);
232
+ color: #93c5fd;
233
+ }
234
+
235
+ .dot-pulse {
236
+ width: 8px; height: 8px;
237
+ border-radius: 50%;
238
+ background: currentColor;
239
+ animation: pulse 1.2s ease-in-out infinite;
240
+ }
241
+ .spin {
242
+ width: 14px; height: 14px;
243
+ border-radius: 50%;
244
+ border: 2px solid rgba(255,255,255,.15);
245
+ border-top-color: currentColor;
246
+ animation: spin .7s linear infinite;
247
+ }
248
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
249
+ @keyframes spin { to{transform:rotate(360deg)} }
250
+ </style>
251
+ {% endblock %}
252
+
253
+ {% block content %}
254
+ <div class="chat-wrap">
255
+
256
+ <!-- header -->
257
+ <div class="chat-header">
258
+ <div style="display:inline-flex;align-items:center;gap:.5rem;background:var(--accent-dim);border:1px solid rgba(99,102,241,.25);border-radius:6px;padding:.25rem .75rem;font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#a5b4fc;margin-bottom:.875rem;">
259
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path stroke-linecap="round" d="M12 8v4l3 3"/></svg>
260
+ Gemini AI + Whisper STT
261
+ </div>
262
+ <h1>Astronomy Chatbot</h1>
263
+ <p>Ask anything about constellations, stars, planets, or space mythology</p>
264
+ </div>
265
+
266
+ <!-- chat box -->
267
+ <div class="chat-container">
268
+
269
+ <!-- messages -->
270
+ <div id="chatMessages">
271
+ <div class="msg-row">
272
+ <div class="msg-avatar ai">
273
+ <svg width="14" height="14" fill="none" stroke="#a5b4fc" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.5"/><circle cx="4" cy="5" r="1.5"/><circle cx="21" cy="3" r="1"/><line x1="4" y1="5" x2="12" y2="12" stroke="rgba(165,180,252,.5)"/><line x1="12" y1="12" x2="21" y2="3" stroke="rgba(165,180,252,.4)"/></svg>
274
  </div>
275
+ <div class="bubble ai">
276
+ Hello! I'm your astronomy assistant. Ask me about constellations, their mythology, best viewing times, or anything else about the night sky.
 
 
 
 
 
277
  </div>
278
  </div>
279
+ </div>
280
+
281
+ <!-- status bars -->
282
+ <div class="status-bar loading" id="loadingIndicator">
283
+ <div class="spin"></div>
284
+ <span>Consulting the stars…</span>
285
+ </div>
286
+ <div class="status-bar recording-bar" id="voiceStatus">
287
+ <div class="dot-pulse"></div>
288
+ <span>Recording — speak now</span>
289
+ </div>
290
+ <div class="status-bar processing-bar" id="processingStatus">
291
+ <div class="spin"></div>
292
+ <span>Transcribing audio…</span>
293
+ </div>
294
+
295
+ <!-- input -->
296
+ <div class="chat-input-area">
297
+ <div class="input-row">
298
+ <!-- mic -->
299
+ <button class="icon-btn" id="micBtn" title="Voice input">
300
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
301
+ <path stroke-linecap="round" d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
302
+ <path stroke-linecap="round" d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
303
+ </svg>
304
+ </button>
305
+
306
+ <input type="text" id="messageInput"
307
+ placeholder="Ask about constellations, stars, planets…">
308
+
309
+ <button class="send-btn" id="sendBtn">Send</button>
310
+ </div>
311
+
312
+ <div class="control-row">
313
+ <button class="ctrl-btn" id="speechToggle">
314
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:3px;"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
315
+ Speech off
316
+ </button>
317
+ <button class="ctrl-btn" id="speakBtn" disabled>
318
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:3px;"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="15.54" y1="8.46" x2="15.54" y2="8.46"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
319
+ Speak response
320
+ </button>
321
+ <button class="ctrl-btn" id="clearBtn">
322
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path stroke-linecap="round" d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
323
+ Clear
324
+ </button>
325
+ <select class="lang-select" id="languageSelect">
326
+ <option value="en">English</option>
327
+ <option value="hi">Hindi</option>
328
+ </select>
329
+ </div>
330
+ </div>
331
  </div>
332
 
333
+ </div>
334
+ {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ {% block extra_js %}
337
+ <script>
338
+ let lastAIResponse = '';
339
+ let speechEnabled = false;
340
+ let mediaRecorder = null;
341
+ let audioChunks = [];
342
+ let isRecording = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
+ const messagesContainer = document.getElementById('chatMessages');
345
+ const messageInput = document.getElementById('messageInput');
346
+ const sendBtn = document.getElementById('sendBtn');
347
+ const micBtn = document.getElementById('micBtn');
348
+ const speechToggle = document.getElementById('speechToggle');
349
+ const speakBtn = document.getElementById('speakBtn');
350
+ const clearBtn = document.getElementById('clearBtn');
351
+ const languageSelect = document.getElementById('languageSelect');
352
+ const loadingIndicator = document.getElementById('loadingIndicator');
353
+ const voiceStatus = document.getElementById('voiceStatus');
354
+ const processingStatus = document.getElementById('processingStatus');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ function setStatus(el, visible) {
357
+ el.classList.toggle('visible', visible);
358
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
+ async function initAudioRecording() {
361
+ try {
362
+ const stream = await navigator.mediaDevices.getUserMedia({
363
+ audio: { sampleRate:16000, channelCount:1, echoCancellation:true, noiseSuppression:true }
364
+ });
365
+ mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
366
+ mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
367
+ mediaRecorder.onstop = async () => {
368
+ const blob = new Blob(audioChunks, { type: 'audio/webm' });
 
 
 
 
 
 
369
  audioChunks = [];
370
+ await processAudioInput(blob);
371
+ };
372
+ return true;
373
+ } catch {
374
+ micBtn.disabled = true;
375
+ return false;
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
+ }
378
+
379
+ async function processAudioInput(audioBlob) {
380
+ setStatus(voiceStatus, false);
381
+ setStatus(processingStatus, true);
382
+ micBtn.disabled = true;
383
+
384
+ try {
385
+ const formData = new FormData();
386
+ formData.append('audio', audioBlob, 'recording.webm');
387
+ formData.append('language', languageSelect.value);
388
 
389
+ const res = await fetch("{% url 'speech_to_text' %}", {
390
+ method: 'POST',
391
+ headers: { 'X-CSRFToken': getCookie('csrftoken') },
392
+ body: formData
393
+ });
394
+ const data = await res.json();
395
+
396
+ if (data.success && data.transcript) {
397
+ addMessage(`🎤 "${data.transcript}"`, true, true);
398
+ messageInput.value = data.transcript;
399
+ setTimeout(() => sendMessage(), 500);
 
 
 
 
 
400
  } else {
401
+ addMessage('Sorry, I couldn\'t understand your speech. Please try again.');
 
 
 
 
 
 
 
402
  }
403
+ } catch {
404
+ addMessage('Error processing your voice. Please try again.');
405
+ } finally {
406
+ setStatus(processingStatus, false);
407
+ micBtn.disabled = false;
408
  }
409
+ }
410
 
411
+ function toggleSpeech() {
412
+ speechEnabled = !speechEnabled;
413
+ speechToggle.classList.toggle('active', speechEnabled);
414
+ speechToggle.innerHTML = speechEnabled
415
+ ? '<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:3px;"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg> Speech on'
416
+ : '<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:3px;"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg> Speech off';
417
+ }
418
+
419
+ async function startRecording() {
420
+ if (!mediaRecorder) {
421
+ const ok = await initAudioRecording();
422
+ if (!ok) return;
423
+ }
424
+ if (isRecording) {
425
+ mediaRecorder.stop();
426
+ isRecording = false;
427
+ setStatus(voiceStatus, false);
428
+ micBtn.classList.remove('recording');
429
+ } else {
430
+ audioChunks = [];
431
+ mediaRecorder.start();
432
+ isRecording = true;
433
+ setStatus(voiceStatus, true);
434
+ micBtn.classList.add('recording');
435
+ setTimeout(() => {
436
+ if (isRecording) {
437
+ mediaRecorder.stop();
438
+ isRecording = false;
439
+ setStatus(voiceStatus, false);
440
+ micBtn.classList.remove('recording');
441
+ }
442
+ }, 30000);
443
  }
444
+ }
445
 
446
+ function addMessage(content, isUser = false, isVoice = false) {
447
+ const row = document.createElement('div');
448
+ row.className = 'msg-row' + (isUser ? ' user' : '');
449
 
450
+ const avatar = document.createElement('div');
451
+ avatar.className = 'msg-avatar ' + (isUser ? 'human' : 'ai');
452
+
453
+ if (isUser) {
454
+ avatar.innerHTML = '<svg width="14" height="14" fill="none" stroke="rgba(221,228,240,.5)" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
455
+ } else {
456
+ avatar.innerHTML = '<svg width="14" height="14" fill="none" stroke="#a5b4fc" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.5"/><circle cx="4" cy="5" r="1.5"/><circle cx="21" cy="3" r="1"/><line x1="4" y1="5" x2="12" y2="12" stroke="rgba(165,180,252,.5)"/><line x1="12" y1="12" x2="21" y2="3" stroke="rgba(165,180,252,.4)"/></svg>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  }
458
 
459
+ const bubble = document.createElement('div');
460
+ bubble.className = 'bubble ' + (isUser ? 'user' : 'ai');
461
+ bubble.textContent = content;
462
+
463
+ row.appendChild(avatar);
464
+ row.appendChild(bubble);
465
+ messagesContainer.appendChild(row);
466
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
467
+ }
468
+
469
+ function showLoading(show) {
470
+ setStatus(loadingIndicator, show);
471
+ sendBtn.disabled = show;
472
+ messageInput.disabled = show;
473
+ if (!isRecording) micBtn.disabled = show;
474
+ }
475
+
476
+ async function sendMessage() {
477
+ const message = messageInput.value.trim();
478
+ if (!message) return;
479
+
480
+ if (!message.startsWith('🎤')) addMessage(message, true);
481
+ messageInput.value = '';
482
+ showLoading(true);
483
+
484
+ try {
485
+ const res = await fetch("{% url 'ask_ai' %}", {
486
+ method: 'POST',
487
+ headers: {
488
+ 'Content-Type': 'application/json',
489
+ 'X-CSRFToken': getCookie('csrftoken')
490
+ },
491
+ body: JSON.stringify({ message: message.replace(/🎤 "(.+)"/, '$1') })
492
+ });
493
+ const data = await res.json();
494
+
495
+ if (data.success) {
496
+ lastAIResponse = data.reply;
497
+ addMessage(data.reply);
498
  speakBtn.disabled = false;
499
+ if (speechEnabled) setTimeout(() => speakText(), 500);
500
+ } else {
501
+ addMessage('Sorry, I encountered an error: ' + (data.error || 'Unknown error'));
502
  }
503
+ } catch {
504
+ addMessage('Connection error. Please try again.');
505
+ } finally {
506
+ showLoading(false);
507
  }
508
+ }
509
 
510
+ async function speakText() {
511
+ if (!lastAIResponse || !speechEnabled) return;
512
+ speakBtn.disabled = true;
513
+
514
+ try {
515
+ const res = await fetch("{% url 'text_to_speech' %}", {
516
+ method: 'POST',
517
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') },
518
+ body: JSON.stringify({ text: lastAIResponse, language: languageSelect.value })
519
+ });
520
+ const data = await res.json();
521
+ if (data.success) {
522
+ const audio = new Audio('data:audio/mp3;base64,' + data.audio_data);
523
+ audio.play();
524
+ audio.onended = () => { speakBtn.disabled = false; };
525
+ } else {
526
+ throw new Error(data.error);
527
  }
528
+ } catch {
529
+ addMessage('Speech synthesis failed.');
530
+ speakBtn.disabled = false;
531
  }
532
+ }
533
 
534
+ function getCookie(name) {
535
+ let v = null;
536
+ if (document.cookie) {
537
+ document.cookie.split(';').forEach(c => {
538
+ const t = c.trim();
539
+ if (t.startsWith(name + '=')) v = decodeURIComponent(t.slice(name.length + 1));
540
+ });
 
 
 
 
 
 
 
 
541
  }
542
+ return v;
543
+ }
544
 
545
+ function clearChat() {
546
+ messagesContainer.innerHTML = '';
547
+ const row = document.createElement('div');
548
+ row.className = 'msg-row';
549
+ row.innerHTML = `
550
+ <div class="msg-avatar ai"><svg width="14" height="14" fill="none" stroke="#a5b4fc" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.5"/><circle cx="4" cy="5" r="1.5"/><circle cx="21" cy="3" r="1"/><line x1="4" y1="5" x2="12" y2="12" stroke="rgba(165,180,252,.5)"/><line x1="12" y1="12" x2="21" y2="3" stroke="rgba(165,180,252,.4)"/></svg></div>
551
+ <div class="bubble ai">Chat cleared. What would you like to know about the night sky?</div>
552
+ `;
553
+ messagesContainer.appendChild(row);
554
+ lastAIResponse = '';
555
+ speakBtn.disabled = true;
556
+ }
557
 
558
+ sendBtn.addEventListener('click', sendMessage);
559
+ micBtn.addEventListener('click', startRecording);
560
+ speechToggle.addEventListener('click', toggleSpeech);
561
+ speakBtn.addEventListener('click', speakText);
562
+ clearBtn.addEventListener('click', clearChat);
563
+ messageInput.addEventListener('keypress', e => {
564
+ if (e.key === 'Enter' && !sendBtn.disabled) sendMessage();
565
+ });
566
+
567
+ messageInput.focus();
568
+ </script>
569
+ {% endblock %}