claude-code-templates 1.8.0 → 1.8.2

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.
@@ -0,0 +1,596 @@
1
+ /**
2
+ * SessionTimer Component
3
+ * Displays current session information, timing, token usage, and Max plan limits
4
+ */
5
+ class SessionTimer {
6
+ constructor(container, dataService, stateService) {
7
+ this.container = container;
8
+ this.dataService = dataService;
9
+ this.stateService = stateService;
10
+ this.sessionData = null;
11
+ this.updateInterval = null;
12
+ this.isInitialized = false;
13
+ this.refreshInterval = 1000; // 1 second for real-time updates
14
+ this.SESSION_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
15
+ this.isTooltipVisible = false; // Track tooltip state globally
16
+ }
17
+
18
+ /**
19
+ * Initialize the session timer component
20
+ */
21
+ async initialize() {
22
+ if (this.isInitialized) return;
23
+
24
+ try {
25
+ await this.render();
26
+ await this.loadSessionData();
27
+ this.startAutoUpdate();
28
+ this.isInitialized = true;
29
+
30
+ console.log('📊 SessionTimer component initialized');
31
+ } catch (error) {
32
+ console.error('Error initializing SessionTimer:', error);
33
+ this.showError('Failed to initialize session timer');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Render the session timer UI
39
+ */
40
+ async render() {
41
+ this.container.innerHTML = `
42
+ <div class="session-timer-accordion">
43
+ <div class="session-timer-header" onclick="window.sessionTimer?.toggleAccordion()">
44
+ <div class="session-timer-title-section">
45
+ <span class="session-timer-chevron">▼</span>
46
+ <h3 class="session-timer-title">Current Session</h3>
47
+ </div>
48
+ <div class="session-timer-status-inline">
49
+ <span class="session-timer-status-dot"></span>
50
+ <span class="session-timer-status-text">Loading...</span>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="session-timer-content" id="session-timer-content">
55
+ <div class="session-loading-state">
56
+ <div class="session-spinner"></div>
57
+ <span>Loading session data...</span>
58
+ </div>
59
+
60
+ <div class="session-display" style="display: none;">
61
+ <!-- Session timer display will be populated here -->
62
+ </div>
63
+
64
+ <div class="session-warnings">
65
+ <!-- Warnings will be displayed here -->
66
+ </div>
67
+ </div>
68
+ </div>
69
+ `;
70
+
71
+ // Make component globally accessible for button clicks
72
+ window.sessionTimer = this;
73
+ this.isExpanded = true; // Start expanded
74
+ }
75
+
76
+ /**
77
+ * Load session data from API
78
+ */
79
+ async loadSessionData() {
80
+ try {
81
+ this.sessionData = await this.dataService.getSessionData();
82
+ this.updateDisplay();
83
+ } catch (error) {
84
+ console.error('Error loading session data:', error);
85
+ this.showError('Failed to load session data');
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Refresh session data manually
91
+ */
92
+ async refreshSessionData() {
93
+ const refreshBtn = this.container.querySelector('.session-refresh-btn button');
94
+ if (refreshBtn) {
95
+ refreshBtn.disabled = true;
96
+ refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
97
+ }
98
+
99
+ try {
100
+ await this.loadSessionData();
101
+ } finally {
102
+ if (refreshBtn) {
103
+ refreshBtn.disabled = false;
104
+ refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i>';
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Update the display with current session data
111
+ */
112
+ updateDisplay() {
113
+ if (!this.sessionData) return;
114
+
115
+ const loadingState = this.container.querySelector('.session-loading-state');
116
+ const sessionDisplay = this.container.querySelector('.session-display');
117
+ const warningsContainer = this.container.querySelector('.session-warnings');
118
+
119
+ // Update title with plan name
120
+ const titleElement = this.container.querySelector('.session-timer-title');
121
+ if (titleElement && this.sessionData.limits) {
122
+ titleElement.textContent = `Current Session - ${this.sessionData.limits.name}`;
123
+ }
124
+
125
+ if (loadingState) loadingState.style.display = 'none';
126
+ if (sessionDisplay) sessionDisplay.style.display = 'block';
127
+
128
+ // Update session display
129
+ this.renderSessionInfo(sessionDisplay);
130
+
131
+ // Update warnings
132
+ this.renderWarnings(warningsContainer);
133
+ }
134
+
135
+ /**
136
+ * Load Claude session information
137
+ */
138
+ async loadClaudeSessionInfo() {
139
+ try {
140
+ const response = await fetch('/api/claude/session');
141
+ if (!response.ok) throw new Error('Failed to fetch session info');
142
+ return await response.json();
143
+ } catch (error) {
144
+ console.error('Error loading Claude session info:', error);
145
+ return null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Render session information
151
+ */
152
+ async renderSessionInfo(container) {
153
+ const { timer, userPlan, monthlyUsage, limits } = this.sessionData;
154
+
155
+ // Load Claude session info
156
+ const claudeSessionInfo = await this.loadClaudeSessionInfo();
157
+
158
+ // Update header status
159
+ this.updateHeaderStatus(timer, claudeSessionInfo);
160
+
161
+ if (!timer.hasActiveSession) {
162
+ container.innerHTML = `
163
+ <div class="session-timer-empty">
164
+ <div class="session-timer-empty-text">No active session</div>
165
+ <div class="session-timer-empty-subtext">Start a conversation to begin tracking</div>
166
+ </div>
167
+ `;
168
+ return;
169
+ }
170
+
171
+ // Calculate progress colors based on usage
172
+ const progressPercentage = Math.round(timer.sessionProgress);
173
+ const timeProgressPercentage = Math.round(((this.SESSION_DURATION - timer.timeRemaining) / this.SESSION_DURATION) * 100);
174
+
175
+ const getProgressColor = (percentage) => {
176
+ if (percentage < 50) return '#3fb950';
177
+ if (percentage < 80) return '#f97316';
178
+ return '#f85149';
179
+ };
180
+
181
+ const messageProgressColor = getProgressColor(progressPercentage);
182
+ const timeProgressColor = getProgressColor(timeProgressPercentage);
183
+
184
+ // Format time remaining with better UX
185
+ const formatTimeRemaining = (ms) => {
186
+ const hours = Math.floor(ms / (1000 * 60 * 60));
187
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
188
+ const seconds = Math.floor((ms % (1000 * 60)) / 1000);
189
+
190
+ if (hours > 0) {
191
+ return `${hours}h ${minutes}m`;
192
+ } else if (minutes > 0) {
193
+ return `${minutes}m ${seconds}s`;
194
+ } else {
195
+ return `${seconds}s`;
196
+ }
197
+ };
198
+
199
+ container.innerHTML = `
200
+ <div class="session-timer-compact">
201
+
202
+ <div class="session-timer-row">
203
+ <div class="session-timer-time-compact">
204
+ <div class="session-timer-time-value">${claudeSessionInfo && claudeSessionInfo.hasSession ?
205
+ (claudeSessionInfo.estimatedTimeRemaining.isExpired ? 'Expired' : claudeSessionInfo.estimatedTimeRemaining.formatted) :
206
+ formatTimeRemaining(timer.timeRemaining)
207
+ }</div>
208
+ <div class="session-timer-time-label">remaining</div>
209
+ </div>
210
+
211
+ <div class="session-timer-progress-compact">
212
+ <div class="session-timer-progress-item">
213
+ <div class="session-timer-progress-header">
214
+ <span class="session-timer-progress-label">Messages</span>
215
+ <span class="session-timer-progress-value">
216
+ ${timer.messagesUsed}/${timer.messagesLimit}
217
+ <span class="session-timer-info-icon" data-tooltip="message-info" title="Message calculation info">
218
+ ℹ️
219
+ </span>
220
+ </span>
221
+ </div>
222
+ <div class="session-timer-progress-bar">
223
+ <div class="session-timer-progress-fill"
224
+ style="width: ${progressPercentage}%; background-color: ${messageProgressColor};"></div>
225
+ </div>
226
+ ${timer.usageDetails && timer.usageDetails.shortMessages > 0 ? `
227
+ <div class="session-timer-usage-details">
228
+ <small>Short: ${timer.usageDetails.shortMessages}, Long: ${timer.usageDetails.longMessages}</small>
229
+ </div>
230
+ ` : ''}
231
+ </div>
232
+
233
+ <div class="session-timer-progress-item">
234
+ <div class="session-timer-progress-header">
235
+ <span class="session-timer-progress-label">Session Time</span>
236
+ <span class="session-timer-progress-value">${claudeSessionInfo && claudeSessionInfo.hasSession ?
237
+ `${claudeSessionInfo.sessionDuration.formatted}/${claudeSessionInfo.sessionLimit.formatted}` :
238
+ `${formatTimeRemaining(this.SESSION_DURATION - timer.timeRemaining)}/5h`
239
+ }</span>
240
+ </div>
241
+ <div class="session-timer-progress-bar">
242
+ <div class="session-timer-progress-fill"
243
+ style="width: ${claudeSessionInfo && claudeSessionInfo.hasSession ?
244
+ Math.min(100, (claudeSessionInfo.sessionDuration.ms / claudeSessionInfo.sessionLimit.ms) * 100) :
245
+ timeProgressPercentage
246
+ }%; background-color: ${claudeSessionInfo && claudeSessionInfo.hasSession ?
247
+ (claudeSessionInfo.estimatedTimeRemaining.isExpired ? '#f85149' :
248
+ claudeSessionInfo.estimatedTimeRemaining.ms < 600000 ? '#f97316' : '#3fb950') :
249
+ timeProgressColor
250
+ };"></div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ </div>
257
+ `;
258
+
259
+ // Add popover to the container
260
+ this.addPopover(container);
261
+
262
+ // Add popover event listeners
263
+ this.setupPopoverEvents(container);
264
+ }
265
+
266
+ /**
267
+ * Add popover to the container
268
+ */
269
+ addPopover(container) {
270
+ // Check if popover already exists
271
+ const existingPopover = document.getElementById('message-info-tooltip');
272
+ if (existingPopover) {
273
+ // Don't recreate if it already exists, just return
274
+ return;
275
+ }
276
+
277
+ // Create popover HTML
278
+ const popoverHTML = `
279
+ <div class="session-timer-tooltip" id="message-info-tooltip" style="display: ${this.isTooltipVisible ? 'block' : 'none'};">
280
+ <div class="session-timer-tooltip-content">
281
+ <h4>Message Count Calculation</h4>
282
+ <p>This count includes only user messages (your prompts) within the current 5-hour session window. Assistant responses are not counted toward usage limits.</p>
283
+ <p>The actual limit varies based on message length, conversation context, and current system capacity. The displayed limit is an estimate for typical usage.</p>
284
+ <div class="session-timer-tooltip-link">
285
+ <a href="https://support.anthropic.com/en/articles/9797557-usage-limit-best-practices" target="_blank" rel="noopener noreferrer">
286
+ <i class="fas fa-external-link-alt"></i> Usage Limit Best Practices
287
+ </a>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ `;
292
+
293
+ // Add popover to document body for better positioning
294
+ document.body.insertAdjacentHTML('beforeend', popoverHTML);
295
+ }
296
+
297
+ /**
298
+ * Setup popover event listeners
299
+ */
300
+ setupPopoverEvents(container) {
301
+ const infoIcon = container.querySelector('.session-timer-info-icon');
302
+ const tooltip = document.getElementById('message-info-tooltip');
303
+
304
+ if (infoIcon && tooltip) {
305
+ // Remove existing listeners to prevent duplicates
306
+ const existingClickHandler = infoIcon.clickHandler;
307
+ if (existingClickHandler) {
308
+ infoIcon.removeEventListener('click', existingClickHandler);
309
+ }
310
+
311
+ // Create new click handler
312
+ const clickHandler = (e) => {
313
+ e.stopPropagation();
314
+ if (this.isTooltipVisible) {
315
+ this.hideTooltip(tooltip);
316
+ this.isTooltipVisible = false;
317
+ } else {
318
+ this.showTooltip(tooltip, infoIcon);
319
+ this.isTooltipVisible = true;
320
+ }
321
+ };
322
+
323
+ // Store handler reference for cleanup
324
+ infoIcon.clickHandler = clickHandler;
325
+
326
+ // Add click listener
327
+ infoIcon.addEventListener('click', clickHandler);
328
+
329
+ // Setup document click listener only once
330
+ if (!this.documentClickSetup) {
331
+ document.addEventListener('click', (e) => {
332
+ if (this.isTooltipVisible && !tooltip.contains(e.target) && !infoIcon.contains(e.target)) {
333
+ this.hideTooltip(tooltip);
334
+ this.isTooltipVisible = false;
335
+ }
336
+ });
337
+ this.documentClickSetup = true;
338
+ }
339
+
340
+ // Prevent tooltip from closing when clicking inside it
341
+ tooltip.addEventListener('click', (e) => {
342
+ e.stopPropagation();
343
+ });
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Show tooltip with positioning
349
+ */
350
+ showTooltip(tooltip, trigger) {
351
+ const rect = trigger.getBoundingClientRect();
352
+ tooltip.style.display = 'block';
353
+ const tooltipRect = tooltip.getBoundingClientRect();
354
+
355
+ // Position tooltip below the icon
356
+ tooltip.style.left = `${rect.left - tooltipRect.width / 2 + rect.width / 2}px`;
357
+ tooltip.style.top = `${rect.bottom + 10}px`;
358
+
359
+ // Adjust position if tooltip goes off-screen horizontally
360
+ const viewportWidth = window.innerWidth;
361
+ const tooltipLeft = parseInt(tooltip.style.left);
362
+
363
+ if (tooltipLeft < 10) {
364
+ tooltip.style.left = '10px';
365
+ } else if (tooltipLeft + tooltipRect.width > viewportWidth - 10) {
366
+ tooltip.style.left = `${viewportWidth - tooltipRect.width - 10}px`;
367
+ }
368
+
369
+ // Adjust position if tooltip goes off-screen vertically
370
+ const viewportHeight = window.innerHeight;
371
+ const tooltipTop = parseInt(tooltip.style.top);
372
+
373
+ if (tooltipTop + tooltipRect.height > viewportHeight - 10) {
374
+ // If it goes off-screen below, position it above the trigger
375
+ tooltip.style.top = `${rect.top - tooltipRect.height - 10}px`;
376
+ }
377
+
378
+ this.isTooltipVisible = true;
379
+ }
380
+
381
+ /**
382
+ * Hide tooltip
383
+ */
384
+ hideTooltip(tooltip) {
385
+ tooltip.style.display = 'none';
386
+ this.isTooltipVisible = false;
387
+ }
388
+
389
+ /**
390
+ * Render warnings if any
391
+ */
392
+ renderWarnings(container) {
393
+ if (!this.sessionData || !this.sessionData.warnings || this.sessionData.warnings.length === 0) {
394
+ container.innerHTML = '';
395
+ return;
396
+ }
397
+
398
+ const warnings = this.sessionData.warnings
399
+ .filter(warning => warning.type.includes('session') || warning.type.includes('monthly'))
400
+ .slice(0, 3); // Show max 3 warnings
401
+
402
+ if (warnings.length === 0) {
403
+ container.innerHTML = '';
404
+ return;
405
+ }
406
+
407
+ const warningHtml = warnings.map(warning => `
408
+ <div class="session-warning ${warning.level}">
409
+ <i class="fas ${this.getWarningIcon(warning.level)}"></i>
410
+ <span>${warning.message}</span>
411
+ ${warning.timeRemaining ? `<small>Time remaining: ${this.formatTimeRemaining(warning.timeRemaining)}</small>` : ''}
412
+ </div>
413
+ `).join('');
414
+
415
+ container.innerHTML = `<div class="warnings-list">${warningHtml}</div>`;
416
+ }
417
+
418
+ /**
419
+ * Get message limit based on current plan
420
+ */
421
+ getMessageLimit() {
422
+ if (!this.sessionData || !this.sessionData.limits) return 225;
423
+ return this.sessionData.limits.messagesPerSession;
424
+ }
425
+
426
+ /**
427
+ * Get plan badge CSS class
428
+ */
429
+ getPlanBadgeClass(planType) {
430
+ const classes = {
431
+ 'premium': 'plan-premium',
432
+ 'standard': 'plan-standard',
433
+ 'pro': 'plan-pro'
434
+ };
435
+ return classes[planType] || 'plan-standard';
436
+ }
437
+
438
+ /**
439
+ * Get warning icon based on level
440
+ */
441
+ getWarningIcon(level) {
442
+ const icons = {
443
+ 'error': 'fa-exclamation-triangle',
444
+ 'warning': 'fa-exclamation-circle',
445
+ 'info': 'fa-info-circle'
446
+ };
447
+ return icons[level] || 'fa-info-circle';
448
+ }
449
+
450
+ /**
451
+ * Format time remaining for display
452
+ */
453
+ formatTimeRemaining(milliseconds) {
454
+ if (milliseconds <= 0) return '0m';
455
+
456
+ const hours = Math.floor(milliseconds / (60 * 60 * 1000));
457
+ const minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000));
458
+
459
+ if (hours > 0) {
460
+ return `${hours}h ${minutes}m`;
461
+ }
462
+ return `${minutes}m`;
463
+ }
464
+
465
+ /**
466
+ * Format numbers with appropriate units
467
+ */
468
+ formatNumber(num) {
469
+ if (num >= 1000000) {
470
+ return (num / 1000000).toFixed(1) + 'M';
471
+ }
472
+ if (num >= 1000) {
473
+ return (num / 1000).toFixed(1) + 'K';
474
+ }
475
+ return num.toString();
476
+ }
477
+
478
+ /**
479
+ * Show error message
480
+ */
481
+ showError(message) {
482
+ const loadingState = this.container.querySelector('.session-loading-state');
483
+ const sessionDisplay = this.container.querySelector('.session-display');
484
+
485
+ if (loadingState) loadingState.style.display = 'none';
486
+ if (sessionDisplay) {
487
+ sessionDisplay.style.display = 'block';
488
+ sessionDisplay.innerHTML = `
489
+ <div class="session-timer-error">
490
+ <div class="session-timer-error-text">${message}</div>
491
+ <button class="session-timer-retry-btn" onclick="window.sessionTimer?.refreshSessionData()">Retry</button>
492
+ </div>
493
+ `;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Start automatic updates
499
+ */
500
+ startAutoUpdate() {
501
+ // Update every 1 second for real-time display
502
+ this.updateInterval = setInterval(() => {
503
+ this.loadSessionData();
504
+ }, this.refreshInterval);
505
+ }
506
+
507
+ /**
508
+ * Stop automatic updates
509
+ */
510
+ stopAutoUpdate() {
511
+ if (this.updateInterval) {
512
+ clearInterval(this.updateInterval);
513
+ this.updateInterval = null;
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Handle real-time updates
519
+ */
520
+ handleRealtimeUpdate(data) {
521
+ if (data.type === 'session_update' && data.sessionData) {
522
+ this.sessionData = data.sessionData;
523
+ this.updateDisplay();
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Toggle accordion open/closed
529
+ */
530
+ toggleAccordion() {
531
+ const content = this.container.querySelector('#session-timer-content');
532
+ const chevron = this.container.querySelector('.session-timer-chevron');
533
+
534
+ if (this.isExpanded) {
535
+ content.style.display = 'none';
536
+ chevron.textContent = '▶';
537
+ this.isExpanded = false;
538
+ } else {
539
+ content.style.display = 'block';
540
+ chevron.textContent = '▼';
541
+ this.isExpanded = true;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Update header status display
547
+ */
548
+ updateHeaderStatus(timer, claudeSessionInfo) {
549
+ const statusDot = this.container.querySelector('.session-timer-status-dot');
550
+ const statusText = this.container.querySelector('.session-timer-status-text');
551
+
552
+ // If we have Claude session info, prioritize that
553
+ if (claudeSessionInfo && claudeSessionInfo.hasSession) {
554
+ if (claudeSessionInfo.estimatedTimeRemaining.isExpired) {
555
+ statusDot.className = 'session-timer-status-dot expired';
556
+ statusText.textContent = 'Session Expired';
557
+ } else if (claudeSessionInfo.estimatedTimeRemaining.ms < 600000) { // < 10 minutes
558
+ statusDot.className = 'session-timer-status-dot warning';
559
+ statusText.textContent = 'Ending Soon';
560
+ } else {
561
+ statusDot.className = 'session-timer-status-dot active';
562
+ statusText.textContent = 'Active';
563
+ }
564
+ } else if (!timer.hasActiveSession) {
565
+ statusDot.className = 'session-timer-status-dot inactive';
566
+ statusText.textContent = 'Inactive';
567
+ } else if (timer.timeRemaining < 600000) {
568
+ statusDot.className = 'session-timer-status-dot warning';
569
+ statusText.textContent = 'Ending Soon';
570
+ } else {
571
+ statusDot.className = 'session-timer-status-dot active';
572
+ statusText.textContent = 'Active';
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Cleanup component
578
+ */
579
+ destroy() {
580
+ this.stopAutoUpdate();
581
+ if (window.sessionTimer === this) {
582
+ delete window.sessionTimer;
583
+ }
584
+ this.isInitialized = false;
585
+ }
586
+ }
587
+
588
+ // Export for module systems
589
+ if (typeof module !== 'undefined' && module.exports) {
590
+ module.exports = SessionTimer;
591
+ }
592
+
593
+ // Global registration for browser
594
+ if (typeof window !== 'undefined') {
595
+ window.SessionTimer = SessionTimer;
596
+ }