diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80bc69f Binary files /dev/null and b/.gitignore differ diff --git a/Claude.md b/Claude.md new file mode 100644 index 0000000..900d60b --- /dev/null +++ b/Claude.md @@ -0,0 +1,963 @@ +# Fed-Chatbot Codebase Analysis + +> **Comprehensive observations and analysis of the FED Chatbot application** +> +> **Analyzed on:** December 18, 2025 +> +> **Analyzer:** Claude (Antigravity AI Assistant) + +--- + +## 📊 Executive Summary + +The **FED Chatbot** is a well-architected, modern React-based AI chatbot application that serves as an intelligent assistant for the Federation of Entrepreneurship Development (FED) at KIIT University. The codebase demonstrates professional development practices with clean separation of concerns, intelligent caching mechanisms, and comprehensive documentation. + +### Key Metrics +- **Total Files Analyzed:** 20+ source files +- **Lines of Code:** ~2,500+ (excluding node_modules) +- **Tech Stack:** React 18.3.1, Vite 7.2.6, SCSS, Axios, Google Gemini AI +- **Architecture Pattern:** Service-oriented with singleton caching +- **Code Quality:** High (comprehensive JSDoc, error handling, modular design) + +--- + +## 🏗️ Architecture Overview + +### High-Level Structure + +``` +Fed-Chatbot/ +├── src/ +│ ├── components/Chatbot/ # UI Layer +│ ├── services/ # Business Logic Layer +│ ├── config/ # Configuration Layer +│ └── styles/ # Styling Layer +├── public/ # Static Assets +└── Configuration Files # Build & Environment +``` + +### Design Patterns Observed + +1. **Singleton Pattern** - Used in `TeamDataManager` class for application-wide cache consistency +2. **Service Layer Pattern** - Clear separation between UI (Chatbot.jsx) and business logic (services/) +3. **Facade Pattern** - `chatbotAPI.js` acts as a facade orchestrating multiple services +4. **Strategy Pattern** - Retry logic with exponential backoff in API calls +5. **Module Pattern** - Each service exports specific functionality with clear interfaces + +--- + +## 🎯 Core Components Analysis + +### 1. **Chatbot Component** (`src/components/Chatbot/Chatbot.jsx`) + +**Purpose:** Main UI component handling user interactions and message display + +**Key Features:** +- ✅ Voice recognition using Web Speech API +- ✅ Real-time message rendering with markdown support +- ✅ Suggested prompts for quick actions +- ✅ Auto-scroll to latest messages +- ✅ Typing indicator animation +- ✅ Iframe embedding support (postMessage API) + +**State Management:** +```javascript +- messages: Array of message objects +- userInput: Current input text +- isTyping: Bot typing indicator +- isOpen: Chatbot visibility +- isListening: Voice input active state +``` + +**Notable Implementation Details:** +- Uses `dangerouslySetInnerHTML` for rendering formatted messages (with regex-based markdown parsing) +- Implements regex transformations for: + - `@fedkiit` → Instagram link + - `**text**` → Bold text + - `[text](url)` → Clickable links + - Plain URLs → Clickable links + - `\n` → Line breaks + +**Potential Improvements:** +- Consider using a proper markdown parser library (e.g., `marked` or `react-markdown`) instead of regex for more robust parsing +- Voice recognition could benefit from error recovery and browser compatibility warnings + +--- + +### 2. **Service Layer** (`src/services/`) + +#### **chatbotAPI.js** - Main Orchestrator + +**Responsibilities:** +- Coordinates team and events data fetching +- Orchestrates AI response generation +- Provides health check functionality +- Returns statistics about data + +**Key Methods:** +- `sendMessage(message, options)` - Main entry point for chat interactions +- `ping()` - Health check for all services +- `getStats()` - Returns current data statistics + +**Design Strengths:** +- Parallel data fetching using `Promise.all()` for performance +- Graceful error handling with fallback to empty arrays +- Comprehensive logging with prefixed messages +- Metadata in responses for debugging + +--- + +#### **geminiAPI.js** - AI Integration + +**Responsibilities:** +- Interfaces with Google Gemini AI API +- Implements retry logic with exponential backoff +- Prepares context from team and events data +- Validates AI responses + +**Key Features:** +- ✅ Exponential backoff retry mechanism (max 3 attempts) +- ✅ 30-second timeout for API calls +- ✅ Context injection with structured delimiters +- ✅ Response validation and error handling + +**Context Injection Format:** +``` +### LIVE TEAM DATA START ### +[JSON array of team members] +### LIVE TEAM DATA END ### + +### LIVE EVENT DATA START ### +[JSON array of upcoming events] +### LIVE EVENT DATA END ### + +[Past events data] +### LIVE PAST EVENT DATA END ### + +User Query: [user's question] +``` + +**Observations:** +- Well-structured error handling for different HTTP status codes +- Timeout handling for slow responses +- System prompt injection from environment variables + +--- + +#### **teamAPI.js** - Team Data Fetching + +**Responsibilities:** +- Fetches team members from FED backend +- Provides utility methods for filtering and searching +- Implements sorting logic (by year DESC, then name ASC) + +**Key Methods:** +- `fetchTeamMembers()` - Main fetch with filtering and sorting +- `findByName(name)` - Search by member name +- `findByRole(role)` - Filter by access code +- `getBoardMembers()` - Get leadership positions +- `searchMembers(query)` - Full-text search + +**Data Processing:** +- Filters out null names +- Sorts by year (newest first), then alphabetically by name +- 10-second timeout for API calls + +--- + +#### **teamDataManager.js** - Team Data Caching + +**Responsibilities:** +- Manages team data cache with 2-minute TTL +- Prevents duplicate simultaneous fetches +- Provides cache statistics + +**Key Features:** +- ✅ Singleton pattern for application-wide consistency +- ✅ Cache freshness checking +- ✅ Duplicate fetch prevention with `isFetching` flag +- ✅ Fallback to stale cache on error +- ✅ Manual cache refresh and clear methods + +**Cache Logic:** +```javascript +if (cacheAge < 2 minutes) { + return cached data +} else { + fetch fresh data + update cache + return fresh data +} +``` + +**Observations:** +- Smart duplicate fetch prevention using async waiting +- Graceful degradation to cached data on errors +- Comprehensive cache statistics for debugging + +--- + +#### **eventsDataManager.js** - Events Data Caching + +**Responsibilities:** +- Fetches and caches events data +- Separates upcoming vs past events +- Implements retry logic with exponential backoff + +**Key Features:** +- ✅ 2-minute cache duration +- ✅ Automatic separation of upcoming/past events +- ✅ Keeps only last 5 past events +- ✅ Retry logic with exponential backoff +- ✅ Search and filter utilities + +**Data Processing:** +```javascript +upcomingEvents = events.filter(e => !e.info.isEventPast) +pastEvents = events + .filter(e => e.info.isEventPast) + .sort(by date DESC) + .slice(0, 5) + .map(e => e.info) +``` + +**Observations:** +- Efficient data processing with filtering and sorting +- Stale cache fallback on fetch errors +- Comprehensive utility methods for event queries + +--- + +### 3. **Configuration** (`src/config/constants.js`) + +**Purpose:** Centralized configuration management + +**Key Features:** +- ✅ Environment variable validation on module load +- ✅ Frozen objects for immutability +- ✅ Default values for optional configurations +- ✅ Comprehensive error messages and logging prefixes + +**Configuration Sections:** +- `API` - Backend endpoints +- `GEMINI` - AI service configuration +- `CHATBOT` - Chatbot settings +- `CACHE` - Cache durations +- `RETRY` - Retry logic parameters + +**Observations:** +- Excellent use of `Object.freeze()` for immutability +- Validation ensures required env vars are present +- Clear separation of concerns + +--- + +### 4. **Styling** (`src/styles/` & `Chatbot.module.scss`) + +**Approach:** SCSS with CSS Modules for component isolation + +**Design System:** +- **Colors:** FED brand gradient (orange to red) +- **Typography:** Poppins font family +- **Spacing:** Consistent spacing scale (xs to xl) +- **Animations:** Smooth transitions and micro-interactions + +**Key Style Features:** +- ✅ Glassmorphism effects with backdrop-filter +- ✅ Responsive design (mobile, tablet, desktop breakpoints) +- ✅ Custom scrollbar styling +- ✅ Smooth animations (fadeIn, slideIn, bounce, pulse, typing) +- ✅ Accessibility considerations (aria-labels, focus states) + +**Notable Animations:** +- Pulse ring on toggle button +- Bounce animation on toggle button +- Slide-in animation for chatbot container +- Typing indicator with staggered dots +- Message slide-in animation + +**Responsive Breakpoints:** +- Mobile: ≤ 480px +- Tablet: ≤ 768px +- Desktop: ≤ 1024px + +**Observations:** +- Professional, modern design with attention to detail +- Excellent use of CSS variables for maintainability +- Smooth micro-interactions enhance UX +- Glassmorphism creates premium feel + +--- + +## 🔍 Technical Deep Dive + +### Caching Strategy + +**Implementation:** +- **Team Data:** 2-minute TTL, singleton pattern +- **Events Data:** 2-minute TTL, module-level cache +- **Cache Invalidation:** Time-based expiration + +**Benefits:** +- Reduces API calls by ~70-80% (estimated) +- Improves response time from ~3.5s to ~1s +- Reduces backend load +- Provides stale data fallback on errors + +**Potential Improvements:** +- Consider implementing cache warming on app load +- Add manual cache refresh UI control +- Implement cache versioning for data consistency + +--- + +### Error Handling + +**Strategies Observed:** +1. **Graceful Degradation:** Returns empty arrays/objects on errors +2. **Retry Logic:** Exponential backoff for transient failures +3. **User-Friendly Messages:** Clear error messages from constants +4. **Logging:** Comprehensive console logging with prefixes +5. **Fallback Data:** Uses stale cache when fresh fetch fails + +**Error Scenarios Covered:** +- Network failures +- API timeouts +- Invalid responses +- Empty responses +- Rate limiting (429 errors) +- Authentication errors (401, 403) + +--- + +### Performance Optimizations + +1. **Parallel Data Fetching:** Uses `Promise.all()` for team and events +2. **Intelligent Caching:** 2-minute TTL reduces redundant API calls +3. **Duplicate Fetch Prevention:** Prevents concurrent identical requests +4. **Lazy Loading:** Components only render when chatbot is open +5. **Debounced Scrolling:** Auto-scroll only when needed +6. **CSS Animations:** Hardware-accelerated transforms + +**Estimated Performance:** +- First message: ~3.5-5.5s (includes API calls) +- Subsequent messages (cached): ~1-3s (Gemini AI only) +- Cache hit rate: ~70-80% (estimated) + +--- + +### Security Considerations + +**Current Implementations:** +- ✅ Environment variables for sensitive data (.env) +- ✅ .gitignore includes .env files +- ✅ HTTPS required for voice recognition +- ✅ No hardcoded API keys in source code +- ✅ CORS enabled on server + +**Potential Security Improvements:** +- Consider implementing rate limiting on client side +- Add input sanitization for user messages +- Implement CSP (Content Security Policy) headers +- Add API key rotation mechanism +- Consider implementing user session management + +--- + +## 📝 Code Quality Assessment + +### Strengths + +1. **Documentation:** + - ✅ Comprehensive README with detailed explanations + - ✅ JSDoc comments on all major functions + - ✅ Inline comments for complex logic + - ✅ Clear file headers with module descriptions + +2. **Code Organization:** + - ✅ Clear separation of concerns (UI, services, config, styles) + - ✅ Modular design with single responsibility + - ✅ Consistent file naming conventions + - ✅ Logical folder structure + +3. **Error Handling:** + - ✅ Comprehensive try-catch blocks + - ✅ Graceful degradation + - ✅ User-friendly error messages + - ✅ Detailed logging for debugging + +4. **Best Practices:** + - ✅ Immutable configuration objects + - ✅ Environment variable validation + - ✅ Consistent code style + - ✅ DRY principle followed + +### Areas for Improvement + +1. **Testing:** + - ❌ No unit tests found + - ❌ No integration tests + - ❌ No E2E tests + - **Recommendation:** Add Jest + React Testing Library + +2. **Type Safety:** + - ❌ No TypeScript + - ❌ No PropTypes validation + - **Recommendation:** Consider migrating to TypeScript or add PropTypes + +3. **Accessibility:** + - ⚠️ Some aria-labels present, but could be more comprehensive + - ⚠️ Keyboard navigation could be improved + - **Recommendation:** Add full WCAG 2.1 compliance + +4. **Performance Monitoring:** + - ❌ No performance metrics tracking + - ❌ No error tracking (e.g., Sentry) + - **Recommendation:** Add analytics and error tracking + +5. **Code Duplication:** + - ⚠️ Some regex patterns repeated in Chatbot.jsx + - ⚠️ Similar retry logic in multiple services + - **Recommendation:** Extract to shared utilities + +--- + +## 🚀 Feature Analysis + +### Implemented Features + +1. **AI-Powered Responses** ✅ + - Google Gemini 2.5 Flash integration + - Context-aware responses with team/event data + - Retry logic for reliability + +2. **Voice Input** ✅ + - Web Speech API integration + - Visual feedback during recording + - Browser compatibility checks + +3. **Smart Caching** ✅ + - 2-minute TTL for team and events + - Stale cache fallback + - Cache statistics + +4. **Responsive Design** ✅ + - Mobile, tablet, desktop support + - Adaptive layout + - Touch-friendly controls + +5. **Rich Message Formatting** ✅ + - Markdown support (bold, links) + - Auto-linking URLs + - Instagram handle conversion + +6. **Quick Actions** ✅ + - Suggested prompts + - One-click queries + - Contextual suggestions + +7. **Iframe Embedding** ✅ + - PostMessage API support + - Ready event notification + - Standalone and embedded modes + +### Missing Features (Potential Enhancements) + +1. **Message History Persistence** ❌ + - No localStorage/sessionStorage + - Messages lost on refresh + - **Recommendation:** Add conversation persistence + +2. **User Authentication** ❌ + - No user identification + - No personalized responses + - **Recommendation:** Integrate with FED auth system + +3. **File Attachments** ❌ + - No image/document upload + - **Recommendation:** Add file upload for event queries + +4. **Multi-language Support** ❌ + - English only + - **Recommendation:** Add i18n support + +5. **Conversation Export** ❌ + - No way to save/export chats + - **Recommendation:** Add export to PDF/text + +6. **Feedback Mechanism** ❌ + - No thumbs up/down on responses + - **Recommendation:** Add response rating + +7. **Typing Indicators** ⚠️ + - Bot typing indicator exists + - User typing indicator missing + - **Recommendation:** Add user typing broadcast + +--- + +## 🔧 Dependencies Analysis + +### Production Dependencies + +```json +{ + "react": "^18.3.1", // UI framework + "react-dom": "^18.3.1", // React DOM renderer + "react-icons": "^5.4.0", // Icon library + "axios": "^1.7.9" // HTTP client +} +``` + +### Dev Dependencies + +```json +{ + "@vitejs/plugin-react": "^5.1.1", // Vite React plugin + "sass": "^1.83.0", // SCSS compiler + "vite": "^7.2.6" // Build tool +} +``` + +**Observations:** +- ✅ Minimal dependencies (good for bundle size) +- ✅ All dependencies are up-to-date +- ✅ No security vulnerabilities detected (based on versions) +- ⚠️ Could benefit from additional dev tools (ESLint, Prettier, testing libraries) + +--- + +## 📊 Build Configuration + +### Vite Configuration (`vite.config.js`) + +```javascript +{ + plugins: [react()], + server: { + port: 5173, + cors: true, + strictPort: true + }, + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: false + } +} +``` + +**Observations:** +- ✅ Simple, clean configuration +- ✅ CORS enabled for development +- ✅ Strict port enforcement +- ⚠️ Source maps disabled in production (harder to debug) +- **Recommendation:** Enable source maps for staging environment + +--- + +## 🌐 API Integration + +### Backend Endpoints + +1. **Team Data:** `GET /api/user/fetchTeam` + - Returns: `{ success: boolean, data: TeamMember[] }` + - Timeout: 10 seconds + - Retry: 3 attempts with exponential backoff + +2. **Events Data:** `GET /api/form/getAllForms` + - Returns: `{ success: boolean, events: Event[] }` + - Timeout: 10 seconds + - Retry: 3 attempts with exponential backoff + +### External APIs + +1. **Google Gemini AI** + - Endpoint: `https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent` + - Model: `gemini-2.5-flash-preview-09-2025` + - Timeout: 30 seconds + - Retry: 3 attempts with exponential backoff + +**API Design Observations:** +- ✅ Consistent response format +- ✅ Proper error handling +- ✅ Timeout configurations +- ✅ Retry mechanisms +- ⚠️ No API versioning in backend endpoints +- **Recommendation:** Add API versioning (e.g., `/api/v1/user/fetchTeam`) + +--- + +## 🎨 UI/UX Analysis + +### Design Strengths + +1. **Visual Appeal:** + - Modern glassmorphism design + - FED brand colors (orange-red gradient) + - Smooth animations and transitions + - Professional, polished look + +2. **User Experience:** + - Intuitive interface + - Clear visual feedback + - Responsive interactions + - Helpful suggested prompts + +3. **Accessibility:** + - Aria labels on interactive elements + - Keyboard navigation support + - Focus states on inputs + - High contrast text + +### UX Improvements + +1. **Loading States:** + - ⚠️ No loading indicator during initial data fetch + - **Recommendation:** Add skeleton screens + +2. **Error States:** + - ⚠️ Generic error messages + - **Recommendation:** Add specific error UI with retry buttons + +3. **Empty States:** + - ⚠️ No empty state handling + - **Recommendation:** Add helpful messages when no data available + +4. **Onboarding:** + - ⚠️ No first-time user guidance + - **Recommendation:** Add tutorial or tooltip tour + +--- + +## 📈 Performance Metrics (Estimated) + +### Load Time +- **Initial Load:** ~1-2s (with caching) +- **First Interaction:** ~3.5-5.5s (includes all API calls) +- **Subsequent Interactions:** ~1-3s (cached data) + +### Bundle Size (Estimated) +- **JavaScript:** ~150-200 KB (minified + gzipped) +- **CSS:** ~20-30 KB (minified + gzipped) +- **Total:** ~170-230 KB + +### API Call Frequency +- **Team Data:** Once every 2 minutes (cached) +- **Events Data:** Once every 2 minutes (cached) +- **Gemini AI:** Every user message (~1-3s response time) + +### Cache Hit Rate +- **Estimated:** 70-80% for team/events data +- **Benefit:** ~2.5s saved per cached request + +--- + +## 🔐 Environment Variables + +### Required Variables + +```env +VITE_API_BASE_URL # FED backend URL +VITE_GEMINI_API_KEY # Google Gemini API key +VITE_GEMINI_MODEL # AI model name +VITE_CHATBOT_NAME # Chatbot display name +VITE_SYSTEM_PROMPT # AI system instructions +``` + +### Optional Variables + +```env +VITE_GEMINI_API_URL # Gemini API endpoint (has default) +VITE_CHATBOT_MODE # standalone/embedded (default: standalone) +VITE_TEAM_CACHE_DURATION # Cache TTL in ms (default: 120000) +VITE_EVENTS_CACHE_DURATION # Cache TTL in ms (default: 120000) +VITE_MAX_RETRIES # Max retry attempts (default: 3) +VITE_INITIAL_RETRY_DELAY # Initial delay in ms (default: 1000) +``` + +**Observations:** +- ✅ Clear naming convention (VITE_ prefix) +- ✅ Validation on app startup +- ✅ Sensible defaults for optional vars +- ✅ Never committed to git (.gitignore) + +--- + +## 🐛 Potential Issues & Bugs + +### Critical Issues +None identified + +### Medium Priority Issues + +1. **Regex-based Markdown Parsing:** + - **Issue:** Complex regex chains can fail on edge cases + - **Impact:** Malformed message rendering + - **Recommendation:** Use proper markdown parser library + +2. **Voice Recognition Browser Support:** + - **Issue:** Only works in Chrome/Edge + - **Impact:** Feature unavailable in Firefox/Safari + - **Recommendation:** Add polyfill or clearer browser warnings + +3. **No Message Validation:** + - **Issue:** No max length or content validation + - **Impact:** Potential API errors or abuse + - **Recommendation:** Add input validation (max 500 chars) + +### Low Priority Issues + +1. **Hardcoded Suggested Prompts:** + - **Issue:** Prompts are hardcoded in component + - **Impact:** Requires code change to update + - **Recommendation:** Move to configuration or fetch from API + +2. **No Rate Limiting:** + - **Issue:** Users can spam requests + - **Impact:** Potential API quota exhaustion + - **Recommendation:** Add client-side rate limiting + +3. **Stale Cache on Errors:** + - **Issue:** May serve very old data if API is down + - **Impact:** Outdated information + - **Recommendation:** Add cache age warning in UI + +--- + +## 🎯 Recommendations + +### High Priority + +1. **Add Testing Suite** + - Unit tests for services + - Component tests for UI + - E2E tests for critical flows + - **Tools:** Jest, React Testing Library, Playwright + +2. **Implement TypeScript** + - Type safety for better DX + - Catch errors at compile time + - Better IDE support + - **Migration:** Gradual, start with new files + +3. **Add Error Tracking** + - Monitor production errors + - Track API failures + - User session replay + - **Tools:** Sentry, LogRocket + +### Medium Priority + +4. **Improve Accessibility** + - WCAG 2.1 AA compliance + - Screen reader testing + - Keyboard navigation improvements + - **Tools:** axe DevTools, WAVE + +5. **Add Performance Monitoring** + - Track load times + - Monitor API latency + - Bundle size tracking + - **Tools:** Web Vitals, Lighthouse CI + +6. **Implement Message Persistence** + - Save conversation history + - Resume conversations + - Export functionality + - **Storage:** localStorage or backend + +### Low Priority + +7. **Add Internationalization** + - Multi-language support + - RTL language support + - **Tools:** react-i18next + +8. **Enhance Analytics** + - Track user interactions + - Popular queries + - Conversion metrics + - **Tools:** Google Analytics, Mixpanel + +9. **Add Progressive Web App Features** + - Offline support + - Install prompt + - Push notifications + - **Tools:** Workbox, PWA manifest + +--- + +## 📚 Documentation Quality + +### Strengths + +1. **README.md:** + - ✅ Comprehensive (667 lines) + - ✅ Clear structure with TOC + - ✅ Step-by-step installation guide + - ✅ Detailed architecture diagrams + - ✅ Troubleshooting section + - ✅ Code examples + +2. **Code Comments:** + - ✅ JSDoc on all major functions + - ✅ File headers with descriptions + - ✅ Inline comments for complex logic + +3. **Code Examples:** + - ✅ Usage examples in JSDoc + - ✅ Configuration examples in README + +### Areas for Improvement + +1. **API Documentation:** + - ❌ No API documentation for backend endpoints + - **Recommendation:** Add OpenAPI/Swagger docs + +2. **Contributing Guide:** + - ❌ No CONTRIBUTING.md + - **Recommendation:** Add contribution guidelines + +3. **Changelog:** + - ❌ No CHANGELOG.md + - **Recommendation:** Track version changes + +4. **Architecture Diagrams:** + - ⚠️ Text-based diagrams in README + - **Recommendation:** Add visual diagrams (Mermaid, draw.io) + +--- + +## 🔄 Git & Version Control + +### .gitignore Analysis + +**Properly Ignored:** +- ✅ node_modules/ +- ✅ .env files +- ✅ Build outputs (dist/, build/) +- ✅ IDE files (.vscode/, .idea/) +- ✅ OS files (.DS_Store, Thumbs.db) +- ✅ Logs (*.log) + +**Observations:** +- Well-configured .gitignore +- Follows best practices +- Prevents sensitive data commits + +### Version Control Recommendations + +1. **Add Conventional Commits:** + - Standardize commit messages + - Enable automatic changelog generation + - **Tools:** commitlint, husky + +2. **Add Pre-commit Hooks:** + - Lint code before commit + - Run tests before push + - **Tools:** husky, lint-staged + +3. **Add Branch Protection:** + - Require PR reviews + - Require passing tests + - Prevent force pushes to main + +--- + +## 🎓 Learning Opportunities + +### For Junior Developers + +This codebase is an excellent learning resource for: + +1. **React Patterns:** + - Hooks usage (useState, useRef, useEffect) + - Component composition + - Event handling + +2. **Service Architecture:** + - Separation of concerns + - Singleton pattern + - Caching strategies + +3. **API Integration:** + - Axios usage + - Error handling + - Retry logic + +4. **Modern CSS:** + - SCSS/Sass + - CSS Modules + - Animations + +5. **Build Tools:** + - Vite configuration + - Environment variables + - Production builds + +--- + +## 📊 Comparison with Industry Standards + +### Strengths vs Industry + +- ✅ **Code Organization:** Matches enterprise standards +- ✅ **Documentation:** Above average +- ✅ **Error Handling:** Comprehensive +- ✅ **Performance:** Good caching strategy +- ✅ **Security:** Proper env var handling + +### Gaps vs Industry + +- ❌ **Testing:** Below industry standard (0% coverage) +- ❌ **Type Safety:** No TypeScript (industry trend) +- ⚠️ **Monitoring:** No error tracking/analytics +- ⚠️ **CI/CD:** No evidence of automated pipelines +- ⚠️ **Accessibility:** Partial WCAG compliance + +--- + +## 🏆 Overall Assessment + +### Score: 8.5/10 + +**Breakdown:** +- **Code Quality:** 9/10 (excellent organization, documentation) +- **Architecture:** 9/10 (clean separation, good patterns) +- **Performance:** 8/10 (smart caching, could optimize more) +- **Security:** 8/10 (good practices, room for improvement) +- **Testing:** 3/10 (no tests found) +- **Documentation:** 9/10 (comprehensive README, JSDoc) +- **UX/UI:** 9/10 (modern, responsive, accessible) +- **Maintainability:** 8/10 (modular, but needs tests) + +### Final Thoughts + +The **FED Chatbot** is a **professionally developed, production-ready application** with excellent code organization, comprehensive documentation, and modern design. The codebase demonstrates strong software engineering principles and best practices. + +**Key Strengths:** +- Clean, modular architecture +- Intelligent caching mechanism +- Comprehensive error handling +- Excellent documentation +- Modern, responsive UI + +**Primary Improvement Area:** +- **Testing:** The most critical gap is the absence of automated tests + +**Recommendation:** +This codebase is ready for production deployment but would benefit significantly from adding a comprehensive testing suite and TypeScript for long-term maintainability. + +--- + +## 📞 Contact & Support + +For questions about this analysis or the codebase: +- **Email:** fedkiit@gmail.com +- **Instagram:** [@fedkiit](https://www.instagram.com/fedkiit/) + +--- + +**Analysis completed by Claude (Antigravity AI Assistant)** +**Date:** December 18, 2025 diff --git a/README.md b/README.md index d0b794d..3b6d45b 100644 --- a/README.md +++ b/README.md @@ -1 +1,666 @@ -# Fed-Chatbot \ No newline at end of file +# FED Chatbot 🤖 + +> An intelligent AI-powered chatbot for the Federation of Entrepreneurship Development (FED) at KIIT University, built with React and Google Gemini AI. + +[![React](https://img.shields.io/badge/React-18.3.1-blue.svg)](https://reactjs.org/) +[![Vite](https://img.shields.io/badge/Vite-6.0.5-646CFF.svg)](https://vitejs.dev/) +[![Gemini AI](https://img.shields.io/badge/Gemini-2.5--flash-orange.svg)](https://ai.google.dev/) + +--- + +## 📋 Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Tech Stack](#tech-stack) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Environment Setup](#environment-setup) +- [Folder Structure](#folder-structure) +- [How It Works](#how-it-works) +- [Message Flow](#message-flow) +- [APIs Used](#apis-used) +- [Development Guide](#development-guide) +- [Common Issues](#common-issues) + +--- + +## 🎯 Overview + +The FED Chatbot is a modern, responsive web application that serves as an intelligent assistant for FED KIIT. It provides real-time information about: +- Team members and their roles +- Upcoming and past events +- Registration links for live events +- General information about FED + +**Key Capabilities:** +- 🧠 AI-powered responses using Google Gemini 2.5 Flash +- 📊 Live data from FED backend API +- 💬 Natural language understanding +- 🎤 Voice input support +- 🔗 Clickable links for social media and registrations +- ⚡ Intelligent caching for performance + +--- + +## ✨ Features + +### For Users +- **Smart Conversations**: Ask questions in natural language +- **Voice Input**: Speak your queries instead of typing +- **Quick Actions**: Pre-defined prompts for common questions +- **Professional Links**: Clean, clickable links (no messy URLs) +- **Real-time Data**: Always up-to-date team and event information + +### For Developers +- **Modular Architecture**: Clean separation of concerns +- **Environment-Based Config**: Easy to configure and deploy +- **Intelligent Caching**: Reduces API calls, improves performance +- **Error Handling**: Robust retry logic with exponential backoff +- **Type Documentation**: Comprehensive JSDoc comments + +--- + +## 🛠️ Tech Stack + +| Technology | Purpose | Version | +|------------|---------|---------| +| **React** | UI Framework | 18.3.1 | +| **Vite** | Build Tool | 6.0.5 | +| **SCSS** | Styling | - | +| **Axios** | HTTP Client | 1.7.9 | +| **Google Gemini AI** | AI Responses | 2.5-flash-preview | +| **Web Speech API** | Voice Input | Browser Native | + +--- + +## 📦 Prerequisites + +Before you begin, ensure you have the following installed: + +- **Node.js**: v18.0.0 or higher ([Download](https://nodejs.org/)) +- **npm**: v9.0.0 or higher (comes with Node.js) +- **Git**: For version control ([Download](https://git-scm.com/)) +- **Code Editor**: VS Code recommended ([Download](https://code.visualstudio.com/)) + +**Check your versions:** +```bash +node --version +npm --version +git --version +``` + +--- + +## 🚀 Installation + +### Step 1: Clone the Repository +```bash +cd path/to/your/workspace +git clone +cd FED-Chatbot +``` + +### Step 2: Install Dependencies +```bash +npm install +``` + +This will install all required packages listed in `package.json`. + +### Step 3: Set Up Environment Variables + + +**📧 Contact the development team to get the required API keys and configuration values.** + +> **Security Note:** Never commit `.env` to version control! + +### Step 4: Start Development Server +```bash +npm run dev +``` + +The app will open at `http://localhost:5173` + +--- + +## 📁 Folder Structure + +``` +FED-Chatbot/ +├── public/ # Static assets +│ └── index.html # HTML template +│ +├── src/ # Source code +│ ├── components/ # React components +│ │ └── Chatbot/ # Main chatbot component +│ │ ├── Chatbot.jsx # Chatbot UI and logic +│ │ └── Chatbot.module.scss # Chatbot styles +│ │ +│ ├── services/ # Business logic services +│ │ ├── chatbotAPI.js # Main orchestration service +│ │ ├── geminiAPI.js # Google Gemini AI integration +│ │ ├── teamAPI.js # FED team API calls +│ │ ├── teamDataManager.js # Team data caching +│ │ └── eventsDataManager.js # Events data caching +│ │ +│ ├── config/ # Configuration +│ │ └── constants.js # All app constants +│ │ +│ ├── styles/ # Global styles +│ │ ├── global.scss # Global CSS +│ │ └── variables.scss # SCSS variables +│ │ +│ ├── App.jsx # Root component +│ ├── main.jsx # Entry point +│ └── ... +│ +├── .env # Environment variables (DO NOT COMMIT) +├── .env.example # Environment template +├── .gitignore # Git ignore rules +├── package.json # Dependencies +├── vite.config.js # Vite configuration +└── README.md # This file +``` + +### 📂 Key Directories Explained + +#### `src/components/Chatbot/` +Contains the main chatbot UI component: +- **Chatbot.jsx**: Message display, input handling, voice recognition +- **Chatbot.module.scss**: All styling for the chatbot interface + +#### `src/services/` +Core business logic, separated by responsibility: + +1. **chatbotAPI.js** - Main orchestrator + - Coordinates all other services + - Handles error messages + - Provides health checks + +2. **geminiAPI.js** - AI Integration + - Sends queries to Google Gemini + - Injects team/event context + - Handles retry logic + +3. **teamDataManager.js** - Team Data + - Fetches team members from FED API + - Implements 2-minute cache + - Provides filtering/sorting utilities + +4. **eventsDataManager.js** - Events Data + - Fetches events from FED API + - Separates upcoming vs past events + - Implements caching and search + +5. **teamAPI.js** - HTTP Client + - Raw API calls to FED backend + - Error handling + - Timeout management + +#### `src/config/` +Centralized configuration: +- **constants.js**: All environment variables, API URLs, cache settings, error messages + +--- + +## 🔄 How It Works + +### High-Level Architecture + +``` +┌─────────────────────────────────────────────┐ +│ User Interface (React) │ +│ • Message input & display │ +│ • Voice recognition │ +│ • Quick action buttons │ +└────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ chatbotAPI.js (Orchestrator) │ +│ • Coordinates all services │ +│ • Error handling │ +└────────┬────────────────────────────────────┘ + │ + ├──────────────┬──────────────┐ + ▼ ▼ ▼ +┌───────────────┐ ┌──────────┐ ┌─────────────┐ +│ geminiAPI.js │ │ teamData │ │ eventsData │ +│ (AI Service) │ │ Manager │ │ Manager │ +└───────┬───────┘ └────┬─────┘ └──────┬──────┘ + │ │ │ + │ ▼ ▼ + │ ┌────────────────────────┐ + │ │ FED Backend API │ + │ │ [Backend API URL] │ + │ └────────────────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ Google Gemini AI API │ +│ generativelanguage... │ +└──────────────────────────┘ +``` + +--- + +## 💬 Message Flow (Step-by-Step) + +### What Happens When You Send "Who is the president?" + +#### **Step 1: User Input** (Chatbot.jsx) +```javascript +User types → "Who is the president?" + ↓ +Chatbot.jsx captures input in handleSend() + ↓ +Adds message to UI (shows user's message) + ↓ +Calls chatbotAPI.sendMessage(query) +``` + +#### **Step 2: Service Orchestration** (chatbotAPI.js) +```javascript +chatbotAPI.sendMessage("Who is the president?") + ↓ +Validates input (not empty) + ↓ +Calls teamDataManager.fetchAndCacheTeamData() + ↓ +Calls eventsDataManager.fetchAndCacheEvents() +``` + +#### **Step 3: Data Fetching** (Parallel) + +**3a. Team Data Manager** +```javascript +teamDataManager.fetchAndCacheTeamData() + ↓ +Check cache: Is data < 2 minutes old? + YES → Return cached data ✅ + NO → Continue ↓ + ↓ +Call teamAPI.fetchTeam() + ↓ +HTTP GET to: [Backend API]/api/user/fetchTeam + ↓ +Response: { success: true, data: [...team members] } + ↓ +Filter out null names + ↓ +Sort by: year DESC, then name ASC + ↓ +Cache for 2 minutes + ↓ +Return team data +``` + +**3b. Events Data Manager** +```javascript +eventsDataManager.fetchAndCacheEvents() + ↓ +Check cache: Is data < 2 minutes old? + YES → Return cached data ✅ + NO → Continue ↓ + ↓ +HTTP GET to: [Backend API]/api/form/getAllForms + ↓ +Response: { success: true, events: [...] } + ↓ +Filter: upcoming events (isEventPast: false) + ↓ +Sort past events: by date DESC + ↓ +Take top 5 past events + ↓ +Cache for 2 minutes + ↓ +Return { upcomingEvents: [...], pastEvents: [...] } +``` + +#### **Step 4: AI Processing** (geminiAPI.js) +```javascript +geminiAPI.generateAIResponse(query, teamMembers, events) + ↓ +Build context injection: + ### LIVE TEAM DATA START ### + [Full JSON of team members] + ### LIVE TEAM DATA END ### + + ### LIVE EVENT DATA START ### + [Full JSON of events] + ### LIVE EVENT DATA END ### + ↓ +Construct final prompt: + System Prompt (from .env) + + Team Context + + Event Context + + User Query: "Who is the president?" + ↓ +HTTP POST to: + https://generativelanguage.googleapis.com/v1beta/models/ + gemini-2.5-flash-preview-09-2025:generateContent + ?key=YOUR_API_KEY + ↓ +Request Body: + { + contents: [{ role: "user", parts: [{ text: finalPrompt }] }], + systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] } + } + ↓ +Response (after 1-3 seconds): + { + candidates: [{ + content: { + parts: [{ + text: "The current president of FED is [Name]..." + }] + } + }] + } + ↓ +Extract text from response + ↓ +Return AI response to chatbotAPI +``` + +#### **Step 5: Response Rendering** (Chatbot.jsx) +```javascript +chatbotAPI.sendMessage() returns AI response + ↓ +Chatbot.jsx receives response + ↓ +Add bot message to UI + ↓ +Process markdown in message.text: + 1. Convert @fedkiit → Instagram link + 2. Convert **bold** → bold + 3. Convert [text](url) → clickable link + 4. Convert plain URLs → clickable links + 5. Convert \n →
+ ↓ +Render HTML using dangerouslySetInnerHTML + ↓ +User sees: "The current president of FED is John Doe. + Connect on LinkedIn" (LinkedIn is clickable) +``` + +### Timing Breakdown + +| Step | Time (First Call) | Time (Cached) | +|------|-------------------|---------------| +| 1. User Input | ~0ms | ~0ms | +| 2. Orchestration | ~1ms | ~1ms | +| 3. Team API | ~1.15s | ~0ms ✅ | +| 3. Events API | ~1.33s | ~0ms ✅ | +| 4. Gemini AI | ~1-3s | ~1-3s | +| 5. Rendering | ~10ms | ~10ms | +| **Total** | **~3.5-5.5s** | **~1-3s** | + +**Cache saves ~2.5s on subsequent requests!** + +--- + +## 🌐 APIs Used + +### 1. **FED Backend API** (Team Data) +**Endpoint:** `GET [Backend API]/api/user/fetchTeam` + +**Response Structure:** +```json +{ + "success": true, + "data": [ + { + "id": "user_123", + "name": "John Doe", + "access": "PRESIDENT", + "year": 2025, + "extra": { + "designation": "President", + "linkedin": "https://linkedin.com/in/johndoe", + "github": "https://github.com/johndoe" + } + } + ] +} +``` + +**Used For:** +- Team member listings +- Role information +- Social media links + +--- + +### 2. **FED Backend API** (Events Data) +**Endpoint:** `GET [Backend API]/api/form/getAllForms` + +**Response Structure:** +```json +{ + "success": true, + "events": [ + { + "id": "event_456", + "info": { + "eventTitle": "Startup Workshop", + "eventDate": "2025-01-15", + "isEventPast": false, + "registrationLink": "https://forms.gle/xyz123" + }, + "sections": [...] + } + ] +} +``` + +**Used For:** +- Upcoming events +- Past events (last 5) +- Registration links + +--- + +### 3. **Google Gemini AI API** +**Endpoint:** `POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent` + +**Request Structure:** +```json +{ + "contents": [{ + "role": "user", + "parts": [{ "text": "CONTEXT + USER QUERY" }] + }], + "systemInstruction": { + "parts": [{ "text": "SYSTEM PROMPT FROM .ENV" }] + } +} +``` + +**Response Structure:** +```json +{ + "candidates": [{ + "content": { + "parts": [{ "text": "AI generated response..." }] + } + }] +} +``` + +**Used For:** +- Natural language understanding +- Generating intelligent responses +- Context-aware answers + +**Rate Limits:** +- Free tier: 60 requests/minute, 1,500/day +- Enable billing for higher quotas + +--- + +## 👨‍💻 Development Guide + +### Running the Project + +```bash +# Development mode (hot reload) +npm run dev + +# Production build +npm run build + +# Preview production build +npm run preview + +# Lint code +npm run lint +``` + +### Making Changes + +#### 1. **Adding a New Service** + +Create `src/services/myNewService.js`: +```javascript +/** + * @fileoverview My new service description + * @module services/myNewService + */ + +import { CONFIG } from '../config/constants'; + +export const doSomething = async () => { + // Your logic here +}; +``` + +#### 2. **Adding New Environment Variables** + +1. Add to `.env`: +```env +VITE_MY_NEW_VAR=some-value +``` + +2. Add to `src/config/constants.js`: +```javascript +export const CONFIG = { + // ...existing + MY_FEATURE: { + SETTING: import.meta.env.VITE_MY_NEW_VAR || 'default' + } +}; +``` + +3. Update `.env.example` for documentation + +#### 3. **Modifying the System Prompt** + +Edit `.env`: +```env +VITE_SYSTEM_PROMPT="Your updated prompt here...\n\nUse \\n for line breaks" +``` + +Restart the dev server for changes to take effect. + +#### 4. **Adding Quick Action Buttons** + +Edit `src/components/Chatbot/Chatbot.jsx`: +```javascript +const suggestedPrompts = [ + "What is FED?", + "Who is the president?", + "Tell me about FED events", + "How can I join FED?", + "Your new prompt here" // Add here +]; +``` + +--- + +## 🐛 Common Issues + +### Issue: "Module not found" errors +**Solution:** +```bash +rm -rf node_modules package-lock.json +npm install +``` + +### Issue: Environment variables not loading +**Solution:** +- Ensure variables start with `VITE_` +- Restart dev server after editing `.env` +- Check for typos in variable names + +### Issue: 429 Rate Limit from Gemini API +**Causes:** +- Testing too frequently +- Free tier quota exceeded + +**Solutions:** +1. Wait 60 seconds between tests +2. Reduce `VITE_MAX_RETRIES` to 1 +3. Enable billing on Google Cloud + +### Issue: Bold text showing as `**text**` +**Solution:** +- Hard refresh browser: `Ctrl + Shift + R` +- Clear cache and reload + +### Issue: Links showing as HTML code +**Solution:** +- Check `Chatbot.jsx` for correct `dangerouslySetInnerHTML` +- Ensure regex patterns are processing in correct order + +### Issue: Voice input not working +**Causes:** +- HTTPS required for Web Speech API +- Microphone permissions denied +- Browser compatibility (Chrome/Edge works best) + +**Solutions:** +- Use `localhost` (HTTPS not required) +- Check browser console for permission errors +- Try Chrome or Edge browser + +--- + +## 🤝 Contributing + +### For New Developers + +1. **Read this README** completely +2. **Set up your environment** following Installation steps +3. **Explore the code** starting from `Chatbot.jsx` +4. **Make small changes first** to understand the flow +5. **Test thoroughly** before committing + +### Code Guidelines + +- Write JSDoc comments for all functions +- Use descriptive variable names +- Follow existing code style +- Test with real data +- Don't commit `.env` file + +--- + +## 📞 Support + +**Need help?** +- Check [Common Issues](#common-issues) section +- Review [Claude-Chatbot.md](./Claude-Chatbot.md) +- Contact: fedkiit@gmail.com + +--- + +## 📄 License + +This project is for FED KIIT's internal use. + +--- + +**Built with ❤️ by the FED KIIT Development Team** diff --git a/index.html b/index.html new file mode 100644 index 0000000..6e8b4c7 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + + + + FED Chatbot + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..033f2a7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2572 @@ +{ + "name": "@fed/chatbot-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fed/chatbot-ui", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.4.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.83.0", + "vite": "^7.2.6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz", + "integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.263", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", + "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.94.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", + "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz", + "integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..62cc63f --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fed/chatbot-ui", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 5173", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.4.0", + "axios": "^1.7.9" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.83.0", + "vite": "^7.2.6" + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..8bc7ae7 --- /dev/null +++ b/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + FED Chatbot + + +
+ + + diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..a992b74 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,13 @@ +import { useState } from 'react'; +import Chatbot from './components/Chatbot/Chatbot'; +import './App.scss'; + +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 0000000..f82299e --- /dev/null +++ b/src/App.scss @@ -0,0 +1,7 @@ +.App { + width: 100%; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; +} diff --git a/src/components/Chatbot/Chatbot.jsx b/src/components/Chatbot/Chatbot.jsx new file mode 100644 index 0000000..87e3b79 --- /dev/null +++ b/src/components/Chatbot/Chatbot.jsx @@ -0,0 +1,316 @@ +import { useState, useRef, useEffect } from 'react'; +import styles from './Chatbot.module.scss'; +import { IoCloseOutline, IoSend, IoMic, IoMicOff } from 'react-icons/io5'; +import { BiSolidMessageSquareDetail } from 'react-icons/bi'; +import { IoSparkles } from 'react-icons/io5'; +import chatbotAPI from '../../services/chatbotAPI'; + +const Chatbot = () => { + const chatbotName = import.meta.env.VITE_CHATBOT_NAME || 'Sage'; + + const [messages, setMessages] = useState([ + { + id: 1, + text: `Hello! I'm **${chatbotName}**, your personal assistant for FED KIIT. 🚀\n\nAsk me about our team, upcoming events, or how to join the society!`, + isUser: false, + timestamp: new Date(), + } + ]); + const [userInput, setUserInput] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isListening, setIsListening] = useState(false); + const messagesEndRef = useRef(null); + const chatboxRef = useRef(null); + const recognitionRef = useRef(null); + + // Suggested prompts + const suggestedPrompts = [ + "What is FED?", + "Who is the president?", + "Tell me about FED events", + "How can I join FED?" + ]; + + // Auto-scroll to bottom + const scrollToBottom = () => { + if (chatboxRef.current) { + chatboxRef.current.scrollTop = chatboxRef.current.scrollHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [messages, isTyping]); + + // Toggle chatbot + const toggleChatbot = () => { + setIsOpen(!isOpen); + }; + + // Send message handler + const sendMessage = async (messageText = null) => { + const textToSend = messageText || userInput; + if (!textToSend?.trim()) return; + + // Add user message + const userMessage = { + id: messages.length + 1, + text: textToSend, + isUser: true, + timestamp: new Date(), + }; + + setMessages(prev => [...prev, userMessage]); + setUserInput(''); + setIsTyping(true); + + try { + // Call real chatbot API + const response = await chatbotAPI.sendMessage(textToSend); + + const botResponse = { + id: messages.length + 2, + text: response.success ? response.response : 'Sorry, I encountered an error. Please try again.', + isUser: false, + timestamp: new Date(), + }; + + setMessages(prev => [...prev, botResponse]); + } catch (error) { + console.error('Error sending message:', error); + const errorResponse = { + id: messages.length + 2, + text: 'Sorry, I\'m having trouble connecting. Please make sure the backend is running on port 4000.', + isUser: false, + timestamp: new Date(), + }; + setMessages(prev => [...prev, errorResponse]); + } finally { + setIsTyping(false); + } + }; + + // Handle Enter key + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + // Voice recognition + const startVoiceRecognition = () => { + if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { + alert('Voice recognition is not supported in your browser. Please use Chrome or Edge.'); + return; + } + + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + + recognition.continuous = false; + recognition.interimResults = false; + recognition.lang = 'en-US'; + + recognition.onstart = () => { + setIsListening(true); + }; + + recognition.onresult = (event) => { + const transcript = event.results[0][0].transcript; + setUserInput(transcript); + }; + + recognition.onerror = (event) => { + console.error('Speech recognition error:', event.error); + setIsListening(false); + }; + + recognition.onend = () => { + setIsListening(false); + }; + + recognitionRef.current = recognition; + recognition.start(); + }; + + const stopVoiceRecognition = () => { + if (recognitionRef.current) { + recognitionRef.current.stop(); + setIsListening(false); + } + }; + + const toggleVoiceRecognition = () => { + if (isListening) { + stopVoiceRecognition(); + } else { + startVoiceRecognition(); + } + }; + + // Notify parent window that chatbot is ready (for iframe embedding) + useEffect(() => { + if (window.parent !== window) { + window.parent.postMessage({ type: 'CHATBOT_READY' }, '*'); + } + }, []); + + return ( + <> + {/* Toggle Button */} + {!isOpen && ( + + )} + + {/* Backdrop Overlay */} + {isOpen && ( +
+ )} + + {/* Chatbot Container */} + {isOpen && ( +
+ {/* Header */} +
+
+
+ FED Logo +
+
+
+

{chatbotName}

+

+ AI Assistant +

+
+
+ +
+ + {/* Messages Area */} +
+ {messages.map((message, index) => ( +
+ {!message.isUser && ( +
+ Bot +
+ )} +
+
+
$1') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + .replace(/(?$1') + .replace(/\n/g, '
') + }} + /> +
+
+
+ ))} + + {/* Typing Indicator */} + {isTyping && ( +
+
+ Bot +
+
+ + + +
+
+ )} + + {/* Show suggested prompts only for first message */} + {messages.length === 1 && !isTyping && ( +
+

Quick actions:

+
+ {suggestedPrompts.map((prompt, index) => ( + + ))} +
+
+ )} + +
+
+ + {/* Input Area */} +
+ setUserInput(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask me anything about FED..." + className={styles.messageInput} + /> + + +
+
+ )} + + ); +}; + +export default Chatbot; diff --git a/src/components/Chatbot/Chatbot.module.scss b/src/components/Chatbot/Chatbot.module.scss new file mode 100644 index 0000000..ade093e --- /dev/null +++ b/src/components/Chatbot/Chatbot.module.scss @@ -0,0 +1,520 @@ +@import '../../styles/variables.scss'; +@import '../../styles/animations.scss'; + +// Toggle Button with Pulse Ring +.chatbotToggle { + position: fixed; + bottom: $spacing-xl; + right: $spacing-xl; + width: $toggle-button-size; + height: $toggle-button-size; + border-radius: 50%; + background: $primary-gradient; + border: none; + color: $light-text; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 8px 24px rgba(244, 43, 3, 0.4); + z-index: $z-toggle; + transition: all $transition-fast; + animation: bounce 2s infinite; + backdrop-filter: blur($glass-blur); + + &:hover { + transform: scale(1.1); + animation: none; + box-shadow: 0 12px 32px rgba(244, 43, 3, 0.5); + } + + &:active { + transform: scale(0.95); + } +} + +// Pulse ring animation +.pulseRing { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + border-radius: 50%; + border: 3px solid rgba(255, 190, 11, 0.6); + animation: pulseRing 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes pulseRing { + 0% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } + + 100% { + transform: translate(-50%, -50%) scale(1.5); + opacity: 0; + } +} + +// Backdrop Overlay +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 999; + animation: fadeIn 0.3s ease; +} + +// Main Chatbot Container +.chatbotContainer { + position: fixed; + bottom: 20px; + right: 20px; + width: min(400px, calc(100vw - 40px)); + height: calc(100vh - 120px); + background-color: $glass-bg; + border-radius: 24px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + z-index: $z-container; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur($glass-blur); + animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +@keyframes slideInUp { + from { + transform: translateY(20px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +// Header +.chatbotHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: $spacing-md $spacing-lg; + background: linear-gradient(135deg, rgba(28, 28, 28, 0.95) 0%, rgba(20, 20, 20, 0.95) 100%); + border-radius: 24px 24px 0 0; + border-bottom: 1px solid $border-subtle; + min-height: 80px; + backdrop-filter: blur(12px); +} + +.headerContent { + display: flex; + align-items: center; + gap: $spacing-md; +} + +.avatarContainer { + position: relative; +} + +.avatar { + height: 48px; + width: 48px; + border-radius: 50%; +} + +.statusIndicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 12px; + height: 12px; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + border-radius: 50%; + border: 2px solid $dark-bg; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.headerText { + display: flex; + flex-direction: column; + gap: 2px; +} + +.title { + color: $light-text; + font-size: 1.2rem; + font-weight: 600; + margin: 0; + letter-spacing: 0.3px; +} + +.subtitle { + display: flex; + align-items: center; + gap: 4px; + color: rgba(255, 255, 255, 0.6); + font-size: 0.75rem; + margin: 0; +} + +.closeButton { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: all $transition-fast; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.95); + } + + &:active { + transform: scale(0.95); + } +} + +// Messages Area +.messagesArea { + flex: 1; + padding: $spacing-lg; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + gap: $spacing-md; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.03); + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 10px; + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } +} + +// Message Wrapper +.messageWrapper { + display: flex; + gap: 10px; + animation: messageSlide 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes messageSlide { + from { + transform: translateY(10px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +.userWrapper { + justify-content: flex-end; +} + +.botWrapper { + justify-content: flex-start; +} + +.messageAvatar { + flex-shrink: 0; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.messageContent { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 75%; +} + +// Message Bubbles +.message { + padding: 12px 16px; + border-radius: 16px; + word-wrap: break-word; + font-size: 0.9rem; + line-height: 1.5; + transition: all $transition-fast; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } +} + +.userMessage { + background: linear-gradient(135deg, #f0f0f0 0%, #e8e8e8 100%); + color: #1a1a1a; + border-bottom-right-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.botMessage { + background: $primary-gradient; + color: $light-text; + border-bottom-left-radius: 4px; + box-shadow: 0 2px 8px rgba(244, 43, 3, 0.2); +} + +// Typing Indicator +.typingIndicator { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 16px; + background: $primary-gradient; + border-radius: 16px; + border-bottom-left-radius: 4px; + max-width: 70px; + box-shadow: 0 2px 8px rgba(244, 43, 3, 0.2); + + span { + width: 8px; + height: 8px; + background-color: $light-text; + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; + + &:nth-child(2) { + animation-delay: 0.2s; + } + + &:nth-child(3) { + animation-delay: 0.4s; + } + } +} + +// Suggested Prompts +.suggestedPrompts { + margin-top: $spacing-md; + animation: fadeIn 0.5s ease-in-out; +} + +.promptsLabel { + margin: 0 0 $spacing-sm 0; + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + font-weight: 500; + letter-spacing: 0.5px; +} + +.promptsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.promptButton { + padding: 10px 14px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + color: rgba(255, 255, 255, 0.8); + font-size: 0.8rem; + cursor: pointer; + transition: all $transition-fast; + text-align: left; + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 190, 11, 0.5); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(255, 190, 11, 0.2); + } + + &:active { + transform: translateY(0); + } +} + +// Input Area +.inputArea { + display: flex; + padding: $spacing-md; + background: linear-gradient(135deg, rgba(28, 28, 28, 0.95) 0%, rgba(20, 20, 20, 0.95) 100%); + border-radius: 0 0 24px 24px; + border-top: 1px solid $border-subtle; + gap: $spacing-sm; + backdrop-filter: blur(12px); +} + +.messageInput { + flex: 1; + padding: 12px 18px; + border: 1px solid $border-subtle; + border-radius: 14px; + background: rgba(255, 255, 255, 0.08); + color: $light-text; + font-size: 0.9rem; + font-family: 'Poppins', sans-serif; + transition: all $transition-fast; + + &::placeholder { + color: rgba(255, 255, 255, 0.4); + } + + &:focus { + outline: none; + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 190, 11, 0.5); + box-shadow: 0 0 0 3px rgba(255, 190, 11, 0.1); + } +} + +.voiceButton { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: $light-text; + transition: all $transition-fast; + + &:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); + } + + &:active { + transform: translateY(0); + } + + &.listening { + background: $primary-gradient; + border-color: transparent; + animation: pulse 1.5s ease-in-out infinite; + box-shadow: 0 0 20px rgba(255, 140, 40, 0.5); + } +} + +.sendButton { + background: $primary-gradient; + border: none; + border-radius: 12px; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: $light-text; + transition: all $transition-fast; + box-shadow: 0 4px 12px rgba(244, 43, 3, 0.3); + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(244, 43, 3, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +// Responsive Design +@media (max-width: $desktop) { + .chatbotContainer { + height: 65vh; + } +} + +@media (max-width: $tablet) { + .chatbotContainer { + width: calc(100% - 40px); + height: 70vh; + } + + .promptsGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: $mobile) { + .chatbotContainer { + width: calc(100% - 20px); + height: 80vh; + bottom: $spacing-sm; + right: $spacing-sm; + border-radius: $border-radius-medium; + } + + .chatbotToggle { + bottom: $spacing-md; + right: $spacing-md; + width: 64px; + height: 64px; + } + + .message { + font-size: 0.85rem; + } + + .messageContent { + max-width: 82%; + } + + .chatbotHeader { + padding: $spacing-sm $spacing-md; + min-height: 70px; + } + + .avatar { + height: 40px; + width: 40px; + } + + .title { + font-size: 1.1rem; + } +} \ No newline at end of file diff --git a/src/config/constants.js b/src/config/constants.js new file mode 100644 index 0000000..6dfb122 --- /dev/null +++ b/src/config/constants.js @@ -0,0 +1,133 @@ +/** + * @fileoverview Configuration and constants for the FED Chatbot application + * @module config/constants + * @author FED KIIT Development Team + */ + +/** + * Validates that all required environment variables are present + * @throws {Error} If required environment variables are missing + * @returns {void} + */ +const validateEnvironment = () => { + const requiredVars = [ + 'VITE_API_BASE_URL', + 'VITE_GEMINI_API_KEY', + 'VITE_GEMINI_MODEL', + 'VITE_CHATBOT_NAME' + ]; + + const missing = requiredVars.filter(varName => !import.meta.env[varName]); + + if (missing.length > 0) { + throw new Error( + `Missing required environment variables: ${missing.join(', ')}\n` + + 'Please check your .env file and ensure all required variables are set.' + ); + } +}; + +// Validate environment on module load +validateEnvironment(); + +/** + * Application configuration object + * @const {Object} + */ +export const CONFIG = Object.freeze({ + /** + * API endpoints configuration + */ + API: { + BASE_URL: import.meta.env.VITE_API_BASE_URL, + TEAM_ENDPOINT: '/api/user/fetchTeam', + EVENTS_ENDPOINT: '/api/form/getAllForms', + BLOG_ENDPOINT: '/api/blog/getBlog', + }, + + /** + * Gemini AI configuration + */ + GEMINI: { + API_KEY: import.meta.env.VITE_GEMINI_API_KEY, + MODEL: import.meta.env.VITE_GEMINI_MODEL || 'gemini-2.0-flash-exp', + API_URL: import.meta.env.VITE_GEMINI_API_URL || + 'https://generativelanguage.googleapis.com/v1beta/models', + SYSTEM_PROMPT: import.meta.env.VITE_SYSTEM_PROMPT, + }, + + /** + * Chatbot settings + */ + CHATBOT: { + NAME: import.meta.env.VITE_CHATBOT_NAME || 'AskFED', + MODE: import.meta.env.VITE_CHATBOT_MODE || 'standalone', + }, + + /** + * Cache configuration + */ + CACHE: { + TEAM_DURATION: parseInt(import.meta.env.VITE_TEAM_CACHE_DURATION) || 120000, + EVENTS_DURATION: parseInt(import.meta.env.VITE_EVENTS_CACHE_DURATION) || 120000, + BLOG_DURATION: parseInt(import.meta.env.VITE_BLOG_CACHE_DURATION) || 120000, + }, + + /** + * Retry configuration for API calls + */ + RETRY: { + MAX_ATTEMPTS: parseInt(import.meta.env.VITE_MAX_RETRIES) || 3, + INITIAL_DELAY: parseInt(import.meta.env.VITE_INITIAL_RETRY_DELAY) || 1000, + }, +}); + +/** + * HTTP status codes + * @const {Object} + */ +export const HTTP_STATUS = Object.freeze({ + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + SERVER_ERROR: 500, +}); + +/** + * Error messages + * @const {Object} + */ +export const ERROR_MESSAGES = Object.freeze({ + NETWORK_ERROR: "Sorry, I'm having trouble connecting to the server. Please try again later.", + API_CONFIG_ERROR: 'Error: An API configuration issue occurred. Please contact support.', + EMPTY_RESPONSE: 'Sorry, I received an empty response. Please try a different query.', + INVALID_INPUT: 'Please enter a valid message.', + CACHE_ERROR: 'Failed to cache data. Using fresh data.', +}); + +/** + * Success messages + * @const {Object} + */ +export const SUCCESS_MESSAGES = Object.freeze({ + DATA_FETCHED: 'Data fetched successfully', + CACHE_HIT: 'Using cached data', + CACHE_UPDATED: 'Cache updated successfully', +}); + +/** + * Log prefixes for consistent logging + * @const {Object} + */ +export const LOG_PREFIX = Object.freeze({ + TEAM: '[TeamDataManager]', + EVENTS: '[EventsDataManager]', + GEMINI: '[GeminiAPI]', + CHATBOT: '[ChatbotAPI]', + BLOG: '[BlogDataManager]', +}); + +export default CONFIG; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..0cff267 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './styles/global.scss'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +); diff --git a/src/services/blogAPI.js b/src/services/blogAPI.js new file mode 100644 index 0000000..25c50ba --- /dev/null +++ b/src/services/blogAPI.js @@ -0,0 +1,148 @@ +/** + * @fileoverview Blog API service for fetching blog posts from FED backend + * @module services/blogAPI + * @author FED KIIT Development Team + * @requires axios + * @requires config/constants + */ + +import axios from 'axios'; +import { CONFIG, HTTP_STATUS, LOG_PREFIX } from '../config/constants'; + +/** + * Blog API service + * @namespace blogAPI + */ +const blogAPI = { + /** + * Fetch all blog posts from FED backend + * Implements sorting by date (newest first) + * @async + * @returns {Promise} Sorted array of blog posts + * @throws {Error} Logs error but returns empty array to prevent crashes + */ + fetchBlogs: async () => { + try { + const url = `${CONFIG.API.BASE_URL}${CONFIG.API.BLOG_ENDPOINT}`; + const response = await axios.get(url, { + timeout: 10000, // 10 second timeout + }); + + if (response.status === HTTP_STATUS.OK && response.data?.success) { + const blogs = response.data.blogs || []; + + // Sort by date (newest first) + const sortedBlogs = blogs.sort((a, b) => { + const dateA = new Date(a.date); + const dateB = new Date(b.date); + return dateB.getTime() - dateA.getTime(); + }); + + console.log(`${LOG_PREFIX.BLOG} Fetched ${sortedBlogs.length} blog posts`); + return sortedBlogs; + } + + console.warn(`${LOG_PREFIX.BLOG} Unexpected API response format`); + return []; + } catch (error) { + console.error(`${LOG_PREFIX.BLOG} Error fetching blogs:`, error.message); + return []; + } + }, + + /** + * Find single blog by ID + * @async + * @param {string} id - Blog ID to search for + * @returns {Promise} Blog object or undefined if not found + */ + findById: async (id) => { + if (!id || typeof id !== 'string') { + console.warn(`${LOG_PREFIX.BLOG} Invalid ID parameter`); + return undefined; + } + + const blogs = await blogAPI.fetchBlogs(); + return blogs.find(blog => blog.id === id); + }, + + /** + * Get N most recent blog posts + * @async + * @param {number} count - Number of blogs to return (default: 5) + * @returns {Promise} Array of recent blog posts + */ + getRecentBlogs: async (count = 5) => { + const blogs = await blogAPI.fetchBlogs(); + return blogs.slice(0, count); + }, + + /** + * Search blogs by title or description + * @async + * @param {string} query - Search query + * @returns {Promise} Array of matching blogs + */ + searchBlogs: async (query) => { + if (!query || typeof query !== 'string' || query.trim().length === 0) { + console.warn(`${LOG_PREFIX.BLOG} Invalid search query`); + return []; + } + + const blogs = await blogAPI.fetchBlogs(); + const lowerQuery = query.toLowerCase(); + + return blogs.filter(blog => + blog.title?.toLowerCase().includes(lowerQuery) || + blog.desc?.toLowerCase().includes(lowerQuery) || + blog.summary?.toLowerCase().includes(lowerQuery) + ); + }, + + /** + * Get blogs by category + * @async + * @param {string} category - Category to filter by + * @returns {Promise} Array of blogs in category + */ + getBlogsByCategory: async (category) => { + if (!category || typeof category !== 'string') { + console.warn(`${LOG_PREFIX.BLOG} Invalid category parameter`); + return []; + } + + const blogs = await blogAPI.fetchBlogs(); + return blogs.filter(blog => + blog.category?.toLowerCase() === category.toLowerCase() + ); + }, + + /** + * Get blogs by author + * @async + * @param {string} authorName - Author name to search for + * @returns {Promise} Array of blogs by author + */ + getBlogsByAuthor: async (authorName) => { + if (!authorName || typeof authorName !== 'string') { + console.warn(`${LOG_PREFIX.BLOG} Invalid author name parameter`); + return []; + } + + const blogs = await blogAPI.fetchBlogs(); + const lowerAuthorName = authorName.toLowerCase(); + + return blogs.filter(blog => { + if (typeof blog.author === 'string') { + return blog.author.toLowerCase().includes(lowerAuthorName); + } + if (typeof blog.author === 'object' && blog.author !== null) { + const authorStr = JSON.stringify(blog.author).toLowerCase(); + return authorStr.includes(lowerAuthorName); + } + return false; + }); + } +}; + +export default blogAPI; diff --git a/src/services/blogDataManager.js b/src/services/blogDataManager.js new file mode 100644 index 0000000..10b04c3 --- /dev/null +++ b/src/services/blogDataManager.js @@ -0,0 +1,179 @@ +/** + * @fileoverview Blog data management service with intelligent caching + * @module services/blogDataManager + * @author FED KIIT Development Team + */ + +import blogAPI from './blogAPI'; +import { CONFIG, LOG_PREFIX } from '../config/constants'; + +/** + * Blog data manager with caching and utility methods + * Implements singleton pattern for application-wide cache consistency + * @class + */ +class BlogDataManager { + constructor() { + this.cachedData = null; + this.lastFetch = null; + this.cacheTimeout = CONFIG.CACHE.BLOG_DURATION; + this.isFetching = false; + } + + /** + * Get blog posts with smart caching + * @async + * @returns {Promise} Array of blog post objects + */ + async getBlogs() { + const now = Date.now(); + + // Return cached data if still fresh + if ( + this.cachedData && + this.lastFetch && + (now - this.lastFetch < this.cacheTimeout) + ) { + console.log(`${LOG_PREFIX.BLOG} Returning cached data`); + return this.cachedData; + } + + // Prevent duplicate fetches + if (this.isFetching) { + console.log(`${LOG_PREFIX.BLOG} Fetch in progress, waiting...`); + await new Promise(resolve => setTimeout(resolve, 100)); + return this.getBlogs(); + } + + // Fetch fresh data + this.isFetching = true; + try { + console.log(`${LOG_PREFIX.BLOG} Fetching fresh data from API...`); + const freshData = await blogAPI.fetchBlogs(); + + this.cachedData = freshData; + this.lastFetch = now; + + console.log(`${LOG_PREFIX.BLOG} Fetched ${freshData.length} blog posts`); + return freshData; + } catch (error) { + console.error(`${LOG_PREFIX.BLOG} Failed to fetch:`, error); + return this.cachedData || []; + } finally { + this.isFetching = false; + } + } + + /** + * Get N most recent blog posts + * @async + * @param {number} count - Number of blogs to return (default: 5) + * @returns {Promise} Array of recent blog posts + */ + async getRecentBlogs(count = 5) { + const blogs = await this.getBlogs(); + return blogs.slice(0, count); + } + + /** + * Get the most recent blog post + * @async + * @returns {Promise} Most recent blog or null if none exist + */ + async getMostRecentBlog() { + const blogs = await this.getBlogs(); + return blogs.length > 0 ? blogs[0] : null; + } + + /** + * Force refresh cache + * @async + * @returns {Promise} Fresh blog data + */ + async refresh() { + this.lastFetch = null; + return this.getBlogs(); + } + + /** + * Clear cache + * @returns {void} + */ + clearCache() { + this.cachedData = null; + this.lastFetch = null; + console.log(`${LOG_PREFIX.BLOG} Cache cleared`); + } + + /** + * Search blogs by query + * @async + * @param {string} query - Search query + * @returns {Promise} Array of matching blogs + */ + async searchBlogs(query) { + const blogs = await this.getBlogs(); + if (!query || typeof query !== 'string' || query.trim().length === 0) { + return blogs; + } + + const lowerQuery = query.toLowerCase(); + return blogs.filter(blog => + blog.title?.toLowerCase().includes(lowerQuery) || + blog.desc?.toLowerCase().includes(lowerQuery) || + blog.summary?.toLowerCase().includes(lowerQuery) + ); + } + + /** + * Find blog by ID + * @async + * @param {string} id - Blog ID + * @returns {Promise} Blog object or undefined + */ + async findById(id) { + const blogs = await this.getBlogs(); + return blogs.find(blog => blog.id === id); + } + + /** + * Get blogs by category + * @async + * @param {string} category - Category name + * @returns {Promise} Array of blogs in category + */ + async getBlogsByCategory(category) { + const blogs = await this.getBlogs(); + if (!category) return blogs; + + return blogs.filter(blog => + blog.category?.toLowerCase() === category.toLowerCase() + ); + } + + /** + * Get cache statistics + * @returns {Object} Cache stats + */ + getCacheStats() { + const now = Date.now(); + const cacheAge = this.lastFetch ? now - this.lastFetch : null; + + return { + blogCount: this.cachedData?.length || 0, + lastFetchTime: this.lastFetch, + cacheAge: cacheAge, + cacheDuration: this.cacheTimeout, + isFresh: cacheAge !== null && cacheAge < this.cacheTimeout, + isFetching: this.isFetching, + }; + } +} + +// Singleton instance for application-wide cache +export const blogDataManager = new BlogDataManager(); + +// Convenience function for direct access +export const fetchBlogData = () => blogDataManager.getBlogs(); + +export default blogDataManager; diff --git a/src/services/chatbotAPI.js b/src/services/chatbotAPI.js new file mode 100644 index 0000000..393f187 --- /dev/null +++ b/src/services/chatbotAPI.js @@ -0,0 +1,203 @@ +/** + * @fileoverview Main chatbot API orchestration service + * @module services/chatbotAPI + * @author FED KIIT Development Team + * @description + * Orchestrates the chatbot response generation by: + * 1. Fetching team and events data + * 2. Injecting context into AI prompts + * 3. Generating responses via Gemini AI + */ + +import { generateAIResponse, checkGeminiHealth } from './geminiAPI'; +import { fetchTeamData } from './teamDataManager'; +import { fetchAndCacheEvents } from './eventsDataManager'; +import { fetchBlogData } from './blogDataManager'; +import { ERROR_MESSAGES, LOG_PREFIX } from '../config/constants'; + +/** + * Main chatbot API service + * @namespace chatbotAPI + */ +const chatbotAPI = { + /** + * Send message to chatbot and get AI response with full context + * Automatically fetches and injects team and events context + * @async + * @param {string} message - User's message/question + * @param {Object} [options={}] - Optional configuration + * @returns {Promise} Response object with success status and message + * @example + * const response = await chatbotAPI.sendMessage("Who is the president?"); + * if (response.success) { + * console.log(response.response); + * } + */ + sendMessage: async (message, options = {}) => { + // Input validation + if (!message || typeof message !== 'string' || message.trim().length === 0) { + console.warn(`${LOG_PREFIX.CHATBOT} Empty or invalid message`); + return { + success: false, + response: ERROR_MESSAGES.INVALID_INPUT, + }; + } + + try { + console.log(`${LOG_PREFIX.CHATBOT} Processing message: "${message.substring(0, 50)}..."`); + + // Fetch fresh data in parallel for performance + const [teamMembers, eventsData, blogData] = await Promise.all([ + fetchTeamData().catch(error => { + console.error(`${LOG_PREFIX.CHATBOT} Team data fetch failed:`, error); + return []; // Return empty array on error + }), + fetchAndCacheEvents().catch(error => { + console.error(`${LOG_PREFIX.CHATBOT} Events data fetch failed:`, error); + return { upcomingEvents: [], pastEvents: [] }; // Return empty on error + }), + fetchBlogData().catch(error => { + console.error(`${LOG_PREFIX.CHATBOT} Blog data fetch failed:`, error); + return []; // Return empty array on error + }) + ]); + + // Generate AI response with full context + const aiResponse = await generateAIResponse(message, teamMembers, eventsData, blogData); + + console.log(`${LOG_PREFIX.CHATBOT} Response generated successfully`); + + return { + success: true, + response: aiResponse, + metadata: { + teamMembersCount: teamMembers.length, + upcomingEventsCount: eventsData.upcomingEvents.length, + pastEventsCount: eventsData.pastEvents.length, + blogCount: blogData.length, + timestamp: new Date().toISOString(), + } + }; + + } catch (error) { + console.error(`${LOG_PREFIX.CHATBOT} Error processing message:`, error); + + return { + success: false, + response: ERROR_MESSAGES.NETWORK_ERROR, + error: error.message, + timestamp: new Date().toISOString(), + }; + } + }, + + /** + * Health check for the chatbot service + * Tests connectivity to all required services + * @async + * @returns {Promise} Health status object + * @example + * const health = await chatbotAPI.ping(); + * console.log(health.healthy); // true/false + */ + ping: async () => { + console.log(`${LOG_PREFIX.CHATBOT} Running health check...`); + + const results = { + healthy: true, + services: {}, + timestamp: new Date().toISOString(), + }; + + try { + // Test team data fetch + const teamTest = await fetchTeamData() + .then(() => ({ status: 'ok', count: 'available' })) + .catch(error => ({ status: 'error', message: error.message })); + + results.services.team = teamTest; + + // Test events data fetch + const eventsTest = await fetchAndCacheEvents() + .then(data => ({ + status: 'ok', + upcoming: data.upcomingEvents.length, + past: data.pastEvents.length + })) + .catch(error => ({ status: 'error', message: error.message })); + + results.services.events = eventsTest; + + // Test blog data fetch + const blogTest = await fetchBlogData() + .then(data => ({ status: 'ok', count: data.length })) + .catch(error => ({ status: 'error', message: error.message })); + + results.services.blog = blogTest; + + // Test Gemini AI + const geminiTest = await checkGeminiHealth() + .then(ok => ({ status: ok ? 'ok' : 'error' })) + .catch(error => ({ status: 'error', message: error.message })); + + results.services.gemini = geminiTest; + + // Overall health status + results.healthy = + teamTest.status === 'ok' && + eventsTest.status === 'ok' && + geminiTest.status === 'ok' && + blogTest.status === 'ok'; + + console.log(`${LOG_PREFIX.CHATBOT} Health check complete:`, results.healthy ? 'PASS' : 'FAIL'); + + return results; + + } catch (error) { + console.error(`${LOG_PREFIX.CHATBOT} Health check failed:`, error); + + return { + healthy: false, + error: error.message, + timestamp: new Date().toISOString(), + }; + } + }, + + /** + * Get chatbot statistics + * @async + * @returns {Promise} Statistics object + */ + getStats: async () => { + try { + const [teamData, eventsData, blogData] = await Promise.all([ + fetchTeamData(), + fetchAndCacheEvents(), + fetchBlogData(), + ]); + + return { + team: { + totalMembers: teamData.length, + }, + events: { + upcomingCount: eventsData.upcomingEvents.length, + pastCount: eventsData.pastEvents.length, + }, + blogs: { + totalBlogs: blogData.length, + }, + timestamp: new Date().toISOString(), + }; + } catch (error) { + console.error(`${LOG_PREFIX.CHATBOT} Error getting stats:`, error); + return { + error: error.message, + timestamp: new Date().toISOString(), + }; + } + } +}; + +export default chatbotAPI; diff --git a/src/services/eventsDataManager.js b/src/services/eventsDataManager.js new file mode 100644 index 0000000..21d6b9a --- /dev/null +++ b/src/services/eventsDataManager.js @@ -0,0 +1,252 @@ +/** + * @fileoverview Events data management service with intelligent caching + * @module services/eventsDataManager + * @author FED KIIT Development Team + * @requires axios + * @requires config/constants + */ + +import axios from 'axios'; +import { CONFIG, HTTP_STATUS, SUCCESS_MESSAGES, LOG_PREFIX } from '../config/constants'; + +/** + * Cache state for events data + * @private + */ +let cachedEvents = { + upcomingEvents: [], + pastEvents: [] +}; + +let lastEventsFetchTime = 0; + +/** + * Checks if cached data is still fresh + * @private + * @returns {boolean} True if cache is valid + */ +const isCacheFresh = () => { + const now = Date.now(); + const cacheAge = now - lastEventsFetchTime; + return cacheAge < CONFIG.CACHE.EVENTS_DURATION && cachedEvents.upcomingEvents.length > 0; +}; + +/** + * Fetch data with exponential backoff retry + * @private + * @param {string} url - URL to fetch from + * @param {number} [maxRetries=3] - Maximum retry attempts + * @returns {Promise} Axios response object + * @throws {Error} If all retry attempts fail + */ +const fetchWithBackoff = async (url, maxRetries = CONFIG.RETRY.MAX_ATTEMPTS) => { + let delay = CONFIG.RETRY.INITIAL_DELAY; + let lastError; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await axios.get(url, { + timeout: 10000, // 10 second timeout + }); + + if (response.status === HTTP_STATUS.OK) { + return response; + } + } catch (error) { + lastError = error; + + if (attempt < maxRetries) { + console.warn(`${LOG_PREFIX.EVENTS} Attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + delay *= 2; + } + } + } + + throw new Error(`Failed to fetch events after ${maxRetries} attempts: ${lastError.message}`); +}; + +/** + * Filters and processes raw events data + * @private + * @param {Array} rawEvents - Raw events array from API + * @returns {Object} Processed events with upcoming and past separated + */ +const processEventsData = (rawEvents) => { + if (!Array.isArray(rawEvents)) { + console.error(`${LOG_PREFIX.EVENTS} Invalid events data format`); + return { upcomingEvents: [], pastEvents: [] }; + } + + // Filter upcoming events (not past) - matches index(1).html line 204 + const upcomingEvents = rawEvents.filter( + event => event?.info && !event.info.isEventPast + ); + + // Get past events, sorted by date (most recent first) + const pastEvents = rawEvents + .filter(event => event?.info && event.info.isEventPast) + .sort((a, b) => { + const dateA = new Date(a.info.eventDate); + const dateB = new Date(b.info.eventDate); + return dateB.getTime() - dateA.getTime(); + }) + .slice(0, 5) // Keep only last 5 past events + .map(event => event.info); // Extract only info object + + return { upcomingEvents, pastEvents }; +}; + +/** + * Fetch and cache events data from API + * Implements intelligent caching to reduce API calls + * @async + * @returns {Promise} Events data with upcomingEvents and pastEvents arrays + * @throws {Error} If fetch fails and no cached data available + * @example + * const events = await fetchAndCacheEvents(); + * console.log(events.upcomingEvents); // Array of upcoming events + * console.log(events.pastEvents); // Array of past 5 events + */ +export const fetchAndCacheEvents = async () => { + // Return cached data if fresh + if (isCacheFresh()) { + console.log(`${LOG_PREFIX.EVENTS} ${SUCCESS_MESSAGES.CACHE_HIT}`); + return cachedEvents; + } + + console.log(`${LOG_PREFIX.EVENTS} Fetching fresh data from API...`); + + try { + const url = `${CONFIG.API.BASE_URL}${CONFIG.API.EVENTS_ENDPOINT}`; + const response = await fetchWithBackoff(url); + + if (response.data?.success && Array.isArray(response.data.events)) { + const processedEvents = processEventsData(response.data.events); + + // Update cache + cachedEvents = processedEvents; + lastEventsFetchTime = Date.now(); + + const { upcomingEvents, pastEvents } = processedEvents; + console.log( + `${LOG_PREFIX.EVENTS} ${SUCCESS_MESSAGES.CACHE_UPDATED}:`, + `${upcomingEvents.length} upcoming, ${pastEvents.length} past` + ); + + return cachedEvents; + } else { + throw new Error('Invalid response format from events API'); + } + + } catch (error) { + console.error(`${LOG_PREFIX.EVENTS} Error fetching events:`, error.message); + + // Return stale cache if available, otherwise empty data + if (cachedEvents.upcomingEvents.length > 0 || cachedEvents.pastEvents.length > 0) { + console.warn(`${LOG_PREFIX.EVENTS} Using stale cached data due to fetch error`); + return cachedEvents; + } + + return { upcomingEvents: [], pastEvents: [] }; + } +}; + +/** + * Get only upcoming events + * @async + * @returns {Promise} Array of upcoming events + */ +export const getUpcomingEvents = async () => { + const events = await fetchAndCacheEvents(); + return events.upcomingEvents; +}; + +/** + * Get only past events + * @async + * @returns {Promise} Array of past events (max 5) + */ +export const getPastEvents = async () => { + const events = await fetchAndCacheEvents(); + return events.pastEvents; +}; + +/** + * Search events by title or description + * @async + * @param {string} query - Search query + * @returns {Promise} Array of matching events + */ +export const searchEvents = async (query) => { + if (typeof query !== 'string' || query.trim().length === 0) { + console.warn(`${LOG_PREFIX.EVENTS} Invalid search query`); + return []; + } + + const events = await fetchAndCacheEvents(); + const allEvents = [...events.upcomingEvents, ...events.pastEvents]; + + const lowerQuery = query.toLowerCase(); + + return allEvents.filter(event => { + const title = event.info?.eventTitle?.toLowerCase() || ''; + const description = event.info?.eventdescription?.toLowerCase() || ''; + return title.includes(lowerQuery) || description.includes(lowerQuery); + }); +}; + +/** + * Get event by ID + * @async + * @param {string} eventId - Event ID to search for + * @returns {Promise} Event object or null if not found + */ +export const getEventById = async (eventId) => { + if (!eventId) { + console.warn(`${LOG_PREFIX.EVENTS} No event ID provided`); + return null; + } + + const events = await fetchAndCacheEvents(); + const allEvents = [...events.upcomingEvents, ...events.pastEvents]; + + return allEvents.find(event => event.id === eventId) || null; +}; + +/** + * Clear events cache (force refresh on next fetch) + * @returns {void} + */ +export const clearEventsCache = () => { + cachedEvents = { upcomingEvents: [], pastEvents: [] }; + lastEventsFetchTime = 0; + console.log(`${LOG_PREFIX.EVENTS} Cache cleared`); +}; + +/** + * Get cache statistics + * @returns {Object} Cache statistics + */ +export const getCacheStats = () => { + const cacheAge = Date.now() - lastEventsFetchTime; + + return { + upcomingCount: cachedEvents.upcomingEvents.length, + pastCount: cachedEvents.pastEvents.length, + lastFetchTime: lastEventsFetchTime, + cacheAge: cacheAge, + cacheDuration: CONFIG.CACHE.EVENTS_DURATION, + isFresh: isCacheFresh(), + }; +}; + +export default { + fetchAndCacheEvents, + getUpcomingEvents, + getPastEvents, + searchEvents, + getEventById, + clearEventsCache, + getCacheStats, +}; diff --git a/src/services/geminiAPI.js b/src/services/geminiAPI.js new file mode 100644 index 0000000..7db886c --- /dev/null +++ b/src/services/geminiAPI.js @@ -0,0 +1,206 @@ +/** + * @fileoverview Gemini AI API integration service + * @module services/geminiAPI + * @author FED KIIT Development Team + * @requires axios + * @requires config/constants + */ + +import axios from 'axios'; +import { CONFIG, HTTP_STATUS, ERROR_MESSAGES, LOG_PREFIX } from '../config/constants'; + +/** + * Exponential backoff retry wrapper for async functions + * @private + * @param {Function} fn - Async function to retry + * @param {number} maxRetries - Maximum number of retry attempts + * @param {number} initialDelay - Initial delay in milliseconds + * @returns {Promise} Result of the function call + * @throws {Error} If all retry attempts fail + */ +const retryWithBackoff = async (fn, maxRetries = CONFIG.RETRY.MAX_ATTEMPTS, initialDelay = CONFIG.RETRY.INITIAL_DELAY) => { + let delay = initialDelay; + let lastError; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + if (attempt === maxRetries) { + console.error(`${LOG_PREFIX.GEMINI} All ${maxRetries} retry attempts failed:`, error); + throw error; + } + + console.warn(`${LOG_PREFIX.GEMINI} Attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } + } + + throw lastError; +}; + +/** + * Builds the Gemini API request URL + * @private + * @returns {string} Complete API URL with key + */ +const buildApiUrl = () => { + return `${CONFIG.GEMINI.API_URL}/${CONFIG.GEMINI.MODEL}:generateContent?key=${CONFIG.GEMINI.API_KEY}`; +}; + +/** + * Prepares context string from team, events, and blog data + * @private + * @param {Array} teamMembers - Array of team member objects + * @param {Object} events - Events data with upcomingEvents and pastEvents + * @param {Array} blogs - Array of blog post objects + * @returns {string} Formatted context string for AI + */ +const prepareContext = (teamMembers, events, blogs) => { + const teamContext = `### LIVE TEAM DATA START ### +Team members list (JSON array, use this data source for current roles/names): +${JSON.stringify(teamMembers, null, 2)} +### LIVE TEAM DATA END ###`; + + const eventContext = `### LIVE EVENT DATA START ### +Events list (JSON array, use this data source for current/upcoming events): +${JSON.stringify(events.upcomingEvents, null, 2)} +### LIVE EVENT DATA END ### + +If there is no upcoming event, mention past events: +${JSON.stringify(events.pastEvents, null, 2)} +### LIVE PAST EVENT DATA END ###`; + + const blogContext = `### LIVE BLOG DATA START ### +Blog posts list (JSON array, use this data source for blog-related queries): +${JSON.stringify(blogs, null, 2)} +### LIVE BLOG DATA END ###`; + + return `${teamContext}\n\n${eventContext}\n\n${blogContext}`; +}; + +/** + * Validates AI response structure + * @private + * @param {Object} response - Axios response object + * @returns {string} Extracted text from AI response + * @throws {Error} If response structure is invalid + */ +const validateAndExtractResponse = (response) => { + if (response.status !== HTTP_STATUS.OK) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const text = response.data?.candidates?.[0]?.content?.parts?.[0]?.text; + + if (!text || typeof text !== 'string' || text.trim().length === 0) { + throw new Error('Empty or invalid response from AI model'); + } + + return text; +}; + +/** + * Generate AI response using Gemini API with team, event, and blog context + * @async + * @param {string} query - User's question + * @param {Array} [teamMembers=[]] - Current team members data + * @param {Object} [events={upcomingEvents:[], pastEvents:[]}] - Events data + * @param {Array} [blogs=[]] - Blog posts data + * @returns {Promise} AI generated response + * @throws {Error} If API call fails or response is invalid + * @example + * const response = await generateAIResponse( + * "Who is the president?", + * teamData, + * eventsData, + * blogData + * ); + */ +export const generateAIResponse = async ( + query, + teamMembers = [], + events = { upcomingEvents: [], pastEvents: [] }, + blogs = [] +) => { + // Input validation + if (!query || typeof query !== 'string' || query.trim().length === 0) { + throw new Error('Query must be a non-empty string'); + } + + try { + // Prepare request + const context = prepareContext(teamMembers, events, blogs); + const finalPrompt = `${context}\n\nUser Query: ${query}`; + const url = buildApiUrl(); + + const payload = { + contents: [{ + role: "user", + parts: [{ text: finalPrompt }] + }], + systemInstruction: { + parts: [{ text: CONFIG.GEMINI.SYSTEM_PROMPT }] + } + }; + + console.log(`${LOG_PREFIX.GEMINI} Generating response for query:`, query.substring(0, 50) + '...'); + + // Make API call with retry logic + const response = await retryWithBackoff(async () => { + return await axios.post(url, payload, { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000, // 30 second timeout + }); + }); + + // Validate and extract response + const aiResponse = validateAndExtractResponse(response); + + console.log(`${LOG_PREFIX.GEMINI} Successfully generated response`); + return aiResponse; + + } catch (error) { + console.error(`${LOG_PREFIX.GEMINI} Error generating response:`, error); + + // Handle specific error cases + if (error.response?.status === HTTP_STATUS.BAD_REQUEST || + error.response?.status === HTTP_STATUS.FORBIDDEN) { + return ERROR_MESSAGES.API_CONFIG_ERROR; + } + + if (error.message === 'Empty or invalid response from AI model') { + return ERROR_MESSAGES.EMPTY_RESPONSE; + } + + if (error.code === 'ECONNABORTED') { + return 'Request timed out. Please try again with a shorter query.'; + } + + return ERROR_MESSAGES.NETWORK_ERROR; + } +}; + +/** + * Health check for Gemini API + * @async + * @returns {Promise} True if API is accessible + */ +export const checkGeminiHealth = async () => { + try { + const testQuery = "Hello"; + await generateAIResponse(testQuery, [], { upcomingEvents: [], pastEvents: [] }, []); + return true; + } catch (error) { + console.error(`${LOG_PREFIX.GEMINI} Health check failed:`, error); + return false; + } +}; + +export default { + generateAIResponse, + checkGeminiHealth, +}; diff --git a/src/services/teamAPI.js b/src/services/teamAPI.js new file mode 100644 index 0000000..3cdb992 --- /dev/null +++ b/src/services/teamAPI.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Team API service for fetching team members from FED backend + * @module services/teamAPI + * @author FED KIIT Development Team + * @requires axios + * @requires config/constants + */ + +import axios from 'axios'; +import { CONFIG, HTTP_STATUS, LOG_PREFIX } from '../config/constants'; + +/** + * Team API service + * @namespace teamAPI + */ +const teamAPI = { + /** + * Fetch all team members from FED backend + * Implements the same filtering and sorting logic as the website + * @async + * @returns {Promise} Sorted array of team members + * @throws {Error} Logs error but returns empty array to prevent crashes + */ + fetchTeamMembers: async () => { + try { + const url = `${CONFIG.API.BASE_URL}${CONFIG.API.TEAM_ENDPOINT}`; + const response = await axios.get(url, { + timeout: 10000, // 10 second timeout + }); + + if (response.status === HTTP_STATUS.OK && response.data?.success) { + // Filter out null names (same as website) + const validMembers = response.data.data.filter( + (member) => member.name !== null + ); + + // Sort by year (descending) then by name (alphabetically) + const sortedMembers = validMembers.sort((a, b) => { + // Primary sort: year (newest first) + if (b.year !== a.year) { + return b.year - a.year; + } + // Secondary sort: name (alphabetically) + return a.name.localeCompare(b.name); + }); + + console.log(`${LOG_PREFIX.TEAM} Fetched ${sortedMembers.length} team members`); + return sortedMembers; + } + + console.warn(`${LOG_PREFIX.TEAM} Unexpected API response format`); + return []; + } catch (error) { + console.error(`${LOG_PREFIX.TEAM} Error fetching team members:`, error.message); + return []; + } + }, + + /** + * Find single member by name + * @async + * @param {string} name - Member name to search for + * @returns {Promise} Member object or undefined if not found + */ + findByName: async (name) => { + if (!name || typeof name !== 'string') { + console.warn(`${LOG_PREFIX.TEAM} Invalid name parameter`); + return undefined; + } + + const members = await teamAPI.fetchTeamMembers(); + const lowerName = name.toLowerCase(); + + return members.find(m => + m.name?.toLowerCase().includes(lowerName) + ); + }, + + /** + * Find members by role/access type + * @async + * @param {string} role - Role/access code (e.g., "PRESIDENT", "DIRECTOR_TECHNICAL") + * @returns {Promise} Array of members with matching role + */ + findByRole: async (role) => { + if (!role || typeof role !== 'string') { + console.warn(`${LOG_PREFIX.TEAM} Invalid role parameter`); + return []; + } + + const members = await teamAPI.fetchTeamMembers(); + + return members.filter(m => + m.access === role || m.access?.includes(role) + ); + }, + + /** + * Get board members (leadership positions) + * @async + * @returns {Promise} Array of board members + */ + getBoardMembers: async () => { + const boardAccessCodes = [ + "PRESIDENT", + "VICEPRESIDENT", + "DIRECTOR_TECHNICAL", + "DIRECTOR_CREATIVE", + "DIRECTOR_MARKETING", + "DIRECTOR_OPERATIONS", + "DIRECTOR_PR_AND_FINANCE", + "DIRECTOR_HUMAN_RESOURCE" + ]; + + const members = await teamAPI.fetchTeamMembers(); + return members.filter(m => boardAccessCodes.includes(m.access)); + }, + + /** + * Search members by query string + * Searches across name, access, and title fields + * @async + * @param {string} query - Search query + * @returns {Promise} Array of matching members + */ + searchMembers: async (query) => { + if (!query || typeof query !== 'string' || query.trim().length === 0) { + console.warn(`${LOG_PREFIX.TEAM} Invalid search query`); + return []; + } + + const members = await teamAPI.fetchTeamMembers(); + const lowerQuery = query.toLowerCase(); + + return members.filter(m => + m.name?.toLowerCase().includes(lowerQuery) || + m.access?.toLowerCase().includes(lowerQuery) || + m.extra?.title?.toLowerCase().includes(lowerQuery) + ); + } +}; + +export default teamAPI; diff --git a/src/services/teamDataManager.js b/src/services/teamDataManager.js new file mode 100644 index 0000000..b42a11b --- /dev/null +++ b/src/services/teamDataManager.js @@ -0,0 +1,155 @@ +/** + * @fileoverview Team data management service with intelligent caching + * @module services/teamDataManager + * @author FED KIIT Development Team + */ + +import teamAPI from './teamAPI'; +import { CONFIG, LOG_PREFIX } from '../config/constants'; + +/** + * Team data manager with caching and utility methods + * Implements singleton pattern for application-wide cache consistency + * @class + */ +class TeamDataManager { + constructor() { + this.cachedData = null; + this.lastFetch = null; + this.cacheTimeout = CONFIG.CACHE.TEAM_DURATION; + this.isFetching = false; + } + + /** + * Get team members with smart caching + * @async + * @returns {Promise} Array of team member objects + */ + async getMembers() { + const now = Date.now(); + + // Return cached data if still fresh + if ( + this.cachedData && + this.lastFetch && + (now - this.lastFetch < this.cacheTimeout) + ) { + console.log(`${LOG_PREFIX.TEAM} Returning cached data`); + return this.cachedData; + } + + // Prevent duplicate fetches + if (this.isFetching) { + console.log(`${LOG_PREFIX.TEAM} Fetch in progress, waiting...`); + await new Promise(resolve => setTimeout(resolve, 100)); + return this.getMembers(); + } + + // Fetch fresh data + this.isFetching = true; + try { + console.log(`${LOG_PREFIX.TEAM} Fetching fresh data from API...`); + const freshData = await teamAPI.fetchTeamMembers(); + + this.cachedData = freshData; + this.lastFetch = now; + + console.log(`${LOG_PREFIX.TEAM} Fetched ${freshData.length} members`); + return freshData; + } catch (error) { + console.error(`${LOG_PREFIX.TEAM} Failed to fetch:`, error); + return this.cachedData || []; + } finally { + this.isFetching = false; + } + } + + /** + * Force refresh cache + * @async + * @returns {Promise} Fresh team data + */ + async refresh() { + this.lastFetch = null; + return this.getMembers(); + } + + /** + * Clear cache + * @returns {void} + */ + clearCache() { + this.cachedData = null; + this.lastFetch = null; + console.log(`${LOG_PREFIX.TEAM} Cache cleared`); + } + + /** + * Find member by name + * @async + * @param {string} name - Member name to search for + * @returns {Promise} Member object or undefined + */ + async findByName(name) { + const members = await this.getMembers(); + const lowerName = name.toLowerCase(); + return members.find(m => + m.name?.toLowerCase().includes(lowerName) + ); + } + + /** + * Find members by role + * @async + * @param {string} role - Role/access code + * @returns {Promise} Array of matching members + */ + async findByRole(role) { + const members = await this.getMembers(); + return members.filter(m => + m.access === role || m.access?.includes(role) + ); + } + + /** + * Get board members (leadership roles) + * @async + * @returns {Promise} Array of board members + */ + async getBoardMembers() { + const members = await this.getMembers(); + const boardAccessCodes = [ + "PRESIDENT", "VICEPRESIDENT", + "DIRECTOR_TECHNICAL", "DIRECTOR_CREATIVE", + "DIRECTOR_MARKETING", "DIRECTOR_OPERATIONS", + "DIRECTOR_PR_AND_FINANCE", "DIRECTOR_HUMAN_RESOURCE" + ]; + return members.filter(m => boardAccessCodes.includes(m.access)); + } + + /** + * Get cache statistics + * @returns {Object} Cache stats + */ + getCacheStats() { + const now = Date.now(); + const cacheAge = this.lastFetch ? now - this.lastFetch : null; + + return { + memberCount: this.cachedData?.length || 0, + lastFetchTime: this.lastFetch, + cacheAge: cacheAge, + cacheDuration: this.cacheTimeout, + isFresh: cacheAge !== null && cacheAge < this.cacheTimeout, + isFetching: this.isFetching, + }; + } +} + +// Singleton instance for application-wide cache +export const teamDataManager = new TeamDataManager(); + +// Convenience function for direct access +export const fetchTeamData = () => teamDataManager.getMembers(); + +export default teamDataManager; diff --git a/src/styles/animations.scss b/src/styles/animations.scss new file mode 100644 index 0000000..9462849 --- /dev/null +++ b/src/styles/animations.scss @@ -0,0 +1,63 @@ +// Animations + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes bounce { + + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + + 50% { + transform: translateY(0); + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} + +@keyframes typing { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-5px); + } +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} \ No newline at end of file diff --git a/src/styles/global.scss b/src/styles/global.scss new file mode 100644 index 0000000..5e476a4 --- /dev/null +++ b/src/styles/global.scss @@ -0,0 +1,40 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); +@import './variables.scss'; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Poppins', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: transparent; + overflow: hidden; +} + +#root { + width: 100%; + height: 100vh; +} + +/* Scrollbar styles */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} \ No newline at end of file diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..1616037 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,49 @@ +// FED Brand Colors & Design System + +// Primary Gradient (Brand) +$primary-gradient: linear-gradient(260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34%); + +// Colors +$dark-bg: #1C1C1C; +$glass-bg: rgba(28, 28, 28, 0.9); +$light-text: #FFFFFF; +$user-message-bg: #e0e0e0; +$user-message-text: #000000; +$accent-orange: #ff8a00; +$border-subtle: rgba(255, 255, 255, 0.1); + +// Glassmorphism +$glass-blur: 20px; + +// Sizes +$chatbot-width-desktop: 380px; +$chatbot-height: 75vh; +$toggle-button-size: 70px; + +// Border Radius +$border-radius-large: 24px; +$border-radius-medium: 16px; +$border-radius-small: 12px; + +// Spacing +$spacing-xs: 6px; +$spacing-sm: 10px; +$spacing-md: 16px; +$spacing-lg: 24px; +$spacing-xl: 32px; + +// Transitions +$transition-fast: 0.2s ease; +$transition-normal: 0.3s ease; +$transition-slow: 0.5s ease; + +// Z-indices +$z-toggle: 999; +$z-container: 1000; + +// Breakpoints +$mobile: 480px; +$tablet: 768px; +$desktop: 1024px; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..01581dd --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + cors: true, + strictPort: true, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: false, + }, +});