ft-scout 7.0.7 → 7.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.firebase/hosting.d2Vi.cache +6 -3
- package/README.md +1 -1
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +2 -1
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +24 -11
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/appControl.d.ts.map +1 -1
- package/bin/src/engine/appControl.js +438 -79
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/firebase.json +13 -1
- package/package.json +1 -1
- package/web/app.js +768 -310
- package/web/styles.css +931 -1634
package/web/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
FrontTerrain Scout — Multi-Page Application Engine v7.0.8
|
|
3
|
+
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
6
6
|
apiKey: 'AIzaSyCMVNDgR8EejfXofUy87L7hsKxi3E_C3lg',
|
|
@@ -15,19 +15,19 @@ const TIER_DETAILS = {
|
|
|
15
15
|
name: 'Developer Free Tier',
|
|
16
16
|
badge: 'ACTIVE',
|
|
17
17
|
desc: 'Standard access to Scout CLI features & local execution.',
|
|
18
|
-
icon: '<i class="fa-solid fa-star text-warning" style="font-size:
|
|
18
|
+
icon: '<i class="fa-solid fa-star text-warning" style="font-size: 28px;"></i>',
|
|
19
19
|
},
|
|
20
20
|
pro: {
|
|
21
21
|
name: 'Pro Developer Tier',
|
|
22
22
|
badge: 'PRO ACTIVE',
|
|
23
23
|
desc: 'High-power AI co-pilot capabilities, priority quota & 5 active CLI sessions.',
|
|
24
|
-
icon: '<i class="fa-solid fa-bolt text-primary" style="font-size:
|
|
24
|
+
icon: '<i class="fa-solid fa-bolt text-primary" style="font-size: 28px;"></i>',
|
|
25
25
|
},
|
|
26
26
|
enterprise: {
|
|
27
27
|
name: 'Team & Enterprise Tier',
|
|
28
28
|
badge: 'ENTERPRISE ACTIVE',
|
|
29
29
|
desc: 'Organization controls, security policies & dedicated scale.',
|
|
30
|
-
icon: '<i class="fa-solid fa-building text-warning" style="font-size:
|
|
30
|
+
icon: '<i class="fa-solid fa-building text-warning" style="font-size: 28px;"></i>',
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -36,7 +36,6 @@ let state = {
|
|
|
36
36
|
user: null,
|
|
37
37
|
token: null,
|
|
38
38
|
refreshToken: null,
|
|
39
|
-
activeTab: 'account',
|
|
40
39
|
serverConnected: false,
|
|
41
40
|
apiBaseUrl: '',
|
|
42
41
|
sessions: [],
|
|
@@ -53,103 +52,73 @@ let state = {
|
|
|
53
52
|
|
|
54
53
|
let pendingCheckoutTier = null;
|
|
55
54
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
checkboxEnableByok: document.getElementById('checkbox-enable-byok'),
|
|
109
|
-
byokStatusBadge: document.getElementById('byok-status-badge'),
|
|
110
|
-
|
|
111
|
-
// Profile Settings Inputs
|
|
112
|
-
settingsDisplayName: document.getElementById('settings-display-name'),
|
|
113
|
-
settingsEmail: document.getElementById('settings-email'),
|
|
114
|
-
settingsUid: document.getElementById('settings-uid'),
|
|
115
|
-
settingsActiveTier: document.getElementById('settings-active-tier'),
|
|
116
|
-
profileForm: document.getElementById('profile-form'),
|
|
117
|
-
btnAccountLogout: document.getElementById('btn-account-logout'),
|
|
118
|
-
|
|
119
|
-
// Paid Tiers UI
|
|
120
|
-
currentPlanTitle: document.getElementById('current-plan-title'),
|
|
121
|
-
currentPlanBadge: document.getElementById('current-plan-badge'),
|
|
122
|
-
currentPlanDesc: document.getElementById('current-plan-desc'),
|
|
123
|
-
tierActiveIcon: document.getElementById('tier-active-icon'),
|
|
124
|
-
toggleBillingCycle: document.getElementById('toggle-billing-cycle'),
|
|
125
|
-
priceProVal: document.getElementById('price-pro-val'),
|
|
126
|
-
priceProPeriod: document.getElementById('price-pro-period'),
|
|
127
|
-
priceEntVal: document.getElementById('price-ent-val'),
|
|
128
|
-
priceEntPeriod: document.getElementById('price-ent-period'),
|
|
129
|
-
|
|
130
|
-
// Checkout Modal Controls
|
|
131
|
-
checkoutModal: document.getElementById('checkout-modal'),
|
|
132
|
-
checkoutModalTitle: document.getElementById('checkout-modal-title'),
|
|
133
|
-
checkoutPlanName: document.getElementById('checkout-plan-name'),
|
|
134
|
-
checkoutBillingCycle: document.getElementById('checkout-billing-cycle'),
|
|
135
|
-
checkoutPlanPrice: document.getElementById('checkout-plan-price'),
|
|
136
|
-
checkoutPricePeriod: document.getElementById('checkout-price-period'),
|
|
137
|
-
checkoutQuotaVal: document.getElementById('checkout-quota-val'),
|
|
138
|
-
btnCloseCheckout: document.getElementById('btn-close-checkout'),
|
|
139
|
-
btnCancelCheckout: document.getElementById('btn-cancel-checkout'),
|
|
140
|
-
btnConfirmCheckout: document.getElementById('btn-confirm-checkout'),
|
|
141
|
-
|
|
142
|
-
// Toast Container
|
|
143
|
-
toastContainer: document.getElementById('toast-container'),
|
|
144
|
-
};
|
|
55
|
+
// Audio Synthesizer via Web Audio API
|
|
56
|
+
let audioCtx = null;
|
|
57
|
+
|
|
58
|
+
function playRetroBeep(freq = 440, duration = 0.08, type = 'square') {
|
|
59
|
+
try {
|
|
60
|
+
if (!audioCtx) {
|
|
61
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
62
|
+
audioCtx = new AudioContext();
|
|
63
|
+
}
|
|
64
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
65
|
+
|
|
66
|
+
const osc = audioCtx.createOscillator();
|
|
67
|
+
const gain = audioCtx.createGain();
|
|
68
|
+
osc.type = type;
|
|
69
|
+
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
|
|
70
|
+
|
|
71
|
+
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
|
72
|
+
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
|
|
73
|
+
|
|
74
|
+
osc.connect(gain);
|
|
75
|
+
gain.connect(audioCtx.destination);
|
|
76
|
+
osc.start();
|
|
77
|
+
osc.stop(audioCtx.currentTime + duration);
|
|
78
|
+
} catch { }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// DOM Helper Getter
|
|
82
|
+
function getEl(id) {
|
|
83
|
+
return document.getElementById(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Helper to determine current page basename
|
|
87
|
+
function getCurrentPage() {
|
|
88
|
+
const path = window.location.pathname.toLowerCase();
|
|
89
|
+
if (path.includes('/auth')) return '/auth';
|
|
90
|
+
if (path.includes('/plans')) return '/plans';
|
|
91
|
+
if (path.includes('/cli')) return '/cli';
|
|
92
|
+
return 'index.html';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Highlight current active nav link in navbar
|
|
96
|
+
function highlightActiveNav() {
|
|
97
|
+
const currentPage = getCurrentPage();
|
|
98
|
+
document.querySelectorAll('.nav-links .nav-btn').forEach((link) => {
|
|
99
|
+
const href = link.getAttribute('href');
|
|
100
|
+
if (href === currentPage || (currentPage === 'index.html' && (href === 'index.html' || href === './'))) {
|
|
101
|
+
link.classList.add('active');
|
|
102
|
+
} else {
|
|
103
|
+
link.classList.remove('active');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
145
107
|
|
|
146
108
|
// Initialize Application
|
|
147
109
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
110
|
+
highlightActiveNav();
|
|
148
111
|
loadTierAndCreditsFromStorage();
|
|
149
112
|
loadSessionsFromStorage();
|
|
150
113
|
initEventListeners();
|
|
114
|
+
initFAQAccordions();
|
|
115
|
+
initTerminalSimulator();
|
|
151
116
|
renderAllDynamicComponents();
|
|
152
117
|
await checkServerStatusAndSession();
|
|
118
|
+
|
|
119
|
+
if (getEl('pacman-canvas')) {
|
|
120
|
+
initPacmanGame();
|
|
121
|
+
}
|
|
153
122
|
});
|
|
154
123
|
|
|
155
124
|
// Time Helper Function for Dynamic Relative Timestamps
|
|
@@ -169,7 +138,7 @@ function timeAgo(dateInput) {
|
|
|
169
138
|
return `${days}d ago`;
|
|
170
139
|
}
|
|
171
140
|
|
|
172
|
-
// Load Subscription Tier & Dynamic Credits from Local Storage
|
|
141
|
+
// Load Subscription Tier & Dynamic Credits from Local Storage
|
|
173
142
|
function loadTierAndCreditsFromStorage() {
|
|
174
143
|
const savedTier = localStorage.getItem('scout_current_tier');
|
|
175
144
|
if (savedTier && TIER_DETAILS[savedTier]) {
|
|
@@ -177,11 +146,7 @@ function loadTierAndCreditsFromStorage() {
|
|
|
177
146
|
}
|
|
178
147
|
|
|
179
148
|
const savedUsed = localStorage.getItem('scout_credits_used');
|
|
180
|
-
|
|
181
|
-
state.creditsUsed = parseInt(savedUsed, 10) || 0;
|
|
182
|
-
} else {
|
|
183
|
-
state.creditsUsed = 0;
|
|
184
|
-
}
|
|
149
|
+
state.creditsUsed = savedUsed !== null ? parseInt(savedUsed, 10) || 0 : 0;
|
|
185
150
|
|
|
186
151
|
const savedTotal = localStorage.getItem('scout_credits_total');
|
|
187
152
|
if (savedTotal !== null) {
|
|
@@ -209,6 +174,7 @@ function renderAllDynamicComponents() {
|
|
|
209
174
|
renderSessionsTable();
|
|
210
175
|
renderCreditUsageUI();
|
|
211
176
|
renderTiersUI();
|
|
177
|
+
updateUserUI();
|
|
212
178
|
}
|
|
213
179
|
|
|
214
180
|
// Open / Close Checkout Modal
|
|
@@ -228,67 +194,71 @@ function openCheckoutModal(targetTier) {
|
|
|
228
194
|
quota = '10,000 Credits / mo';
|
|
229
195
|
}
|
|
230
196
|
|
|
231
|
-
if (
|
|
232
|
-
if (
|
|
233
|
-
if (
|
|
234
|
-
if (
|
|
235
|
-
if (
|
|
197
|
+
if (getEl('checkout-plan-name')) getEl('checkout-plan-name').innerText = tierInfo.name;
|
|
198
|
+
if (getEl('checkout-billing-cycle')) getEl('checkout-billing-cycle').innerText = isAnnual ? 'Billed Annually (20% Off)' : 'Billed Monthly';
|
|
199
|
+
if (getEl('checkout-plan-price')) getEl('checkout-plan-price').innerText = price;
|
|
200
|
+
if (getEl('checkout-price-period')) getEl('checkout-price-period').innerText = isAnnual ? '/ mo (annual)' : '/ mo';
|
|
201
|
+
if (getEl('checkout-quota-val')) getEl('checkout-quota-val').innerText = quota;
|
|
236
202
|
|
|
237
|
-
|
|
203
|
+
const modal = getEl('checkout-modal');
|
|
204
|
+
if (modal) modal.classList.remove('hidden');
|
|
238
205
|
}
|
|
239
206
|
|
|
240
207
|
function closeCheckoutModal() {
|
|
241
208
|
pendingCheckoutTier = null;
|
|
242
|
-
|
|
209
|
+
const modal = getEl('checkout-modal');
|
|
210
|
+
if (modal) modal.classList.add('hidden');
|
|
243
211
|
}
|
|
244
212
|
|
|
245
213
|
// Setup Event Handlers
|
|
246
214
|
function initEventListeners() {
|
|
247
215
|
// Auth Form Tabs Switcher
|
|
248
|
-
|
|
249
|
-
|
|
216
|
+
getEl('tab-login-btn')?.addEventListener('click', () => switchAuthForm('login'));
|
|
217
|
+
getEl('tab-signup-btn')?.addEventListener('click', () => switchAuthForm('signup'));
|
|
250
218
|
|
|
251
219
|
// Form Submissions
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
220
|
+
getEl('login-form')?.addEventListener('submit', handleSignIn);
|
|
221
|
+
getEl('signup-form')?.addEventListener('submit', handleSignUp);
|
|
222
|
+
getEl('btn-demo-mode')?.addEventListener('click', launchDemoSession);
|
|
223
|
+
|
|
224
|
+
// Password Strength Checker
|
|
225
|
+
getEl('signup-password')?.addEventListener('input', (e) => {
|
|
226
|
+
const val = e.target.value;
|
|
227
|
+
const bar = getEl('pwd-strength-bar');
|
|
228
|
+
if (!bar) return;
|
|
229
|
+
|
|
230
|
+
let strength = 0;
|
|
231
|
+
if (val.length >= 6) strength += 33;
|
|
232
|
+
if (/[A-Z]/.test(val) && /[0-9]/.test(val)) strength += 33;
|
|
233
|
+
if (/[^A-Za-z0-9]/.test(val) && val.length >= 10) strength += 34;
|
|
234
|
+
|
|
235
|
+
bar.style.width = `${strength}%`;
|
|
236
|
+
bar.style.backgroundColor = strength < 40 ? '#ef4444' : strength < 80 ? '#eab308' : '#22c55e';
|
|
237
|
+
});
|
|
255
238
|
|
|
256
|
-
|
|
239
|
+
getEl('btn-forgot-pwd')?.addEventListener('click', (e) => {
|
|
257
240
|
e.preventDefault();
|
|
258
241
|
handleForgotPassword();
|
|
259
242
|
});
|
|
260
243
|
|
|
261
|
-
// Navigation Tabs Switcher
|
|
262
|
-
document.querySelectorAll('.nav-btn').forEach((btn) => {
|
|
263
|
-
btn.addEventListener('click', () => {
|
|
264
|
-
const targetTab = btn.getAttribute('data-tab');
|
|
265
|
-
if (targetTab) switchDashboardTab(targetTab);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
244
|
// User Dropdown Menu
|
|
270
|
-
|
|
245
|
+
getEl('user-menu-toggle')?.addEventListener('click', (e) => {
|
|
271
246
|
e.stopPropagation();
|
|
272
|
-
|
|
247
|
+
getEl('user-dropdown')?.classList.toggle('show');
|
|
273
248
|
});
|
|
274
249
|
|
|
275
250
|
document.addEventListener('click', () => {
|
|
276
|
-
|
|
251
|
+
getEl('user-dropdown')?.classList.remove('show');
|
|
277
252
|
});
|
|
278
253
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
elements.btnDropdownProfile?.addEventListener('click', () => switchDashboardTab('account'));
|
|
282
|
-
elements.btnDropdownTiers?.addEventListener('click', () => switchDashboardTab('tiers'));
|
|
283
|
-
elements.btnNavTiersHero?.addEventListener('click', () => switchDashboardTab('tiers'));
|
|
284
|
-
elements.btnUpgradeCreditsHero?.addEventListener('click', () => switchDashboardTab('tiers'));
|
|
285
|
-
elements.btnOpenAuth?.addEventListener('click', showAuthSection);
|
|
254
|
+
getEl('btn-dropdown-logout')?.addEventListener('click', handleLogout);
|
|
255
|
+
getEl('btn-account-logout')?.addEventListener('click', handleLogout);
|
|
286
256
|
|
|
287
257
|
// Checkout Modal Events
|
|
288
|
-
|
|
289
|
-
|
|
258
|
+
getEl('btn-close-checkout')?.addEventListener('click', closeCheckoutModal);
|
|
259
|
+
getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
|
|
290
260
|
|
|
291
|
-
|
|
261
|
+
getEl('btn-confirm-checkout')?.addEventListener('click', () => {
|
|
292
262
|
if (!pendingCheckoutTier) return;
|
|
293
263
|
|
|
294
264
|
const targetTier = pendingCheckoutTier;
|
|
@@ -314,7 +284,7 @@ function initEventListeners() {
|
|
|
314
284
|
document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
|
|
315
285
|
btn.addEventListener('click', () => {
|
|
316
286
|
const targetId = btn.getAttribute('data-target');
|
|
317
|
-
const input =
|
|
287
|
+
const input = getEl(targetId);
|
|
318
288
|
if (input) {
|
|
319
289
|
const isPwd = input.type === 'password';
|
|
320
290
|
input.type = isPwd ? 'text' : 'password';
|
|
@@ -324,9 +294,10 @@ function initEventListeners() {
|
|
|
324
294
|
});
|
|
325
295
|
|
|
326
296
|
// Profile Form Submission
|
|
327
|
-
|
|
297
|
+
getEl('profile-form')?.addEventListener('submit', (e) => {
|
|
328
298
|
e.preventDefault();
|
|
329
|
-
const
|
|
299
|
+
const inputDisp = getEl('settings-display-name');
|
|
300
|
+
const newName = inputDisp ? inputDisp.value.trim() : '';
|
|
330
301
|
if (newName && state.user) {
|
|
331
302
|
state.user.name = newName;
|
|
332
303
|
saveSessionToStorage();
|
|
@@ -336,7 +307,7 @@ function initEventListeners() {
|
|
|
336
307
|
}
|
|
337
308
|
});
|
|
338
309
|
|
|
339
|
-
|
|
310
|
+
getEl('select-byok-provider')?.addEventListener('change', (e) => {
|
|
340
311
|
const provider = e.target.value;
|
|
341
312
|
const defaultEndpoints = {
|
|
342
313
|
nvidia: 'https://integrate.api.nvidia.com/v1',
|
|
@@ -345,18 +316,19 @@ function initEventListeners() {
|
|
|
345
316
|
openai: 'https://api.openai.com/v1',
|
|
346
317
|
custom: 'http://localhost:11434/v1',
|
|
347
318
|
};
|
|
348
|
-
|
|
349
|
-
|
|
319
|
+
const epInput = getEl('input-byok-endpoint');
|
|
320
|
+
if (epInput && defaultEndpoints[provider]) {
|
|
321
|
+
epInput.value = defaultEndpoints[provider];
|
|
350
322
|
}
|
|
351
323
|
});
|
|
352
324
|
|
|
353
325
|
// BYOK & Custom Proxy Form Submission
|
|
354
|
-
|
|
326
|
+
getEl('byok-form')?.addEventListener('submit', (e) => {
|
|
355
327
|
e.preventDefault();
|
|
356
|
-
state.customLlmProvider =
|
|
357
|
-
state.customBaseUrl =
|
|
358
|
-
state.customApiKey =
|
|
359
|
-
state.byokEnabled =
|
|
328
|
+
state.customLlmProvider = getEl('select-byok-provider')?.value || 'nvidia';
|
|
329
|
+
state.customBaseUrl = getEl('input-byok-endpoint')?.value.trim() || '';
|
|
330
|
+
state.customApiKey = getEl('input-byok-key')?.value.trim() || '';
|
|
331
|
+
state.byokEnabled = getEl('checkbox-enable-byok')?.checked || false;
|
|
360
332
|
|
|
361
333
|
localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
|
|
362
334
|
localStorage.setItem('scout_byok_provider', state.customLlmProvider);
|
|
@@ -374,7 +346,7 @@ function initEventListeners() {
|
|
|
374
346
|
}
|
|
375
347
|
});
|
|
376
348
|
|
|
377
|
-
|
|
349
|
+
getEl('checkbox-enable-byok')?.addEventListener('change', (e) => {
|
|
378
350
|
state.byokEnabled = e.target.checked;
|
|
379
351
|
localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
|
|
380
352
|
syncAuthWithServer();
|
|
@@ -382,7 +354,7 @@ function initEventListeners() {
|
|
|
382
354
|
});
|
|
383
355
|
|
|
384
356
|
// Billing Cycle Toggle Switcher
|
|
385
|
-
|
|
357
|
+
getEl('toggle-billing-cycle')?.addEventListener('change', (e) => {
|
|
386
358
|
state.billingCycle = e.target.checked ? 'annual' : 'monthly';
|
|
387
359
|
renderTiersUI();
|
|
388
360
|
});
|
|
@@ -409,7 +381,105 @@ function initEventListeners() {
|
|
|
409
381
|
});
|
|
410
382
|
}
|
|
411
383
|
|
|
412
|
-
//
|
|
384
|
+
// FAQ Accordion Handler
|
|
385
|
+
function initFAQAccordions() {
|
|
386
|
+
document.querySelectorAll('.faq-question').forEach((btn) => {
|
|
387
|
+
btn.addEventListener('click', () => {
|
|
388
|
+
const item = btn.closest('.faq-item');
|
|
389
|
+
if (item) item.classList.toggle('open');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Interactive Web Terminal Simulator
|
|
395
|
+
function initTerminalSimulator() {
|
|
396
|
+
const form = getEl('cli-terminal-form');
|
|
397
|
+
const input = getEl('terminal-input-field');
|
|
398
|
+
const logs = getEl('terminal-logs');
|
|
399
|
+
|
|
400
|
+
if (!form || !input || !logs) return;
|
|
401
|
+
|
|
402
|
+
// Quick Command Chip Click Handlers
|
|
403
|
+
document.querySelectorAll('.btn-cli-chip').forEach((chip) => {
|
|
404
|
+
chip.addEventListener('click', () => {
|
|
405
|
+
const cmd = chip.getAttribute('data-cmd');
|
|
406
|
+
if (cmd) {
|
|
407
|
+
input.value = cmd;
|
|
408
|
+
runTerminalCommand(cmd);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
form.addEventListener('submit', (e) => {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
const cmd = input.value.trim();
|
|
416
|
+
if (cmd) {
|
|
417
|
+
runTerminalCommand(cmd);
|
|
418
|
+
input.value = '';
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
function runTerminalCommand(cmd) {
|
|
423
|
+
const line = document.createElement('div');
|
|
424
|
+
line.className = 'terminal-line';
|
|
425
|
+
line.innerHTML = `<span class="prompt-user">developer@scout</span>:$ <span class="cmd-text">${escapeHtml(cmd)}</span>`;
|
|
426
|
+
logs.appendChild(line);
|
|
427
|
+
|
|
428
|
+
const response = document.createElement('div');
|
|
429
|
+
response.style.margin = '4px 0 12px 0';
|
|
430
|
+
|
|
431
|
+
const lower = cmd.toLowerCase().trim();
|
|
432
|
+
if (lower === 'clear') {
|
|
433
|
+
logs.innerHTML = '';
|
|
434
|
+
return;
|
|
435
|
+
} else if (lower === 'help' || lower === 'scout help' || lower === 'scout --help' || lower === 'scout') {
|
|
436
|
+
response.innerHTML = `
|
|
437
|
+
<span class="term-yellow">Available Scout Commands:</span><br>
|
|
438
|
+
↳ <code>scout agent</code> - Launch autonomous AI co-pilot agent & reasoning loop<br>
|
|
439
|
+
↳ <code>scout status</code> - Inspect CLI authentication, active workspace & credit balance<br>
|
|
440
|
+
↳ <code>scout analyze</code> - Run instant AST codebase teardown and entry point indexing<br>
|
|
441
|
+
↳ <code>scout teardown</code> - Generate strategic onboarding map & architectural moat report<br>
|
|
442
|
+
↳ <code>scout byok</code> - Verify custom LLM proxy endpoint connection<br>
|
|
443
|
+
↳ <code>clear</code> - Clear terminal buffer<br>
|
|
444
|
+
`;
|
|
445
|
+
} else if (lower.includes('agent')) {
|
|
446
|
+
response.innerHTML = `
|
|
447
|
+
<span class="term-bg-purple"> SCOUT AUTONOMOUS CO-PILOT AGENT </span><br>
|
|
448
|
+
<span class="term-green">✔ Agent Orchestrator active (Gemini 2.5 Flash / Deep Reasoning Engine)</span><br>
|
|
449
|
+
<span class="term-cyan">↳ Workspace Vector Memory:</span> 124 files indexed and loaded<br>
|
|
450
|
+
<span class="term-yellow">↳ Pair-Programming Status:</span> Ready for interactive tasks & refactoring loops<br>
|
|
451
|
+
<span class="term-dim">↳ Try typing 'scout analyze' or 'scout teardown' to execute automated codebase operations.</span><br>
|
|
452
|
+
`;
|
|
453
|
+
} else if (lower.includes('status')) {
|
|
454
|
+
response.innerHTML = `
|
|
455
|
+
<span class="term-green">✔ Scout CLI Engine: v7.0.8 Online</span><br>
|
|
456
|
+
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
457
|
+
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
458
|
+
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
459
|
+
`;
|
|
460
|
+
} else if (lower.includes('analyze') || lower.includes('teardown')) {
|
|
461
|
+
response.innerHTML = `
|
|
462
|
+
<span class="term-bg-cyan"> FRONTTERRAIN SCOUT AST ENGINE </span><br>
|
|
463
|
+
<span class="term-green">✔ Indexed 124 workspace files in 42ms.</span><br>
|
|
464
|
+
<span class="term-yellow">↳ Primary Entry Point:</span> src/index.ts<br>
|
|
465
|
+
<span class="term-purple">↳ Core Dependencies:</span> TypeScript, Commander, Clack, Express, Firebase SDK<br>
|
|
466
|
+
`;
|
|
467
|
+
} else if (lower.includes('byok')) {
|
|
468
|
+
response.innerHTML = `
|
|
469
|
+
<span class="term-yellow">↳ BYOK Mode Status:</span> ${state.byokEnabled ? '<span class="term-green">ACTIVE (Scout Credits Bypassed)</span>' : '<span class="term-dim">DISABLED (Using Scout Default Key)</span>'}<br>
|
|
470
|
+
↳ Provider: ${escapeHtml(state.customLlmProvider)}<br>
|
|
471
|
+
↳ Endpoint: ${escapeHtml(state.customBaseUrl || 'Default Proxy')}<br>
|
|
472
|
+
`;
|
|
473
|
+
} else {
|
|
474
|
+
response.innerHTML = `<span class="term-purple">scout: command not found: '${escapeHtml(cmd)}'. Type 'help' for available commands.</span>`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
logs.appendChild(response);
|
|
478
|
+
logs.scrollTop = logs.scrollHeight;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Sessions Storage Loader
|
|
413
483
|
function loadSessionsFromStorage() {
|
|
414
484
|
try {
|
|
415
485
|
const stored = localStorage.getItem('scout_cli_sessions');
|
|
@@ -440,10 +510,11 @@ function saveSessionsToStorage() {
|
|
|
440
510
|
}
|
|
441
511
|
|
|
442
512
|
function renderSessionsTable() {
|
|
443
|
-
|
|
513
|
+
const tbody = getEl('session-table-body');
|
|
514
|
+
if (!tbody) return;
|
|
444
515
|
|
|
445
516
|
if (!state.sessions || state.sessions.length === 0) {
|
|
446
|
-
|
|
517
|
+
tbody.innerHTML = `
|
|
447
518
|
<tr>
|
|
448
519
|
<td colspan="4" class="empty-table-notice">No active CLI authorization sessions found.</td>
|
|
449
520
|
</tr>
|
|
@@ -451,12 +522,12 @@ function renderSessionsTable() {
|
|
|
451
522
|
return;
|
|
452
523
|
}
|
|
453
524
|
|
|
454
|
-
|
|
525
|
+
tbody.innerHTML = state.sessions
|
|
455
526
|
.map((sess) => {
|
|
456
527
|
return `
|
|
457
528
|
<tr data-id="${escapeHtml(sess.id)}">
|
|
458
529
|
<td>
|
|
459
|
-
<div class="flex-center-gap">
|
|
530
|
+
<div class="flex-center-gap" style="display: flex; align-items: center; gap: 8px;">
|
|
460
531
|
<i class="fa-solid fa-terminal text-primary"></i>
|
|
461
532
|
<div>
|
|
462
533
|
<strong>${escapeHtml(sess.host)}</strong>
|
|
@@ -482,40 +553,49 @@ function revokeSession(sessionId) {
|
|
|
482
553
|
|
|
483
554
|
// Render Dynamic Credit Usage & BYOK Status UI
|
|
484
555
|
function renderCreditUsageUI() {
|
|
556
|
+
const byokStatusBadge = getEl('byok-status-badge');
|
|
557
|
+
const creditStatusBadge = getEl('credit-status-badge');
|
|
558
|
+
const valUsed = getEl('val-credits-used');
|
|
559
|
+
const valRemaining = getEl('val-credits-remaining');
|
|
560
|
+
const valTotal = getEl('val-credits-total');
|
|
561
|
+
const valPct = getEl('val-credits-pct');
|
|
562
|
+
const progressFill = getEl('credit-progress-fill');
|
|
563
|
+
const labelConsumption = getEl('label-credit-consumption');
|
|
564
|
+
const byokNote = getEl('byok-banner-note');
|
|
565
|
+
const valRenew = getEl('val-renew-datetime');
|
|
566
|
+
|
|
485
567
|
if (state.byokEnabled) {
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
568
|
+
if (byokStatusBadge) {
|
|
569
|
+
byokStatusBadge.innerText = 'BYOK Mode Active';
|
|
570
|
+
byokStatusBadge.className = 'badge badge-accent';
|
|
489
571
|
}
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
572
|
+
if (creditStatusBadge) {
|
|
573
|
+
creditStatusBadge.innerText = 'Custom Endpoint Active';
|
|
574
|
+
creditStatusBadge.className = 'badge badge-accent';
|
|
493
575
|
}
|
|
494
|
-
if (
|
|
495
|
-
if (
|
|
496
|
-
if (
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
576
|
+
if (valUsed) valUsed.innerText = 'Unmonitored';
|
|
577
|
+
if (valRemaining) valRemaining.innerText = 'Unlimited (BYOK)';
|
|
578
|
+
if (valTotal) valTotal.innerText = 'Custom Provider';
|
|
579
|
+
if (valPct) valPct.innerText = 'BYOK';
|
|
580
|
+
|
|
581
|
+
if (progressFill) {
|
|
582
|
+
progressFill.style.width = '100%';
|
|
583
|
+
progressFill.style.background = 'linear-gradient(90deg, #a855f7 0%, #ec4899 100%)';
|
|
502
584
|
}
|
|
503
585
|
|
|
504
|
-
if (
|
|
505
|
-
|
|
586
|
+
if (labelConsumption) {
|
|
587
|
+
labelConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
|
|
506
588
|
}
|
|
507
589
|
|
|
508
|
-
if (
|
|
509
|
-
elements.byokBannerNote.classList.remove('hidden');
|
|
510
|
-
}
|
|
590
|
+
if (byokNote) byokNote.classList.remove('hidden');
|
|
511
591
|
} else {
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
592
|
+
if (byokStatusBadge) {
|
|
593
|
+
byokStatusBadge.innerText = 'Scout Default';
|
|
594
|
+
byokStatusBadge.className = 'badge';
|
|
515
595
|
}
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
596
|
+
if (creditStatusBadge) {
|
|
597
|
+
creditStatusBadge.innerText = 'Quota Active';
|
|
598
|
+
creditStatusBadge.className = 'badge badge-success';
|
|
519
599
|
}
|
|
520
600
|
|
|
521
601
|
const defaultTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
|
|
@@ -524,23 +604,21 @@ function renderCreditUsageUI() {
|
|
|
524
604
|
const remaining = Math.max(0, total - used);
|
|
525
605
|
const pct = Math.min(100, Math.round((used / total) * 100));
|
|
526
606
|
|
|
527
|
-
if (
|
|
528
|
-
if (
|
|
529
|
-
if (
|
|
530
|
-
if (
|
|
607
|
+
if (valUsed) valUsed.innerText = used.toLocaleString();
|
|
608
|
+
if (valRemaining) valRemaining.innerText = remaining.toLocaleString();
|
|
609
|
+
if (valTotal) valTotal.innerText = total.toLocaleString();
|
|
610
|
+
if (valPct) valPct.innerText = `${pct}%`;
|
|
531
611
|
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
612
|
+
if (progressFill) {
|
|
613
|
+
progressFill.style.width = `${pct}%`;
|
|
614
|
+
progressFill.style.background = 'linear-gradient(90deg, #6366f1 0%, #38bdf8 60%, #34d399 100%)';
|
|
535
615
|
}
|
|
536
616
|
|
|
537
|
-
if (
|
|
538
|
-
|
|
617
|
+
if (labelConsumption) {
|
|
618
|
+
labelConsumption.innerText = 'Monthly Credit Consumption';
|
|
539
619
|
}
|
|
540
620
|
|
|
541
|
-
if (
|
|
542
|
-
elements.byokBannerNote.classList.add('hidden');
|
|
543
|
-
}
|
|
621
|
+
if (byokNote) byokNote.classList.add('hidden');
|
|
544
622
|
}
|
|
545
623
|
|
|
546
624
|
let renewDateStr = '';
|
|
@@ -556,32 +634,28 @@ function renderCreditUsageUI() {
|
|
|
556
634
|
renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
|
|
557
635
|
}
|
|
558
636
|
|
|
559
|
-
if (
|
|
560
|
-
elements.valRenewDatetime.innerText = renewDateStr;
|
|
561
|
-
}
|
|
637
|
+
if (valRenew) valRenew.innerText = renewDateStr;
|
|
562
638
|
}
|
|
563
639
|
|
|
564
640
|
// Render Paid Tiers UI
|
|
565
641
|
function renderTiersUI() {
|
|
566
642
|
const current = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
|
|
567
643
|
|
|
568
|
-
if (
|
|
569
|
-
if (
|
|
570
|
-
if (
|
|
571
|
-
if (
|
|
644
|
+
if (getEl('current-plan-title')) getEl('current-plan-title').innerText = current.name;
|
|
645
|
+
if (getEl('current-plan-badge')) getEl('current-plan-badge').innerText = current.badge;
|
|
646
|
+
if (getEl('current-plan-desc')) getEl('current-plan-desc').innerText = current.desc;
|
|
647
|
+
if (getEl('tier-active-icon')) getEl('tier-active-icon').innerHTML = current.icon;
|
|
572
648
|
|
|
573
|
-
// Billing Cycle Pricing Adjustments
|
|
574
649
|
const isAnnual = state.billingCycle === 'annual';
|
|
575
|
-
if (
|
|
576
|
-
if (
|
|
650
|
+
if (getEl('price-pro-val')) getEl('price-pro-val').innerText = isAnnual ? '15' : '19';
|
|
651
|
+
if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
|
|
577
652
|
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
653
|
+
if (getEl('price-ent-val')) getEl('price-ent-val').innerText = isAnnual ? '39' : '49';
|
|
654
|
+
if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
|
|
580
655
|
|
|
581
|
-
// Highlight Active Tier Card
|
|
582
656
|
['free', 'pro', 'enterprise'].forEach((t) => {
|
|
583
|
-
const card =
|
|
584
|
-
const btn =
|
|
657
|
+
const card = getEl(`tier-card-${t}`);
|
|
658
|
+
const btn = getEl(`btn-tier-${t}`);
|
|
585
659
|
if (!card || !btn) return;
|
|
586
660
|
|
|
587
661
|
if (t === state.currentTier) {
|
|
@@ -635,10 +709,6 @@ async function checkServerStatusAndSession() {
|
|
|
635
709
|
if (connected && data) {
|
|
636
710
|
state.serverConnected = true;
|
|
637
711
|
|
|
638
|
-
if (elements.navTenantName) {
|
|
639
|
-
elements.navTenantName.innerText = `CLI Local Sync Active (${data.repoName || 'Workspace'})`;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
712
|
if (data.isLoggedIn && data.auth?.user) {
|
|
643
713
|
state.user = data.auth.user;
|
|
644
714
|
state.token = data.auth.token;
|
|
@@ -653,19 +723,6 @@ async function checkServerStatusAndSession() {
|
|
|
653
723
|
if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
|
|
654
724
|
if (typeof data.auth.customBaseUrl === 'string') state.customBaseUrl = data.auth.customBaseUrl;
|
|
655
725
|
|
|
656
|
-
const localCliSession = {
|
|
657
|
-
id: 'sess_cli_local',
|
|
658
|
-
host: `Local Workstation (${data.repoName || 'FT-CLI'})`,
|
|
659
|
-
platform: `Node.js CLI (~/.ft/auth.json)`,
|
|
660
|
-
lastActive: data.auth.loggedInAt || new Date().toISOString(),
|
|
661
|
-
ip: '127.0.2.1 (Local Sync)',
|
|
662
|
-
active: true,
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
const otherSessions = state.sessions.filter(s => s.id !== 'sess_cli_local');
|
|
666
|
-
state.sessions = [localCliSession, ...otherSessions];
|
|
667
|
-
saveSessionsToStorage();
|
|
668
|
-
|
|
669
726
|
saveSessionToStorage();
|
|
670
727
|
showDashboard();
|
|
671
728
|
updateUserUI();
|
|
@@ -676,10 +733,6 @@ async function checkServerStatusAndSession() {
|
|
|
676
733
|
}
|
|
677
734
|
|
|
678
735
|
state.serverConnected = false;
|
|
679
|
-
if (elements.navTenantName) {
|
|
680
|
-
elements.navTenantName.innerText = FIREBASE_CONFIG.tenantId;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
736
|
checkLocalStorageSession();
|
|
684
737
|
}
|
|
685
738
|
|
|
@@ -707,27 +760,31 @@ function checkLocalStorageSession() {
|
|
|
707
760
|
// Switch Auth View Forms
|
|
708
761
|
function switchAuthForm(mode) {
|
|
709
762
|
if (mode === 'login') {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
763
|
+
getEl('tab-login-btn')?.classList.add('active');
|
|
764
|
+
getEl('tab-signup-btn')?.classList.remove('active');
|
|
765
|
+
getEl('login-form')?.classList.remove('hidden');
|
|
766
|
+
getEl('signup-form')?.classList.add('hidden');
|
|
714
767
|
} else {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
768
|
+
getEl('tab-signup-btn')?.classList.add('active');
|
|
769
|
+
getEl('tab-login-btn')?.classList.remove('active');
|
|
770
|
+
getEl('signup-form')?.classList.remove('hidden');
|
|
771
|
+
getEl('login-form')?.classList.add('hidden');
|
|
719
772
|
}
|
|
720
773
|
}
|
|
721
774
|
|
|
722
775
|
// Handle Sign In Submission
|
|
723
776
|
async function handleSignIn(e) {
|
|
724
777
|
e.preventDefault();
|
|
725
|
-
const email =
|
|
726
|
-
const password =
|
|
727
|
-
const btnSubmit =
|
|
778
|
+
const email = getEl('login-email')?.value.trim();
|
|
779
|
+
const password = getEl('login-password')?.value;
|
|
780
|
+
const btnSubmit = getEl('btn-submit-login');
|
|
728
781
|
|
|
729
|
-
|
|
730
|
-
|
|
782
|
+
if (!email || !password) return;
|
|
783
|
+
|
|
784
|
+
if (btnSubmit) {
|
|
785
|
+
btnSubmit.disabled = true;
|
|
786
|
+
btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
|
|
787
|
+
}
|
|
731
788
|
|
|
732
789
|
try {
|
|
733
790
|
let authRes;
|
|
@@ -757,33 +814,42 @@ async function handleSignIn(e) {
|
|
|
757
814
|
|
|
758
815
|
saveSessionToStorage();
|
|
759
816
|
syncAuthWithServer();
|
|
760
|
-
showDashboard();
|
|
761
817
|
updateUserUI();
|
|
762
818
|
showToast(`Signed in successfully as ${state.user.name}!`, 'success');
|
|
819
|
+
|
|
820
|
+
if (getCurrentPage() === '/auth') {
|
|
821
|
+
setTimeout(() => { window.location.href = 'index.html'; }, 400);
|
|
822
|
+
} else {
|
|
823
|
+
showDashboard();
|
|
824
|
+
}
|
|
763
825
|
} catch (error) {
|
|
764
826
|
showToast(error.message || 'Login failed.', 'error');
|
|
765
827
|
} finally {
|
|
766
|
-
btnSubmit
|
|
767
|
-
|
|
828
|
+
if (btnSubmit) {
|
|
829
|
+
btnSubmit.disabled = false;
|
|
830
|
+
btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
|
|
831
|
+
}
|
|
768
832
|
}
|
|
769
833
|
}
|
|
770
834
|
|
|
771
835
|
// Handle Sign Up Submission
|
|
772
836
|
async function handleSignUp(e) {
|
|
773
837
|
e.preventDefault();
|
|
774
|
-
const name =
|
|
775
|
-
const email =
|
|
776
|
-
const password =
|
|
777
|
-
const confirmPassword =
|
|
778
|
-
const btnSubmit =
|
|
838
|
+
const name = getEl('signup-name')?.value.trim();
|
|
839
|
+
const email = getEl('signup-email')?.value.trim();
|
|
840
|
+
const password = getEl('signup-password')?.value;
|
|
841
|
+
const confirmPassword = getEl('signup-confirm-password')?.value;
|
|
842
|
+
const btnSubmit = getEl('btn-submit-signup');
|
|
779
843
|
|
|
780
844
|
if (password !== confirmPassword) {
|
|
781
845
|
showToast('Passwords do not match.', 'error');
|
|
782
846
|
return;
|
|
783
847
|
}
|
|
784
848
|
|
|
785
|
-
btnSubmit
|
|
786
|
-
|
|
849
|
+
if (btnSubmit) {
|
|
850
|
+
btnSubmit.disabled = true;
|
|
851
|
+
btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
|
|
852
|
+
}
|
|
787
853
|
|
|
788
854
|
try {
|
|
789
855
|
let authRes;
|
|
@@ -813,18 +879,24 @@ async function handleSignUp(e) {
|
|
|
813
879
|
|
|
814
880
|
saveSessionToStorage();
|
|
815
881
|
syncAuthWithServer();
|
|
816
|
-
showDashboard();
|
|
817
882
|
updateUserUI();
|
|
818
883
|
showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
|
|
884
|
+
|
|
885
|
+
if (getCurrentPage() === '/auth') {
|
|
886
|
+
setTimeout(() => { window.location.href = 'index.html'; }, 400);
|
|
887
|
+
} else {
|
|
888
|
+
showDashboard();
|
|
889
|
+
}
|
|
819
890
|
} catch (error) {
|
|
820
891
|
showToast(error.message || 'Registration failed.', 'error');
|
|
821
892
|
} finally {
|
|
822
|
-
btnSubmit
|
|
823
|
-
|
|
893
|
+
if (btnSubmit) {
|
|
894
|
+
btnSubmit.disabled = false;
|
|
895
|
+
btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
|
|
896
|
+
}
|
|
824
897
|
}
|
|
825
898
|
}
|
|
826
899
|
|
|
827
|
-
// Forgot Password Flow
|
|
828
900
|
function handleForgotPassword() {
|
|
829
901
|
const email = prompt('Enter your registered email address for password reset:');
|
|
830
902
|
if (email && email.includes('@')) {
|
|
@@ -834,7 +906,7 @@ function handleForgotPassword() {
|
|
|
834
906
|
}
|
|
835
907
|
}
|
|
836
908
|
|
|
837
|
-
// Demo
|
|
909
|
+
// Demo Session Launcher
|
|
838
910
|
function launchDemoSession() {
|
|
839
911
|
state.token = 'ft_demo_token_' + Date.now().toString(36);
|
|
840
912
|
state.user = {
|
|
@@ -846,9 +918,14 @@ function launchDemoSession() {
|
|
|
846
918
|
|
|
847
919
|
saveSessionToStorage();
|
|
848
920
|
syncAuthWithServer();
|
|
849
|
-
showDashboard();
|
|
850
921
|
updateUserUI();
|
|
851
922
|
showToast('Demo Developer Session Active!', 'info');
|
|
923
|
+
|
|
924
|
+
if (getCurrentPage() === '/auth') {
|
|
925
|
+
setTimeout(() => { window.location.href = 'index.html'; }, 300);
|
|
926
|
+
} else {
|
|
927
|
+
showDashboard();
|
|
928
|
+
}
|
|
852
929
|
}
|
|
853
930
|
|
|
854
931
|
// Firebase Direct REST API
|
|
@@ -920,64 +997,65 @@ function parseFirebaseError(errData, defaultMsg) {
|
|
|
920
997
|
|
|
921
998
|
// UI State Renderers
|
|
922
999
|
function showAuthSection() {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1000
|
+
const authSec = getEl('auth-section');
|
|
1001
|
+
const dashSec = getEl('dashboard-section');
|
|
1002
|
+
const userMenu = getEl('user-menu');
|
|
1003
|
+
const btnOpenAuth = getEl('btn-open-auth');
|
|
1004
|
+
|
|
1005
|
+
if (authSec) authSec.classList.remove('hidden');
|
|
1006
|
+
if (dashSec) dashSec.classList.add('hidden');
|
|
1007
|
+
if (userMenu) userMenu.classList.add('hidden');
|
|
1008
|
+
if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
|
|
927
1009
|
}
|
|
928
1010
|
|
|
929
1011
|
function showDashboard() {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1012
|
+
const authSec = getEl('auth-section');
|
|
1013
|
+
const dashSec = getEl('dashboard-section');
|
|
1014
|
+
const userMenu = getEl('user-menu');
|
|
1015
|
+
const btnOpenAuth = getEl('btn-open-auth');
|
|
1016
|
+
|
|
1017
|
+
if (authSec) authSec.classList.add('hidden');
|
|
1018
|
+
if (dashSec) dashSec.classList.remove('hidden');
|
|
1019
|
+
if (userMenu) userMenu.classList.remove('hidden');
|
|
1020
|
+
if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
|
|
934
1021
|
|
|
935
1022
|
renderAllDynamicComponents();
|
|
936
1023
|
}
|
|
937
1024
|
|
|
938
1025
|
function updateUserUI() {
|
|
939
|
-
if (!state.user)
|
|
1026
|
+
if (!state.user) {
|
|
1027
|
+
const userMenu = getEl('user-menu');
|
|
1028
|
+
const btnOpenAuth = getEl('btn-open-auth');
|
|
1029
|
+
if (userMenu) userMenu.classList.add('hidden');
|
|
1030
|
+
if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const userMenu = getEl('user-menu');
|
|
1035
|
+
const btnOpenAuth = getEl('btn-open-auth');
|
|
1036
|
+
if (userMenu) userMenu.classList.remove('hidden');
|
|
1037
|
+
if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
|
|
940
1038
|
|
|
941
1039
|
const name = state.user.name || 'Developer';
|
|
942
1040
|
const initial = name.charAt(0).toUpperCase();
|
|
943
1041
|
|
|
944
|
-
if (
|
|
945
|
-
if (
|
|
946
|
-
if (
|
|
947
|
-
if (
|
|
948
|
-
if (
|
|
1042
|
+
if (getEl('nav-avatar')) getEl('nav-avatar').innerText = initial;
|
|
1043
|
+
if (getEl('nav-user-name')) getEl('nav-user-name').innerText = name;
|
|
1044
|
+
if (getEl('user-display-name')) getEl('user-display-name').innerText = name;
|
|
1045
|
+
if (getEl('dropdown-name')) getEl('dropdown-name').innerText = name;
|
|
1046
|
+
if (getEl('dropdown-email')) getEl('dropdown-email').innerText = state.user.email;
|
|
949
1047
|
|
|
950
|
-
if (
|
|
951
|
-
if (
|
|
952
|
-
if (
|
|
1048
|
+
if (getEl('settings-display-name')) getEl('settings-display-name').value = name;
|
|
1049
|
+
if (getEl('settings-email')) getEl('settings-email').value = state.user.email;
|
|
1050
|
+
if (getEl('settings-uid')) getEl('settings-uid').value = state.user.id;
|
|
953
1051
|
|
|
954
1052
|
const tierInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
if (elements.selectByokProvider) elements.selectByokProvider.value = state.customLlmProvider || 'nvidia';
|
|
958
|
-
if (elements.inputByokEndpoint) elements.inputByokEndpoint.value = state.customBaseUrl || '';
|
|
959
|
-
if (elements.inputByokKey) elements.inputByokKey.value = state.customApiKey || '';
|
|
960
|
-
if (elements.checkboxEnableByok) elements.checkboxEnableByok.checked = Boolean(state.byokEnabled);
|
|
961
|
-
}
|
|
1053
|
+
if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
|
|
962
1054
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (btn.getAttribute('data-tab') === tabId) {
|
|
968
|
-
btn.classList.add('active');
|
|
969
|
-
} else {
|
|
970
|
-
btn.classList.remove('active');
|
|
971
|
-
}
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
document.querySelectorAll('.tab-pane').forEach((pane) => {
|
|
975
|
-
if (pane.id === `tab-${tabId}`) {
|
|
976
|
-
pane.classList.add('active');
|
|
977
|
-
} else {
|
|
978
|
-
pane.classList.remove('active');
|
|
979
|
-
}
|
|
980
|
-
});
|
|
1055
|
+
if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
|
|
1056
|
+
if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
|
|
1057
|
+
if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
|
|
1058
|
+
if (getEl('checkbox-enable-byok')) getEl('checkbox-enable-byok').checked = Boolean(state.byokEnabled);
|
|
981
1059
|
}
|
|
982
1060
|
|
|
983
1061
|
async function handleLogout() {
|
|
@@ -995,8 +1073,8 @@ async function handleLogout() {
|
|
|
995
1073
|
localStorage.removeItem('scout_auth_token');
|
|
996
1074
|
localStorage.removeItem('scout_refresh_token');
|
|
997
1075
|
|
|
998
|
-
showAuthSection();
|
|
999
1076
|
showToast('Signed out of Scout session.', 'info');
|
|
1077
|
+
window.location.href = '/auth';
|
|
1000
1078
|
}
|
|
1001
1079
|
|
|
1002
1080
|
function saveSessionToStorage() {
|
|
@@ -1039,23 +1117,24 @@ async function syncAuthWithServer() {
|
|
|
1039
1117
|
|
|
1040
1118
|
// Copy Utility
|
|
1041
1119
|
function copyText(text, successMsg = 'Copied to clipboard!') {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1120
|
+
navigator.clipboard.writeText(text).then(
|
|
1121
|
+
() => showToast(successMsg, 'success'),
|
|
1122
|
+
() => showToast('Failed to copy to clipboard', 'error')
|
|
1123
|
+
);
|
|
1046
1124
|
}
|
|
1047
1125
|
|
|
1048
1126
|
document.addEventListener('click', (e) => {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1127
|
+
const copyBtn = e.target.closest('#btn-copy-terminal-cmd');
|
|
1128
|
+
if (copyBtn) {
|
|
1129
|
+
const cmdText = getEl('terminal-cmd-display')?.innerText || 'npx -y ft-scout@latest';
|
|
1130
|
+
copyText(cmdText, 'CLI Command copied to clipboard!');
|
|
1131
|
+
}
|
|
1054
1132
|
});
|
|
1055
1133
|
|
|
1056
1134
|
// Toast System
|
|
1057
1135
|
function showToast(message, type = 'info') {
|
|
1058
|
-
|
|
1136
|
+
const container = getEl('toast-container');
|
|
1137
|
+
if (!container) return;
|
|
1059
1138
|
|
|
1060
1139
|
const toast = document.createElement('div');
|
|
1061
1140
|
toast.className = `toast toast-${type}`;
|
|
@@ -1064,8 +1143,8 @@ function showToast(message, type = 'info') {
|
|
|
1064
1143
|
if (type === 'success') iconClass = 'fa-circle-check';
|
|
1065
1144
|
if (type === 'error') iconClass = 'fa-circle-exclamation';
|
|
1066
1145
|
|
|
1067
|
-
toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${message}</span>`;
|
|
1068
|
-
|
|
1146
|
+
toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${escapeHtml(message)}</span>`;
|
|
1147
|
+
container.appendChild(toast);
|
|
1069
1148
|
|
|
1070
1149
|
setTimeout(() => {
|
|
1071
1150
|
toast.style.opacity = '0';
|
|
@@ -1083,3 +1162,382 @@ function escapeHtml(str) {
|
|
|
1083
1162
|
.replace(/>/g, '>')
|
|
1084
1163
|
.replace(/"/g, '"');
|
|
1085
1164
|
}
|
|
1165
|
+
|
|
1166
|
+
/* ==========================================================================
|
|
1167
|
+
Interactive 60 FPS Pacman Retro Arcade Game Loop Engine
|
|
1168
|
+
========================================================================== */
|
|
1169
|
+
function initPacmanGame() {
|
|
1170
|
+
const canvas = getEl('pacman-canvas');
|
|
1171
|
+
if (!canvas) return;
|
|
1172
|
+
|
|
1173
|
+
const ctx = canvas.getContext('2d');
|
|
1174
|
+
const overlay = getEl('arcade-overlay');
|
|
1175
|
+
const overlayTitle = getEl('overlay-title');
|
|
1176
|
+
const overlaySubtitle = getEl('overlay-subtitle');
|
|
1177
|
+
const btnAction = getEl('btn-arcade-action');
|
|
1178
|
+
const scoreValEl = getEl('pacman-score');
|
|
1179
|
+
const highScoreValEl = getEl('pacman-highscore');
|
|
1180
|
+
|
|
1181
|
+
let gameLoopId = null;
|
|
1182
|
+
let score = 0;
|
|
1183
|
+
let highScore = parseInt(localStorage.getItem('scout_pacman_highscore') || '12400', 10);
|
|
1184
|
+
let level = 1;
|
|
1185
|
+
let lives = 3;
|
|
1186
|
+
let gameState = 'STOPPED'; // STOPPED, PLAYING, PAUSED, GAMEOVER
|
|
1187
|
+
let crtEnabled = true;
|
|
1188
|
+
let soundEnabled = true;
|
|
1189
|
+
|
|
1190
|
+
if (highScoreValEl) highScoreValEl.innerText = String(highScore).padStart(5, '0');
|
|
1191
|
+
|
|
1192
|
+
// Classic Grid Map (1: wall, 0: pellet, 2: power pellet, 3: empty)
|
|
1193
|
+
const originalGrid = [
|
|
1194
|
+
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
1195
|
+
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
|
1196
|
+
[1, 2, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 1],
|
|
1197
|
+
[1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
|
|
1198
|
+
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
|
1199
|
+
[1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1],
|
|
1200
|
+
[1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
|
1201
|
+
[1, 1, 1, 1, 0, 1, 1, 1, 3, 1, 3, 1, 1, 1, 0, 1, 1, 1, 1],
|
|
1202
|
+
[3, 3, 3, 1, 0, 1, 3, 3, 3, 3, 3, 3, 3, 1, 0, 1, 3, 3, 3],
|
|
1203
|
+
[1, 1, 1, 1, 0, 1, 3, 1, 1, 3, 1, 1, 3, 1, 0, 1, 1, 1, 1],
|
|
1204
|
+
[3, 3, 3, 3, 0, 3, 3, 1, 3, 3, 3, 1, 3, 3, 0, 3, 3, 3, 3],
|
|
1205
|
+
[1, 1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1, 3, 1, 0, 1, 1, 1, 1],
|
|
1206
|
+
[3, 3, 3, 1, 0, 1, 3, 3, 3, 3, 3, 3, 3, 1, 0, 1, 3, 3, 3],
|
|
1207
|
+
[1, 1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1, 3, 1, 0, 1, 1, 1, 1],
|
|
1208
|
+
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
|
1209
|
+
[1, 2, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 1],
|
|
1210
|
+
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
|
|
1211
|
+
[1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1],
|
|
1212
|
+
[1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
|
1213
|
+
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
1214
|
+
];
|
|
1215
|
+
|
|
1216
|
+
let grid = JSON.parse(JSON.stringify(originalGrid));
|
|
1217
|
+
const tileSize = 22;
|
|
1218
|
+
const offsetX = (canvas.width - 19 * tileSize) / 2;
|
|
1219
|
+
const offsetY = (canvas.height - 20 * tileSize) / 2;
|
|
1220
|
+
|
|
1221
|
+
const pacman = {
|
|
1222
|
+
x: 9,
|
|
1223
|
+
y: 14,
|
|
1224
|
+
px: 9 * tileSize + tileSize / 2,
|
|
1225
|
+
py: 14 * tileSize + tileSize / 2,
|
|
1226
|
+
dirX: 0,
|
|
1227
|
+
dirY: 0,
|
|
1228
|
+
nextDirX: 0,
|
|
1229
|
+
nextDirY: 0,
|
|
1230
|
+
speed: 2,
|
|
1231
|
+
mouthAngle: 0.2,
|
|
1232
|
+
mouthSpeed: 0.03,
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
const ghosts = [
|
|
1236
|
+
{ name: 'Blinky', color: '#ef4444', x: 9, y: 9, px: 9 * tileSize + tileSize / 2, py: 9 * tileSize + tileSize / 2, dirX: 1, dirY: 0, scared: 0 },
|
|
1237
|
+
{ name: 'Pinky', color: '#ec4899', x: 9, y: 10, px: 9 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: -1, dirY: 0, scared: 0 },
|
|
1238
|
+
{ name: 'Inky', color: '#38bdf8', x: 8, y: 10, px: 8 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: 0, dirY: -1, scared: 0 },
|
|
1239
|
+
{ name: 'Clyde', color: '#fbbf24', x: 10, y: 10, px: 10 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: 0, dirY: 1, scared: 0 },
|
|
1240
|
+
];
|
|
1241
|
+
|
|
1242
|
+
function resetPositions() {
|
|
1243
|
+
pacman.x = 9;
|
|
1244
|
+
pacman.y = 14;
|
|
1245
|
+
pacman.px = 9 * tileSize + tileSize / 2;
|
|
1246
|
+
pacman.py = 14 * tileSize + tileSize / 2;
|
|
1247
|
+
pacman.dirX = 0;
|
|
1248
|
+
pacman.dirY = 0;
|
|
1249
|
+
pacman.nextDirX = 0;
|
|
1250
|
+
pacman.nextDirY = 0;
|
|
1251
|
+
|
|
1252
|
+
ghosts[0].px = 9 * tileSize + tileSize / 2; ghosts[0].py = 8 * tileSize + tileSize / 2; ghosts[0].dirX = 1; ghosts[0].dirY = 0; ghosts[0].scared = 0;
|
|
1253
|
+
ghosts[1].px = 9 * tileSize + tileSize / 2; ghosts[1].py = 10 * tileSize + tileSize / 2; ghosts[1].dirX = -1; ghosts[1].dirY = 0; ghosts[1].scared = 0;
|
|
1254
|
+
ghosts[2].px = 8 * tileSize + tileSize / 2; ghosts[2].py = 10 * tileSize + tileSize / 2; ghosts[2].dirX = 0; ghosts[2].dirY = -1; ghosts[2].scared = 0;
|
|
1255
|
+
ghosts[3].px = 10 * tileSize + tileSize / 2; ghosts[3].py = 10 * tileSize + tileSize / 2; ghosts[3].dirX = 0; ghosts[3].dirY = 1; ghosts[3].scared = 0;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function startGame() {
|
|
1259
|
+
score = 0;
|
|
1260
|
+
lives = 3;
|
|
1261
|
+
level = 1;
|
|
1262
|
+
grid = JSON.parse(JSON.stringify(originalGrid));
|
|
1263
|
+
resetPositions();
|
|
1264
|
+
gameState = 'PLAYING';
|
|
1265
|
+
if (overlay) overlay.classList.add('hidden');
|
|
1266
|
+
if (scoreValEl) scoreValEl.innerText = '00000';
|
|
1267
|
+
updateLivesUI();
|
|
1268
|
+
if (!gameLoopId) loop();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function togglePause() {
|
|
1272
|
+
if (gameState === 'PLAYING') {
|
|
1273
|
+
gameState = 'PAUSED';
|
|
1274
|
+
if (overlayTitle) overlayTitle.innerText = 'PAUSED';
|
|
1275
|
+
if (overlaySubtitle) overlaySubtitle.innerText = 'Press SPACE to Resume';
|
|
1276
|
+
if (btnAction) btnAction.innerHTML = '<i class="fa-solid fa-play"></i> RESUME';
|
|
1277
|
+
if (overlay) overlay.classList.remove('hidden');
|
|
1278
|
+
} else if (gameState === 'PAUSED') {
|
|
1279
|
+
gameState = 'PLAYING';
|
|
1280
|
+
if (overlay) overlay.classList.add('hidden');
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function updateLivesUI() {
|
|
1285
|
+
const livesEl = getEl('pacman-lives');
|
|
1286
|
+
if (livesEl) {
|
|
1287
|
+
livesEl.innerHTML = Array(Math.max(0, lives)).fill('<span class="life-pacman">🟡</span>').join('');
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function canMoveTo(gx, gy) {
|
|
1292
|
+
if (gy < 0 || gy >= grid.length || gx < 0 || gx >= grid[0].length) return false;
|
|
1293
|
+
return grid[gy][gx] !== 1;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function update() {
|
|
1297
|
+
if (gameState !== 'PLAYING') return;
|
|
1298
|
+
|
|
1299
|
+
// Pacman mouth animation
|
|
1300
|
+
pacman.mouthAngle += pacman.mouthSpeed;
|
|
1301
|
+
if (pacman.mouthAngle > 0.4 || pacman.mouthAngle < 0.05) pacman.mouthSpeed = -pacman.mouthSpeed;
|
|
1302
|
+
|
|
1303
|
+
const currentTileX = Math.floor(pacman.px / tileSize);
|
|
1304
|
+
const currentTileY = Math.floor(pacman.py / tileSize);
|
|
1305
|
+
const centerX = currentTileX * tileSize + tileSize / 2;
|
|
1306
|
+
const centerY = currentTileY * tileSize + tileSize / 2;
|
|
1307
|
+
|
|
1308
|
+
if (Math.abs(pacman.px - centerX) < pacman.speed && Math.abs(pacman.py - centerY) < pacman.speed) {
|
|
1309
|
+
pacman.px = centerX;
|
|
1310
|
+
pacman.py = centerY;
|
|
1311
|
+
|
|
1312
|
+
if (pacman.nextDirX !== 0 || pacman.nextDirY !== 0) {
|
|
1313
|
+
if (canMoveTo(currentTileX + pacman.nextDirX, currentTileY + pacman.nextDirY)) {
|
|
1314
|
+
pacman.dirX = pacman.nextDirX;
|
|
1315
|
+
pacman.dirY = pacman.nextDirY;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (!canMoveTo(currentTileX + pacman.dirX, currentTileY + pacman.dirY)) {
|
|
1320
|
+
pacman.dirX = 0;
|
|
1321
|
+
pacman.dirY = 0;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Eat Pellets
|
|
1325
|
+
if (grid[currentTileY] && grid[currentTileY][currentTileX] === 0) {
|
|
1326
|
+
grid[currentTileY][currentTileX] = 3;
|
|
1327
|
+
score += 10;
|
|
1328
|
+
if (soundEnabled) playRetroBeep(580, 0.04, 'triangle');
|
|
1329
|
+
} else if (grid[currentTileY] && grid[currentTileY][currentTileX] === 2) {
|
|
1330
|
+
grid[currentTileY][currentTileX] = 3;
|
|
1331
|
+
score += 50;
|
|
1332
|
+
ghosts.forEach(g => g.scared = 300);
|
|
1333
|
+
if (soundEnabled) playRetroBeep(880, 0.12, 'sawtooth');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (score > highScore) {
|
|
1337
|
+
highScore = score;
|
|
1338
|
+
localStorage.setItem('scout_pacman_highscore', String(highScore));
|
|
1339
|
+
if (highScoreValEl) highScoreValEl.innerText = String(highScore).padStart(5, '0');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (scoreValEl) scoreValEl.innerText = String(score).padStart(5, '0');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
pacman.px += pacman.dirX * pacman.speed;
|
|
1346
|
+
pacman.py += pacman.dirY * pacman.speed;
|
|
1347
|
+
|
|
1348
|
+
// Ghosts move
|
|
1349
|
+
ghosts.forEach((g) => {
|
|
1350
|
+
if (g.scared > 0) g.scared--;
|
|
1351
|
+
const gSpeed = g.scared > 0 ? 1 : 2;
|
|
1352
|
+
|
|
1353
|
+
const gtx = Math.floor(g.px / tileSize);
|
|
1354
|
+
const gty = Math.floor(g.py / tileSize);
|
|
1355
|
+
const gcx = gtx * tileSize + tileSize / 2;
|
|
1356
|
+
const gcy = gty * tileSize + tileSize / 2;
|
|
1357
|
+
|
|
1358
|
+
if (Math.abs(g.px - gcx) < gSpeed && Math.abs(g.py - gcy) < gSpeed) {
|
|
1359
|
+
g.px = gcx;
|
|
1360
|
+
g.py = gcy;
|
|
1361
|
+
|
|
1362
|
+
const allDirs = [
|
|
1363
|
+
{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }
|
|
1364
|
+
];
|
|
1365
|
+
let validDirs = allDirs.filter(d =>
|
|
1366
|
+
(d.x !== -g.dirX || d.y !== -g.dirY) && canMoveTo(gtx + d.x, gty + d.y)
|
|
1367
|
+
);
|
|
1368
|
+
if (validDirs.length === 0) {
|
|
1369
|
+
validDirs = allDirs.filter(d => canMoveTo(gtx + d.x, gty + d.y));
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (validDirs.length > 0) {
|
|
1373
|
+
const chosen = validDirs[Math.floor(Math.random() * validDirs.length)];
|
|
1374
|
+
g.dirX = chosen.x;
|
|
1375
|
+
g.dirY = chosen.y;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
g.px += g.dirX * gSpeed;
|
|
1380
|
+
g.py += g.dirY * gSpeed;
|
|
1381
|
+
|
|
1382
|
+
// Collision with Pacman
|
|
1383
|
+
const dist = Math.hypot(pacman.px - g.px, pacman.py - g.py);
|
|
1384
|
+
if (dist < tileSize * 0.85) {
|
|
1385
|
+
if (g.scared > 0) {
|
|
1386
|
+
score += 200;
|
|
1387
|
+
g.px = 9 * tileSize + tileSize / 2;
|
|
1388
|
+
g.py = 8 * tileSize + tileSize / 2;
|
|
1389
|
+
g.scared = 0;
|
|
1390
|
+
if (soundEnabled) playRetroBeep(1200, 0.15, 'sine');
|
|
1391
|
+
} else {
|
|
1392
|
+
lives--;
|
|
1393
|
+
updateLivesUI();
|
|
1394
|
+
if (soundEnabled) playRetroBeep(220, 0.3, 'sawtooth');
|
|
1395
|
+
if (lives <= 0) {
|
|
1396
|
+
gameState = 'GAMEOVER';
|
|
1397
|
+
if (overlayTitle) overlayTitle.innerText = 'GAME OVER';
|
|
1398
|
+
if (overlaySubtitle) overlaySubtitle.innerText = `Final Score: ${score}`;
|
|
1399
|
+
if (btnAction) btnAction.innerHTML = '<i class="fa-solid fa-rotate-right"></i> PLAY AGAIN';
|
|
1400
|
+
if (overlay) overlay.classList.remove('hidden');
|
|
1401
|
+
} else {
|
|
1402
|
+
resetPositions();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function render() {
|
|
1410
|
+
ctx.fillStyle = '#08090c';
|
|
1411
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1412
|
+
|
|
1413
|
+
ctx.save();
|
|
1414
|
+
ctx.translate(offsetX, offsetY);
|
|
1415
|
+
|
|
1416
|
+
// Draw Map Grid
|
|
1417
|
+
for (let r = 0; r < grid.length; r++) {
|
|
1418
|
+
for (let c = 0; c < grid[r].length; c++) {
|
|
1419
|
+
const tile = grid[r][c];
|
|
1420
|
+
const x = c * tileSize;
|
|
1421
|
+
const y = r * tileSize;
|
|
1422
|
+
|
|
1423
|
+
if (tile === 1) {
|
|
1424
|
+
ctx.fillStyle = '#1c202d';
|
|
1425
|
+
ctx.strokeStyle = '#38bdf8';
|
|
1426
|
+
ctx.lineWidth = 1.5;
|
|
1427
|
+
ctx.fillRect(x, y, tileSize, tileSize);
|
|
1428
|
+
ctx.strokeRect(x + 2, y + 2, tileSize - 4, tileSize - 4);
|
|
1429
|
+
} else if (tile === 0) {
|
|
1430
|
+
ctx.fillStyle = '#fbbf24';
|
|
1431
|
+
ctx.beginPath();
|
|
1432
|
+
ctx.arc(x + tileSize / 2, y + tileSize / 2, 3, 0, Math.PI * 2);
|
|
1433
|
+
ctx.fill();
|
|
1434
|
+
} else if (tile === 2) {
|
|
1435
|
+
ctx.fillStyle = '#f8fafc';
|
|
1436
|
+
ctx.beginPath();
|
|
1437
|
+
ctx.arc(x + tileSize / 2, y + tileSize / 2, 7, 0, Math.PI * 2);
|
|
1438
|
+
ctx.fill();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Draw Pacman
|
|
1444
|
+
let startAngle = pacman.mouthAngle;
|
|
1445
|
+
let endAngle = Math.PI * 2 - pacman.mouthAngle;
|
|
1446
|
+
if (pacman.dirX === -1) { startAngle += Math.PI; endAngle += Math.PI; }
|
|
1447
|
+
if (pacman.dirY === 1) { startAngle += Math.PI / 2; endAngle += Math.PI / 2; }
|
|
1448
|
+
if (pacman.dirY === -1) { startAngle -= Math.PI / 2; endAngle -= Math.PI / 2; }
|
|
1449
|
+
|
|
1450
|
+
ctx.fillStyle = '#facc15';
|
|
1451
|
+
ctx.beginPath();
|
|
1452
|
+
ctx.arc(pacman.px, pacman.py, tileSize / 2 - 2, startAngle, endAngle);
|
|
1453
|
+
ctx.lineTo(pacman.px, pacman.py);
|
|
1454
|
+
ctx.fill();
|
|
1455
|
+
|
|
1456
|
+
// Draw Ghosts
|
|
1457
|
+
ghosts.forEach((g) => {
|
|
1458
|
+
ctx.fillStyle = g.scared > 0 ? '#3b82f6' : g.color;
|
|
1459
|
+
ctx.beginPath();
|
|
1460
|
+
ctx.arc(g.px, g.py, tileSize / 2 - 2, Math.PI, 0, false);
|
|
1461
|
+
ctx.lineTo(g.px + tileSize / 2 - 2, g.py + tileSize / 2 - 2);
|
|
1462
|
+
ctx.lineTo(g.px - tileSize / 2 + 2, g.py + tileSize / 2 - 2);
|
|
1463
|
+
ctx.fill();
|
|
1464
|
+
|
|
1465
|
+
// Eyes
|
|
1466
|
+
ctx.fillStyle = '#ffffff';
|
|
1467
|
+
ctx.beginPath();
|
|
1468
|
+
ctx.arc(g.px - 3, g.py - 3, 3, 0, Math.PI * 2);
|
|
1469
|
+
ctx.arc(g.px + 3, g.py - 3, 3, 0, Math.PI * 2);
|
|
1470
|
+
ctx.fill();
|
|
1471
|
+
|
|
1472
|
+
ctx.fillStyle = '#1e1b4b';
|
|
1473
|
+
ctx.beginPath();
|
|
1474
|
+
ctx.arc(g.px - 3 + g.dirX * 1.5, g.py - 3 + g.dirY * 1.5, 1.5, 0, Math.PI * 2);
|
|
1475
|
+
ctx.arc(g.px + 3 + g.dirX * 1.5, g.py - 3 + g.dirY * 1.5, 1.5, 0, Math.PI * 2);
|
|
1476
|
+
ctx.fill();
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
ctx.restore();
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function loop() {
|
|
1483
|
+
update();
|
|
1484
|
+
render();
|
|
1485
|
+
gameLoopId = requestAnimationFrame(loop);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Input Listeners
|
|
1489
|
+
window.addEventListener('keydown', (e) => {
|
|
1490
|
+
const active = document.activeElement;
|
|
1491
|
+
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.tagName === 'SELECT' || active.isContentEditable)) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (['ArrowUp', 'KeyW'].includes(e.code)) { pacman.nextDirX = 0; pacman.nextDirY = -1; }
|
|
1496
|
+
if (['ArrowDown', 'KeyS'].includes(e.code)) { pacman.nextDirX = 0; pacman.nextDirY = 1; }
|
|
1497
|
+
if (['ArrowLeft', 'KeyA'].includes(e.code)) { pacman.nextDirX = -1; pacman.nextDirY = 0; }
|
|
1498
|
+
if (['ArrowRight', 'KeyD'].includes(e.code)) { pacman.nextDirX = 1; pacman.nextDirY = 0; }
|
|
1499
|
+
if (e.code === 'Space') {
|
|
1500
|
+
e.preventDefault();
|
|
1501
|
+
if (gameState === 'STOPPED' || gameState === 'GAMEOVER') startGame();
|
|
1502
|
+
else togglePause();
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
btnAction?.addEventListener('click', startGame);
|
|
1507
|
+
getEl('btn-pacman-pause')?.addEventListener('click', togglePause);
|
|
1508
|
+
getEl('btn-pacman-restart')?.addEventListener('click', startGame);
|
|
1509
|
+
|
|
1510
|
+
const handleDir = (dx, dy) => (e) => {
|
|
1511
|
+
if (e.type === 'touchstart') e.preventDefault();
|
|
1512
|
+
pacman.nextDirX = dx;
|
|
1513
|
+
pacman.nextDirY = dy;
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
['click', 'pointerdown', 'touchstart'].forEach((evt) => {
|
|
1517
|
+
getEl('dpad-up')?.addEventListener(evt, handleDir(0, -1));
|
|
1518
|
+
getEl('dpad-down')?.addEventListener(evt, handleDir(0, 1));
|
|
1519
|
+
getEl('dpad-left')?.addEventListener(evt, handleDir(-1, 0));
|
|
1520
|
+
getEl('dpad-right')?.addEventListener(evt, handleDir(1, 0));
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
getEl('btn-toggle-crt')?.addEventListener('click', () => {
|
|
1524
|
+
const screen = getEl('arcade-screen');
|
|
1525
|
+
if (screen) {
|
|
1526
|
+
crtEnabled = !crtEnabled;
|
|
1527
|
+
screen.classList.toggle('crt-active', crtEnabled);
|
|
1528
|
+
showToast(`CRT Filter ${crtEnabled ? 'ENABLED' : 'DISABLED'}`, 'info');
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
getEl('btn-toggle-sound')?.addEventListener('click', () => {
|
|
1533
|
+
soundEnabled = !soundEnabled;
|
|
1534
|
+
const btn = getEl('btn-toggle-sound');
|
|
1535
|
+
if (btn) {
|
|
1536
|
+
btn.innerHTML = soundEnabled
|
|
1537
|
+
? '<i class="fa-solid fa-volume-high text-success"></i> Sound ON'
|
|
1538
|
+
: '<i class="fa-solid fa-volume-xmark text-danger"></i> Sound OFF';
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
render();
|
|
1543
|
+
}
|