diff --git a/apps/web-roo-code/src/components/excel-analyzer/CLAUDE.md b/apps/web-roo-code/src/components/excel-analyzer/CLAUDE.md
new file mode 100644
index 00000000000..14434c1841c
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/CLAUDE.md
@@ -0,0 +1,39 @@
+# CLAUDE.md — Shared Brain / Lessons Learned
+
+Durable project memory for architecture, structure, and workflow decisions.
+
+## Current Snapshot
+
+- Project: **Excel Analyzer**
+- Runtime: Browser-only (client-side)
+- Extensibility: `src/hooks/hook-engine.js`
+- Operational memory:
+ - `.orchestration/active_intents.yaml`
+ - `.orchestration/agent_trace.jsonl`
+ - `.orchestration/intent_map.md`
+
+## Documentation Map
+
+- `README.md` (workspace root)
+- `Excel-Based-Data-Analzer/README.md` (application-level guide)
+- `specify/specification.md`
+- `specify/memory/constitution.md`
+- `specify/plan/plan.md`
+- `specify/architecture/architecture.md`
+- `specify/project_structure/project_structure.md`
+
+## Lessons Learned
+
+1. Keep structure docs synchronized immediately after file moves.
+2. Keep hook-specific logic isolated under `src/hooks/`.
+3. Keep orchestration artifacts lightweight and plain-text for traceability.
+4. Update both root and app README when structure changes.
+
+## Working Agreements
+
+- `.orchestration/active_intents.yaml` tracks active goals.
+- `.orchestration/agent_trace.jsonl` logs key operations.
+- `.orchestration/intent_map.md` maps goals to code areas.
+- `specify/project_structure/project_structure.md` is the canonical structure reference.
+
+
diff --git a/apps/web-roo-code/src/components/excel-analyzer/README.md b/apps/web-roo-code/src/components/excel-analyzer/README.md
new file mode 100644
index 00000000000..5832d27d6fd
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/README.md
@@ -0,0 +1,72 @@
+# Excel Analyzer Project
+
+Web-based Excel analysis application with modular architecture, local-first processing, and extensibility via a dedicated Hook Engine.
+
+## What It Does
+
+- Upload and parse Excel files (`.xlsx`, `.xls`)
+- Preview tabular data with basic quality indicators
+- Analyze/visualize data (Chart.js)
+- View a compact dashboard summary
+- Export results (PDF / Excel / CSV / JSON / PNG)
+
+## Architecture Highlights
+
+- **Client-side only** processing for privacy
+- **Module-oriented JS structure** under `src/js/`
+- **Hook Engine** under `src/hooks/hook-engine.js` for extensibility
+- **Project memory and orchestration artifacts**:
+ - `CLAUDE.md`
+ - `.orchestration/active_intents.yaml`
+ - `.orchestration/agent_trace.jsonl`
+ - `.orchestration/intent_map.md`
+
+## Current Structure
+
+```text
+excel-analyzer/
+├── README.md
+├── CLAUDE.md
+├── .orchestration/
+│ ├── active_intents.yaml
+│ ├── agent_trace.jsonl
+│ └── intent_map.md
+├── specify/
+│ ├── specification.md
+│ ├── memory/
+│ │ └── constitution.md
+│ ├── plan/
+│ │ └── plan.md
+│ ├── architecture/
+│ │ └── architecture.md
+│ └── project_structure/
+│ └── project_structure.md
+└── src/
+ ├── index.html
+ ├── css/
+ │ └── styles.css
+ ├── hooks/
+ │ └── hook-engine.js
+ └── js/
+ ├── main.js
+ ├── config/
+ ├── events/
+ ├── modules/
+ └── utils/
+```
+
+## Run
+
+Open in browser:
+
+```powershell
+start Excel-Based-Data-Analzer/src/index.html
+```
+
+## Related Docs
+
+- Specification: `specify/specification.md`
+- Constitution: `specify/memory/constitution.md`
+- Plan: `specify/plan/plan.md`
+- Architecture: `specify/architecture/architecture.md`
+- Project Structure: `specify/project_structure/project_structure.md`
diff --git a/apps/web-roo-code/src/components/excel-analyzer/specify/architecture/architecture.md b/apps/web-roo-code/src/components/excel-analyzer/specify/architecture/architecture.md
new file mode 100644
index 00000000000..20cb56178aa
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/specify/architecture/architecture.md
@@ -0,0 +1,89 @@
+e# Excel Analyzer Architecture (Revised)
+
+## 1) Architecture Style
+
+The project uses a **client-side modular SPA-style architecture**:
+
+- Runs fully in the browser (no backend required)
+- Processes files locally for privacy
+- Uses module separation for upload, processing, visualization, dashboard, export
+- Supports extensibility through a dedicated **Hook Engine**
+
+## 2) High-Level Layers
+
+1. **Presentation Layer**
+ - `src/index.html`
+ - `src/css/styles.css`
+
+2. **Application Orchestration Layer**
+ - `src/js/main.js`
+ - Coordinates upload → preview → analyze → dashboard → export flow
+
+3. **Domain Modules Layer**
+ - `src/js/modules/*.js`
+ - Encapsulates feature logic (file upload, data processing, charts, dashboard, exports)
+
+4. **Shared Utilities and Config Layer**
+ - `src/js/utils/*.js`
+ - `src/js/config/*.js`
+ - `src/js/events/*.js`
+
+5. **Extensibility Layer (Hooks)**
+ - `src/hooks/hook-engine.js`
+ - Central hook registry/execution engine for plugging cross-cutting behavior
+
+## 3) Hook Engine Design
+
+Location: `src/hooks/hook-engine.js`
+
+Core capabilities:
+
+- `tap(hookName, handler, { priority, once })`
+- `tapOnce(hookName, handler, options)`
+- `untap(hookName, hookId)`
+- `clear(hookName?)`
+- `has(hookName)` / `list(hookName)`
+- `run(hookName, context)` for async handler execution
+- `runWaterfall(hookName, payload, context)` for payload transformation pipeline
+
+Design goals:
+
+- Keep hook logic isolated in `src/hooks/`
+- Make extension points explicit and reusable
+- Allow priority ordering and one-time hooks
+
+## 4) Data Flow Layer
+
+```text
+User File Input
+ -> FileUploadManager
+ -> DataProcessor
+ -> Visualizer / DashboardManager
+ -> Exporter
+ -> Download Output
+```
+
+Potential hook interception points (recommended):
+
+- `beforeFileParse`
+- `afterFileParse`
+- `beforeAnalysis`
+- `afterAnalysis`
+- `beforeExport`
+- `afterExport`
+
+## 5) Documentation and Operational Memory
+
+Beyond source code, the project now includes lightweight operational artifacts:
+
+- `CLAUDE.md` — shared brain / durable lessons learned
+- `.orchestration/active_intents.yaml` — active priorities/intents
+- `.orchestration/agent_trace.jsonl` — chronological action trace
+- `.orchestration/intent_map.md` — mapping goals to code areas
+
+## 6) Architectural Principles
+
+- **Separation of concerns** between UI, orchestration, domain logic, utilities, and hooks
+- **Local-first processing** for privacy and performance
+- **Incremental extensibility** via Hook Engine rather than hardcoded branching
+- **Documentation alignment**: structure docs must reflect actual repository state
diff --git a/apps/web-roo-code/src/components/excel-analyzer/specify/memory/constitution.md b/apps/web-roo-code/src/components/excel-analyzer/specify/memory/constitution.md
new file mode 100644
index 00000000000..6d905a5c43e
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/specify/memory/constitution.md
@@ -0,0 +1,101 @@
+# Excel Analyzer Project Constitution
+
+## Preamble
+
+We, the developers and contributors to the Excel Analyzer Project, recognize the importance of creating a robust, user-friendly, and accessible web-based tool for Excel data analysis. This constitution establishes the fundamental principles, values, and governance the structure that guide our development efforts and ensure the project's success and sustainability.
+
+## Core Principles
+
+### 1. User-Centric Design
+- **1.1** The user experience SHALL be the primary consideration in all design decisions
+- **1.2** The application SHALL be intuitive and accessible to users of all technical backgrounds
+- **1.3** User feedback SHALL be actively sought and incorporated into development cycles
+
+### 2. Data Privacy and Security
+- **2.1** User data SHALL remain on the client-side and SHALL NOT be transmitted to external servers
+- **2.2** No persistent storage of user files SHALL occur without explicit user consent
+- **2.3** All data processing SHALL be transparent and auditable by the user
+
+### 3. Open Source and Collaboration
+- **3.1** The project SHALL remain open source under the MIT License
+- **3.2** Contributions from the community SHALL be welcomed and encouraged
+- **3.3** Development processes SHALL be transparent and documented
+
+### 4. Technical Excellence
+- **4.1** Code quality SHALL meet industry standards and best practices
+- **4.2** Performance optimization SHALL be prioritized for user experience
+- **4.3** Cross-browser compatibility SHALL be maintained and tested
+
+### 5. Accessibility and Inclusion
+- **5.1** The application SHALL comply with WCAG 2.1 accessibility guidelines
+- **5.2** Support for assistive technologies SHALL be maintained
+- **5.3** Internationalization SHALL be considered in design and implementation
+
+## Development Guidelines
+
+### 6. Code Standards
+- **6.1** All code SHALL be well-documented with clear comments and documentation
+- **6.2** Consistent coding style SHALL be enforced using automated tools
+- **6.3** Code reviews SHALL be mandatory for all contributions
+- **6.4** Unit tests SHALL be written for all new functionality
+
+### 7. Quality Assurance
+- **7.1** All features SHALL be thoroughly tested before release
+- **7.2** Performance benchmarks SHALL be established and monitored
+- **7.3** Security vulnerabilities SHALL be addressed promptly
+- **7.4** User acceptance testing SHALL be conducted for major features
+
+### 8. Version Control and Releases
+- **8.1** Git SHALL be used for version control with a clear branching strategy
+- **8.2** Semantic versioning SHALL be followed for releases
+- **8.3** Release notes SHALL document all changes and improvements
+- **8.4** Backward compatibility SHALL be maintained whenever possible
+
+## Project Governance
+
+### 9. Decision Making
+- **9.1** Technical decisions SHALL be made through consensus among core contributors
+- **9.2** Major architectural changes SHALL require community discussion
+- **9.3** User-facing changes SHALL be validated through user testing
+- **9.4** Emergency fixes MAY be implemented immediately with post-facto documentation
+
+### 10. Contribution Process
+- **10.1** All contributors SHALL adhere to the project's code of conduct
+- **10.2** Pull requests SHALL include appropriate tests and documentation
+- **10.3** Contributors SHALL be respectful and constructive in all interactions
+- **10.4** Mentorship of new contributors SHALL be encouraged
+
+### 11. Maintenance and Support
+- **11.1** Regular maintenance updates SHALL be scheduled and published
+- **11.2** Bug reports SHALL be acknowledged and addressed in a timely manner
+- **11.3** Security issues SHALL be handled with appropriate urgency
+- **11.4** Legacy support SHALL be maintained for reasonable time periods
+
+## Ethical Guidelines
+
+### 12. Data Ethics
+- **12.1** The application SHALL NOT make assumptions or inferences about user data
+- **12.2** Data analysis features SHALL be transparent about their methodologies
+- **12.3** Users SHALL retain full control over their data and analysis results
+- **12.4** The application SHALL NOT collect or analyze user behavior without consent
+
+### 13. Responsible Development
+- **13.1** Environmental impact of the application SHALL be considered
+- **13.2** Resource efficiency SHALL be optimized to reduce computational waste
+- **13.3** Dependencies SHALL be chosen with security and maintenance in mind
+- **13.4** The project SHALL contribute positively to the open-source ecosystem
+
+## Amendments
+
+### 14. Constitution Updates
+- **14.1** This constitution MAY be amended through community consensus
+- **14.2** Proposed amendments SHALL be discussed publicly for a minimum of 30 days
+- **14.3** Amendments SHALL require approval from the core development team
+- **14.4** All amendments SHALL be documented with rationale and impact assessment
+
+## Commitment
+
+By contributing to this project, all participants agree to uphold these principles and work towards creating an Excel analysis tool that is not only technically excellent but also ethically sound, user-focused, and accessible to all.
+
+**Adopted**: February 17, 2026
+**Version**: 1.0
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/specify/plan/plan.md b/apps/web-roo-code/src/components/excel-analyzer/specify/plan/plan.md
new file mode 100644
index 00000000000..d1d2c89683c
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/specify/plan/plan.md
@@ -0,0 +1,220 @@
+# Excel Analyzer Project Implementation Plan
+
+## Project Overview
+
+This document outlines the comprehensive implementation plan for the web-based Excel Analyzer project, detailing the development phases, milestones, and resource allocation required to deliver a complete solution.
+
+## Project Phases
+
+### Phase 1: Foundation Setup (Week 1)
+**Duration**: 2 days
+**Objective**: Establish project structure and basic infrastructure
+
+#### 1.1 Project Initialization
+- [ ] Create project directory structure
+- [ ] Set up HTML, CSS, and JavaScript file organization
+- [ ] Configure development environment
+- [ ] Initialize documentation files
+
+#### 1.2 Core Dependencies Setup
+- [ ] Integrate SheetJS (xlsx.js) for Excel file processing
+- [ ] Add Chart.js for data visualization
+- [ ] Include jsPDF for PDF export functionality
+- [ ] Set up CSS framework (Bootstrap or custom styles)
+
+#### 1.3 Basic File Upload Interface
+- [ ] Create main HTML structure
+- [ ] Implement file input and drag-and-drop functionality
+- [ ] Add file validation and error handling
+- [ ] Create basic progress indicators
+
+**Deliverable**: Basic file upload and validation system
+
+### Phase 2: Data Processing Engine (Week 2)
+**Duration**: 1 days
+**Objective**: Implement core data analysis and preprocessing capabilities
+
+#### 2.1 Data Parsing and Analysis
+- [ ] Implement Excel file parsing using SheetJS
+- [ ] Create data structure for internal data representation
+- [ ] Develop data type detection algorithms
+- [ ] Implement basic data statistics calculation
+
+#### 2.2 Data Preprocessing Module
+- [ ] Missing value detection and handling
+- [ ] Outlier identification algorithms
+- [ ] Data cleaning strategies implementation
+- [ ] Data validation and quality checks
+
+#### 2.3 Data Observation Interface
+- [ ] Create data preview table component
+- [ ] Implement data summary display
+- [ ] Add data quality indicators
+- [ ] Create interactive data exploration features
+
+**Deliverable**: Complete data processing and observation system
+
+### Phase 3: Visualization System (Week 3)
+**Duration**: 1-2 days
+**Objective**: Develop comprehensive data visualization capabilities
+
+#### 3.1 Chart Generation Engine
+- [ ] Implement automatic chart type selection based on data
+- [ ] Create bar chart visualization
+- [ ] Create line chart visualization
+- [ ] Create pie chart visualization
+- [ ] Create scatter plot visualization
+
+#### 3.2 Interactive Visualization Features
+- [ ] Add chart customization options
+- [ ] Implement zoom and pan functionality
+- [ ] Add tooltips and data point highlighting
+- [ ] Create real-time chart updates
+
+#### 3.3 Visualization Interface
+- [ ] Create chart selection and configuration panel
+- [ ] Implement chart arrangement and layout options
+- [ ] Add chart export functionality
+- [ ] Create visualization history and presets
+
+**Deliverable**: Complete interactive visualization system
+
+### Phase 4: Dashboard and User Interface (Week 4)
+**Duration**: 1 day
+**Objective**: Build comprehensive dashboard and enhance user experience
+
+#### 4.1 Dashboard Framework
+- [ ] Create dashboard layout system
+- [ ] Implement widget-based dashboard components
+- [ ] Add dashboard customization and arrangement
+- [ ] Create dashboard state management
+
+#### 4.2 Advanced User Interface
+- [ ] Implement responsive design for mobile devices
+- [ ] Add accessibility features (WCAG compliance)
+- [ ] Create user preferences and settings
+- [ ] Implement keyboard navigation
+
+#### 4.3 Data Filtering and Interaction
+- [ ] Add real-time data filtering capabilities
+- [ ] Implement cross-chart data linking
+- [ ] Create data drill-down features
+- [ ] Add dashboard export and sharing options
+
+**Deliverable**: Complete interactive dashboard system
+
+### Phase 5: Export and Advanced Features (Week 5)
+**Duration**: 1 day
+**Objective**: Implement export functionality and advanced features
+
+#### 5.1 Export Module
+- [ ] Implement PDF export with charts and analysis
+- [ ] Create Excel export for processed data
+- [ ] Add export progress indicators
+- [ ] Implement batch export functionality
+
+#### 5.2 Advanced Analytics
+- [ ] Add statistical analysis features
+- [ ] Implement trend analysis
+- [ ] Create data comparison tools
+- [ ] Add custom calculation support
+
+#### 5.3 Performance Optimization
+- [ ] Optimize file processing for large datasets
+- [ ] Implement lazy loading for charts
+- [ ] Add memory management for large files
+- [ ] Optimize export generation speed
+
+**Deliverable**: Complete export system and advanced analytics
+
+### Phase 6: Testing and Documentation (Week 6)
+**Duration**: 1 days
+**Objective**: Ensure quality and create comprehensive documentation
+
+#### 6.1 Testing and Quality Assurance
+- [ ] Create comprehensive test suite
+- [ ] Perform cross-browser compatibility testing
+- [ ] Conduct performance testing
+- [ ] Implement user acceptance testing
+
+#### 6.2 Documentation and Help System
+- [ ] Create user guide and documentation
+- [ ] Implement in-app help system
+- [ ] Create API documentation
+- [ ] Add tooltips and contextual help
+
+#### 6.3 Final Polish and Optimization
+- [ ] Address all identified bugs and issues
+- [ ] Optimize code for production
+- [ ] Create deployment package
+- [ ] Final performance tuning
+
+**Deliverable**: Production-ready application with complete documentation
+
+## Resource Allocation
+
+### Development Team
+- **Project Manager**: 1 person (part-time)
+- **Frontend Developer**: 1 person (full-time)
+- **UI/UX Designer**: 1 person (part-time)
+- **Quality Assurance**: 1 person (part-time)
+
+### Technology Resources
+- **Development Environment**: Modern web browser, code editor
+- **Libraries**: SheetJS, Chart.js, jsPDF, Bootstrap
+- **Testing Tools**: Browser developer tools, manual testing
+- **Documentation**: Markdown, HTML documentation
+
+### Time Allocation
+- **Total Duration**: 1 weeks
+- **Development Time**: 60-70 hours
+- **Testing Time**: 10-12 hours
+- **Documentation Time**: 2-3 hours
+
+## Risk Management
+
+### Technical Risks
+- **Large File Processing**: Mitigation through chunking and optimization
+- **Browser Compatibility**: Mitigation through comprehensive testing
+- **Performance Issues**: Mitigation through optimization and best practices
+
+### Project Risks
+- **Scope Creep**: Mitigation through clear requirements and phase boundaries
+- **Resource Constraints**: Mitigation through careful planning and prioritization
+- **Timeline Delays**: Mitigation through buffer time and parallel development
+
+## Success Criteria
+
+### Functional Success
+- [ ] All specified features implemented and working
+- [ ] File processing handles up to 10,000 rows efficiently
+- [ ] Export functionality produces valid files
+- [ ] Dashboard provides interactive data exploration
+
+### Quality Success
+- [ ] Application works across all major browsers
+- [ ] Performance meets specified benchmarks
+- [ ] Code quality meets established standards
+- [ ] User interface is intuitive and accessible
+
+### User Success
+- [ ] Users can complete analysis tasks within 15 minutes
+- [ ] Application receives positive user feedback
+- [ ] Documentation enables self-service usage
+- [ ] Application meets accessibility standards
+
+## Post-Implementation
+
+### Maintenance Plan
+- Regular security updates for dependencies
+- Performance monitoring and optimization
+- User feedback collection and implementation
+- Bug fixes and minor feature enhancements
+
+### Future Enhancements
+- Cloud storage integration
+- Advanced machine learning features
+- Collaboration and sharing capabilities
+- Mobile app development
+
+This implementation plan provides a structured approach to developing the Excel Analyzer project, ensuring all requirements are met while maintaining quality and user satisfaction.
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/specify/project_structure/project_structure.md b/apps/web-roo-code/src/components/excel-analyzer/specify/project_structure/project_structure.md
new file mode 100644
index 00000000000..2e36793aacc
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/specify/project_structure/project_structure.md
@@ -0,0 +1,76 @@
+# Excel Analyzer — Project Structure (Revised)
+
+This document reflects the current repository layout after adding:
+
+- `src/hooks/hook-engine.js`
+- `.orchestration/*`
+- `CLAUDE.md`
+
+## Repository Tree
+
+```text
+Week1_TRP1_Intent_based_planning/
+├── README.md
+├── specify/
+│ └── specification.md
+└── Excel-Based-Data-Analzer/
+ ├── README.md
+ ├── CLAUDE.md
+ ├── .orchestration/
+ │ ├── active_intents.yaml
+ │ ├── agent_trace.jsonl
+ │ └── intent_map.md
+ ├── specify/
+ │ ├── specification.md
+ │ ├── memory/
+ │ │ └── constitution.md
+ │ ├── plan/
+ │ │ └── plan.md
+ │ ├── architecture/
+ │ │ └── architecture.md
+ │ └── project_structure/
+ │ └── project_structure.md
+ └── src/
+ ├── index.html
+ ├── css/
+ │ └── styles.css
+ ├── hooks/
+ │ └── hook-engine.js
+ └── js/
+ ├── main.js
+ ├── config/
+ │ ├── defaults.js
+ │ ├── settings.js
+ │ └── themes.js
+ ├── events/
+ │ ├── event-handlers.js
+ │ └── event-system.js
+ ├── modules/
+ │ ├── analytics.js
+ │ ├── dashboard.js
+ │ ├── data-processor.js
+ │ ├── exporter.js
+ │ ├── file-upload.js
+ │ ├── help.js
+ │ ├── notifications.js
+ │ ├── settings.js
+ │ └── visualizer.js
+ └── utils/
+ ├── formatters.js
+ ├── helpers.js
+ └── validators.js
+```
+
+## Structure Notes
+
+- `src/hooks/` is reserved for hook-system code (currently `hook-engine.js`).
+- `.orchestration/` stores operational memory and tracing artifacts.
+- `CLAUDE.md` stores durable lessons and working agreements.
+- `specify/` contains project governance/planning/specification documents.
+
+## Maintenance Rule
+
+Whenever files/folders are reorganized, update this
+ document and both README files in the same change.
+
+
diff --git a/apps/web-roo-code/src/components/excel-analyzer/specify/specification.md b/apps/web-roo-code/src/components/excel-analyzer/specify/specification.md
new file mode 100644
index 00000000000..56a71ddf9ba
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/specify/specification.md
@@ -0,0 +1,132 @@
+# Excel Analyzer Project Specification
+
+## 1. Functional Requirements
+
+### 1.1 File Upload Module
+- **FR-001**: System SHALL accept Excel files (.xlsx, .xls .csv) from users
+- **FR-002**: System SHALL validate file format and size (max 50MB)
+- **FR-003**: System SHALL display file upload progress
+- **FR-004**: System SHALL provide error messages for invalid files
+- **FR-005**: System SHALL support drag-and-drop file upload
+
+### 1.2 Data Observation Module
+- **FR-006**: System SHALL display uploaded data in a tabular format
+- **FR-007**: System SHALL show basic data statistics (rows, columns, data types)
+- **FR-008**: System SHALL allow users to preview first 100 rows
+- **FR-009**: System SHALL highlight missing values and data anomalies
+- **FR-010**: System SHALL provide data summary information
+
+### 1.3 Data Preprocessing Module
+- **FR-011**: System SHALL detect and handle missing values
+- **FR-012**: System SHALL identify and flag outliers
+- **FR-013**: System SHALL provide options for data cleaning strategies
+- **FR-014**: System SHALL allow users to apply preprocessing rules
+- **FR-015**: System SHALL maintain data integrity during preprocessing
+
+### 1.4 Visualization Module
+- **FR-016**: System SHALL generate automatic charts based on data types
+- **FR-017**: System SHALL support multiple chart types (bar, line, pie, scatter)
+- **FR-018**: System SHALL allow users to customize chart appearance
+- **FR-019**: System SHALL provide interactive chart features (zoom, pan, tooltips)
+- **FR-020**: System SHALL support real-time chart updates
+
+### 1.5 Dashboard Module
+- **FR-021**: System SHALL provide an interactive dashboard interface
+- **FR-022**: System SHALL allow users to arrange and customize dashboard widgets
+- **FR-023**: System SHALL support multiple dashboard layouts
+- **FR-024**: System SHALL provide real-time data filtering capabilities
+- **FR-025**: System SHALL maintain dashboard state between sessions
+
+### 1.6 Export Module
+- **FR-026**: System SHALL export analysis results as PDF files
+- **FR-027**: System SHALL export processed data as Excel files
+- **FR-028**: System SHALL include charts and visualizations in exports
+- **FR-029**: System SHALL provide export progress indication
+- **FR-030**: System SHALL support batch export functionality
+
+## 2. Non-Functional Requirements
+
+### 2.1 Performance Requirements
+- **NFR-001**: System SHALL load and process files up to 10,000 rows in under 10 seconds
+- **NFR-002**: System SHALL handle concurrent user sessions (up to 100 users)
+- **NFR-003**: Dashboard updates SHALL respond within 2 seconds
+- **NFR-004**: Chart rendering SHALL complete within 3 seconds
+
+### 2.2 Usability Requirements
+- **NFR-005**: System SHALL be usable by users with basic computer skills
+- **NFR-006**: Learning curve SHALL be under 15 minutes for basic functionality
+- **NFR-007**: System SHALL provide clear error messages and help documentation
+- **NFR-008**: Interface SHALL be responsive and work on various screen sizes
+
+### 2.3 Security Requirements
+- **NFR-009**: System SHALL not store uploaded files permanently
+- **NFR-010**: Data processing SHALL occur client-side only
+- **NFR-011**: System SHALL validate all user inputs
+- **NFR-012**: No sensitive data SHALL be transmitted to external servers
+
+### 2.4 Compatibility Requirements
+- **NFR-013**: System SHALL work in modern web browsers (Chrome, Firefox, Safari, Edge)
+- **NFR-014**: System SHALL support both Windows and macOS operating systems
+- **NFR-015**: System SHALL be accessible on mobile devices with limited functionality
+
+## 3. System Constraints
+
+### 3.1 Technical Constraints
+- **C-001**: Application MUST run entirely in the browser (no server required)
+- **C-002**: File processing MUST use client-side JavaScript libraries
+- **C-003**: Application MUST work offline after initial load
+- **C-004**: Total application size MUST be under 5MB
+
+### 3.2 Business Constraints
+- **C-005**: Development MUST use open-source technologies
+- **C-006**: Application MUST be free to use
+- **C-007**: No user registration or authentication required
+- **C-008**: Application MUST comply with data privacy regulations
+
+## 4. User Interface Requirements
+
+### 4.1 Layout Requirements
+- **UI-001**: Application SHALL have a clean, intuitive interface
+- **UI-002**: Main workspace SHALL be divided into logical sections
+- **UI-003**: Navigation SHALL be consistent across all pages
+- **UI-004**: Color scheme SHALL be professional and accessible
+
+### 4.2 Interaction Requirements
+- **UI-005**: All interactive elements SHALL provide visual feedback
+- **UI-006**: Keyboard navigation SHALL be supported
+- **UI-007**: Touch interactions SHALL work on mobile devices
+- **UI-008**: Loading states SHALL be clearly indicated
+
+## 5. Data Requirements
+
+### 5.1 Input Data
+- **D-001**: Excel files (.xlsx, .xls) with maximum 10,000 rows
+- **D-002**: Data types: text, numbers, dates, boolean values
+- **D-003**: Multiple worksheets support (primary focus on first sheet)
+- **D-004**: File size limit: 50MB maximum
+
+### 5.2 Output Data
+- **D-005**: Processed data in JSON format for internal use
+- **D-006**: PDF reports with charts and analysis summary
+- **D-007**: Excel files with cleaned and processed data
+- **D-008**: Chart configurations and dashboard layouts
+
+## 6. Quality Attributes
+
+### 6.1 Reliability
+- **QA-001**: System SHALL handle file parsing errors gracefully
+- **QA-002**: Data processing SHALL be consistent across browser sessions
+- **QA-003**: System SHALL recover from JavaScript errors without crashing
+- **QA-004**: Export functionality SHALL produce valid files
+
+### 6.2 Maintainability
+- **QA-005**: Code SHALL be well-documented and modular
+- **QA-006**: System SHALL use established JavaScript libraries
+- **QA-007**: Components SHALL be easily testable
+- **QA-008**: Code SHALL follow consistent naming conventions
+
+### 6.3 Scalability
+- **QA-009**: System SHALL handle increasing data complexity
+- **QA-010**: Visualization components SHALL be extensible
+- **QA-011**: New chart types SHALL be easily added
+- **QA-012**: Dashboard widgets SHALL be modular and reusable
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/css/styles.css b/apps/web-roo-code/src/components/excel-analyzer/src/css/styles.css
new file mode 100644
index 00000000000..f8c895ba449
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/css/styles.css
@@ -0,0 +1,737 @@
+/* Excel Analyzer - Main Styles of Web Application */
+
+/* CSS Custom Properties for Theming — Purple, Gold, Black & White Palette */
+:root {
+ --primary-color: #6a0dad; /* Deep Royal Purple */
+ --secondary-color: #FFD700; /* Gold */
+ --success-color: #4caf50;
+ --danger-color: #e53935;
+ --warning-color: #FFD700; /* Gold as warning accent */
+ --info-color: #ab47bc; /* Light Purple */
+ --light-color: #ffffff;
+ --dark-color: #0d0d0d; /* Near Black */
+
+ --bg-primary: #ffffff; /* White background */
+ --bg-secondary: #f5f0ff; /* Very light purple tint */
+ --bg-accent: #ede7f6; /* Soft lavender accent */
+
+ --text-primary: #0d0d0d; /* Near Black text */
+ --text-secondary: #4a148c; /* Dark Purple text */
+ --text-muted: #7b5ea7; /* Muted purple */
+
+ --border-color: #ce93d8; /* Light purple border */
+ --shadow: 0 0.125rem 0.25rem rgba(106, 13, 173, 0.15);
+ --shadow-lg: 0 0.5rem 1rem rgba(106, 13, 173, 0.25);
+
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --font-size-base: 1rem;
+ --line-height-base: 1.5;
+ --border-radius: 0.375rem;
+ --border-radius-lg: 0.5rem;
+ --border-radius-sm: 0.25rem;
+
+ /* Gold accent for highlights */
+ --gold-color: #FFD700;
+ --gold-dark: #c9a800;
+ --purple-dark: #4a148c;
+ --purple-mid: #6a0dad;
+ --purple-light: #ab47bc;
+}
+
+/* Dark Theme — Black, Purple & Gold */
+[data-theme="dark"] {
+ --primary-color: #ce93d8; /* Light Purple on dark */
+ --secondary-color: #FFD700; /* Gold */
+ --success-color: #66bb6a;
+ --danger-color: #ef5350;
+ --warning-color: #FFD700;
+ --info-color: #ba68c8;
+
+ --bg-primary: #0d0d0d; /* True Black */
+ --bg-secondary: #1a0a2e; /* Very Dark Purple */
+ --bg-accent: #2d1b4e; /* Dark Purple accent */
+
+ --text-primary: #ffffff; /* White text */
+ --text-secondary: #FFD700; /* Gold text */
+ --text-muted: #ce93d8; /* Light purple muted */
+
+ --border-color: #4a148c; /* Dark purple border */
+ --shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.6);
+ --shadow-lg: 0 0.5rem 1rem rgba(206, 147, 216, 0.2);
+}
+
+/* Reset and Base Styles */
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-base);
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ margin: 0;
+ padding: 0;
+}
+
+a {
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+a:hover {
+ color: var(--primary-color);
+ text-decoration: underline;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ line-height: 1.2;
+ color: var(--text-primary);
+}
+
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ color: var(--text-secondary);
+}
+
+/* Typography */
+.text-muted {
+ color: var(--text-muted) !important;
+}
+
+.text-primary {
+ color: var(--primary-color) !important;
+}
+
+.text-success {
+ color: var(--success-color) !important;
+}
+
+.text-danger {
+ color: var(--danger-color) !important;
+}
+
+.text-warning {
+ color: var(--warning-color) !important;
+}
+
+.text-info {
+ color: var(--info-color) !important;
+}
+
+/* Layout */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 15px;
+}
+
+/* Cards */
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: var(--bg-primary);
+ background-clip: border-box;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow);
+ transition: box-shadow 0.15s ease-in-out;
+}
+
+.card:hover {
+ box-shadow: var(--shadow-lg);
+}
+
+.card-header {
+ padding: 1rem 1.25rem;
+ margin-bottom: 0;
+ background-color: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-color);
+ border-top-left-radius: calc(var(--border-radius) - 1px);
+ border-top-right-radius: calc(var(--border-radius) - 1px);
+}
+
+.card-body {
+ flex: 1 1 auto;
+ padding: 1.25rem;
+}
+
+.card-footer {
+ padding: 1rem 1.25rem;
+ background-color: var(--bg-secondary);
+ border-top: 1px solid var(--border-color);
+ border-bottom-right-radius: calc(var(--border-radius) - 1px);
+ border-bottom-left-radius: calc(var(--border-radius) - 1px);
+}
+
+/* Buttons */
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--text-primary);
+ text-align: center;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ border-radius: var(--border-radius);
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.btn-primary:hover {
+ color: #fff;
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+ filter: brightness(1.1);
+}
+
+.btn-outline-primary {
+ color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.btn-outline-primary:hover {
+ color: #fff;
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.btn-success {
+ color: #fff;
+ background-color: var(--success-color);
+ border-color: var(--success-color);
+}
+
+.btn-danger {
+ color: #fff;
+ background-color: var(--danger-color);
+ border-color: var(--danger-color);
+}
+
+.btn-warning {
+ color: #212529;
+ background-color: var(--warning-color);
+ border-color: var(--warning-color);
+}
+
+.btn-info {
+ color: #fff;
+ background-color: var(--info-color);
+ border-color: var(--info-color);
+}
+
+.btn-group .btn {
+ position: relative;
+ flex: 1 1 auto;
+ margin-bottom: 0;
+}
+
+/* Forms */
+.form-control {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ background-clip: padding-box;
+ border: 1px solid var(--border-color);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: var(--border-radius);
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.form-control:focus {
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ border-color: var(--primary-color);
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(var(--primary-color), 0.25);
+}
+
+.form-select {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 2.25rem 0.375rem 0.75rem;
+ -moz-padding-start: calc(0.75rem - 3px);
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 16px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+.form-select:focus {
+ border-color: var(--primary-color);
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(var(--primary-color), 0.25);
+}
+
+/* Progress Bars */
+.progress {
+ display: flex;
+ height: 1rem;
+ overflow: hidden;
+ font-size: 0.75rem;
+ background-color: var(--bg-accent);
+ border-radius: var(--border-radius);
+}
+
+.progress-bar {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ background-color: var(--primary-color);
+ transition: width 0.6s ease;
+}
+
+/* Tables */
+.table {
+ --bs-table-bg: transparent;
+ --bs-table-accent-bg: var(--bg-secondary);
+ --bs-table-striped-color: var(--text-primary);
+ --bs-table-striped-bg: var(--bg-accent);
+ --bs-table-active-color: var(--text-primary);
+ --bs-table-active-bg: var(--bg-accent);
+ --bs-table-hover-color: var(--text-primary);
+ --bs-table-hover-bg: var(--bg-accent);
+ width: 100%;
+ margin-bottom: 1rem;
+ color: var(--text-primary);
+ vertical-align: top;
+ border-color: var(--border-color);
+}
+
+.table > :not(caption) > * > * {
+ padding: 0.5rem 0.5rem;
+ background-color: var(--bs-table-bg);
+ border-bottom-width: 1px;
+ box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);
+}
+
+.table-striped > tbody > tr:nth-of-type(odd) > * {
+ --bs-table-accent-bg: var(--bs-table-striped-bg);
+ color: var(--bs-table-striped-color);
+}
+
+.table-hover > tbody > tr:hover > * {
+ --bs-table-accent-bg: var(--bs-table-hover-bg);
+ color: var(--bs-table-hover-color);
+}
+
+/* Navigation — Purple & Gold */
+.navbar {
+ background: linear-gradient(135deg, #4a148c 0%, #6a0dad 60%, #0d0d0d 100%) !important;
+ box-shadow: 0 2px 12px rgba(106, 13, 173, 0.4);
+ border-bottom: 3px solid #FFD700;
+}
+
+.navbar-brand {
+ font-weight: 700;
+ font-size: 1.35rem;
+ color: #FFD700 !important;
+ letter-spacing: 0.5px;
+ text-shadow: 0 1px 4px rgba(0,0,0,0.4);
+}
+
+.navbar-brand:hover {
+ color: #fff !important;
+}
+
+.nav-link {
+ color: rgba(255, 255, 255, 0.85) !important;
+ font-weight: 500;
+ padding: 0.5rem 1rem;
+ border-radius: var(--border-radius);
+ transition: all 0.2s ease-in-out;
+ border-bottom: 2px solid transparent;
+}
+
+.nav-link:hover {
+ color: #FFD700 !important;
+ background-color: rgba(255, 215, 0, 0.1);
+ border-bottom-color: #FFD700;
+}
+
+.nav-link.active {
+ color: #FFD700 !important;
+ background-color: rgba(255, 215, 0, 0.15);
+ border-bottom-color: #FFD700;
+ font-weight: 600;
+}
+
+/* Card Headers — Purple accent */
+.card-header {
+ background: linear-gradient(90deg, var(--purple-dark) 0%, var(--purple-mid) 100%);
+ color: #ffffff;
+ border-bottom: 2px solid var(--gold-color);
+}
+
+.card-header h2,
+.card-header h3,
+.card-header h4,
+.card-header h5,
+.card-header h6 {
+ color: #ffffff;
+}
+
+.card-header .btn-outline-primary {
+ color: #FFD700;
+ border-color: #FFD700;
+}
+
+.card-header .btn-outline-primary:hover {
+ background-color: #FFD700;
+ color: #0d0d0d;
+}
+
+.card-header .btn-outline-secondary {
+ color: #ffffff;
+ border-color: rgba(255,255,255,0.5);
+}
+
+.card-header .btn-outline-secondary:hover {
+ background-color: rgba(255,255,255,0.15);
+ color: #FFD700;
+ border-color: #FFD700;
+}
+
+/* Drop Zone — Purple & Gold */
+.drop-zone {
+ border: 2px dashed var(--purple-light) !important;
+ background-color: var(--bg-secondary);
+ transition: all 0.3s ease;
+}
+
+.drop-zone:hover,
+.drop-zone.drag-over {
+ border-color: var(--gold-color) !important;
+ background-color: rgba(255, 215, 0, 0.05);
+ box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
+}
+
+/* Primary Button — Purple with Gold hover */
+.btn-primary {
+ background: linear-gradient(135deg, #6a0dad, #4a148c);
+ border-color: #6a0dad;
+ color: #ffffff;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+}
+
+.btn-primary:hover,
+.btn-primary:focus {
+ background: linear-gradient(135deg, #FFD700, #c9a800);
+ border-color: #FFD700;
+ color: #0d0d0d;
+ box-shadow: 0 4px 12px rgba(255, 215, 0, 0.4);
+}
+
+/* Footer — Black with Gold & Purple accents */
+footer {
+ background: linear-gradient(135deg, #0d0d0d 0%, #1a0a2e 100%) !important;
+ border-top: 3px solid #FFD700;
+ color: #ffffff !important;
+}
+
+footer h5 {
+ color: #FFD700;
+}
+
+footer p {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+footer .text-muted {
+ color: var(--purple-light) !important;
+}
+
+/* Stats Cards — Purple tinted */
+.card.bg-light {
+ background-color: var(--bg-secondary) !important;
+ border: 1px solid var(--border-color);
+}
+
+.card.bg-light .card-title {
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Progress Bar — Gold fill */
+.progress-bar {
+ background: linear-gradient(90deg, #6a0dad, #FFD700);
+}
+
+/* Table Header — Purple */
+.table thead th {
+ background-color: var(--purple-dark);
+ color: #FFD700;
+ border-color: var(--purple-mid);
+ font-weight: 600;
+ letter-spacing: 0.3px;
+}
+
+/* Scrollbar — Purple & Gold */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--purple-mid);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--gold-color);
+}
+
+/* Selection highlight */
+::selection {
+ background-color: rgba(106, 13, 173, 0.25);
+ color: var(--text-primary);
+}
+
+/* Icons */
+.bi {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ vertical-align: -.125em;
+ fill: currentColor;
+}
+
+/* Utility Classes */
+.text-center {
+ text-align: center !important;
+}
+
+.text-start {
+ text-align: left !important;
+}
+
+.text-end {
+ text-align: right !important;
+}
+
+.d-flex {
+ display: flex !important;
+}
+
+.flex-column {
+ flex-direction: column !important;
+}
+
+.flex-row {
+ flex-direction: row !important;
+}
+
+.justify-content-center {
+ justify-content: center !important;
+}
+
+.justify-content-between {
+ justify-content: space-between !important;
+}
+
+.align-items-center {
+ align-items: center !important;
+}
+
+.mt-3 {
+ margin-top: 1rem !important;
+}
+
+.mb-3 {
+ margin-bottom: 1rem !important;
+}
+
+.mt-4 {
+ margin-top: 1.5rem !important;
+}
+
+.mb-4 {
+ margin-bottom: 1.5rem !important;
+}
+
+.p-4 {
+ padding: 1.5rem !important;
+}
+
+.border {
+ border: 1px solid var(--border-color) !important;
+}
+
+.border-dashed {
+ border-style: dashed !important;
+}
+
+.rounded {
+ border-radius: var(--border-radius) !important;
+}
+
+.shadow {
+ box-shadow: var(--shadow) !important;
+}
+
+/* Loading States */
+.loading {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: -0.125em;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ -webkit-animation: spinner-border .75s linear infinite;
+ animation: spinner-border .75s linear infinite;
+}
+
+@keyframes spinner-border {
+ to { transform: rotate(360deg); }
+}
+
+/* Status Indicators */
+.status-badge {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border-radius: var(--border-radius-sm);
+ background-color: var(--bg-accent);
+ color: var(--text-secondary);
+}
+
+.status-success {
+ background-color: rgba(40, 167, 69, 0.1);
+ color: var(--success-color);
+ border: 1px solid rgba(40, 167, 69, 0.3);
+}
+
+.status-warning {
+ background-color: rgba(255, 193, 7, 0.1);
+ color: var(--warning-color);
+ border: 1px solid rgba(255, 193, 7, 0.3);
+}
+
+.status-danger {
+ background-color: rgba(220, 53, 69, 0.1);
+ color: var(--danger-color);
+ border: 1px solid rgba(220, 53, 69, 0.3);
+}
+
+/* Animations */
+.fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.slide-up {
+ animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Print Styles */
+@media print {
+ body {
+ background-color: #fff;
+ color: #000;
+ }
+
+ .navbar,
+ .btn,
+ .progress,
+ .card-footer {
+ display: none !important;
+ }
+
+ .card {
+ box-shadow: none;
+ border: 1px solid #000;
+ }
+
+ .table {
+ border-collapse: collapse;
+ }
+
+ .table th,
+ .table td {
+ border: 1px solid #000;
+ padding: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/hooks/hook-engine.js b/apps/web-roo-code/src/components/excel-analyzer/src/hooks/hook-engine.js
new file mode 100644
index 00000000000..b0922e811f2
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/hooks/hook-engine.js
@@ -0,0 +1,156 @@
+/**
+ * Hook Engine
+ *
+ * Lightweight hook system for registering and executing synchronous/asynchronous hooks
+ * with priority and one-time execution support.
+ */
+
+class HookEngine {
+ constructor() {
+ this.hooks = new Map();
+ this.sequence = 0;
+ }
+
+ /**
+ * Register a hook handler.
+ * @param {string} hookName
+ * @param {Function} handler
+ * @param {{ priority?: number, once?: boolean }} options
+ * @returns {string} Hook id
+ */
+ tap(hookName, handler, options = {}) {
+ if (!hookName || typeof hookName !== 'string') {
+ throw new Error('hookName must be a non-empty string');
+ }
+
+ if (typeof handler !== 'function') {
+ throw new Error('handler must be a function');
+ }
+
+ const priority = Number.isFinite(options.priority) ? options.priority : 0;
+ const once = Boolean(options.once);
+ const id = `${hookName}:${Date.now()}:${this.sequence++}`;
+
+ const record = {
+ id,
+ hookName,
+ handler,
+ priority,
+ once,
+ order: this.sequence
+ };
+
+ if (!this.hooks.has(hookName)) {
+ this.hooks.set(hookName, []);
+ }
+
+ const items = this.hooks.get(hookName);
+ items.push(record);
+ items.sort((a, b) => {
+ if (b.priority !== a.priority) return b.priority - a.priority;
+ return a.order - b.order;
+ });
+
+ return id;
+ }
+
+ /**
+ * Register a one-time hook handler.
+ */
+ tapOnce(hookName, handler, options = {}) {
+ return this.tap(hookName, handler, { ...options, once: true });
+ }
+
+ /**
+ * Remove a specific hook by id.
+ */
+ untap(hookName, hookId) {
+ const items = this.hooks.get(hookName);
+ if (!items || !items.length) return false;
+
+ const next = items.filter((entry) => entry.id !== hookId);
+ this.hooks.set(hookName, next);
+ return next.length !== items.length;
+ }
+
+ /**
+ * Remove all handlers for a hook, or all hooks when no name provided.
+ */
+ clear(hookName = null) {
+ if (!hookName) {
+ this.hooks.clear();
+ return;
+ }
+ this.hooks.delete(hookName);
+ }
+
+ /**
+ * Check if a hook has handlers.
+ */
+ has(hookName) {
+ const items = this.hooks.get(hookName);
+ return Boolean(items && items.length);
+ }
+
+ /**
+ * List registered handlers for a hook.
+ */
+ list(hookName) {
+ return [...(this.hooks.get(hookName) || [])];
+ }
+
+ /**
+ * Execute all handlers for a hook with a shared context object.
+ * Returns collected results and errors.
+ */
+ async run(hookName, context = {}) {
+ const handlers = [...(this.hooks.get(hookName) || [])];
+ const results = [];
+ const errors = [];
+
+ for (const entry of handlers) {
+ try {
+ const value = await entry.handler(context);
+ results.push({ id: entry.id, value });
+ } catch (error) {
+ errors.push({ id: entry.id, error });
+ } finally {
+ if (entry.once) {
+ this.untap(hookName, entry.id);
+ }
+ }
+ }
+
+ return { results, errors };
+ }
+
+ /**
+ * Execute hook handlers in waterfall mode.
+ * Each handler can transform the payload and pass to the next handler.
+ */
+ async runWaterfall(hookName, payload, context = {}) {
+ const handlers = [...(this.hooks.get(hookName) || [])];
+ let current = payload;
+
+ for (const entry of handlers) {
+ try {
+ const next = await entry.handler(current, context);
+ if (typeof next !== 'undefined') {
+ current = next;
+ }
+ } finally {
+ if (entry.once) {
+ this.untap(hookName, entry.id);
+ }
+ }
+ }
+
+ return current;
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = HookEngine;
+} else {
+ window.HookEngine = HookEngine;
+}
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/index.html b/apps/web-roo-code/src/components/excel-analyzer/src/index.html
new file mode 100644
index 00000000000..dbd9582e659
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/index.html
@@ -0,0 +1,338 @@
+
+
+
+
+
+ Excel Analyzer - Web-based Data Analysis Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Drag and drop your Excel file here
+
or
+
+
+
+
+
+
+
+
+ File:
+
+
+ Size:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total Columns
+ 0
+
+
+
+
+
+
+
Missing Values
+ 0
+
+
+
+
+
+
+
Data Quality
+ 100%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
PDF Report
+
Export analysis with charts
+
+
+
+
+
+
+
+
+
Excel File
+
Export processed data
+
+
+
+
+
+
+
+
+
Chart Images
+
Export individual charts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/config/defaults.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/defaults.js
new file mode 100644
index 00000000000..64261ccea5f
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/defaults.js
@@ -0,0 +1,506 @@
+/**
+ * Default Values of Web Application
+ *
+ * Default configuration values and presets for the Excel Analyzer application.
+ */
+
+class DefaultValues {
+ constructor() {
+ this.defaults = {
+ // Application Defaults
+ app: {
+ name: 'Excel Analyzer',
+ version: '1.0.0',
+ theme: 'light',
+ language: 'en',
+ maxFileSize: 50 * 1024 * 1024, // 50MB
+ maxRows: 10000,
+ maxColumns: 100
+ },
+
+ // File Upload Defaults
+ fileUpload: {
+ allowedTypes: [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel'
+ ],
+ maxFileSize: 50 * 1024 * 1024,
+ enableDragDrop: true,
+ enableMultiple: false,
+ previewRows: 100
+ },
+
+ // Data Processing Defaults
+ dataProcessing: {
+ defaultMissingValueHandling: 'remove',
+ defaultOutlierHandling: 'remove',
+ outlierThreshold: 3,
+ autoDetectDataTypes: true,
+ enableDataValidation: true,
+ maxPreviewRows: 100,
+ enableCaching: true,
+ cacheTimeout: 300000 // 5 minutes
+ },
+
+ // Visualization Defaults
+ visualization: {
+ defaultChartType: 'auto',
+ chartColors: [
+ '#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8',
+ '#6f42c1', '#20c997', '#fd7e14', '#e83e8c', '#6c757d'
+ ],
+ chartAnimation: true,
+ chartResponsive: true,
+ chartLegendPosition: 'top',
+ chartTooltip: true,
+ chartHeight: 400,
+ chartWidth: 600,
+ enableZoom: true,
+ enablePan: true
+ },
+
+ // Dashboard Defaults
+ dashboard: {
+ defaultLayout: 'grid',
+ maxWidgets: 10,
+ enableWidgetResize: true,
+ enableWidgetDrag: true,
+ autoSaveDashboard: true,
+ defaultWidgetSize: {
+ width: 4,
+ height: 3
+ },
+ gridColumns: 12,
+ gridRows: 12,
+ widgetMargin: 10
+ },
+
+ // Export Defaults
+ export: {
+ defaultFormat: 'pdf',
+ includeChartsInExport: true,
+ includeDataInExport: true,
+ includeSummaryInExport: true,
+ exportImageQuality: 'high',
+ pdfPageSize: 'A4',
+ pdfOrientation: 'portrait',
+ excelSheetName: 'Processed Data',
+ csvDelimiter: ',',
+ jsonIndent: 2
+ },
+
+ // UI Defaults
+ ui: {
+ theme: 'light',
+ language: 'en',
+ enableTooltips: true,
+ enableAnimations: true,
+ fontSize: 'medium',
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial',
+ borderRadius: '0.375rem',
+ boxShadow: '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ transitionDuration: '0.15s'
+ },
+
+ // Performance Defaults
+ performance: {
+ enableVirtualization: true,
+ maxPreviewRows: 100,
+ enableCaching: true,
+ cacheTimeout: 300000, // 5 minutes
+ debounceDelay: 300,
+ throttleDelay: 1000,
+ maxConcurrentOperations: 3
+ },
+
+ // Privacy Defaults
+ privacy: {
+ enableAnalytics: false,
+ saveUserPreferences: true,
+ clearDataOnExit: false,
+ storeFileData: false,
+ storeProcessingHistory: false
+ }
+ };
+ }
+
+ /**
+ * Get default value by path
+ */
+ get(path, defaultValue = null) {
+ const keys = path.split('.');
+ let current = this.defaults;
+
+ for (const key of keys) {
+ if (current && typeof current === 'object' && key in current) {
+ current = current[key];
+ } else {
+ return defaultValue;
+ }
+ }
+
+ return current;
+ }
+
+ /**
+ * Get all defaults
+ */
+ getAll() {
+ return { ...this.defaults };
+ }
+
+ /**
+ * Get application defaults
+ */
+ getAppDefaults() {
+ return { ...this.defaults.app };
+ }
+
+ /**
+ * Get file upload defaults
+ */
+ getFileUploadDefaults() {
+ return { ...this.defaults.fileUpload };
+ }
+
+ /**
+ * Get data processing defaults
+ */
+ getDataProcessingDefaults() {
+ return { ...this.defaults.dataProcessing };
+ }
+
+ /**
+ * Get visualization defaults
+ */
+ getVisualizationDefaults() {
+ return { ...this.defaults.visualization };
+ }
+
+ /**
+ * Get dashboard defaults
+ */
+ getDashboardDefaults() {
+ return { ...this.defaults.dashboard };
+ }
+
+ /**
+ * Get export defaults
+ */
+ getExportDefaults() {
+ return { ...this.defaults.export };
+ }
+
+ /**
+ * Get UI defaults
+ */
+ getUIDefaults() {
+ return { ...this.defaults.ui };
+ }
+
+ /**
+ * Get performance defaults
+ */
+ getPerformanceDefaults() {
+ return { ...this.defaults.performance };
+ }
+
+ /**
+ * Get privacy defaults
+ */
+ getPrivacyDefaults() {
+ return { ...this.defaults.privacy };
+ }
+
+ /**
+ * Get chart color presets
+ */
+ getChartColorPresets() {
+ return {
+ primary: ['#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8'],
+ pastel: ['#a8d8f0', '#b8e6b8', '#ffd1d1', '#fff7b3', '#b3e6ff'],
+ vibrant: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'],
+ monochrome: ['#333333', '#666666', '#999999', '#cccccc', '#ffffff'],
+ autumn: ['#d35400', '#e67e22', '#f1c40f', '#f39c12', '#e74c3c']
+ };
+ }
+
+ /**
+ * Get chart type presets
+ */
+ getChartTypePresets() {
+ return {
+ auto: 'Automatic selection based on data',
+ bar: 'Bar chart for categorical data',
+ line: 'Line chart for time series data',
+ pie: 'Pie chart for proportional data',
+ scatter: 'Scatter plot for correlation analysis',
+ doughnut: 'Doughnut chart for proportional data',
+ area: 'Area chart for cumulative data',
+ radar: 'Radar chart for multi-dimensional data'
+ };
+ }
+
+ /**
+ * Get dashboard layout presets
+ */
+ getDashboardLayoutPresets() {
+ return {
+ grid: {
+ name: 'Grid Layout',
+ description: 'Responsive grid-based layout',
+ columns: 12,
+ rows: 12,
+ margin: 10,
+ autoResize: true
+ },
+ vertical: {
+ name: 'Vertical Layout',
+ description: 'Stacked widgets vertically',
+ columns: 1,
+ rows: 20,
+ margin: 15,
+ autoResize: false
+ },
+ horizontal: {
+ name: 'Horizontal Layout',
+ description: 'Side-by-side widgets',
+ columns: 20,
+ rows: 6,
+ margin: 10,
+ autoResize: false
+ }
+ };
+ }
+
+ /**
+ * Get export format presets
+ */
+ getExportFormatPresets() {
+ return {
+ pdf: {
+ name: 'PDF Report',
+ description: 'Professional PDF with charts and analysis',
+ extension: '.pdf',
+ includeCharts: true,
+ includeData: true,
+ includeSummary: true
+ },
+ excel: {
+ name: 'Excel File',
+ description: 'Processed data in Excel format',
+ extension: '.xlsx',
+ includeCharts: false,
+ includeData: true,
+ includeSummary: false
+ },
+ csv: {
+ name: 'CSV File',
+ description: 'Comma-separated values',
+ extension: '.csv',
+ includeCharts: false,
+ includeData: true,
+ includeSummary: false
+ },
+ json: {
+ name: 'JSON File',
+ description: 'Structured data in JSON format',
+ extension: '.json',
+ includeCharts: false,
+ includeData: true,
+ includeSummary: true
+ }
+ };
+ }
+
+ /**
+ * Get theme presets
+ */
+ getThemePresets() {
+ return {
+ light: {
+ name: 'Light Theme',
+ description: 'Clean and bright interface',
+ colors: {
+ primary: '#007bff',
+ secondary: '#6c757d',
+ success: '#28a745',
+ danger: '#dc3545',
+ warning: '#ffc107',
+ info: '#17a2b8',
+ light: '#f8f9fa',
+ dark: '#343a40'
+ },
+ background: '#ffffff',
+ text: '#212529'
+ },
+ dark: {
+ name: 'Dark Theme',
+ description: 'Dark interface for reduced eye strain',
+ colors: {
+ primary: '#4dabf7',
+ secondary: '#66d9ef',
+ success: '#51cf66',
+ danger: '#ff6b6b',
+ warning: '#ffd43b',
+ info: '#22d3ee',
+ light: '#121212',
+ dark: '#ffffff'
+ },
+ background: '#121212',
+ text: '#ffffff'
+ },
+ blue: {
+ name: 'Blue Theme',
+ description: 'Professional blue color scheme',
+ colors: {
+ primary: '#1976d2',
+ secondary: '#424242',
+ success: '#2e7d32',
+ danger: '#c62828',
+ warning: '#ef6c00',
+ info: '#0277bd',
+ light: '#e3f2fd',
+ dark: '#212121'
+ },
+ background: '#ffffff',
+ text: '#212121'
+ }
+ };
+ }
+
+ /**
+ * Get data type detection rules
+ */
+ getDataTypeDetectionRules() {
+ return {
+ date: {
+ patterns: [
+ /^\d{4}-\d{2}-\d{2}$/,
+ /^\d{2}\/\d{2}\/\d{4}$/,
+ /^\d{2}-\d{2}-\d{4}$/,
+ /^\d{4}\/\d{2}\/\d{2}$/
+ ],
+ validators: [
+ (value) => !isNaN(Date.parse(value))
+ ]
+ },
+ time: {
+ patterns: [
+ /^\d{2}:\d{2}:\d{2}$/,
+ /^\d{2}:\d{2}$/
+ ],
+ validators: [
+ (value) => {
+ const time = value.split(':');
+ return time.length >= 2 &&
+ parseInt(time[0]) >= 0 && parseInt(time[0]) <= 23 &&
+ parseInt(time[1]) >= 0 && parseInt(time[1]) <= 59;
+ }
+ ]
+ },
+ numeric: {
+ patterns: [
+ /^-?\d+\.?\d*$/
+ ],
+ validators: [
+ (value) => !isNaN(parseFloat(value)) && isFinite(value)
+ ]
+ },
+ boolean: {
+ patterns: [
+ /^(true|false|yes|no|1|0)$/i
+ ],
+ validators: [
+ (value) => ['true', 'false', 'yes', 'no', '1', '0'].includes(value.toLowerCase())
+ ]
+ }
+ };
+ }
+
+ /**
+ * Get processing strategies
+ */
+ getProcessingStrategies() {
+ return {
+ missingValues: {
+ remove: 'Remove rows with missing values',
+ mean: 'Fill with mean (numeric only)',
+ median: 'Fill with median (numeric only)',
+ mode: 'Fill with most frequent value',
+ forwardFill: 'Forward fill from previous value',
+ backwardFill: 'Backward fill from next value',
+ custom: 'Use custom value'
+ },
+ outliers: {
+ remove: 'Remove outlier values',
+ cap: 'Cap to threshold values',
+ mean: 'Replace with mean',
+ median: 'Replace with median',
+ ignore: 'Keep outliers as-is'
+ }
+ };
+ }
+
+ /**
+ * Get validation rules
+ */
+ getValidationRules() {
+ return {
+ file: {
+ maxSize: 50 * 1024 * 1024, // 50MB
+ allowedTypes: [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel'
+ ],
+ maxRows: 10000,
+ maxColumns: 100
+ },
+ data: {
+ maxPreviewRows: 100,
+ maxCells: 1000000,
+ maxStringLength: 10000
+ },
+ charts: {
+ maxDataPoints: 1000,
+ maxSeries: 10,
+ maxLabels: 50
+ },
+ dashboard: {
+ maxWidgets: 20,
+ minWidgetSize: { width: 2, height: 2 },
+ maxWidgetSize: { width: 12, height: 12 }
+ }
+ };
+ }
+
+ /**
+ * Get performance thresholds
+ */
+ getPerformanceThresholds() {
+ return {
+ fileProcessing: {
+ small: 1000, // rows
+ medium: 10000, // rows
+ large: 50000 // rows
+ },
+ chartRendering: {
+ fast: 100, // data points
+ medium: 1000, // data points
+ slow: 10000 // data points
+ },
+ memoryUsage: {
+ low: 50 * 1024 * 1024, // 50MB
+ medium: 100 * 1024 * 1024, // 100MB
+ high: 500 * 1024 * 1024 // 500MB
+ }
+ };
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = DefaultValues;
+} else {
+ window.DefaultValues = DefaultValues;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/config/settings.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/settings.js
new file mode 100644
index 00000000000..9c82515b648
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/settings.js
@@ -0,0 +1,124 @@
+/**
+ * Settings Manager of Web Application
+ *
+ * Lightweight settings storage for the Excel Analyzer app.
+ */
+
+class SettingsManager {
+ constructor() {
+ this.storageKey = 'excel-analyzer-settings';
+ this.defaultSettings = {
+ maxFileSize: 50 * 1024 * 1024,
+ maxRows: 10000,
+ previewRows: 100,
+ theme: 'light',
+ ui: {
+ theme: 'light'
+ },
+ dashboard: {
+ layout: 'layout1'
+ },
+ export: {
+ includeCharts: true,
+ includeData: true,
+ includeSummary: true
+ }
+ };
+
+ this.settings = this.load();
+ }
+
+ load() {
+ try {
+ const raw = localStorage.getItem(this.storageKey);
+ if (!raw) return this.clone(this.defaultSettings);
+ const parsed = JSON.parse(raw);
+ return this.merge(this.defaultSettings, parsed);
+ } catch (error) {
+ console.warn('Failed to load settings, using defaults.', error);
+ return this.clone(this.defaultSettings);
+ }
+ }
+
+ save() {
+ try {
+ localStorage.setItem(this.storageKey, JSON.stringify(this.settings));
+ } catch (error) {
+ console.warn('Failed to save settings.', error);
+ }
+ }
+
+ get(path, fallback = null) {
+ if (!path) return this.settings;
+ const keys = path.split('.');
+ let current = this.settings;
+
+ for (const key of keys) {
+ if (current && typeof current === 'object' && key in current) {
+ current = current[key];
+ } else {
+ return fallback;
+ }
+ }
+
+ return current;
+ }
+
+ set(path, value) {
+ const keys = path.split('.');
+ let current = this.settings;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key] || typeof current[key] !== 'object') {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+
+ current[keys[keys.length - 1]] = value;
+ this.save();
+ }
+
+ update(values = {}) {
+ this.settings = this.merge(this.settings, values);
+ this.save();
+ }
+
+ reset() {
+ this.settings = this.clone(this.defaultSettings);
+ this.save();
+ }
+
+ clone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+ }
+
+ merge(base, extra) {
+ const merged = this.clone(base);
+ Object.keys(extra || {}).forEach((key) => {
+ const baseValue = merged[key];
+ const extraValue = extra[key];
+
+ if (
+ baseValue &&
+ typeof baseValue === 'object' &&
+ !Array.isArray(baseValue) &&
+ extraValue &&
+ typeof extraValue === 'object' &&
+ !Array.isArray(extraValue)
+ ) {
+ merged[key] = this.merge(baseValue, extraValue);
+ } else {
+ merged[key] = extraValue;
+ }
+ });
+ return merged;
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SettingsManager;
+} else {
+ window.SettingsManager = SettingsManager;
+}
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/config/themes.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/themes.js
new file mode 100644
index 00000000000..1cc4199bf42
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/config/themes.js
@@ -0,0 +1,567 @@
+/**
+ * Theme Manager of Web Application
+ *
+ * Manages theme switching and CSS custom properties for the Excel Analyzer application.
+ */
+
+class ThemeManager {
+ constructor() {
+ this.themes = {
+ royal: {
+ name: '👑 Royal',
+ description: 'Purple, Gold, Black & White — elegant royal palette',
+ cssVariables: {
+ '--primary-color': '#6a0dad',
+ '--secondary-color': '#FFD700',
+ '--success-color': '#4caf50',
+ '--danger-color': '#e53935',
+ '--warning-color': '#FFD700',
+ '--info-color': '#ab47bc',
+ '--light-color': '#ffffff',
+ '--dark-color': '#0d0d0d',
+
+ '--bg-primary': '#ffffff',
+ '--bg-secondary': '#f5f0ff',
+ '--bg-accent': '#ede7f6',
+
+ '--text-primary': '#0d0d0d',
+ '--text-secondary': '#4a148c',
+ '--text-muted': '#7b5ea7',
+
+ '--border-color': '#ce93d8',
+ '--shadow': '0 0.125rem 0.25rem rgba(106, 13, 173, 0.15)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(106, 13, 173, 0.25)',
+
+ '--gold-color': '#FFD700',
+ '--gold-dark': '#c9a800',
+ '--purple-dark': '#4a148c',
+ '--purple-mid': '#6a0dad',
+ '--purple-light': '#ab47bc',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ 'royal-dark': {
+ name: '🌑 Royal Dark',
+ description: 'Black, Deep Purple & Gold — dark royal palette',
+ cssVariables: {
+ '--primary-color': '#ce93d8',
+ '--secondary-color': '#FFD700',
+ '--success-color': '#66bb6a',
+ '--danger-color': '#ef5350',
+ '--warning-color': '#FFD700',
+ '--info-color': '#ba68c8',
+ '--light-color': '#0d0d0d',
+ '--dark-color': '#ffffff',
+
+ '--bg-primary': '#0d0d0d',
+ '--bg-secondary': '#1a0a2e',
+ '--bg-accent': '#2d1b4e',
+
+ '--text-primary': '#ffffff',
+ '--text-secondary': '#FFD700',
+ '--text-muted': '#ce93d8',
+
+ '--border-color': '#4a148c',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.6)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(206, 147, 216, 0.2)',
+
+ '--gold-color': '#FFD700',
+ '--gold-dark': '#c9a800',
+ '--purple-dark': '#4a148c',
+ '--purple-mid': '#6a0dad',
+ '--purple-light': '#ce93d8',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ light: {
+ name: 'Light',
+ description: 'Clean and bright interface',
+ cssVariables: {
+ '--primary-color': '#007bff',
+ '--secondary-color': '#6c757d',
+ '--success-color': '#28a745',
+ '--danger-color': '#dc3545',
+ '--warning-color': '#ffc107',
+ '--info-color': '#17a2b8',
+ '--light-color': '#f8f9fa',
+ '--dark-color': '#343a40',
+
+ '--bg-primary': '#ffffff',
+ '--bg-secondary': '#f8f9fa',
+ '--bg-accent': '#e9ecef',
+
+ '--text-primary': '#212529',
+ '--text-secondary': '#6c757d',
+ '--text-muted': '#6c757d',
+
+ '--border-color': '#dee2e6',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
+
+ '--gold-color': '#ffc107',
+ '--gold-dark': '#e0a800',
+ '--purple-dark': '#6f42c1',
+ '--purple-mid': '#7b1fa2',
+ '--purple-light': '#ab47bc',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ dark: {
+ name: 'Dark',
+ description: 'Dark interface for reduced eye strain',
+ cssVariables: {
+ '--primary-color': '#4dabf7',
+ '--secondary-color': '#66d9ef',
+ '--success-color': '#51cf66',
+ '--danger-color': '#ff6b6b',
+ '--warning-color': '#ffd43b',
+ '--info-color': '#22d3ee',
+ '--light-color': '#121212',
+ '--dark-color': '#ffffff',
+
+ '--bg-primary': '#121212',
+ '--bg-secondary': '#1e1e1e',
+ '--bg-accent': '#2d2d2d',
+
+ '--text-primary': '#ffffff',
+ '--text-secondary': '#b0b0b0',
+ '--text-muted': '#8e8e8e',
+
+ '--border-color': '#3a3a3a',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.5)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.3)',
+
+ '--gold-color': '#ffd43b',
+ '--gold-dark': '#e0a800',
+ '--purple-dark': '#6f42c1',
+ '--purple-mid': '#7b1fa2',
+ '--purple-light': '#ab47bc',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ blue: {
+ name: 'Blue',
+ description: 'Professional blue color scheme',
+ cssVariables: {
+ '--primary-color': '#1976d2',
+ '--secondary-color': '#424242',
+ '--success-color': '#2e7d32',
+ '--danger-color': '#c62828',
+ '--warning-color': '#ef6c00',
+ '--info-color': '#0277bd',
+ '--light-color': '#e3f2fd',
+ '--dark-color': '#212121',
+
+ '--bg-primary': '#ffffff',
+ '--bg-secondary': '#f5f5f5',
+ '--bg-accent': '#e8eaf6',
+
+ '--text-primary': '#212121',
+ '--text-secondary': '#424242',
+ '--text-muted': '#757575',
+
+ '--border-color': '#e0e0e0',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ green: {
+ name: 'Green',
+ description: 'Fresh and modern green theme',
+ cssVariables: {
+ '--primary-color': '#2e7d32',
+ '--secondary-color': '#5d4037',
+ '--success-color': '#4caf50',
+ '--danger-color': '#d32f2f',
+ '--warning-color': '#f57c00',
+ '--info-color': '#00838f',
+ '--light-color': '#e8f5e9',
+ '--dark-color': '#1b5e20',
+
+ '--bg-primary': '#ffffff',
+ '--bg-secondary': '#f1f8e9',
+ '--bg-accent': '#e8f5e9',
+
+ '--text-primary': '#1b5e20',
+ '--text-secondary': '#4caf50',
+ '--text-muted': '#757575',
+
+ '--border-color': '#c8e6c9',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ },
+ purple: {
+ name: 'Purple',
+ description: 'Elegant purple color scheme',
+ cssVariables: {
+ '--primary-color': '#7b1fa2',
+ '--secondary-color': '#4527a0',
+ '--success-color': '#6a1b9a',
+ '--danger-color': '#d81b60',
+ '--warning-color': '#ef6c00',
+ '--info-color': '#00acc1',
+ '--light-color': '#f3e5f5',
+ '--dark-color': '#4a148c',
+
+ '--bg-primary': '#ffffff',
+ '--bg-secondary': '#f3e5f5',
+ '--bg-accent': '#e1bee7',
+
+ '--text-primary': '#4a148c',
+ '--text-secondary': '#7b1fa2',
+ '--text-muted': '#757575',
+
+ '--border-color': '#ce93d8',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ }
+ };
+
+ this.currentTheme = 'royal';
+ this.element = document.documentElement;
+ }
+
+ /**
+ * Set theme by name
+ */
+ setTheme(themeName) {
+ if (!this.themes[themeName]) {
+ console.warn(`Theme '${themeName}' not found. Using default theme.`);
+ themeName = 'light';
+ }
+
+ this.currentTheme = themeName;
+ this.applyTheme(themeName);
+
+ // Save to localStorage
+ localStorage.setItem('app-theme', themeName);
+
+ // Dispatch theme change event
+ this.dispatchThemeChange(themeName);
+ }
+
+ /**
+ * Get current theme
+ */
+ getCurrentTheme() {
+ return this.currentTheme;
+ }
+
+ /**
+ * Get theme by name
+ */
+ getTheme(themeName) {
+ return this.themes[themeName] || this.themes['light'];
+ }
+
+ /**
+ * Get all available themes
+ */
+ getThemes() {
+ return { ...this.themes };
+ }
+
+ /**
+ * Apply theme to document
+ */
+ applyTheme(themeName) {
+ const theme = this.themes[themeName];
+ if (!theme) return;
+
+ // Remove existing theme classes
+ Object.keys(this.themes).forEach(name => {
+ this.element.classList.remove(`theme-${name}`);
+ });
+
+ // Add current theme class
+ this.element.classList.add(`theme-${themeName}`);
+
+ // Apply CSS custom properties
+ const root = document.documentElement;
+ Object.entries(theme.cssVariables).forEach(([property, value]) => {
+ root.style.setProperty(property, value);
+ });
+
+ // Update meta theme-color for mobile browsers
+ this.updateMetaThemeColor(theme.cssVariables['--primary-color']);
+ }
+
+ /**
+ * Update meta theme-color for mobile browsers
+ */
+ updateMetaThemeColor(color) {
+ const metaThemeColor = document.querySelector('meta[name="theme-color"]');
+ if (metaThemeColor) {
+ metaThemeColor.setAttribute('content', color);
+ }
+ }
+
+ /**
+ * Toggle between Royal (light) and Royal Dark themes
+ */
+ toggleTheme() {
+ const current = this.getCurrentTheme();
+ const newTheme = current === 'royal-dark' ? 'royal' : 'royal-dark';
+ this.setTheme(newTheme);
+ }
+
+ /**
+ * Dispatch theme change event
+ */
+ dispatchThemeChange(themeName) {
+ const event = new CustomEvent('theme:changed', {
+ detail: {
+ theme: themeName,
+ timestamp: Date.now()
+ }
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ /**
+ * Initialize theme from localStorage or system preference
+ * Defaults to the Royal (Purple/Gold/Black/White) theme
+ */
+ init() {
+ // Check localStorage first
+ const savedTheme = localStorage.getItem('app-theme');
+ if (savedTheme && this.themes[savedTheme]) {
+ this.setTheme(savedTheme);
+ return;
+ }
+
+ // Check system preference — map to Royal variants
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ this.setTheme('royal-dark');
+ } else {
+ this.setTheme('royal');
+ }
+ }
+
+ /**
+ * Create theme switcher component
+ */
+ createThemeSwitcher(container) {
+ if (!container) return;
+
+ const switcher = document.createElement('div');
+ switcher.className = 'theme-switcher';
+ switcher.innerHTML = `
+
+ `;
+
+ container.appendChild(switcher);
+
+ // Add event listener
+ const select = switcher.querySelector('.theme-switcher-select');
+ select.addEventListener('change', (e) => {
+ this.setTheme(e.target.value);
+ });
+
+ return switcher;
+ }
+
+ /**
+ * Create theme preview component
+ */
+ createThemePreview(container) {
+ if (!container) return;
+
+ const preview = document.createElement('div');
+ preview.className = 'theme-preview';
+ preview.innerHTML = `
+
+ ${Object.entries(this.themes).map(([name, theme]) => `
+
+
+
+
+
+
+ ${theme.name}
+
+
+
+ `).join('')}
+
+ `;
+
+ container.appendChild(preview);
+
+ // Add click listeners
+ const themeCards = preview.querySelectorAll('.theme-card');
+ themeCards.forEach(card => {
+ card.addEventListener('click', () => {
+ const themeName = card.getAttribute('data-theme');
+ this.setTheme(themeName);
+
+ // Update selection indicators
+ themeCards.forEach(c => c.style.borderColor = 'transparent');
+ card.style.borderColor = this.themes[themeName].cssVariables['--primary-color'];
+ });
+ });
+
+ return preview;
+ }
+
+ /**
+ * Export current theme
+ */
+ exportTheme() {
+ const theme = this.getTheme(this.currentTheme);
+ return {
+ name: theme.name,
+ description: theme.description,
+ cssVariables: { ...theme.cssVariables },
+ timestamp: Date.now()
+ };
+ }
+
+ /**
+ * Import custom theme
+ */
+ importTheme(themeData) {
+ if (!themeData || !themeData.cssVariables) {
+ throw new Error('Invalid theme data');
+ }
+
+ const themeName = themeData.name || 'custom';
+ this.themes[themeName] = {
+ name: themeData.name || 'Custom Theme',
+ description: themeData.description || 'Custom imported theme',
+ cssVariables: { ...themeData.cssVariables }
+ };
+
+ this.setTheme(themeName);
+ }
+
+ /**
+ * Create custom theme from colors
+ */
+ createCustomTheme(colors) {
+ const customTheme = {
+ name: 'Custom',
+ description: 'User-defined theme',
+ cssVariables: {
+ '--primary-color': colors.primary || '#007bff',
+ '--secondary-color': colors.secondary || '#6c757d',
+ '--success-color': colors.success || '#28a745',
+ '--danger-color': colors.danger || '#dc3545',
+ '--warning-color': colors.warning || '#ffc107',
+ '--info-color': colors.info || '#17a2b8',
+ '--light-color': colors.light || '#f8f9fa',
+ '--dark-color': colors.dark || '#343a40',
+
+ '--bg-primary': colors.bgPrimary || '#ffffff',
+ '--bg-secondary': colors.bgSecondary || '#f8f9fa',
+ '--bg-accent': colors.bgAccent || '#e9ecef',
+
+ '--text-primary': colors.textPrimary || '#212529',
+ '--text-secondary': colors.textSecondary || '#6c757d',
+ '--text-muted': colors.textMuted || '#6c757d',
+
+ '--border-color': colors.borderColor || '#dee2e6',
+ '--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ '--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
+
+ '--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '--font-size-base': '1rem',
+ '--line-height-base': '1.5',
+ '--border-radius': '0.375rem',
+ '--border-radius-lg': '0.5rem',
+ '--border-radius-sm': '0.25rem'
+ }
+ };
+
+ this.themes.custom = customTheme;
+ this.setTheme('custom');
+ }
+
+ /**
+ * Reset to default theme
+ */
+ resetTheme() {
+ this.setTheme('light');
+ }
+
+ /**
+ * Check if theme exists
+ */
+ hasTheme(themeName) {
+ return !!this.themes[themeName];
+ }
+
+ /**
+ * Remove theme
+ */
+ removeTheme(themeName) {
+ if (themeName === this.currentTheme) {
+ this.setTheme('light');
+ }
+ delete this.themes[themeName];
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ThemeManager;
+} else {
+ window.ThemeManager = ThemeManager;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-handlers.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-handlers.js
new file mode 100644
index 00000000000..bca3d30fbe1
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-handlers.js
@@ -0,0 +1,729 @@
+/**
+ * Event Handlers of Web Application
+ *
+ * Centralized event handling for the Excel Analyzer application.
+ */
+
+class EventHandlers {
+ constructor(options = {}) {
+ this.appState = options.appState || {};
+ this.modules = options.modules || {};
+ this.utils = options.utils || {};
+ this.config = options.config || {};
+ this.events = options.events || {};
+
+ this.init();
+ }
+
+ init() {
+ this.setupGlobalEventListeners();
+ this.setupModuleEventListeners();
+ this.setupUIEventListeners();
+ }
+
+ /**
+ * Setup global event listeners
+ */
+ setupGlobalEventListeners() {
+ // Window resize events
+ window.addEventListener('resize', this.handleWindowResize.bind(this));
+
+ // Keyboard shortcuts
+ window.addEventListener('keydown', this.handleKeyDown.bind(this));
+
+ // Before unload confirmation
+ window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
+
+ // Theme change events
+ window.addEventListener('theme:changed', this.handleThemeChange.bind(this));
+
+ // App initialization events
+ this.events.on('app:initialized', this.handleAppInitialized.bind(this));
+ }
+
+ /**
+ * Setup module-specific event listeners
+ */
+ setupModuleEventListeners() {
+ // File upload events
+ if (this.modules.fileUpload) {
+ this.modules.fileUpload.onFileSelected = this.handleFileSelected.bind(this);
+ this.modules.fileUpload.onFileParsed = this.handleFileParsed.bind(this);
+ this.modules.fileUpload.onError = this.handleModuleError.bind(this);
+ }
+
+ // Data processing events
+ if (this.modules.dataProcessor) {
+ this.modules.dataProcessor.onDataAnalyzed = this.handleDataAnalyzed.bind(this);
+ this.modules.dataProcessor.onDataProcessed = this.handleDataProcessed.bind(this);
+ this.modules.dataProcessor.onError = this.handleModuleError.bind(this);
+ }
+
+ // Visualization events
+ if (this.modules.visualizer) {
+ this.modules.visualizer.onChartRendered = this.handleChartRendered.bind(this);
+ this.modules.visualizer.onChartUpdated = this.handleChartUpdated.bind(this);
+ this.modules.visualizer.onError = this.handleModuleError.bind(this);
+ }
+
+ // Dashboard events
+ if (this.modules.dashboard) {
+ this.modules.dashboard.onDashboardUpdated = this.handleDashboardUpdated.bind(this);
+ this.modules.dashboard.onWidgetAdded = this.handleWidgetAdded.bind(this);
+ this.modules.dashboard.onError = this.handleModuleError.bind(this);
+ }
+
+ // Export events
+ if (this.modules.exporter) {
+ this.modules.exporter.onExportComplete = this.handleExportComplete.bind(this);
+ this.modules.exporter.onExportProgress = this.handleExportProgress.bind(this);
+ this.modules.exporter.onError = this.handleModuleError.bind(this);
+ }
+ }
+
+ /**
+ * Setup UI event listeners
+ */
+ setupUIEventListeners() {
+ // Navigation events
+ this.setupNavigationEvents();
+
+ // Chart selection events
+ this.setupChartSelectionEvents();
+
+ // Dashboard layout events
+ this.setupDashboardLayoutEvents();
+
+ // Export events
+ this.setupExportEvents();
+ }
+
+ /**
+ * Handle window resize
+ */
+ handleWindowResize() {
+ // Resize charts if visualizer is available
+ if (this.modules.visualizer && this.modules.visualizer.resizeCharts) {
+ this.modules.visualizer.resizeCharts();
+ }
+
+ // Update dashboard layout if needed
+ if (this.modules.dashboard && this.modules.dashboard.handleResize) {
+ this.modules.dashboard.handleResize();
+ }
+ }
+
+ /**
+ * Handle keyboard shortcuts
+ */
+ handleKeyDown(event) {
+ // Ctrl/Cmd + S: Save dashboard
+ if ((event.ctrlKey || event.metaKey) && event.key === 's') {
+ event.preventDefault();
+ this.handleSaveDashboard();
+ }
+
+ // Ctrl/Cmd + E: Export
+ if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
+ event.preventDefault();
+ this.handleExport();
+ }
+
+ // Ctrl/Cmd + T: Toggle theme
+ if ((event.ctrlKey || event.metaKey) && event.key === 't') {
+ event.preventDefault();
+ this.handleThemeToggle();
+ }
+
+ // Escape: Close modals
+ if (event.key === 'Escape') {
+ this.handleEscapeKey();
+ }
+ }
+
+ /**
+ * Handle before unload
+ */
+ handleBeforeUnload(event) {
+ if (this.appState.rawData && this.appState.rawData.length > 0) {
+ event.preventDefault();
+ event.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
+ }
+ }
+
+ /**
+ * Handle theme change
+ */
+ handleThemeChange(event) {
+ const theme = event.detail.theme;
+ console.log(`Theme changed to: ${theme}`);
+
+ // Update any theme-dependent components
+ if (this.modules.visualizer) {
+ this.modules.visualizer.updateTheme(theme);
+ }
+
+ if (this.modules.dashboard) {
+ this.modules.dashboard.updateTheme(theme);
+ }
+ }
+
+ /**
+ * Handle app initialization
+ */
+ handleAppInitialized(event) {
+ console.log('Application initialized successfully');
+
+ // Setup any post-initialization tasks
+ this.setupPostInitialization();
+ }
+
+ /**
+ * Handle file selected
+ */
+ handleFileSelected(file) {
+ console.log('File selected:', file.name);
+
+ // Update app state
+ this.appState.currentFile = file;
+ this.appState.rawData = [];
+ this.appState.processedData = [];
+ this.appState.dataStructure = null;
+ this.appState.visualizations = [];
+
+ // Update UI
+ this.updateFileInfo(file);
+ this.showSection('preview');
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle file parsed
+ */
+ handleFileParsed(data) {
+ console.log('File parsed successfully');
+
+ this.appState.rawData = data;
+
+ // Update UI
+ this.updateDataPreview(data);
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle data analyzed
+ */
+ handleDataAnalyzed(structure) {
+ console.log('Data analyzed');
+
+ this.appState.dataStructure = structure;
+
+ // Update UI
+ this.updateColumnAnalysis(structure);
+ this.showSection('analyze');
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle data processed
+ */
+ handleDataProcessed(processedData) {
+ console.log('Data processed');
+
+ this.appState.processedData = processedData;
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle chart rendered
+ */
+ handleChartRendered(chart) {
+ console.log('Chart rendered');
+
+ this.appState.visualizations.push(chart);
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle chart updated
+ */
+ handleChartUpdated(chart) {
+ console.log('Chart updated');
+
+ const index = this.appState.visualizations.findIndex(c => c.id === chart.id);
+ if (index !== -1) {
+ this.appState.visualizations[index] = chart;
+ }
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle dashboard updated
+ */
+ handleDashboardUpdated(dashboard) {
+ console.log('Dashboard updated');
+
+ this.appState.dashboard = dashboard;
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle widget added
+ */
+ handleWidgetAdded(widget) {
+ console.log('Widget added to dashboard');
+
+ // Dashboard manager handles this internally
+
+ // Save state
+ this.saveAppState();
+ }
+
+ /**
+ * Handle export complete
+ */
+ handleExportComplete(result) {
+ console.log('Export completed:', result);
+
+ this.showSuccessMessage(`Export completed successfully: ${result.filename}`);
+ }
+
+ /**
+ * Handle export progress
+ */
+ handleExportProgress(progress) {
+ console.log('Export progress:', progress);
+
+ this.updateExportProgress(progress);
+ }
+
+ /**
+ * Handle module error
+ */
+ handleModuleError(error) {
+ console.error('Module error:', error);
+
+ this.showErrorMessage(error.message || 'An error occurred. Please try again.');
+ }
+
+ /**
+ * Setup navigation events
+ */
+ setupNavigationEvents() {
+ const links = document.querySelectorAll('.nav-link[data-section]');
+ links.forEach(link => {
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ const sectionId = link.getAttribute('data-section');
+ this.showSection(sectionId);
+ });
+ });
+ }
+
+ /**
+ * Setup chart selection events
+ */
+ setupChartSelectionEvents() {
+ const chartTypeSelector = document.getElementById('chartTypeSelector');
+ const xAxisSelector = document.getElementById('xAxisSelector');
+ const yAxisSelector = document.getElementById('yAxisSelector');
+
+ if (chartTypeSelector) {
+ chartTypeSelector.addEventListener('change', (e) => {
+ this.handleChartTypeChange(e.target.value);
+ });
+ }
+
+ if (xAxisSelector) {
+ xAxisSelector.addEventListener('change', (e) => {
+ this.handleAxisChange('x', e.target.value);
+ });
+ }
+
+ if (yAxisSelector) {
+ yAxisSelector.addEventListener('change', (e) => {
+ this.handleAxisChange('y', e.target.value);
+ });
+ }
+ }
+
+ /**
+ * Setup dashboard layout events
+ */
+ setupDashboardLayoutEvents() {
+ const layoutButtons = document.querySelectorAll('.btn[data-layout]');
+ layoutButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ const layout = e.target.getAttribute('data-layout');
+ this.handleLayoutChange(layout);
+ });
+ });
+ }
+
+ /**
+ * Setup export events
+ */
+ setupExportEvents() {
+ const exportButtons = document.querySelectorAll('[data-export-format]');
+ exportButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ const format = e.target.getAttribute('data-export-format');
+ this.handleExport(format);
+ });
+ });
+ }
+
+ /**
+ * Handle chart type change
+ */
+ handleChartTypeChange(chartType) {
+ console.log('Chart type changed to:', chartType);
+
+ // Update chart configuration
+ if (this.modules.visualizer) {
+ this.modules.visualizer.updateChartType(chartType);
+ }
+ }
+
+ /**
+ * Handle axis change
+ */
+ handleAxisChange(axis, value) {
+ console.log(`${axis} axis changed to:`, value);
+
+ // Update chart configuration
+ if (this.modules.visualizer) {
+ this.modules.visualizer.updateAxis(axis, value);
+ }
+ }
+
+ /**
+ * Handle layout change
+ */
+ handleLayoutChange(layout) {
+ console.log('Layout changed to:', layout);
+
+ // Update dashboard layout
+ if (this.modules.dashboard) {
+ this.modules.dashboard.setLayout(layout);
+ }
+ }
+
+ /**
+ * Handle export
+ */
+ handleExport(format = 'pdf') {
+ console.log('Export requested:', format);
+
+ if (this.modules.exporter) {
+ this.modules.exporter.export(format);
+ }
+ }
+
+ /**
+ * Handle save dashboard
+ */
+ handleSaveDashboard() {
+ console.log('Save dashboard requested');
+
+ if (this.modules.dashboard) {
+ this.modules.dashboard.save();
+ }
+ }
+
+ /**
+ * Handle theme toggle
+ */
+ handleThemeToggle() {
+ console.log('Theme toggle requested');
+
+ if (this.config.themes) {
+ this.config.themes.toggleTheme();
+ }
+ }
+
+ /**
+ * Handle escape key
+ */
+ handleEscapeKey() {
+ console.log('Escape key pressed');
+
+ // Close any open modals or dropdowns
+ const modals = document.querySelectorAll('.modal.show');
+ modals.forEach(modal => {
+ const modalInstance = bootstrap.Modal.getInstance(modal);
+ if (modalInstance) {
+ modalInstance.hide();
+ }
+ });
+
+ const dropdowns = document.querySelectorAll('.dropdown.show');
+ dropdowns.forEach(dropdown => {
+ const dropdownInstance = bootstrap.Dropdown.getInstance(dropdown);
+ if (dropdownInstance) {
+ dropdownInstance.hide();
+ }
+ });
+ }
+
+ /**
+ * Setup post-initialization tasks
+ */
+ setupPostInitialization() {
+ // Load saved state if available
+ this.loadAppState();
+
+ // Initialize tooltips
+ this.initializeTooltips();
+
+ // Setup theme switcher if available
+ this.setupThemeSwitcher();
+ }
+
+ /**
+ * Update file info display
+ */
+ updateFileInfo(file) {
+ const fileNameEl = document.getElementById('fileName');
+ const fileSizeEl = document.getElementById('fileSize');
+
+ if (fileNameEl) fileNameEl.textContent = file.name;
+ if (fileSizeEl) fileSizeEl.textContent = this.utils.formatters.formatFileSize(file.size);
+ }
+
+ /**
+ * Update data preview
+ */
+ updateDataPreview(data) {
+ // Update statistics
+ const totalRowsEl = document.getElementById('totalRows');
+ const totalColsEl = document.getElementById('totalCols');
+ const missingValuesEl = document.getElementById('missingValues');
+ const dataQualityEl = document.getElementById('dataQuality');
+
+ if (totalRowsEl) totalRowsEl.textContent = data.length;
+ if (totalColsEl && data.length > 0) totalColsEl.textContent = Object.keys(data[0]).length;
+
+ // Calculate missing values and data quality
+ const stats = this.calculateDataStats(data);
+ if (missingValuesEl) missingValuesEl.textContent = stats.missingCount;
+ if (dataQualityEl) dataQualityEl.textContent = `${stats.qualityPercentage}%`;
+
+ // Update table
+ this.renderDataTable(data);
+ }
+
+ /**
+ * Update column analysis
+ */
+ updateColumnAnalysis(structure) {
+ const container = document.getElementById('columnAnalysis');
+ if (container && structure) {
+ container.innerHTML = this.generateColumnAnalysisHTML(structure);
+ }
+ }
+
+ /**
+ * Show section
+ */
+ showSection(sectionId) {
+ const sections = ['upload', 'preview', 'analyze', 'dashboard', 'export'];
+ sections.forEach(section => {
+ const element = document.getElementById(section);
+ if (element) {
+ element.style.display = section === sectionId ? 'block' : 'none';
+ if (section === sectionId) {
+ element.classList.add('fade-in');
+ }
+ }
+ });
+
+ // Update navigation
+ this.updateNavigationActive(sectionId);
+ }
+
+ /**
+ * Update navigation active state
+ */
+ updateNavigationActive(sectionId) {
+ const links = document.querySelectorAll('.nav-link');
+ links.forEach(link => {
+ if (link.getAttribute('data-section') === sectionId) {
+ link.classList.add('active');
+ } else {
+ link.classList.remove('active');
+ }
+ });
+ }
+
+ /**
+ * Show success message
+ */
+ showSuccessMessage(message) {
+ const statusEl = document.getElementById('exportStatus');
+ if (statusEl) {
+ statusEl.innerHTML = `${message}
`;
+ }
+ }
+
+ /**
+ * Show error message
+ */
+ showErrorMessage(message) {
+ const statusEl = document.getElementById('uploadStatus') || document.getElementById('exportStatus');
+ if (statusEl) {
+ statusEl.innerHTML = `${message}
`;
+ }
+ }
+
+ /**
+ * Update export progress
+ */
+ updateExportProgress(progress) {
+ const progressEl = document.getElementById('exportProgress');
+ const progressBarEl = progressEl ? progressEl.querySelector('.progress-bar') : null;
+
+ if (progressEl) {
+ progressEl.style.display = 'block';
+ }
+
+ if (progressBarEl) {
+ progressBarEl.style.width = `${progress.percentage}%`;
+ progressBarEl.textContent = `${progress.percentage}%`;
+ }
+ }
+
+ /**
+ * Render data table
+ */
+ renderDataTable(data) {
+ const tableBody = document.getElementById('tableBody');
+ const tableHeader = document.getElementById('tableHeader');
+
+ if (!tableBody || !tableHeader || data.length === 0) return;
+
+ // Generate headers
+ const headers = Object.keys(data[0]);
+ tableHeader.innerHTML = '' + headers.map(header => `| ${header} | `).join('') + '
';
+
+ // Generate rows (limit to first 100 for performance)
+ const displayData = data.slice(0, 100);
+ tableBody.innerHTML = displayData.map(row => {
+ return '' + headers.map(header => `| ${row[header] || ''} | `).join('') + '
';
+ }).join('');
+ }
+
+ /**
+ * Calculate data statistics
+ */
+ calculateDataStats(data) {
+ if (data.length === 0) return { missingCount: 0, qualityPercentage: 100 };
+
+ const headers = Object.keys(data[0]);
+ let missingCount = 0;
+ let totalCount = 0;
+
+ data.forEach(row => {
+ headers.forEach(header => {
+ totalCount++;
+ if (row[header] === null || row[header] === undefined || row[header] === '') {
+ missingCount++;
+ }
+ });
+ });
+
+ const qualityPercentage = Math.round(((totalCount - missingCount) / totalCount) * 100);
+
+ return {
+ missingCount,
+ qualityPercentage
+ };
+ }
+
+ /**
+ * Generate column analysis HTML
+ */
+ generateColumnAnalysisHTML(structure) {
+ if (!structure || !structure.columns) return '';
+
+ return structure.columns.map(col => `
+
+
+
${col.name}
+
+ Type: ${col.type}
+ Unique Values: ${col.uniqueValues}
+ Missing: ${col.missingCount}
+
+
+
+ `).join('');
+ }
+
+ /**
+ * Initialize tooltips
+ */
+ initializeTooltips() {
+ if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
+ return new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+ }
+ }
+
+ /**
+ * Setup theme switcher
+ */
+ setupThemeSwitcher() {
+ const themeContainer = document.getElementById('theme-switcher-container');
+ if (themeContainer && this.config.themes) {
+ this.config.themes.createThemeSwitcher(themeContainer);
+ }
+ }
+
+ /**
+ * Save application state
+ */
+ saveAppState() {
+ localStorage.setItem('excel-analyzer-state', JSON.stringify(this.appState));
+ }
+
+ /**
+ * Load application state
+ */
+ loadAppState() {
+ const savedState = localStorage.getItem('excel-analyzer-state');
+ if (savedState) {
+ try {
+ const state = JSON.parse(savedState);
+ Object.assign(this.appState, state);
+ console.log('Application state loaded from localStorage');
+ } catch (error) {
+ console.warn('Failed to load saved state:', error);
+ localStorage.removeItem('excel-analyzer-state');
+ }
+ }
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = EventHandlers;
+} else {
+ window.EventHandlers = EventHandlers;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-system.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-system.js
new file mode 100644
index 00000000000..6b60d1bf827
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/events/event-system.js
@@ -0,0 +1,352 @@
+/**
+ * Event System of Web Application
+ *
+ * Custom event management system for the Excel Analyzer application.
+ */
+
+class EventSystem {
+ constructor() {
+ this.events = new Map();
+ this.maxListeners = 10;
+ }
+
+ /**
+ * Add event listener
+ */
+ on(event, callback, context = null) {
+ if (typeof callback !== 'function') {
+ throw new Error('Callback must be a function');
+ }
+
+ if (!this.events.has(event)) {
+ this.events.set(event, new Set());
+ }
+
+ const listeners = this.events.get(event);
+
+ // Check max listeners limit
+ if (listeners.size >= this.maxListeners) {
+ console.warn(`Maximum number of listeners (${this.maxListeners}) reached for event: ${event}`);
+ }
+
+ listeners.add({ callback, context });
+
+ return this;
+ }
+
+ /**
+ * Add one-time event listener
+ */
+ once(event, callback, context = null) {
+ const onceCallback = (...args) => {
+ this.off(event, onceCallback);
+ callback.apply(context, args);
+ };
+
+ return this.on(event, onceCallback, context);
+ }
+
+ /**
+ * Remove event listener
+ */
+ off(event, callback) {
+ if (!this.events.has(event)) {
+ return this;
+ }
+
+ const listeners = this.events.get(event);
+
+ if (callback) {
+ // Remove specific callback
+ for (const listener of listeners) {
+ if (listener.callback === callback) {
+ listeners.delete(listener);
+ break;
+ }
+ }
+ } else {
+ // Remove all listeners for this event
+ listeners.clear();
+ }
+
+ // Clean up empty event sets
+ if (listeners.size === 0) {
+ this.events.delete(event);
+ }
+
+ return this;
+ }
+
+ /**
+ * Emit event
+ */
+ emit(event, data = null) {
+ if (!this.events.has(event)) {
+ return false;
+ }
+
+ const listeners = this.events.get(event);
+ let hasListeners = false;
+
+ for (const listener of listeners) {
+ hasListeners = true;
+ try {
+ listener.callback.call(listener.context, data);
+ } catch (error) {
+ console.error(`Error in event listener for '${event}':`, error);
+ }
+ }
+
+ return hasListeners;
+ }
+
+ /**
+ * Get all listeners for an event
+ */
+ listeners(event) {
+ if (!this.events.has(event)) {
+ return [];
+ }
+
+ return Array.from(this.events.get(event)).map(listener => listener.callback);
+ }
+
+ /**
+ * Check if event has listeners
+ */
+ hasListeners(event) {
+ return this.events.has(event) && this.events.get(event).size > 0;
+ }
+
+ /**
+ * Remove all listeners for all events
+ */
+ removeAllListeners() {
+ this.events.clear();
+ return this;
+ }
+
+ /**
+ * Remove all listeners for specific event
+ */
+ removeAllListenersForEvent(event) {
+ if (this.events.has(event)) {
+ this.events.get(event).clear();
+ this.events.delete(event);
+ }
+ return this;
+ }
+
+ /**
+ * Set max listeners limit
+ */
+ setMaxListeners(max) {
+ if (typeof max !== 'number' || max < 0) {
+ throw new Error('Max listeners must be a non-negative number');
+ }
+
+ this.maxListeners = max;
+ return this;
+ }
+
+ /**
+ * Get event names
+ */
+ eventNames() {
+ return Array.from(this.events.keys());
+ }
+
+ /**
+ * Get listener count for event
+ */
+ listenerCount(event) {
+ return this.events.has(event) ? this.events.get(event).size : 0;
+ }
+
+ /**
+ * Create namespaced event
+ */
+ namespace(namespace) {
+ return {
+ on: (event, callback, context) => this.on(`${namespace}:${event}`, callback, context),
+ once: (event, callback, context) => this.once(`${namespace}:${event}`, callback, context),
+ off: (event, callback) => this.off(`${namespace}:${event}`, callback),
+ emit: (event, data) => this.emit(`${namespace}:${event}`, data),
+ removeAllListeners: () => {
+ const eventsToRemove = [];
+ for (const eventName of this.eventNames()) {
+ if (eventName.startsWith(`${namespace}:`)) {
+ eventsToRemove.push(eventName);
+ }
+ }
+ eventsToRemove.forEach(event => this.removeAllListenersForEvent(event));
+ }
+ };
+ }
+
+ /**
+ * Create event chain
+ */
+ chain(events) {
+ if (!Array.isArray(events) || events.length < 2) {
+ throw new Error('Event chain requires at least 2 events');
+ }
+
+ return {
+ start: (initialData) => {
+ this.emit(events[0], initialData);
+ },
+ link: (event, callback) => {
+ this.on(event, callback);
+ }
+ };
+ }
+
+ /**
+ * Debounced event emitter
+ */
+ debouncedEmit(event, data, delay = 300) {
+ if (!this._debounceTimers) {
+ this._debounceTimers = new Map();
+ }
+
+ if (this._debounceTimers.has(event)) {
+ clearTimeout(this._debounceTimers.get(event));
+ }
+
+ const timer = setTimeout(() => {
+ this.emit(event, data);
+ this._debounceTimers.delete(event);
+ }, delay);
+
+ this._debounceTimers.set(event, timer);
+ }
+
+ /**
+ * Throttled event emitter
+ */
+ throttledEmit(event, data, limit = 1000) {
+ if (!this._throttleTimers) {
+ this._throttleTimers = new Map();
+ }
+
+ const lastEmit = this._throttleTimers.get(event);
+ const now = Date.now();
+
+ if (!lastEmit || (now - lastEmit) >= limit) {
+ this.emit(event, data);
+ this._throttleTimers.set(event, now);
+ }
+ }
+
+ /**
+ * Event logger
+ */
+ enableLogging(enabled = true) {
+ if (enabled) {
+ this._originalEmit = this.emit;
+ this.emit = (event, data) => {
+ console.log(`Event emitted: ${event}`, data);
+ return this._originalEmit.call(this, event, data);
+ };
+ } else if (this._originalEmit) {
+ this.emit = this._originalEmit;
+ delete this._originalEmit;
+ }
+ }
+
+ /**
+ * Event history
+ */
+ enableHistory(enabled = true, maxSize = 100) {
+ if (enabled) {
+ this._eventHistory = [];
+ this._historyMaxSize = maxSize;
+
+ this._originalEmit = this.emit;
+ this.emit = (event, data) => {
+ const result = this._originalEmit.call(this, event, data);
+
+ // Add to history
+ this._eventHistory.push({
+ event,
+ data,
+ timestamp: Date.now()
+ });
+
+ // Limit history size
+ if (this._eventHistory.length > this._historyMaxSize) {
+ this._eventHistory.shift();
+ }
+
+ return result;
+ };
+ } else if (this._originalEmit) {
+ this.emit = this._originalEmit;
+ delete this._originalEmit;
+ delete this._eventHistory;
+ delete this._historyMaxSize;
+ }
+ }
+
+ /**
+ * Get event history
+ */
+ getHistory() {
+ return this._eventHistory || [];
+ }
+
+ /**
+ * Clear event history
+ */
+ clearHistory() {
+ if (this._eventHistory) {
+ this._eventHistory = [];
+ }
+ }
+
+ /**
+ * Event filter
+ */
+ filter(event, filterFn) {
+ if (typeof filterFn !== 'function') {
+ throw new Error('Filter function must be provided');
+ }
+
+ const originalEmit = this.emit;
+ this.emit = (eventName, data) => {
+ if (eventName === event && !filterFn(data)) {
+ return false; // Don't emit if filter returns false
+ }
+ return originalEmit.call(this, eventName, data);
+ };
+
+ return this;
+ }
+
+ /**
+ * Event transformer
+ */
+ transform(event, transformFn) {
+ if (typeof transformFn !== 'function') {
+ throw new Error('Transform function must be provided');
+ }
+
+ const originalEmit = this.emit;
+ this.emit = (eventName, data) => {
+ if (eventName === event) {
+ data = transformFn(data);
+ }
+ return originalEmit.call(this, eventName, data);
+ };
+
+ return this;
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = EventSystem;
+} else {
+ window.EventSystem = EventSystem;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/main.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/main.js
new file mode 100644
index 00000000000..d670bf89dfe
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/main.js
@@ -0,0 +1,467 @@
+/**
+ * Excel Analyzer - Main Application Entry Point
+ *
+ * Provides an end-to-end browser workflow:
+ * upload -> preview -> analysis/chart -> dashboard -> export in PDF, Excel or chart.
+ */
+
+const appState = {
+ currentFile: null,
+ rawData: [],
+ processedData: [],
+ dataStructure: null,
+ currentChart: null,
+ previewLimit: 100
+};
+
+const modules = {
+ fileUpload: null,
+ dataProcessor: null,
+ visualizer: null,
+ dashboard: null,
+ exporter: null,
+ notifications: null
+};
+
+const config = {
+ settings: null,
+ themes: null,
+ defaults: null
+};
+
+function init() {
+ try {
+ initConfig();
+ initModules();
+ initUI();
+ showInlineStatus('uploadStatus', 'success', 'Application ready. Upload an Excel file to begin.');
+ } catch (error) {
+ console.error('Initialization failed:', error);
+ showInlineStatus('uploadStatus', 'danger', `Initialization failed: ${error.message}`);
+ }
+}
+
+function initConfig() {
+ config.settings = window.SettingsManager ? new SettingsManager() : null;
+ config.themes = window.ThemeManager ? new ThemeManager() : null;
+ config.defaults = window.DefaultValues ? new DefaultValues() : null;
+
+ const savedTheme =
+ (config.settings && config.settings.get('ui.theme')) ||
+ localStorage.getItem('app-theme') ||
+ 'light';
+
+ if (config.themes) {
+ config.themes.setTheme(savedTheme);
+ }
+}
+
+function initModules() {
+ modules.notifications = window.Notifications ? new Notifications() : null;
+
+ modules.fileUpload = new FileUploadManager({
+ onFileSelected: handleFileSelected,
+ onFileParsed: handleFileParsed,
+ onError: handleModuleError
+ });
+
+ modules.dataProcessor = new DataProcessor({ onError: handleModuleError });
+
+ modules.visualizer = new Visualizer({
+ onChartRendered: (chart) => {
+ appState.currentChart = chart;
+ updateDashboard();
+ },
+ onError: handleModuleError
+ });
+
+ modules.dashboard = new DashboardManager({
+ containerId: 'dashboardGrid',
+ onDashboardUpdated: () => {}
+ });
+
+ modules.exporter = new Exporter({
+ onExportProgress: (progress) => updateExportProgress(progress),
+ onExportComplete: ({ filename }) => {
+ showInlineStatus('exportStatus', 'success', `Export completed: ${filename}`);
+ notify('success', `Export completed: ${filename}`);
+ },
+ onError: (error) => {
+ showInlineStatus('exportStatus', 'danger', error.message || 'Export failed');
+ notify('error', error.message || 'Export failed');
+ }
+ });
+}
+
+function initUI() {
+ hideAllSections();
+ showSection('upload');
+
+ setupNavigation();
+ setupPreviewControls();
+ setupAnalysisControls();
+ setupDashboardControls();
+ setupExportControls();
+ setupThemeToggle();
+}
+
+function setupNavigation() {
+ document.querySelectorAll('.nav-link[data-section]').forEach((link) => {
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+ const sectionId = link.getAttribute('data-section');
+ if (sectionId !== 'upload' && !hasData()) {
+ showInlineStatus('uploadStatus', 'warning', 'Please upload and parse a file first.');
+ return;
+ }
+ showSection(sectionId);
+ });
+ });
+}
+
+function setupPreviewControls() {
+ const btnFirst100 = document.getElementById('btnFirst100');
+ const btnAllData = document.getElementById('btnAllData');
+
+ if (btnFirst100) {
+ btnFirst100.addEventListener('click', () => {
+ appState.previewLimit = 100;
+ renderDataTable(appState.processedData, appState.previewLimit);
+ });
+ }
+
+ if (btnAllData) {
+ btnAllData.addEventListener('click', () => {
+ appState.previewLimit = Number.MAX_SAFE_INTEGER;
+ renderDataTable(appState.processedData, appState.previewLimit);
+ });
+ }
+}
+
+function setupAnalysisControls() {
+ const chartTypeSelector = document.getElementById('chartTypeSelector');
+ const xAxisSelector = document.getElementById('xAxisSelector');
+ const yAxisSelector = document.getElementById('yAxisSelector');
+
+ if (chartTypeSelector) chartTypeSelector.addEventListener('change', renderSelectedChart);
+ if (xAxisSelector) xAxisSelector.addEventListener('change', renderSelectedChart);
+ if (yAxisSelector) yAxisSelector.addEventListener('change', renderSelectedChart);
+}
+
+function setupDashboardControls() {
+ const layouts = {
+ layout1: 'layout1',
+ layout2: 'layout2',
+ layout3: 'layout3'
+ };
+
+ Object.keys(layouts).forEach((id) => {
+ const button = document.getElementById(id);
+ if (!button) return;
+ button.addEventListener('click', () => {
+ modules.dashboard.setLayout(layouts[id]);
+ updateDashboard();
+ });
+ });
+}
+
+function setupExportControls() {
+ const exportPDF = document.getElementById('exportPDF');
+ const exportExcel = document.getElementById('exportExcel');
+ const exportCharts = document.getElementById('exportCharts');
+
+ if (exportPDF) exportPDF.addEventListener('click', () => exportData('pdf'));
+ if (exportExcel) exportExcel.addEventListener('click', () => exportData('excel'));
+ if (exportCharts) exportCharts.addEventListener('click', () => exportData('png'));
+}
+
+function setupThemeToggle() {
+ const toggle = document.getElementById('themeToggle');
+ if (!toggle || !config.themes) return;
+
+ toggle.addEventListener('click', () => {
+ const current = config.themes.getCurrentTheme();
+ const next = current === 'dark' ? 'light' : 'dark';
+ config.themes.setTheme(next);
+ if (config.settings) {
+ config.settings.set('ui.theme', next);
+ }
+ });
+}
+
+function handleFileSelected(file) {
+ appState.currentFile = file;
+ appState.rawData = [];
+ appState.processedData = [];
+ appState.dataStructure = null;
+ appState.currentChart = null;
+
+ updateFileInfo(file);
+ showInlineStatus('uploadStatus', 'info', `Selected file: ${file.name}`);
+}
+
+function handleFileParsed(data) {
+ if (!Array.isArray(data) || data.length === 0) {
+ showInlineStatus('uploadStatus', 'warning', 'File parsed, but no usable rows found.');
+ return;
+ }
+
+ appState.rawData = data;
+ appState.processedData = data;
+ appState.dataStructure = modules.dataProcessor.analyzeDataStructure(data);
+
+ updateDataPreview(appState.processedData);
+ updateColumnAnalysis(appState.dataStructure);
+ populateAxisSelectors(appState.processedData);
+ renderSelectedChart();
+ updateDashboard();
+
+ showSection('preview');
+ notify('success', `Parsed successfully: ${data.length} rows.`);
+}
+
+function handleModuleError(error) {
+ console.error(error);
+ const message = error?.message || 'An unexpected error occurred.';
+ showInlineStatus('uploadStatus', 'danger', message);
+ notify('error', message);
+}
+
+function hasData() {
+ return Array.isArray(appState.processedData) && appState.processedData.length > 0;
+}
+
+function hideAllSections() {
+ ['upload', 'preview', 'analyze', 'dashboard', 'export'].forEach((id) => {
+ const section = document.getElementById(id);
+ if (section) section.style.display = 'none';
+ });
+}
+
+function showSection(sectionId) {
+ hideAllSections();
+ const section = document.getElementById(sectionId);
+ if (section) {
+ section.style.display = 'block';
+ section.classList.add('fade-in');
+ }
+
+ document.querySelectorAll('.nav-link[data-section]').forEach((link) => {
+ link.classList.toggle('active', link.getAttribute('data-section') === sectionId);
+ });
+}
+
+function updateFileInfo(file) {
+ const info = document.getElementById('fileInfo');
+ const fileName = document.getElementById('fileName');
+ const fileSize = document.getElementById('fileSize');
+
+ if (info) info.style.display = 'block';
+ if (fileName) fileName.textContent = file.name;
+ if (fileSize) fileSize.textContent = formatFileSize(file.size);
+}
+
+function updateDataPreview(data) {
+ const stats = calculateDataStats(data);
+
+ const totalRows = document.getElementById('totalRows');
+ const totalCols = document.getElementById('totalCols');
+ const missingValues = document.getElementById('missingValues');
+ const dataQuality = document.getElementById('dataQuality');
+
+ if (totalRows) totalRows.textContent = data.length;
+ if (totalCols) totalCols.textContent = data.length ? Object.keys(data[0]).length : 0;
+ if (missingValues) missingValues.textContent = stats.missingCount;
+ if (dataQuality) dataQuality.textContent = `${stats.qualityPercentage}%`;
+
+ renderDataTable(data, appState.previewLimit);
+}
+
+function renderDataTable(data, limit = 100) {
+ const tableHeader = document.getElementById('tableHeader');
+ const tableBody = document.getElementById('tableBody');
+ if (!tableHeader || !tableBody) return;
+
+ if (!data || data.length === 0) {
+ tableHeader.innerHTML = '';
+ tableBody.innerHTML = '';
+ return;
+ }
+
+ const headers = Object.keys(data[0]);
+ tableHeader.innerHTML = `${headers.map((h) => `| ${h} | `).join('')}
`;
+
+ const displayRows = data.slice(0, Math.min(limit, data.length));
+ tableBody.innerHTML = displayRows
+ .map((row) => {
+ const cells = headers
+ .map((header) => {
+ const value = row[header];
+ const missing = value === null || value === undefined || value === '';
+ return `${missing ? '' : String(value)} | `;
+ })
+ .join('');
+ return `${cells}
`;
+ })
+ .join('');
+}
+
+function calculateDataStats(data) {
+ if (!Array.isArray(data) || data.length === 0) {
+ return { missingCount: 0, qualityPercentage: 100 };
+ }
+
+ const headers = Object.keys(data[0]);
+ let missingCount = 0;
+ let totalCount = 0;
+
+ data.forEach((row) => {
+ headers.forEach((header) => {
+ totalCount += 1;
+ const value = row[header];
+ if (value === null || value === undefined || value === '') {
+ missingCount += 1;
+ }
+ });
+ });
+
+ const qualityPercentage = totalCount ? Math.round(((totalCount - missingCount) / totalCount) * 100) : 100;
+ return { missingCount, qualityPercentage };
+}
+
+function updateColumnAnalysis(structure) {
+ const container = document.getElementById('columnAnalysis');
+ if (!container || !structure || !Array.isArray(structure.columns)) return;
+
+ container.innerHTML = structure.columns
+ .map((col) => {
+ const missing = col.missingCount ?? col.nullCount ?? 0;
+ return `
+
+
+
${col.name}
+ Type: ${col.type}
+ Unique: ${col.uniqueValues ?? 0}
+ Missing: ${missing}
+
+
+ `;
+ })
+ .join('');
+}
+
+function populateAxisSelectors(data) {
+ const xAxisSelector = document.getElementById('xAxisSelector');
+ const yAxisSelector = document.getElementById('yAxisSelector');
+ if (!xAxisSelector || !yAxisSelector || !data || !data.length) return;
+
+ const headers = Object.keys(data[0]);
+ const numericHeaders = headers.filter((header) =>
+ data.every((row) => row[header] === null || row[header] === '' || !Number.isNaN(Number(row[header])))
+ );
+
+ xAxisSelector.innerHTML = headers.map((header) => ``).join('');
+ yAxisSelector.innerHTML = headers.map((header) => ``).join('');
+
+ xAxisSelector.value = headers[0];
+ yAxisSelector.value = numericHeaders[0] || headers[Math.min(1, headers.length - 1)] || headers[0];
+}
+
+async function renderSelectedChart() {
+ if (!hasData()) return;
+
+ const chartTypeSelector = document.getElementById('chartTypeSelector');
+ const xAxisSelector = document.getElementById('xAxisSelector');
+ const yAxisSelector = document.getElementById('yAxisSelector');
+
+ const type = chartTypeSelector ? chartTypeSelector.value : 'auto';
+ const xAxis = xAxisSelector ? xAxisSelector.value : undefined;
+ const yAxis = yAxisSelector ? yAxisSelector.value : undefined;
+
+ try {
+ const chart = await modules.visualizer.renderChart(appState.processedData.slice(0, 1000), {
+ type,
+ xAxis,
+ yAxis
+ });
+ appState.currentChart = chart;
+ updateDashboard();
+ } catch (error) {
+ handleModuleError(error);
+ }
+}
+
+function updateDashboard() {
+ if (!modules.dashboard) return;
+
+ const chartDataUrl =
+ appState.currentChart && typeof appState.currentChart.toBase64Image === 'function'
+ ? appState.currentChart.toBase64Image('image/png', 1)
+ : null;
+
+ modules.dashboard.render({
+ data: appState.processedData,
+ structure: appState.dataStructure,
+ chartDataUrl
+ });
+}
+
+async function exportData(format) {
+ if (!hasData()) {
+ showInlineStatus('exportStatus', 'warning', 'No data available to export.');
+ return;
+ }
+
+ const baseName = (appState.currentFile?.name || 'excel-analyzer-data').replace(/\.[^.]+$/, '');
+
+ showInlineStatus('exportStatus', 'info', `Preparing ${format.toUpperCase()} export...`);
+
+ await modules.exporter.export(format, {
+ filenameBase: baseName,
+ title: `Excel Analyzer Report - ${baseName}`,
+ rawData: appState.rawData,
+ processedData: appState.processedData,
+ dataStructure: appState.dataStructure,
+ currentChart: appState.currentChart
+ });
+}
+
+function updateExportProgress(progress) {
+ const wrapper = document.getElementById('exportProgress');
+ const bar = wrapper ? wrapper.querySelector('.progress-bar') : null;
+ if (!wrapper || !bar) return;
+
+ wrapper.style.display = 'block';
+ const value = Math.max(0, Math.min(100, Number(progress?.percentage || 0)));
+ bar.style.width = `${value}%`;
+ bar.textContent = `${value}%`;
+}
+
+function showInlineStatus(elementId, type, message) {
+ const container = document.getElementById(elementId);
+ if (!container) return;
+ container.innerHTML = `${message}
`;
+}
+
+function notify(level, message) {
+ if (!modules.notifications) return;
+ const fn = modules.notifications[level] || modules.notifications.info;
+ fn.call(modules.notifications, message);
+}
+
+function formatFileSize(bytes) {
+ if (!bytes) return '0 Bytes';
+ const units = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
+ const value = bytes / Math.pow(1024, i);
+ return `${value.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
+}
+
+document.addEventListener('DOMContentLoaded', init);
+
+window.ExcelAnalyzer = {
+ appState,
+ modules,
+ config,
+ init
+};
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/analytics.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/analytics.js
new file mode 100644
index 00000000000..b40cc445669
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/analytics.js
@@ -0,0 +1,654 @@
+/**
+ * Analytics Module of Web Application
+ *
+ * Handles data analysis, statistical calculations, and insights generation.
+ */
+
+class Analytics {
+ constructor(options = {}) {
+ this.options = options;
+ this.analysisCache = new Map();
+ this.maxCacheSize = 100;
+ }
+
+ /**
+ * Analyze data structure
+ */
+ analyzeDataStructure(data) {
+ if (!data || data.length === 0) {
+ return {
+ columns: [],
+ rowCount: 0,
+ columnCount: 0,
+ dataTypes: {},
+ missingValues: {},
+ uniqueValues: {},
+ statistics: {}
+ };
+ }
+
+ const headers = Object.keys(data[0]);
+ const rowCount = data.length;
+ const columnCount = headers.length;
+
+ // Analyze each column
+ const columns = headers.map(header => {
+ const values = data.map(row => row[header]);
+ const dataType = this.detectDataType(values);
+ const missingCount = this.countMissingValues(values);
+ const uniqueCount = this.countUniqueValues(values);
+
+ return {
+ name: header,
+ type: dataType,
+ missingCount: missingCount,
+ uniqueValues: uniqueCount,
+ sampleValues: values.slice(0, 5)
+ };
+ });
+
+ // Calculate statistics for numeric columns
+ const statistics = this.calculateStatistics(data, headers);
+
+ return {
+ columns: columns,
+ rowCount: rowCount,
+ columnCount: columnCount,
+ dataTypes: this.getColumnDataTypes(columns),
+ missingValues: this.getMissingValuesSummary(columns),
+ uniqueValues: this.getUniqueValuesSummary(columns),
+ statistics: statistics
+ };
+ }
+
+ /**
+ * Detect data type for a column
+ */
+ detectDataType(values) {
+ if (values.length === 0) return 'empty';
+
+ // Remove null/undefined values for analysis
+ const cleanValues = values.filter(v => v !== null && v !== undefined && v !== '');
+
+ if (cleanValues.length === 0) return 'empty';
+
+ // Check for date patterns
+ const datePatterns = [
+ /^\d{4}-\d{2}-\d{2}$/,
+ /^\d{2}\/\d{2}\/\d{4}$/,
+ /^\d{2}-\d{2}-\d{4}$/,
+ /^\d{4}\/\d{2}\/\d{2}$/
+ ];
+
+ let dateCount = 0;
+ let numericCount = 0;
+ let booleanCount = 0;
+
+ for (const value of cleanValues) {
+ const strValue = String(value).trim();
+
+ // Check for date patterns
+ if (datePatterns.some(pattern => pattern.test(strValue))) {
+ if (!isNaN(Date.parse(strValue))) {
+ dateCount++;
+ continue;
+ }
+ }
+
+ // Check for numeric values
+ if (!isNaN(parseFloat(strValue)) && isFinite(strValue)) {
+ numericCount++;
+ continue;
+ }
+
+ // Check for boolean values
+ if (['true', 'false', 'yes', 'no', '1', '0'].includes(strValue.toLowerCase())) {
+ booleanCount++;
+ continue;
+ }
+ }
+
+ // Determine type based on majority
+ const total = cleanValues.length;
+ const dateRatio = dateCount / total;
+ const numericRatio = numericCount / total;
+ const booleanRatio = booleanCount / total;
+
+ if (dateRatio > 0.8) return 'date';
+ if (numericRatio > 0.8) return 'numeric';
+ if (booleanRatio > 0.8) return 'boolean';
+
+ return 'categorical';
+ }
+
+ /**
+ * Count missing values
+ */
+ countMissingValues(values) {
+ return values.filter(v => v === null || v === undefined || v === '').length;
+ }
+
+ /**
+ * Count unique values
+ */
+ countUniqueValues(values) {
+ const unique = new Set(values.map(v => String(v)));
+ return unique.size;
+ }
+
+ /**
+ * Calculate statistics for numeric columns
+ */
+ calculateStatistics(data, headers) {
+ const stats = {};
+
+ headers.forEach(header => {
+ const values = data.map(row => parseFloat(row[header])).filter(v => !isNaN(v));
+
+ if (values.length > 0) {
+ stats[header] = {
+ min: Math.min(...values),
+ max: Math.max(...values),
+ mean: this.calculateMean(values),
+ median: this.calculateMedian(values),
+ stdDev: this.calculateStandardDeviation(values),
+ variance: this.calculateVariance(values),
+ sum: this.calculateSum(values),
+ count: values.length,
+ quartiles: this.calculateQuartiles(values)
+ };
+ }
+ });
+
+ return stats;
+ }
+
+ /**
+ * Calculate mean
+ */
+ calculateMean(values) {
+ return values.reduce((sum, val) => sum + val, 0) / values.length;
+ }
+
+ /**
+ * Calculate median
+ */
+ calculateMedian(values) {
+ const sorted = [...values].sort((a, b) => a - b);
+ const mid = Math.floor(sorted.length / 2);
+
+ if (sorted.length % 2 === 0) {
+ return (sorted[mid - 1] + sorted[mid]) / 2;
+ } else {
+ return sorted[mid];
+ }
+ }
+
+ /**
+ * Calculate standard deviation
+ */
+ calculateStandardDeviation(values) {
+ const mean = this.calculateMean(values);
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
+ return Math.sqrt(variance);
+ }
+
+ /**
+ * Calculate variance
+ */
+ calculateVariance(values) {
+ const mean = this.calculateMean(values);
+ return values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
+ }
+
+ /**
+ * Calculate sum
+ */
+ calculateSum(values) {
+ return values.reduce((sum, val) => sum + val, 0);
+ }
+
+ /**
+ * Calculate quartiles
+ */
+ calculateQuartiles(values) {
+ const sorted = [...values].sort((a, b) => a - b);
+ const q1 = this.calculatePercentile(sorted, 25);
+ const q2 = this.calculatePercentile(sorted, 50); // median
+ const q3 = this.calculatePercentile(sorted, 75);
+
+ return {
+ q1: q1,
+ q2: q2,
+ q3: q3,
+ iqr: q3 - q1
+ };
+ }
+
+ /**
+ * Calculate percentile
+ */
+ calculatePercentile(sortedValues, percentile) {
+ const index = (percentile / 100) * (sortedValues.length - 1);
+ const lower = Math.floor(index);
+ const upper = Math.ceil(index);
+ const weight = index % 1;
+
+ if (upper >= sortedValues.length) return sortedValues[sortedValues.length - 1];
+
+ return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
+ }
+
+ /**
+ * Get column data types summary
+ */
+ getColumnDataTypes(columns) {
+ const types = {};
+ columns.forEach(col => {
+ types[col.name] = col.type;
+ });
+ return types;
+ }
+
+ /**
+ * Get missing values summary
+ */
+ getMissingValuesSummary(columns) {
+ const summary = {};
+ columns.forEach(col => {
+ summary[col.name] = col.missingCount;
+ });
+ return summary;
+ }
+
+ /**
+ * Get unique values summary
+ */
+ getUniqueValuesSummary(columns) {
+ const summary = {};
+ columns.forEach(col => {
+ summary[col.name] = col.uniqueValues;
+ });
+ return summary;
+ }
+
+ /**
+ * Detect outliers using IQR method
+ */
+ detectOutliers(data, columnName, method = 'iqr') {
+ const values = data.map(row => parseFloat(row[columnName])).filter(v => !isNaN(v));
+
+ if (values.length === 0) return { outliers: [], bounds: null };
+
+ if (method === 'iqr') {
+ return this.detectOutliersIQR(values);
+ } else if (method === 'zscore') {
+ return this.detectOutliersZScore(values);
+ }
+
+ return { outliers: [], bounds: null };
+ }
+
+ /**
+ * Detect outliers using IQR method
+ */
+ detectOutliersIQR(values) {
+ const sorted = [...values].sort((a, b) => a - b);
+ const q1 = this.calculatePercentile(sorted, 25);
+ const q3 = this.calculatePercentile(sorted, 75);
+ const iqr = q3 - q1;
+
+ const lowerBound = q1 - 1.5 * iqr;
+ const upperBound = q3 + 1.5 * iqr;
+
+ const outliers = values.filter(v => v < lowerBound || v > upperBound);
+
+ return {
+ outliers: outliers,
+ bounds: { lower: lowerBound, upper: upperBound },
+ count: outliers.length
+ };
+ }
+
+ /**
+ * Detect outliers using Z-score method
+ */
+ detectOutliersZScore(values, threshold = 3) {
+ const mean = this.calculateMean(values);
+ const stdDev = this.calculateStandardDeviation(values);
+
+ const outliers = values.filter(v => Math.abs((v - mean) / stdDev) > threshold);
+
+ return {
+ outliers: outliers,
+ bounds: { mean: mean, stdDev: stdDev, threshold: threshold },
+ count: outliers.length
+ };
+ }
+
+ /**
+ * Calculate correlation between two columns
+ */
+ calculateCorrelation(data, column1, column2) {
+ const x = data.map(row => parseFloat(row[column1])).filter(v => !isNaN(v));
+ const y = data.map(row => parseFloat(row[column2])).filter(v => !isNaN(v));
+
+ if (x.length !== y.length || x.length === 0) {
+ return { correlation: 0, strength: 'none', direction: 'none' };
+ }
+
+ const n = x.length;
+ const sumX = x.reduce((a, b) => a + b, 0);
+ const sumY = y.reduce((a, b) => a + b, 0);
+ const sumXY = x.reduce((sum, val, i) => sum + val * y[i], 0);
+ const sumX2 = x.reduce((sum, val) => sum + val * val, 0);
+ const sumY2 = y.reduce((sum, val) => sum + val * val, 0);
+
+ const numerator = n * sumXY - sumX * sumY;
+ const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
+
+ if (denominator === 0) {
+ return { correlation: 0, strength: 'none', direction: 'none' };
+ }
+
+ const correlation = numerator / denominator;
+
+ return {
+ correlation: correlation,
+ strength: this.getCorrelationStrength(Math.abs(correlation)),
+ direction: correlation > 0 ? 'positive' : 'negative'
+ };
+ }
+
+ /**
+ * Get correlation strength description
+ */
+ getCorrelationStrength(value) {
+ if (value < 0.3) return 'weak';
+ if (value < 0.7) return 'moderate';
+ return 'strong';
+ }
+
+ /**
+ * Generate insights
+ */
+ generateInsights(dataStructure) {
+ const insights = [];
+
+ // Data quality insights
+ const totalMissing = Object.values(dataStructure.missingValues).reduce((sum, val) => sum + val, 0);
+ const totalCells = dataStructure.rowCount * dataStructure.columnCount;
+ const dataQuality = ((totalCells - totalMissing) / totalCells) * 100;
+
+ if (dataQuality < 80) {
+ insights.push({
+ type: 'data_quality',
+ severity: 'warning',
+ message: `Data quality is ${dataQuality.toFixed(1)}%. Consider cleaning missing values.`,
+ details: { quality: dataQuality, missing: totalMissing, total: totalCells }
+ });
+ }
+
+ // Column type insights
+ const numericColumns = dataStructure.columns.filter(col => col.type === 'numeric');
+ const categoricalColumns = dataStructure.columns.filter(col => col.type === 'categorical');
+
+ if (numericColumns.length > 1) {
+ insights.push({
+ type: 'analysis_opportunity',
+ severity: 'info',
+ message: `Found ${numericColumns.length} numeric columns. Consider correlation analysis.`,
+ details: { columns: numericColumns.map(c => c.name) }
+ });
+ }
+
+ if (categoricalColumns.length > 0) {
+ insights.push({
+ type: 'visualization_opportunity',
+ severity: 'info',
+ message: `Found ${categoricalColumns.length} categorical columns. Good for bar charts and pie charts.`,
+ details: { columns: categoricalColumns.map(c => c.name) }
+ });
+ }
+
+ // Outlier insights
+ numericColumns.forEach(col => {
+ const stats = dataStructure.statistics[col.name];
+ if (stats) {
+ const outliers = this.detectOutliers(dataStructure.rawData, col.name);
+ if (outliers.count > 0) {
+ insights.push({
+ type: 'outliers',
+ severity: 'warning',
+ message: `Column "${col.name}" has ${outliers.count} outliers.`,
+ details: { column: col.name, count: outliers.count, bounds: outliers.bounds }
+ });
+ }
+ }
+ });
+
+ return insights;
+ }
+
+ /**
+ * Perform trend analysis
+ */
+ analyzeTrends(data, timeColumn, valueColumn) {
+ const timeValues = data.map(row => row[timeColumn]);
+ const numericValues = data.map(row => parseFloat(row[valueColumn])).filter(v => !isNaN(v));
+
+ if (timeValues.length === 0 || numericValues.length === 0) {
+ return { trend: 'none', slope: 0, rSquared: 0 };
+ }
+
+ // Simple linear regression
+ const n = numericValues.length;
+ const sumX = timeValues.reduce((sum, val, i) => sum + i, 0);
+ const sumY = numericValues.reduce((sum, val) => sum + val, 0);
+ const sumXY = timeValues.reduce((sum, val, i) => sum + i * numericValues[i], 0);
+ const sumX2 = timeValues.reduce((sum, val, i) => sum + i * i, 0);
+
+ const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
+ const intercept = (sumY - slope * sumX) / n;
+
+ // Calculate R-squared
+ const yMean = sumY / n;
+ const ssRes = numericValues.reduce((sum, val, i) => {
+ const predicted = slope * i + intercept;
+ return sum + Math.pow(val - predicted, 2);
+ }, 0);
+ const ssTot = numericValues.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0);
+ const rSquared = 1 - (ssRes / ssTot);
+
+ let trend = 'none';
+ if (Math.abs(slope) > 0.1) {
+ trend = slope > 0 ? 'increasing' : 'decreasing';
+ }
+
+ return {
+ trend: trend,
+ slope: slope,
+ rSquared: rSquared,
+ intercept: intercept,
+ equation: `y = ${slope.toFixed(4)}x + ${intercept.toFixed(4)}`
+ };
+ }
+
+ /**
+ * Cache analysis results
+ */
+ cacheAnalysis(key, result) {
+ if (this.analysisCache.size >= this.maxCacheSize) {
+ // Remove oldest entry
+ const oldestKey = this.analysisCache.keys().next().value;
+ this.analysisCache.delete(oldestKey);
+ }
+
+ this.analysisCache.set(key, {
+ result: result,
+ timestamp: Date.now(),
+ size: JSON.stringify(result).length
+ });
+ }
+
+ /**
+ * Get cached analysis
+ */
+ getCachedAnalysis(key) {
+ const cached = this.analysisCache.get(key);
+ if (cached) {
+ // Check if cache is still valid (10 minutes)
+ if (Date.now() - cached.timestamp < 600000) {
+ return cached.result;
+ } else {
+ this.analysisCache.delete(key);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clear cache
+ */
+ clearCache() {
+ this.analysisCache.clear();
+ }
+
+ /**
+ * Get cache statistics
+ */
+ getCacheStats() {
+ return {
+ size: this.analysisCache.size,
+ maxSize: this.maxCacheSize,
+ entries: Array.from(this.analysisCache.keys()),
+ totalSize: Array.from(this.analysisCache.values()).reduce((sum, entry) => sum + entry.size, 0)
+ };
+ }
+
+ /**
+ * Perform comprehensive analysis
+ */
+ async performAnalysis(data, options = {}) {
+ const cacheKey = this.generateCacheKey(data, options);
+ const cached = this.getCachedAnalysis(cacheKey);
+
+ if (cached) {
+ return cached;
+ }
+
+ const startTime = Date.now();
+
+ // Perform analysis
+ const structure = this.analyzeDataStructure(data);
+ const insights = this.generateInsights(structure);
+
+ const result = {
+ structure: structure,
+ insights: insights,
+ analysisTime: Date.now() - startTime,
+ timestamp: Date.now(),
+ options: options
+ };
+
+ // Cache result
+ this.cacheAnalysis(cacheKey, result);
+
+ return result;
+ }
+
+ /**
+ * Generate cache key
+ */
+ generateCacheKey(data, options) {
+ const dataHash = this.hashData(data);
+ const optionsHash = JSON.stringify(options);
+ return `${dataHash}-${optionsHash}`;
+ }
+
+ /**
+ * Simple data hash function
+ */
+ hashData(data) {
+ const str = JSON.stringify(data.slice(0, 100)); // Hash only first 100 rows
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+ return hash.toString(36);
+ }
+
+ /**
+ * Export analysis report
+ */
+ exportAnalysisReport(analysisResult) {
+ const report = {
+ metadata: {
+ title: 'Data Analysis Report',
+ generatedAt: new Date().toISOString(),
+ version: '1.0.0'
+ },
+ summary: {
+ rowCount: analysisResult.structure.rowCount,
+ columnCount: analysisResult.structure.columnCount,
+ analysisTime: analysisResult.analysisTime
+ },
+ dataStructure: analysisResult.structure,
+ insights: analysisResult.insights,
+ recommendations: this.generateRecommendations(analysisResult)
+ };
+
+ return JSON.stringify(report, null, 2);
+ }
+
+ /**
+ * Generate recommendations based on analysis
+ */
+ generateRecommendations(analysisResult) {
+ const recommendations = [];
+
+ // Data quality recommendations
+ const dataQuality = analysisResult.insights.find(i => i.type === 'data_quality');
+ if (dataQuality && dataQuality.severity === 'warning') {
+ recommendations.push({
+ category: 'Data Quality',
+ priority: 'high',
+ action: 'Clean missing values',
+ description: 'Consider filling or removing missing data to improve analysis quality.'
+ });
+ }
+
+ // Visualization recommendations
+ const vizOpportunities = analysisResult.insights.filter(i => i.type === 'visualization_opportunity');
+ if (vizOpportunities.length > 0) {
+ recommendations.push({
+ category: 'Visualization',
+ priority: 'medium',
+ action: 'Create charts',
+ description: 'Generate visualizations for categorical data to identify patterns.'
+ });
+ }
+
+ // Analysis recommendations
+ const analysisOpportunities = analysisResult.insights.filter(i => i.type === 'analysis_opportunity');
+ if (analysisOpportunities.length > 0) {
+ recommendations.push({
+ category: 'Analysis',
+ priority: 'medium',
+ action: 'Perform correlation analysis',
+ description: 'Analyze relationships between numeric variables.'
+ });
+ }
+
+ return recommendations;
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Analytics;
+} else {
+ window.Analytics = Analytics;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/dashboard.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/dashboard.js
new file mode 100644
index 00000000000..6832c403eb6
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/dashboard.js
@@ -0,0 +1,121 @@
+/**
+ * Dashboard Module of Web Application
+ *
+ * Renders lightweight KPI and chart widgets into the dashboard section.
+ */
+
+class DashboardManager {
+ constructor(options = {}) {
+ this.options = options;
+ this.containerId = options.containerId || 'dashboardGrid';
+ this.container = document.getElementById(this.containerId);
+ this.layout = 'layout1';
+ }
+
+ setLayout(layout) {
+ this.layout = layout || 'layout1';
+ if (this.container) {
+ this.container.dataset.layout = this.layout;
+ }
+ this.emitUpdate();
+ }
+
+ render({ data = [], structure = null, chartDataUrl = null } = {}) {
+ if (!this.container) return;
+
+ const stats = this.calculateStats(data, structure);
+ const chartCard = chartDataUrl
+ ? `
+
+
+
+

+
+
+ `
+ : `
+
+
+
+ No chart rendered yet.
+
+
+ `;
+
+ const columns = (structure?.columns || [])
+ .slice(0, 8)
+ .map((c) => `${c.name}${c.type}`)
+ .join('');
+
+ this.container.innerHTML = `
+
+
+
+
+
+
Rows: ${stats.rows}
+
Columns: ${stats.columns}
+
Missing: ${stats.missing}
+
Quality: ${stats.quality}%
+
+
+
+
+
+
+
+
+ ${columns || '- No column info available.
'}
+
+
+
+
+
+ ${chartCard}
+
+
+ `;
+
+ this.emitUpdate();
+ }
+
+ calculateStats(data, structure) {
+ const rows = Array.isArray(data) ? data.length : 0;
+ const columns = structure?.columnCount || (rows ? Object.keys(data[0]).length : 0);
+
+ let missing = 0;
+ if (rows && columns) {
+ const headers = Object.keys(data[0]);
+ data.forEach((row) => {
+ headers.forEach((h) => {
+ const v = row[h];
+ if (v === null || v === undefined || v === '') missing += 1;
+ });
+ });
+ }
+
+ const totalCells = rows * columns;
+ const quality = totalCells ? Math.round(((totalCells - missing) / totalCells) * 100) : 100;
+
+ return { rows, columns, missing, quality };
+ }
+
+ exportState() {
+ return {
+ layout: this.layout,
+ html: this.container ? this.container.innerHTML : ''
+ };
+ }
+
+ emitUpdate() {
+ if (typeof this.options.onDashboardUpdated === 'function') {
+ this.options.onDashboardUpdated(this.exportState());
+ }
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = DashboardManager;
+} else {
+ window.DashboardManager = DashboardManager;
+}
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/data-processor.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/data-processor.js
new file mode 100644
index 00000000000..abf8ce342da
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/data-processor.js
@@ -0,0 +1,374 @@
+/**
+ * Data Processing Module of Web Application
+ *
+ * Handles data analysis, preprocessing, missing value detection,
+ * outlier identification, and data cleaning.
+ */
+
+class DataProcessor {
+ constructor(options = {}) {
+ this.options = options;
+ this.isProcessing = false;
+ }
+
+ /**
+ * Analyze data structure and characteristics
+ */
+ analyzeDataStructure(data) {
+ if (!data || data.length === 0) {
+ throw new Error('No data to analyze');
+ }
+
+ const headers = Object.keys(data[0]);
+ const rowCount = data.length;
+
+ const columns = headers.map(header => {
+ const values = data.map(row => row[header]);
+ return this.analyzeColumn(header, values);
+ });
+
+ const structure = {
+ rowCount,
+ columnCount: headers.length,
+ columns,
+ totalCells: rowCount * headers.length,
+ missingCells: this.countMissingValues(data),
+ dataTypes: this.detectDataTypes(data)
+ };
+
+ if (this.options.onDataAnalyzed) {
+ this.options.onDataAnalyzed(structure);
+ }
+
+ return structure;
+ }
+
+ /**
+ * Analyze individual column characteristics
+ */
+ analyzeColumn(name, values) {
+ const nonNullValues = values.filter(v => v !== null && v !== undefined && v !== '');
+ const nullCount = values.length - nonNullValues.length;
+ const uniqueValues = new Set(nonNullValues.map(v => v.toString())).size;
+
+ const dataType = this.detectColumnType(nonNullValues);
+ const statistics = this.calculateStatistics(nonNullValues, dataType);
+
+ return {
+ name,
+ type: dataType,
+ uniqueValues,
+ nullCount,
+ nullPercentage: Math.round((nullCount / values.length) * 100),
+ statistics
+ };
+ }
+
+ /**
+ * Detect column data type
+ */
+ detectColumnType(values) {
+ if (values.length === 0) return 'unknown';
+
+ // Check for date/time
+ const datePattern = /^\d{4}-\d{2}-\d{2}/;
+ const timePattern = /^\d{2}:\d{2}:\d{2}/;
+
+ for (let value of values) {
+ if (typeof value === 'string') {
+ if (datePattern.test(value) || timePattern.test(value)) {
+ return 'datetime';
+ }
+ if (!isNaN(Date.parse(value))) {
+ return 'datetime';
+ }
+ }
+ }
+
+ // Check for numeric
+ const numericValues = values.filter(v => !isNaN(v) && v !== '');
+ if (numericValues.length / values.length > 0.8) {
+ return 'numeric';
+ }
+
+ // Check for boolean
+ const booleanValues = values.filter(v =>
+ v === true || v === false ||
+ v === 'true' || v === 'false' ||
+ v === 'TRUE' || v === 'FALSE' ||
+ v === 0 || v === 1
+ );
+ if (booleanValues.length / values.length > 0.9) {
+ return 'boolean';
+ }
+
+ // Default to text
+ return 'text';
+ }
+
+ /**
+ * Calculate column statistics
+ */
+ calculateStatistics(values, dataType) {
+ if (dataType !== 'numeric' || values.length === 0) {
+ return {
+ min: null,
+ max: null,
+ mean: null,
+ median: null,
+ stdDev: null,
+ outliers: []
+ };
+ }
+
+ const numericValues = values.map(v => parseFloat(v)).filter(v => !isNaN(v));
+
+ if (numericValues.length === 0) {
+ return { min: null, max: null, mean: null, median: null, stdDev: null, outliers: [] };
+ }
+
+ numericValues.sort((a, b) => a - b);
+
+ const min = Math.min(...numericValues);
+ const max = Math.max(...numericValues);
+ const mean = this.calculateMean(numericValues);
+ const median = this.calculateMedian(numericValues);
+ const stdDev = this.calculateStdDev(numericValues, mean);
+ const outliers = this.detectOutliers(numericValues, mean, stdDev);
+
+ return {
+ min,
+ max,
+ mean,
+ median,
+ stdDev,
+ outliers
+ };
+ }
+
+ /**
+ * Calculate mean
+ */
+ calculateMean(values) {
+ return values.reduce((sum, val) => sum + val, 0) / values.length;
+ }
+
+ /**
+ * Calculate median
+ */
+ calculateMedian(values) {
+ const mid = Math.floor(values.length / 2);
+ return values.length % 2 !== 0
+ ? values[mid]
+ : (values[mid - 1] + values[mid]) / 2;
+ }
+
+ /**
+ * Calculate standard deviation
+ */
+ calculateStdDev(values, mean) {
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
+ return Math.sqrt(variance);
+ }
+
+ /**
+ * Detect outliers using IQR method
+ */
+ detectOutliers(values, mean, stdDev) {
+ // Using Z-score method for outlier detection
+ const threshold = 3;
+ const outliers = [];
+
+ values.forEach((value, index) => {
+ const zScore = Math.abs((value - mean) / stdDev);
+ if (zScore > threshold) {
+ outliers.push({
+ index,
+ value,
+ zScore
+ });
+ }
+ });
+
+ return outliers;
+ }
+
+ /**
+ * Count missing values in dataset
+ */
+ countMissingValues(data) {
+ let count = 0;
+ const headers = Object.keys(data[0]);
+
+ data.forEach(row => {
+ headers.forEach(header => {
+ if (row[header] === null || row[header] === undefined || row[header] === '') {
+ count++;
+ }
+ });
+ });
+
+ return count;
+ }
+
+ /**
+ * Detect data types for all columns
+ */
+ detectDataTypes(data) {
+ if (data.length === 0) return {};
+
+ const headers = Object.keys(data[0]);
+ const types = {};
+
+ headers.forEach(header => {
+ const values = data.map(row => row[header]);
+ types[header] = this.detectColumnType(values);
+ });
+
+ return types;
+ }
+
+ /**
+ * Clean data by handling missing values and outliers
+ */
+ cleanData(data, rules = {}) {
+ if (!data || data.length === 0) return data;
+
+ const cleanedData = JSON.parse(JSON.stringify(data)); // Deep copy
+ const headers = Object.keys(data[0]);
+
+ headers.forEach(header => {
+ const columnRules = rules[header] || {};
+ const values = cleanedData.map(row => row[header]);
+ const dataType = this.detectColumnType(values);
+
+ // Handle missing values
+ if (columnRules.handleMissing) {
+ cleanedData.forEach(row => {
+ if (row[header] === null || row[header] === undefined || row[header] === '') {
+ row[header] = this.handleMissingValue(row[header], dataType, columnRules);
+ }
+ });
+ }
+
+ // Handle outliers
+ if (columnRules.handleOutliers && dataType === 'numeric') {
+ const numericValues = values
+ .filter(v => !isNaN(v) && v !== '')
+ .map(v => parseFloat(v));
+
+ if (numericValues.length > 0) {
+ const mean = this.calculateMean(numericValues);
+ const stdDev = this.calculateStdDev(numericValues, mean);
+
+ cleanedData.forEach(row => {
+ const value = parseFloat(row[header]);
+ if (!isNaN(value)) {
+ const zScore = Math.abs((value - mean) / stdDev);
+ if (zScore > 3) { // Outlier threshold
+ row[header] = this.handleOutlier(value, mean, stdDev, columnRules);
+ }
+ }
+ });
+ }
+ }
+ });
+
+ if (this.options.onDataProcessed) {
+ this.options.onDataProcessed(cleanedData);
+ }
+
+ return cleanedData;
+ }
+
+ /**
+ * Handle missing values based on strategy
+ */
+ handleMissingValue(value, dataType, rules) {
+ const strategy = rules.missingStrategy || 'remove';
+
+ switch (strategy) {
+ case 'mean':
+ return dataType === 'numeric' ? 0 : '';
+ case 'median':
+ return dataType === 'numeric' ? 0 : '';
+ case 'mode':
+ return '';
+ case 'forward_fill':
+ return value; // Would need previous value
+ case 'backward_fill':
+ return value; // Would need next value
+ case 'remove':
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Handle outliers based on strategy
+ */
+ handleOutlier(value, mean, stdDev, rules) {
+ const strategy = rules.outlierStrategy || 'remove';
+
+ switch (strategy) {
+ case 'cap':
+ return Math.sign(value) * 3 * stdDev + mean;
+ case 'mean':
+ return mean;
+ case 'remove':
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get data quality report
+ */
+ getDataQualityReport(data) {
+ if (!data || data.length === 0) {
+ return { quality: 0, issues: ['No data available'] };
+ }
+
+ const headers = Object.keys(data[0]);
+ const totalCells = data.length * headers.length;
+ const missingCells = this.countMissingValues(data);
+ const missingPercentage = (missingCells / totalCells) * 100;
+
+ const issues = [];
+ let quality = 100;
+
+ if (missingPercentage > 10) {
+ issues.push(`High missing value rate: ${missingPercentage.toFixed(1)}%`);
+ quality -= 20;
+ } else if (missingPercentage > 5) {
+ issues.push(`Moderate missing value rate: ${missingPercentage.toFixed(1)}%`);
+ quality -= 10;
+ }
+
+ // Check for data type inconsistencies
+ headers.forEach(header => {
+ const values = data.map(row => row[header]);
+ const dataType = this.detectColumnType(values);
+
+ if (dataType === 'unknown') {
+ issues.push(`Column "${header}" has inconsistent data types`);
+ quality -= 5;
+ }
+ });
+
+ return {
+ quality: Math.max(0, quality),
+ missingPercentage,
+ issues,
+ totalRecords: data.length,
+ totalFields: headers.length
+ };
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = DataProcessor;
+} else {
+ window.DataProcessor = DataProcessor;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/exporter.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/exporter.js
new file mode 100644
index 00000000000..c785f69a340
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/exporter.js
@@ -0,0 +1,246 @@
+/**
+ * Export Module of Web Application
+ *
+ * Handles export to PDF, Excel, CSV, JSON and PNG chart image.
+ */
+
+class Exporter {
+ constructor(options = {}) {
+ this.options = options;
+ }
+
+ async export(format, options = {}) {
+ const normalizedFormat = String(format || '').toLowerCase();
+ const payload = this.preparePayload(options);
+
+ this.emitProgress(15, 'Preparing export data...');
+
+ let result;
+ switch (normalizedFormat) {
+ case 'pdf':
+ result = await this.exportPDF(payload, options);
+ break;
+ case 'excel':
+ case 'xlsx':
+ result = await this.exportExcel(payload, options);
+ break;
+ case 'csv':
+ result = await this.exportCSV(payload, options);
+ break;
+ case 'json':
+ result = await this.exportJSON(payload, options);
+ break;
+ case 'png':
+ result = await this.exportPNG(payload, options);
+ break;
+ default:
+ throw new Error(`Unsupported export format: ${format}`);
+ }
+
+ this.emitProgress(90, 'Downloading file...');
+ this.downloadBlob(result.blob, result.filename);
+ this.emitProgress(100, 'Export complete.');
+
+ if (typeof this.options.onExportComplete === 'function') {
+ this.options.onExportComplete({
+ format: normalizedFormat,
+ filename: result.filename
+ });
+ }
+
+ return result;
+ }
+
+ preparePayload(options = {}) {
+ return {
+ rawData: Array.isArray(options.rawData) ? options.rawData : [],
+ processedData: Array.isArray(options.processedData) ? options.processedData : [],
+ dataStructure: options.dataStructure || null,
+ title: options.title || 'Excel Analyzer Report',
+ chart: options.currentChart || null,
+ exportedAt: new Date()
+ };
+ }
+
+ getFileBase(options = {}) {
+ const rawBase = options.filenameBase || 'excel-analyzer-export';
+ return rawBase.replace(/[<>:"/\\|?*]+/g, '-');
+ }
+
+ getTimestamp() {
+ return new Date().toISOString().replace(/[:.]/g, '-');
+ }
+
+ async exportPDF(payload, options = {}) {
+ if (!window.jspdf || !window.jspdf.jsPDF) {
+ throw new Error('jsPDF is not available in this page.');
+ }
+
+ const doc = new window.jspdf.jsPDF();
+ const lineHeight = 7;
+ let y = 15;
+
+ doc.setFontSize(16);
+ doc.text(payload.title, 14, y);
+ y += 10;
+
+ doc.setFontSize(10);
+ doc.text(`Exported: ${payload.exportedAt.toLocaleString()}`, 14, y);
+ y += lineHeight;
+ doc.text(`Rows: ${payload.processedData.length}`, 14, y);
+ y += lineHeight;
+ doc.text(`Columns: ${payload.processedData.length ? Object.keys(payload.processedData[0]).length : 0}`, 14, y);
+ y += lineHeight * 2;
+
+ const previewRows = payload.processedData.slice(0, 20);
+ if (previewRows.length > 0) {
+ const headers = Object.keys(previewRows[0]);
+ doc.setFont(undefined, 'bold');
+ doc.text(headers.join(' | ').slice(0, 180), 14, y);
+ y += lineHeight;
+ doc.setFont(undefined, 'normal');
+
+ previewRows.forEach((row) => {
+ if (y > 280) {
+ doc.addPage();
+ y = 15;
+ }
+ const line = headers.map((h) => (row[h] ?? '')).join(' | ');
+ doc.text(String(line).slice(0, 180), 14, y);
+ y += lineHeight;
+ });
+ }
+
+ if (payload.chart && typeof payload.chart.toBase64Image === 'function') {
+ try {
+ const img = payload.chart.toBase64Image('image/png', 1);
+ doc.addPage();
+ doc.setFontSize(12);
+ doc.text('Chart Snapshot', 14, 15);
+ doc.addImage(img, 'PNG', 14, 24, 180, 100);
+ } catch (error) {
+ console.warn('Could not embed chart image in PDF.', error);
+ }
+ }
+
+ const blob = doc.output('blob');
+ const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.pdf`;
+ return { blob, filename };
+ }
+
+ async exportExcel(payload, options = {}) {
+ if (!window.XLSX) {
+ throw new Error('SheetJS (XLSX) is not available in this page.');
+ }
+
+ const workbook = window.XLSX.utils.book_new();
+ const data = payload.processedData.length ? payload.processedData : payload.rawData;
+
+ const dataSheet = window.XLSX.utils.json_to_sheet(data);
+ window.XLSX.utils.book_append_sheet(workbook, dataSheet, 'Processed Data');
+
+ const summaryRows = [
+ { Metric: 'Title', Value: payload.title },
+ { Metric: 'Exported At', Value: payload.exportedAt.toLocaleString() },
+ { Metric: 'Total Rows', Value: data.length },
+ { Metric: 'Total Columns', Value: data.length ? Object.keys(data[0]).length : 0 }
+ ];
+ const summarySheet = window.XLSX.utils.json_to_sheet(summaryRows);
+ window.XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
+
+ const arrayBuffer = window.XLSX.write(workbook, {
+ bookType: 'xlsx',
+ type: 'array'
+ });
+
+ const blob = new Blob([arrayBuffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ });
+ const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.xlsx`;
+ return { blob, filename };
+ }
+
+ async exportCSV(payload, options = {}) {
+ const data = payload.processedData.length ? payload.processedData : payload.rawData;
+ const csv = this.toCSV(data);
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
+ const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.csv`;
+ return { blob, filename };
+ }
+
+ async exportJSON(payload, options = {}) {
+ const content = JSON.stringify(
+ {
+ title: payload.title,
+ exportedAt: payload.exportedAt.toISOString(),
+ dataStructure: payload.dataStructure,
+ data: payload.processedData.length ? payload.processedData : payload.rawData
+ },
+ null,
+ 2
+ );
+
+ const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
+ const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.json`;
+ return { blob, filename };
+ }
+
+ async exportPNG(payload, options = {}) {
+ if (!payload.chart || typeof payload.chart.toBase64Image !== 'function') {
+ throw new Error('No chart is available yet for PNG export. Render a chart first.');
+ }
+
+ const dataUrl = payload.chart.toBase64Image('image/png', 1);
+ const blob = await (await fetch(dataUrl)).blob();
+ const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.png`;
+ return { blob, filename };
+ }
+
+ toCSV(rows) {
+ if (!Array.isArray(rows) || rows.length === 0) {
+ return '';
+ }
+
+ const headers = Object.keys(rows[0]);
+ const csvRows = [headers.join(',')];
+
+ rows.forEach((row) => {
+ const line = headers
+ .map((header) => {
+ const value = row[header] ?? '';
+ const text = String(value);
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
+ })
+ .join(',');
+ csvRows.push(line);
+ });
+
+ return csvRows.join('\n');
+ }
+
+ downloadBlob(blob, filename) {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ setTimeout(() => {
+ URL.revokeObjectURL(url);
+ }, 1000);
+ }
+
+ emitProgress(percentage, message) {
+ if (typeof this.options.onExportProgress === 'function') {
+ this.options.onExportProgress({ percentage, message });
+ }
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Exporter;
+} else {
+ window.Exporter = Exporter;
+}
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/file-upload.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/file-upload.js
new file mode 100644
index 00000000000..204b3b2611e
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/file-upload.js
@@ -0,0 +1,280 @@
+/**
+ * File Upload Module of Web Application
+ *
+ * Handles file input, drag-and-drop functionality, file validation,
+ * and Excel file parsing using SheetJS.
+ */
+
+class FileUploadManager {
+ constructor(options = {}) {
+ this.options = options;
+ this.dropZone = null;
+ this.fileInput = null;
+ this.selectFileBtn = null;
+ this.isProcessing = false;
+
+ this.init();
+ }
+
+ init() {
+ this.dropZone = document.getElementById('dropZone');
+ this.fileInput = document.getElementById('fileInput');
+ this.selectFileBtn = document.getElementById('selectFileBtn');
+
+ if (!this.dropZone || !this.fileInput) {
+ throw new Error('File upload elements not found in DOM');
+ }
+
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ const openFileDialog = () => {
+ if (this.isProcessing) return;
+ // Reset so selecting the same file again still triggers `change`.
+ this.fileInput.value = '';
+ this.fileInput.click();
+ };
+
+ // File input change event
+ this.fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ this.handleFile(file);
+ }
+ });
+
+ // Drag and drop events
+ this.dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ this.dropZone.classList.add('drag-over');
+ });
+
+ this.dropZone.addEventListener('dragleave', () => {
+ this.dropZone.classList.remove('drag-over');
+ });
+
+ this.dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ this.dropZone.classList.remove('drag-over');
+
+ const file = e.dataTransfer.files[0];
+ if (file) {
+ this.handleFile(file);
+ }
+ });
+
+ // Dedicated select button (prevents duplicate bubbling click behavior)
+ if (this.selectFileBtn) {
+ this.selectFileBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openFileDialog();
+ });
+ }
+
+ // Click anywhere on drop zone to upload (except button which handles itself)
+ this.dropZone.addEventListener('click', (e) => {
+ if (e.target && e.target.closest && e.target.closest('#selectFileBtn')) {
+ return;
+ }
+ openFileDialog();
+ });
+ }
+
+ async handleFile(file) {
+ if (this.isProcessing) {
+ return;
+ }
+
+ this.isProcessing = true;
+
+ try {
+ // Validate file
+ const validation = this.validateFile(file);
+ if (!validation.isValid) {
+ this.showError(validation.error);
+ this.isProcessing = false;
+ return;
+ }
+
+ // Notify file selected
+ if (this.options.onFileSelected) {
+ this.options.onFileSelected(file);
+ }
+
+ // Show progress
+ this.showProgress(0);
+
+ // Parse file
+ const data = await this.parseExcelFile(file);
+
+ // Notify file parsed
+ if (this.options.onFileParsed) {
+ this.options.onFileParsed(data);
+ }
+
+ this.showProgress(100);
+ this.hideProgress();
+
+ } catch (error) {
+ this.showError(error.message);
+ if (this.options.onError) {
+ this.options.onError(error);
+ }
+ } finally {
+ this.isProcessing = false;
+ this.fileInput.value = '';
+ }
+ }
+
+ validateFile(file) {
+ // Check file type (MIME + extension fallback)
+ const allowedTypes = [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel'
+ ];
+ const lowerName = (file.name || '').toLowerCase();
+ const hasValidExtension = lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls');
+ const hasValidMime = allowedTypes.includes(file.type);
+
+ if (!hasValidMime && !hasValidExtension) {
+ return { isValid: false, error: 'Invalid file type. Please upload an Excel file (.xlsx or .xls)' };
+ }
+
+ // Check file size (max 50MB)
+ const maxSize = 50 * 1024 * 1024; // 50MB
+ if (file.size > maxSize) {
+ return { isValid: false, error: 'File too large. Maximum file size is 50MB' };
+ }
+
+ return { isValid: true };
+ }
+
+ async parseExcelFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ try {
+ const data = new Uint8Array(e.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+
+ // Get first worksheet
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName];
+
+ // Convert to JSON
+ const jsonData = XLSX.utils.sheet_to_json(worksheet, {
+ header: 1,
+ defval: null
+ });
+
+ // Convert to array of objects
+ const result = this.convertToObjects(jsonData);
+
+ resolve(result);
+ } catch (error) {
+ reject(new Error('Failed to parse Excel file: ' + error.message));
+ }
+ };
+
+ reader.onerror = () => {
+ reject(new Error('Failed to read file'));
+ };
+
+ reader.readAsArrayBuffer(file);
+ });
+ }
+
+ convertToObjects(jsonData) {
+ if (jsonData.length === 0) {
+ return [];
+ }
+
+ // First row contains headers
+ const headers = jsonData[0];
+ const rows = jsonData.slice(1);
+
+ // Clean headers (remove empty headers and create unique names)
+ const cleanHeaders = headers.map((header, index) => {
+ if (!header || header === '') {
+ return `Column_${index + 1}`;
+ }
+ return header.toString().trim();
+ });
+
+ // Create unique column names if duplicates exist
+ const uniqueHeaders = this.makeUniqueHeaders(cleanHeaders);
+
+ // Convert rows to objects
+ return rows.map(row => {
+ const obj = {};
+ uniqueHeaders.forEach((header, index) => {
+ obj[header] = row[index];
+ });
+ return obj;
+ });
+ }
+
+ makeUniqueHeaders(headers) {
+ const seen = {};
+ return headers.map(header => {
+ if (seen[header]) {
+ const newHeader = `${header}_${seen[header]}`;
+ seen[header]++;
+ return newHeader;
+ } else {
+ seen[header] = 1;
+ return header;
+ }
+ });
+ }
+
+ showProgress(percentage) {
+ const progressEl = document.getElementById('uploadProgress');
+ const progressBarEl = progressEl ? progressEl.querySelector('.progress-bar') : null;
+
+ if (progressEl) {
+ progressEl.style.display = 'block';
+ }
+
+ if (progressBarEl) {
+ progressBarEl.style.width = `${percentage}%`;
+ progressBarEl.textContent = `${percentage}%`;
+ }
+ }
+
+ hideProgress() {
+ const progressEl = document.getElementById('uploadProgress');
+ if (progressEl) {
+ progressEl.style.display = 'none';
+ }
+ }
+
+ showError(message) {
+ const statusEl = document.getElementById('uploadStatus');
+ if (statusEl) {
+ statusEl.innerHTML = `
+
+ Error: ${message}
+
+
+ `;
+ }
+ }
+
+ clearError() {
+ const statusEl = document.getElementById('uploadStatus');
+ if (statusEl) {
+ statusEl.innerHTML = '';
+ }
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = FileUploadManager;
+} else {
+ window.FileUploadManager = FileUploadManager;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/help.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/help.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/notifications.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/notifications.js
new file mode 100644
index 00000000000..8c65085495d
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/notifications.js
@@ -0,0 +1,773 @@
+/**
+ * Notifications Module
+ *
+ * Handles user notifications, alerts, and feedback messages.
+ */
+
+class Notifications {
+ constructor(options = {}) {
+ this.options = options;
+ this.notifications = [];
+ this.maxNotifications = 50;
+ this.container = null;
+
+ this.init();
+ }
+
+ init() {
+ this.createContainer();
+ this.setupEventListeners();
+ }
+
+ /**
+ * Create notifications container
+ */
+ createContainer() {
+ this.container = document.createElement('div');
+ this.container.className = 'notifications-container';
+ this.container.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 400px;
+ `;
+
+ document.body.appendChild(this.container);
+ }
+
+ /**
+ * Setup event listeners
+ */
+ setupEventListeners() {
+ // Auto-dismiss notifications after timeout
+ setInterval(() => {
+ this.notifications.forEach(notification => {
+ if (notification.autoDismiss && Date.now() - notification.timestamp > notification.timeout) {
+ this.removeNotification(notification.id);
+ }
+ });
+ }, 1000);
+ }
+
+ /**
+ * Show success notification
+ */
+ success(message, options = {}) {
+ return this.show({
+ type: 'success',
+ message: message,
+ ...options
+ });
+ }
+
+ /**
+ * Show error notification
+ */
+ error(message, options = {}) {
+ return this.show({
+ type: 'error',
+ message: message,
+ ...options
+ });
+ }
+
+ /**
+ * Show warning notification
+ */
+ warning(message, options = {}) {
+ return this.show({
+ type: 'warning',
+ message: message,
+ ...options
+ });
+ }
+
+ /**
+ * Show info notification
+ */
+ info(message, options = {}) {
+ return this.show({
+ type: 'info',
+ message: message,
+ ...options
+ });
+ }
+
+ /**
+ * Show notification
+ */
+ show(notificationData) {
+ const notification = {
+ id: this.generateId(),
+ type: notificationData.type || 'info',
+ message: notificationData.message || '',
+ title: notificationData.title || this.getNotificationTitle(notificationData.type),
+ timestamp: Date.now(),
+ autoDismiss: notificationData.autoDismiss !== false,
+ timeout: notificationData.timeout || this.getTimeoutForType(notificationData.type),
+ persistent: notificationData.persistent || false,
+ actions: notificationData.actions || [],
+ onDismiss: notificationData.onDismiss || null
+ };
+
+ this.notifications.push(notification);
+ this.renderNotification(notification);
+
+ // Limit notifications
+ if (this.notifications.length > this.maxNotifications) {
+ const oldest = this.notifications.shift();
+ this.removeNotification(oldest.id);
+ }
+
+ return notification.id;
+ }
+
+ /**
+ * Render notification element
+ */
+ renderNotification(notification) {
+ const element = document.createElement('div');
+ element.className = `notification notification-${notification.type}`;
+ element.dataset.notificationId = notification.id;
+ element.style.cssText = `
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-lg);
+ padding: 15px;
+ min-width: 250px;
+ max-width: 400px;
+ animation: slideInRight 0.3s ease-out;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ `;
+
+ // Icon
+ const icon = document.createElement('div');
+ icon.className = 'notification-icon';
+ icon.innerHTML = this.getNotificationIcon(notification.type);
+ icon.style.cssText = `
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ font-size: 18px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `;
+
+ // Content
+ const content = document.createElement('div');
+ content.className = 'notification-content';
+ content.style.cssText = `
+ margin-left: 35px;
+ min-height: 24px;
+ `;
+
+ const title = document.createElement('div');
+ title.className = 'notification-title';
+ title.textContent = notification.title;
+ title.style.cssText = `
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+ `;
+
+ const message = document.createElement('div');
+ message.className = 'notification-message';
+ message.textContent = notification.message;
+ message.style.cssText = `
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1.4;
+ `;
+
+ content.appendChild(title);
+ content.appendChild(message);
+
+ // Actions
+ if (notification.actions.length > 0) {
+ const actions = document.createElement('div');
+ actions.className = 'notification-actions';
+ actions.style.cssText = `
+ margin-left: 35px;
+ display: flex;
+ gap: 8px;
+ margin-top: 8px;
+ `;
+
+ notification.actions.forEach(action => {
+ const button = document.createElement('button');
+ button.className = 'btn btn-sm btn-outline-secondary';
+ button.textContent = action.label;
+ button.style.cssText = `
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background: transparent;
+ color: var(--text-secondary);
+ cursor: pointer;
+ `;
+
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (action.onClick) {
+ action.onClick(notification);
+ }
+ this.removeNotification(notification.id);
+ });
+
+ actions.appendChild(button);
+ });
+
+ content.appendChild(actions);
+ }
+
+ // Dismiss button
+ const dismissBtn = document.createElement('button');
+ dismissBtn.className = 'notification-dismiss';
+ dismissBtn.innerHTML = '×';
+ dismissBtn.style.cssText = `
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: transparent;
+ border: none;
+ font-size: 20px;
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 0;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: background-color 0.2s;
+ `;
+
+ dismissBtn.addEventListener('mouseenter', () => {
+ dismissBtn.style.backgroundColor = 'var(--bg-secondary)';
+ });
+
+ dismissBtn.addEventListener('mouseleave', () => {
+ dismissBtn.style.backgroundColor = 'transparent';
+ });
+
+ dismissBtn.addEventListener('click', () => {
+ this.removeNotification(notification.id);
+ });
+
+ element.appendChild(icon);
+ element.appendChild(content);
+ element.appendChild(dismissBtn);
+
+ this.container.appendChild(element);
+
+ // Trigger animation
+ setTimeout(() => {
+ element.style.animation = 'none';
+ }, 300);
+ }
+
+ /**
+ * Remove notification
+ */
+ removeNotification(id) {
+ const index = this.notifications.findIndex(n => n.id === id);
+ if (index === -1) return;
+
+ const notification = this.notifications[index];
+ const element = this.container.querySelector(`[data-notification-id="${id}"]`);
+
+ if (element) {
+ element.style.animation = 'slideOutRight 0.3s ease-out';
+ setTimeout(() => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ }, 300);
+ }
+
+ this.notifications.splice(index, 1);
+
+ // Call dismiss callback
+ if (notification.onDismiss) {
+ notification.onDismiss(notification);
+ }
+ }
+
+ /**
+ * Clear all notifications
+ */
+ clear() {
+ this.notifications.forEach(notification => {
+ this.removeNotification(notification.id);
+ });
+ }
+
+ /**
+ * Get notification title
+ */
+ getNotificationTitle(type) {
+ const titles = {
+ success: 'Success',
+ error: 'Error',
+ warning: 'Warning',
+ info: 'Information'
+ };
+ return titles[type] || 'Notification';
+ }
+
+ /**
+ * Get notification icon
+ */
+ getNotificationIcon(type) {
+ const icons = {
+ success: '✓',
+ error: '✗',
+ warning: '⚠',
+ info: 'ℹ'
+ };
+ return icons[type] || 'ℹ';
+ }
+
+ /**
+ * Get timeout for notification type
+ */
+ getTimeoutForType(type) {
+ const timeouts = {
+ success: 3000,
+ error: 5000,
+ warning: 4000,
+ info: 3000
+ };
+ return timeouts[type] || 3000;
+ }
+
+ /**
+ * Generate unique ID
+ */
+ generateId() {
+ return 'notification-' + Math.random().toString(36).substr(2, 9);
+ }
+
+ /**
+ * Show loading notification
+ */
+ loading(message, options = {}) {
+ const notification = {
+ id: this.generateId(),
+ type: 'loading',
+ message: message,
+ title: 'Processing',
+ timestamp: Date.now(),
+ autoDismiss: false,
+ persistent: true,
+ actions: [],
+ onDismiss: null
+ };
+
+ this.notifications.push(notification);
+ this.renderLoadingNotification(notification);
+
+ return notification.id;
+ }
+
+ /**
+ * Render loading notification
+ */
+ renderLoadingNotification(notification) {
+ const element = document.createElement('div');
+ element.className = `notification notification-loading`;
+ element.dataset.notificationId = notification.id;
+ element.style.cssText = `
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-lg);
+ padding: 15px;
+ min-width: 250px;
+ max-width: 400px;
+ animation: slideInRight 0.3s ease-out;
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ `;
+
+ // Spinner
+ const spinner = document.createElement('div');
+ spinner.className = 'notification-spinner';
+ spinner.style.cssText = `
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--border-color);
+ border-top: 2px solid var(--primary-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ `;
+
+ // Content
+ const content = document.createElement('div');
+ content.className = 'notification-content';
+ content.style.cssText = `
+ flex: 1;
+ font-size: 14px;
+ color: var(--text-primary);
+ `;
+
+ content.textContent = notification.message;
+
+ element.appendChild(spinner);
+ element.appendChild(content);
+
+ this.container.appendChild(element);
+ }
+
+ /**
+ * Update loading notification
+ */
+ updateLoading(id, message) {
+ const element = this.container.querySelector(`[data-notification-id="${id}"]`);
+ if (element) {
+ const content = element.querySelector('.notification-content');
+ if (content) {
+ content.textContent = message;
+ }
+ }
+ }
+
+ /**
+ * Complete loading notification
+ */
+ completeLoading(id, success = true, message = null) {
+ const element = this.container.querySelector(`[data-notification-id="${id}"]`);
+ if (element) {
+ const type = success ? 'success' : 'error';
+ const title = success ? 'Completed' : 'Failed';
+ const icon = success ? '✓' : '✗';
+
+ // Update content
+ const content = element.querySelector('.notification-content');
+ if (content) {
+ content.textContent = message || (success ? 'Operation completed successfully' : 'Operation failed');
+ }
+
+ // Update styles
+ element.className = `notification notification-${type}`;
+ element.style.borderColor = getComputedStyle(document.documentElement).getPropertyValue(`--${type}-color`);
+
+ // Update spinner to icon
+ const spinner = element.querySelector('.notification-spinner');
+ if (spinner) {
+ spinner.outerHTML = `${icon}
`;
+ }
+
+ // Auto-dismiss after delay
+ setTimeout(() => {
+ this.removeNotification(id);
+ }, success ? 2000 : 4000);
+ }
+ }
+
+ /**
+ * Show confirmation dialog
+ */
+ confirm(message, options = {}) {
+ return new Promise((resolve) => {
+ const notification = {
+ id: this.generateId(),
+ type: 'confirm',
+ message: message,
+ title: options.title || 'Confirm Action',
+ timestamp: Date.now(),
+ autoDismiss: false,
+ persistent: true,
+ actions: [
+ {
+ label: options.cancelLabel || 'Cancel',
+ onClick: () => resolve(false)
+ },
+ {
+ label: options.confirmLabel || 'Confirm',
+ onClick: () => resolve(true)
+ }
+ ],
+ onDismiss: () => resolve(false)
+ };
+
+ this.notifications.push(notification);
+ this.renderNotification(notification);
+ });
+ }
+
+ /**
+ * Show prompt dialog
+ */
+ prompt(message, options = {}) {
+ return new Promise((resolve) => {
+ const notification = {
+ id: this.generateId(),
+ type: 'prompt',
+ message: message,
+ title: options.title || 'Input Required',
+ timestamp: Date.now(),
+ autoDismiss: false,
+ persistent: true,
+ actions: [
+ {
+ label: options.cancelLabel || 'Cancel',
+ onClick: () => resolve(null)
+ },
+ {
+ label: options.confirmLabel || 'Submit',
+ onClick: (notification) => {
+ const input = document.querySelector(`[data-notification-id="${notification.id}"] input`);
+ resolve(input ? input.value : null);
+ }
+ }
+ ],
+ onDismiss: () => resolve(null)
+ };
+
+ this.notifications.push(notification);
+ this.renderPromptNotification(notification);
+ });
+ }
+
+ /**
+ * Render prompt notification
+ */
+ renderPromptNotification(notification) {
+ const element = document.createElement('div');
+ element.className = `notification notification-prompt`;
+ element.dataset.notificationId = notification.id;
+ element.style.cssText = `
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-lg);
+ padding: 15px;
+ min-width: 300px;
+ max-width: 400px;
+ animation: slideInRight 0.3s ease-out;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ `;
+
+ // Title
+ const title = document.createElement('div');
+ title.className = 'notification-title';
+ title.textContent = notification.title;
+ title.style.cssText = `
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-primary);
+ `;
+
+ // Message
+ const message = document.createElement('div');
+ message.className = 'notification-message';
+ message.textContent = notification.message;
+ message.style.cssText = `
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1.4;
+ `;
+
+ // Input
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.placeholder = 'Enter your response...';
+ input.style.cssText = `
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 14px;
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ `;
+
+ // Actions
+ const actions = document.createElement('div');
+ actions.className = 'notification-actions';
+ actions.style.cssText = `
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ `;
+
+ notification.actions.forEach(action => {
+ const button = document.createElement('button');
+ button.className = 'btn btn-sm';
+ button.textContent = action.label;
+ button.style.cssText = `
+ font-size: 12px;
+ padding: 6px 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background: ${action.label.toLowerCase().includes('cancel') ? 'transparent' : 'var(--primary-color)'};
+ color: ${action.label.toLowerCase().includes('cancel') ? 'var(--text-secondary)' : 'white'};
+ cursor: pointer;
+ `;
+
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (action.onClick) {
+ action.onClick(notification);
+ }
+ this.removeNotification(notification.id);
+ });
+
+ actions.appendChild(button);
+ });
+
+ element.appendChild(title);
+ element.appendChild(message);
+ element.appendChild(input);
+ element.appendChild(actions);
+
+ this.container.appendChild(element);
+
+ // Focus input
+ setTimeout(() => input.focus(), 100);
+ }
+
+ /**
+ * Get notification statistics
+ */
+ getStats() {
+ const stats = {
+ total: this.notifications.length,
+ byType: {},
+ oldest: null,
+ newest: null
+ };
+
+ this.notifications.forEach(notification => {
+ stats.byType[notification.type] = (stats.byType[notification.type] || 0) + 1;
+ });
+
+ if (this.notifications.length > 0) {
+ stats.oldest = this.notifications[0];
+ stats.newest = this.notifications[this.notifications.length - 1];
+ }
+
+ return stats;
+ }
+
+ /**
+ * Export notifications
+ */
+ exportNotifications() {
+ return {
+ notifications: this.notifications,
+ stats: this.getStats(),
+ exportedAt: new Date().toISOString()
+ };
+ }
+}
+
+// Add CSS animations
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes slideInRight {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+
+ @keyframes slideOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ .notifications-container {
+ --primary-color: #007bff;
+ --secondary-color: #6c757d;
+ --success-color: #28a745;
+ --danger-color: #dc3545;
+ --warning-color: #ffc107;
+ --info-color: #17a2b8;
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8f9fa;
+ --text-primary: #212529;
+ --text-secondary: #6c757d;
+ --text-muted: #6c757d;
+ --border-color: #dee2e6;
+ --shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ --border-radius: 0.375rem;
+ }
+
+ .notification {
+ transition: all 0.3s ease;
+ }
+
+ .notification:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
+ }
+
+ .notification.notification-success {
+ border-color: var(--success-color);
+ }
+
+ .notification.notification-error {
+ border-color: var(--danger-color);
+ }
+
+ .notification.notification-warning {
+ border-color: var(--warning-color);
+ }
+
+ .notification.notification-info {
+ border-color: var(--info-color);
+ }
+
+ .notification.notification-loading {
+ border-color: var(--primary-color);
+ }
+
+ .notification.notification-confirm,
+ .notification.notification-prompt {
+ border-color: var(--info-color);
+ }
+`;
+document.head.appendChild(style);
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Notifications;
+} else {
+ window.Notifications = Notifications;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/settings.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/settings.js
new file mode 100644
index 00000000000..41065e60e66
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/settings.js
@@ -0,0 +1,971 @@
+/**
+ * Settings Module
+ *
+ * Manages application settings, user preferences, and configuration.
+ */
+
+class Settings {
+ constructor(options = {}) {
+ this.options = options;
+ this.settings = {};
+ this.defaultSettings = {
+ // File Upload Settings
+ fileUpload: {
+ maxFileSize: 50 * 1024 * 1024, // 50MB
+ enableDragDrop: true,
+ enableMultiple: false,
+ previewRows: 100
+ },
+
+ // Data Processing Settings
+ dataProcessing: {
+ defaultMissingValueHandling: 'remove',
+ defaultOutlierHandling: 'remove',
+ outlierThreshold: 3,
+ autoDetectDataTypes: true,
+ enableDataValidation: true,
+ maxPreviewRows: 100,
+ enableCaching: true,
+ cacheTimeout: 300000 // 5 minutes
+ },
+
+ // Visualization Settings
+ visualization: {
+ defaultChartType: 'auto',
+ chartColors: [
+ '#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8',
+ '#6f42c1', '#20c997', '#fd7e14', '#e83e8c', '#6c757d'
+ ],
+ chartAnimation: true,
+ chartResponsive: true,
+ chartLegendPosition: 'top',
+ chartTooltip: true,
+ chartHeight: 400,
+ chartWidth: 600,
+ enableZoom: true,
+ enablePan: true
+ },
+
+ // Dashboard Settings
+ dashboard: {
+ defaultLayout: 'grid',
+ maxWidgets: 10,
+ enableWidgetResize: true,
+ enableWidgetDrag: true,
+ autoSaveDashboard: true,
+ defaultWidgetSize: {
+ width: 4,
+ height: 3
+ },
+ gridColumns: 12,
+ gridRows: 12,
+ widgetMargin: 10
+ },
+
+ // Export Settings
+ export: {
+ defaultFormat: 'pdf',
+ includeChartsInExport: true,
+ includeDataInExport: true,
+ includeSummaryInExport: true,
+ exportImageQuality: 'high',
+ pdfPageSize: 'A4',
+ pdfOrientation: 'portrait',
+ excelSheetName: 'Processed Data',
+ csvDelimiter: ',',
+ jsonIndent: 2
+ },
+
+ // UI Settings
+ ui: {
+ theme: 'light',
+ language: 'en',
+ enableTooltips: true,
+ enableAnimations: true,
+ fontSize: 'medium',
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial',
+ borderRadius: '0.375rem',
+ boxShadow: '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
+ transitionDuration: '0.15s'
+ },
+
+ // Performance Settings
+ performance: {
+ enableVirtualization: true,
+ maxPreviewRows: 100,
+ enableCaching: true,
+ cacheTimeout: 300000, // 5 minutes
+ debounceDelay: 300,
+ throttleDelay: 1000,
+ maxConcurrentOperations: 3
+ },
+
+ // Privacy Settings
+ privacy: {
+ enableAnalytics: false,
+ saveUserPreferences: true,
+ clearDataOnExit: false,
+ storeFileData: false,
+ storeProcessingHistory: false
+ }
+ };
+
+ this.init();
+ }
+
+ init() {
+ this.loadSettings();
+ this.setupEventListeners();
+ }
+
+ /**
+ * Load settings from localStorage
+ */
+ loadSettings() {
+ try {
+ const savedSettings = localStorage.getItem('excel-analyzer-settings');
+ if (savedSettings) {
+ const parsed = JSON.parse(savedSettings);
+ this.settings = this.mergeSettings(this.defaultSettings, parsed);
+ } else {
+ this.settings = { ...this.defaultSettings };
+ }
+ } catch (error) {
+ console.warn('Failed to load settings, using defaults:', error);
+ this.settings = { ...this.defaultSettings };
+ }
+ }
+
+ /**
+ * Save settings to localStorage
+ */
+ saveSettings() {
+ if (this.settings.privacy.saveUserPreferences) {
+ localStorage.setItem('excel-analyzer-settings', JSON.stringify(this.settings));
+ }
+ }
+
+ /**
+ * Get setting value
+ */
+ get(path, defaultValue = null) {
+ const keys = path.split('.');
+ let current = this.settings;
+
+ for (const key of keys) {
+ if (current && typeof current === 'object' && key in current) {
+ current = current[key];
+ } else {
+ return defaultValue;
+ }
+ }
+
+ return current;
+ }
+
+ /**
+ * Set setting value
+ */
+ set(path, value) {
+ const keys = path.split('.');
+ let current = this.settings;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!(key in current)) {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+
+ current[keys[keys.length - 1]] = value;
+
+ // Save to localStorage
+ this.saveSettings();
+
+ // Notify change
+ this.notifyChange(path, value);
+ }
+
+ /**
+ * Reset settings to defaults
+ */
+ reset() {
+ this.settings = { ...this.defaultSettings };
+ this.saveSettings();
+
+ // Notify reset
+ this.notifyReset();
+ }
+
+ /**
+ * Get all settings
+ */
+ getAll() {
+ return { ...this.settings };
+ }
+
+ /**
+ * Get settings section
+ */
+ getSection(section) {
+ return this.settings[section] || {};
+ }
+
+ /**
+ * Update settings section
+ */
+ updateSection(section, values) {
+ if (!this.settings[section]) {
+ this.settings[section] = {};
+ }
+
+ this.settings[section] = { ...this.settings[section], ...values };
+ this.saveSettings();
+
+ // Notify change
+ this.notifyChange(section, this.settings[section]);
+ }
+
+ /**
+ * Merge settings with defaults
+ */
+ mergeSettings(defaults, userSettings) {
+ const result = { ...defaults };
+
+ for (const [key, value] of Object.entries(userSettings)) {
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ result[key] = this.mergeSettings(result[key] || {}, value);
+ } else {
+ result[key] = value;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Setup event listeners
+ */
+ setupEventListeners() {
+ // Listen for theme changes
+ window.addEventListener('theme:changed', (event) => {
+ this.set('ui.theme', event.detail.theme);
+ });
+ }
+
+ /**
+ * Notify setting change
+ */
+ notifyChange(path, value) {
+ const event = new CustomEvent('settings:changed', {
+ detail: {
+ path: path,
+ value: value,
+ timestamp: Date.now()
+ }
+ });
+
+ window.dispatchEvent(event);
+
+ // Call callback if provided
+ if (this.options.onSettingChanged) {
+ this.options.onSettingChanged(path, value);
+ }
+ }
+
+ /**
+ * Notify settings reset
+ */
+ notifyReset() {
+ const event = new CustomEvent('settings:reset', {
+ detail: {
+ timestamp: Date.now()
+ }
+ });
+
+ window.dispatchEvent(event);
+
+ // Call callback if provided
+ if (this.options.onSettingsReset) {
+ this.options.onSettingsReset();
+ }
+ }
+
+ /**
+ * Create settings UI
+ */
+ createSettingsUI(container) {
+ if (!container) return;
+
+ const settingsHTML = `
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ ${this.generateGeneralTab()}
+ ${this.generateUploadTab()}
+ ${this.generateProcessingTab()}
+ ${this.generateVisualizationTab()}
+ ${this.generateExportTab()}
+ ${this.generatePrivacyTab()}
+
+
+ `;
+
+ container.innerHTML = settingsHTML;
+
+ // Setup tab interactions
+ this.setupSettingsTabs();
+
+ // Setup form interactions
+ this.setupSettingsForms();
+
+ return container;
+ }
+
+ /**
+ * Generate general settings tab
+ */
+ generateGeneralTab() {
+ const theme = this.get('ui.theme');
+ const language = this.get('ui.language');
+ const fontSize = this.get('ui.fontSize');
+ const enableTooltips = this.get('ui.enableTooltips');
+ const enableAnimations = this.get('ui.enableAnimations');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Generate upload settings tab
+ */
+ generateUploadTab() {
+ const maxFileSize = this.get('fileUpload.maxFileSize');
+ const enableDragDrop = this.get('fileUpload.enableDragDrop');
+ const enableMultiple = this.get('fileUpload.enableMultiple');
+ const previewRows = this.get('fileUpload.previewRows');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Generate processing settings tab
+ */
+ generateProcessingTab() {
+ const autoDetectDataTypes = this.get('dataProcessing.autoDetectDataTypes');
+ const enableDataValidation = this.get('dataProcessing.enableDataValidation');
+ const enableCaching = this.get('dataProcessing.enableCaching');
+ const maxPreviewRows = this.get('dataProcessing.maxPreviewRows');
+ const outlierThreshold = this.get('dataProcessing.outlierThreshold');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Generate visualization settings tab
+ */
+ generateVisualizationTab() {
+ const defaultChartType = this.get('visualization.defaultChartType');
+ const chartAnimation = this.get('visualization.chartAnimation');
+ const chartResponsive = this.get('visualization.chartResponsive');
+ const chartTooltip = this.get('visualization.chartTooltip');
+ const enableZoom = this.get('visualization.enableZoom');
+ const enablePan = this.get('visualization.enablePan');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Generate export settings tab
+ */
+ generateExportTab() {
+ const defaultFormat = this.get('export.defaultFormat');
+ const includeChartsInExport = this.get('export.includeChartsInExport');
+ const includeDataInExport = this.get('export.includeDataInExport');
+ const includeSummaryInExport = this.get('export.includeSummaryInExport');
+ const exportImageQuality = this.get('export.exportImageQuality');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Generate privacy settings tab
+ */
+ generatePrivacyTab() {
+ const enableAnalytics = this.get('privacy.enableAnalytics');
+ const saveUserPreferences = this.get('privacy.saveUserPreferences');
+ const clearDataOnExit = this.get('privacy.clearDataOnExit');
+ const storeFileData = this.get('privacy.storeFileData');
+ const storeProcessingHistory = this.get('privacy.storeProcessingHistory');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Privacy Notice
+
These settings control how your data is stored and processed. Disabling certain features may affect application functionality.
+
+
+ `;
+ }
+
+ /**
+ * Setup settings tabs
+ */
+ setupSettingsTabs() {
+ const tabs = document.querySelectorAll('.nav-link');
+ tabs.forEach(tab => {
+ tab.addEventListener('shown.bs.tab', (e) => {
+ // Handle tab-specific logic if needed
+ });
+ });
+ }
+
+ /**
+ * Setup settings forms
+ */
+ setupSettingsForms() {
+ // General settings
+ const themeSelect = document.getElementById('themeSelect');
+ if (themeSelect) {
+ themeSelect.addEventListener('change', (e) => {
+ this.set('ui.theme', e.target.value);
+ if (this.options.themeManager) {
+ this.options.themeManager.setTheme(e.target.value);
+ }
+ });
+ }
+
+ const languageSelect = document.getElementById('languageSelect');
+ if (languageSelect) {
+ languageSelect.addEventListener('change', (e) => {
+ this.set('ui.language', e.target.value);
+ });
+ }
+
+ const fontSizeSelect = document.getElementById('fontSizeSelect');
+ if (fontSizeSelect) {
+ fontSizeSelect.addEventListener('change', (e) => {
+ this.set('ui.fontSize', e.target.value);
+ });
+ }
+
+ // Boolean settings
+ const booleanSettings = [
+ { id: 'enableTooltips', path: 'ui.enableTooltips' },
+ { id: 'enableAnimations', path: 'ui.enableAnimations' },
+ { id: 'enableDragDrop', path: 'fileUpload.enableDragDrop' },
+ { id: 'enableMultiple', path: 'fileUpload.enableMultiple' },
+ { id: 'autoDetectDataTypes', path: 'dataProcessing.autoDetectDataTypes' },
+ { id: 'enableDataValidation', path: 'dataProcessing.enableDataValidation' },
+ { id: 'enableCaching', path: 'dataProcessing.enableCaching' },
+ { id: 'chartAnimation', path: 'visualization.chartAnimation' },
+ { id: 'chartResponsive', path: 'visualization.chartResponsive' },
+ { id: 'chartTooltip', path: 'visualization.chartTooltip' },
+ { id: 'enableZoom', path: 'visualization.enableZoom' },
+ { id: 'enablePan', path: 'visualization.enablePan' },
+ { id: 'includeChartsInExport', path: 'export.includeChartsInExport' },
+ { id: 'includeDataInExport', path: 'export.includeDataInExport' },
+ { id: 'includeSummaryInExport', path: 'export.includeSummaryInExport' },
+ { id: 'enableAnalytics', path: 'privacy.enableAnalytics' },
+ { id: 'saveUserPreferences', path: 'privacy.saveUserPreferences' },
+ { id: 'clearDataOnExit', path: 'privacy.clearDataOnExit' },
+ { id: 'storeFileData', path: 'privacy.storeFileData' },
+ { id: 'storeProcessingHistory', path: 'privacy.storeProcessingHistory' }
+ ];
+
+ booleanSettings.forEach(setting => {
+ const element = document.getElementById(setting.id);
+ if (element) {
+ element.addEventListener('change', (e) => {
+ this.set(setting.path, e.target.checked);
+ });
+ }
+ });
+
+ // Number settings
+ const numberSettings = [
+ { id: 'maxFileSize', path: 'fileUpload.maxFileSize', multiplier: 1024 * 1024 },
+ { id: 'previewRows', path: 'fileUpload.previewRows' },
+ { id: 'outlierThreshold', path: 'dataProcessing.outlierThreshold' },
+ { id: 'maxPreviewRows', path: 'dataProcessing.maxPreviewRows' },
+ { id: 'chartHeight', path: 'visualization.chartHeight' },
+ { id: 'chartWidth', path: 'visualization.chartWidth' }
+ ];
+
+ numberSettings.forEach(setting => {
+ const element = document.getElementById(setting.id);
+ if (element) {
+ element.addEventListener('change', (e) => {
+ let value = parseInt(e.target.value);
+ if (setting.multiplier) {
+ value = value * setting.multiplier;
+ }
+ this.set(setting.path, value);
+ });
+ }
+ });
+
+ // Select settings
+ const selectSettings = [
+ { id: 'defaultChartType', path: 'visualization.defaultChartType' },
+ { id: 'defaultExportFormat', path: 'export.defaultFormat' },
+ { id: 'exportImageQuality', path: 'export.exportImageQuality' }
+ ];
+
+ selectSettings.forEach(setting => {
+ const element = document.getElementById(setting.id);
+ if (element) {
+ element.addEventListener('change', (e) => {
+ this.set(setting.path, e.target.value);
+ });
+ }
+ });
+
+ // Reset and save buttons
+ const resetBtn = document.getElementById('resetSettingsBtn');
+ if (resetBtn) {
+ resetBtn.addEventListener('click', () => {
+ if (confirm('Are you sure you want to reset all settings to defaults?')) {
+ this.reset();
+ this.updateSettingsUI();
+ }
+ });
+ }
+
+ const saveBtn = document.getElementById('saveSettingsBtn');
+ if (saveBtn) {
+ saveBtn.addEventListener('click', () => {
+ this.saveSettings();
+ alert('Settings saved successfully!');
+ });
+ }
+ }
+
+ /**
+ * Update settings UI with current values
+ */
+ updateSettingsUI() {
+ // This would update all form elements with current setting values
+ // Implementation would depend on the specific UI structure
+ }
+
+ /**
+ * Export settings
+ */
+ exportSettings() {
+ const settingsData = {
+ settings: this.getAll(),
+ exportedAt: new Date().toISOString(),
+ version: '1.0.0'
+ };
+
+ return JSON.stringify(settingsData, null, 2);
+ }
+
+ /**
+ * Import settings
+ */
+ importSettings(settingsData) {
+ try {
+ const parsed = typeof settingsData === 'string' ? JSON.parse(settingsData) : settingsData;
+ if (parsed.settings) {
+ this.settings = this.mergeSettings(this.defaultSettings, parsed.settings);
+ this.saveSettings();
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('Failed to import settings:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Get settings statistics
+ */
+ getSettingsStats() {
+ return {
+ totalSettings: Object.keys(this.settings).length,
+ sections: Object.keys(this.settings),
+ lastModified: localStorage.getItem('excel-analyzer-settings') ?
+ new Date(JSON.parse(localStorage.getItem('excel-analyzer-settings')).timestamp || Date.now()).toISOString() :
+ 'Never'
+ };
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Settings;
+} else {
+ window.Settings = Settings;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/visualizer.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/visualizer.js
new file mode 100644
index 00000000000..2023e052a78
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/modules/visualizer.js
@@ -0,0 +1,591 @@
+/**
+ * Visualization Module
+ *
+ * Handles chart rendering, interactive visualizations, and chart management.
+ */
+
+class Visualizer {
+ constructor(options = {}) {
+ this.options = options;
+ this.charts = new Map();
+ this.currentChart = null;
+ this.chartContainer = null;
+ this.chartCanvas = null;
+
+ this.init();
+ }
+
+ init() {
+ this.chartContainer = document.getElementById('analysisChart');
+ if (!this.chartContainer) {
+ throw new Error('Chart container not found');
+ }
+
+ this.setupChartContainer();
+ }
+
+ setupChartContainer() {
+ // Ensure canvas is properly sized
+ this.chartContainer.style.width = '100%';
+ this.chartContainer.style.height = '400px';
+
+ // Setup responsive behavior
+ this.setupResponsiveChart();
+ }
+
+ /**
+ * Render chart
+ */
+ async renderChart(data, config = {}) {
+ try {
+ // Determine chart type
+ const chartType = this.selectChartType(data, config);
+
+ // Prepare chart data
+ const chartData = this.prepareChartData(data, chartType, config);
+
+ // Prepare chart options
+ const chartOptions = this.prepareChartOptions(chartType, config);
+
+ // Create chart
+ const chart = await this.createChart(chartType, chartData, chartOptions);
+
+ // Store chart
+ this.charts.set(chart.id, chart);
+ this.currentChart = chart;
+
+ // Notify completion
+ if (this.options.onChartRendered) {
+ this.options.onChartRendered(chart);
+ }
+
+ return chart;
+
+ } catch (error) {
+ console.error('Chart rendering failed:', error);
+ if (this.options.onError) {
+ this.options.onError(error);
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * Update existing chart
+ */
+ async updateChart(chartId, newData, config = {}) {
+ try {
+ const chart = this.charts.get(chartId);
+ if (!chart) {
+ throw new Error('Chart not found');
+ }
+
+ // Update chart data
+ const chartData = this.prepareChartData(newData, chart.config.type, config);
+ chart.data = chartData;
+
+ // Update chart options if provided
+ if (Object.keys(config).length > 0) {
+ const chartOptions = this.prepareChartOptions(chart.config.type, config);
+ chart.options = { ...chart.options, ...chartOptions };
+ }
+
+ // Update chart
+ chart.update();
+
+ // Notify update
+ if (this.options.onChartUpdated) {
+ this.options.onChartUpdated(chart);
+ }
+
+ return chart;
+
+ } catch (error) {
+ console.error('Chart update failed:', error);
+ if (this.options.onError) {
+ this.options.onError(error);
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * Select chart type based on data
+ */
+ selectChartType(data, config = {}) {
+ // If explicitly specified, use that
+ if (config.type && config.type !== 'auto') {
+ return config.type;
+ }
+
+ if (!data || data.length === 0) {
+ return 'bar';
+ }
+
+ const headers = Object.keys(data[0]);
+ const numericColumns = this.getNumericColumns(data);
+ const categoricalColumns = this.getCategoricalColumns(data);
+
+ // Auto-select based on data characteristics
+ if (numericColumns.length >= 2 && categoricalColumns.length === 0) {
+ // Multiple numeric columns - scatter plot or line chart
+ return 'scatter';
+ } else if (numericColumns.length === 1 && categoricalColumns.length >= 1) {
+ // One numeric, multiple categorical - bar chart
+ return 'bar';
+ } else if (numericColumns.length === 1 && categoricalColumns.length === 0) {
+ // Single numeric - pie chart for proportions
+ return 'pie';
+ } else {
+ // Default to bar chart
+ return 'bar';
+ }
+ }
+
+ /**
+ * Prepare chart data
+ */
+ prepareChartData(data, chartType, config = {}) {
+ const xColumn = config.xAxis || this.getDefaultXAxis(data, chartType);
+ const yColumn = config.yAxis || this.getDefaultYAxis(data, chartType);
+
+ if (chartType === 'pie' || chartType === 'doughnut') {
+ return this.preparePieChartData(data, yColumn);
+ } else if (chartType === 'scatter') {
+ return this.prepareScatterChartData(data, xColumn, yColumn);
+ } else {
+ return this.prepareStandardChartData(data, xColumn, yColumn);
+ }
+ }
+
+ /**
+ * Prepare pie chart data
+ */
+ preparePieChartData(data, valueColumn) {
+ const labels = data.map(row => row[valueColumn]);
+ const values = data.map(row => parseFloat(row[valueColumn]) || 0);
+
+ return {
+ labels: labels,
+ datasets: [{
+ data: values,
+ backgroundColor: this.generateColors(values.length),
+ borderWidth: 1
+ }]
+ };
+ }
+
+ /**
+ * Prepare scatter chart data
+ */
+ prepareScatterChartData(data, xColumn, yColumn) {
+ const points = data.map(row => ({
+ x: parseFloat(row[xColumn]) || 0,
+ y: parseFloat(row[yColumn]) || 0
+ }));
+
+ return {
+ datasets: [{
+ label: `${xColumn} vs ${yColumn}`,
+ data: points,
+ backgroundColor: this.generateColors(points.length),
+ pointRadius: 5,
+ pointHoverRadius: 7
+ }]
+ };
+ }
+
+ /**
+ * Prepare standard chart data (bar, line, etc.)
+ */
+ prepareStandardChartData(data, xColumn, yColumn) {
+ const labels = data.map(row => row[xColumn]);
+ const values = data.map(row => parseFloat(row[yColumn]) || 0);
+
+ return {
+ labels: labels,
+ datasets: [{
+ label: yColumn,
+ data: values,
+ backgroundColor: this.generateColors(values.length),
+ borderColor: this.generateColors(values.length, 0.8),
+ borderWidth: 2,
+ fill: false
+ }]
+ };
+ }
+
+ /**
+ * Prepare chart options
+ */
+ prepareChartOptions(chartType, config = {}) {
+ const theme = this.getCurrentTheme();
+ const colors = this.getThemeColors(theme);
+
+ const baseOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: {
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ plugins: {
+ legend: {
+ position: config.legendPosition || 'top',
+ labels: {
+ color: colors.text,
+ font: {
+ size: 12
+ }
+ }
+ },
+ tooltip: {
+ enabled: true,
+ backgroundColor: colors.bgSecondary,
+ titleColor: colors.text,
+ bodyColor: colors.text,
+ borderColor: colors.border,
+ borderWidth: 1
+ }
+ },
+ scales: {
+ x: {
+ grid: {
+ color: colors.border
+ },
+ ticks: {
+ color: colors.text
+ }
+ },
+ y: {
+ grid: {
+ color: colors.border
+ },
+ ticks: {
+ color: colors.text
+ }
+ }
+ }
+ };
+
+ // Chart-specific options
+ switch (chartType) {
+ case 'bar':
+ return {
+ ...baseOptions,
+ indexAxis: config.horizontal ? 'y' : 'x',
+ scales: {
+ ...baseOptions.scales,
+ x: {
+ ...baseOptions.scales.x,
+ beginAtZero: true
+ },
+ y: {
+ ...baseOptions.scales.y,
+ beginAtZero: true
+ }
+ }
+ };
+
+ case 'line':
+ return {
+ ...baseOptions,
+ tension: 0.4,
+ pointRadius: 3,
+ pointHoverRadius: 5
+ };
+
+ case 'pie':
+ case 'doughnut':
+ return {
+ ...baseOptions,
+ cutout: chartType === 'doughnut' ? '50%' : '0%',
+ plugins: {
+ ...baseOptions.plugins,
+ legend: {
+ ...baseOptions.plugins.legend,
+ position: 'bottom'
+ }
+ }
+ };
+
+ case 'scatter':
+ return {
+ ...baseOptions,
+ scales: {
+ x: {
+ type: 'linear',
+ position: 'bottom',
+ grid: {
+ color: colors.border
+ },
+ ticks: {
+ color: colors.text
+ }
+ },
+ y: {
+ type: 'linear',
+ grid: {
+ color: colors.border
+ },
+ ticks: {
+ color: colors.text
+ }
+ }
+ }
+ };
+
+ default:
+ return baseOptions;
+ }
+ }
+
+ /**
+ * Create chart instance
+ */
+ async createChart(chartType, data, options) {
+ // Destroy existing chart if any
+ if (this.currentChart) {
+ this.currentChart.destroy();
+ }
+
+ // Create new chart
+ const chart = new Chart(this.chartContainer, {
+ type: chartType,
+ data: data,
+ options: options
+ });
+
+ return chart;
+ }
+
+ /**
+ * Resize charts for responsiveness
+ */
+ resizeCharts() {
+ this.charts.forEach(chart => {
+ if (chart && chart.resize) {
+ chart.resize();
+ }
+ });
+ }
+
+ /**
+ * Update chart theme
+ */
+ updateTheme(theme) {
+ this.charts.forEach(chart => {
+ if (chart) {
+ const colors = this.getThemeColors(theme);
+ chart.options.plugins.legend.labels.color = colors.text;
+ chart.options.plugins.tooltip.backgroundColor = colors.bgSecondary;
+ chart.options.plugins.tooltip.titleColor = colors.text;
+ chart.options.plugins.tooltip.bodyColor = colors.text;
+ chart.options.plugins.tooltip.borderColor = colors.border;
+ chart.options.scales.x.grid.color = colors.border;
+ chart.options.scales.x.ticks.color = colors.text;
+ chart.options.scales.y.grid.color = colors.border;
+ chart.options.scales.y.ticks.color = colors.text;
+ chart.update();
+ }
+ });
+ }
+
+ /**
+ * Export chart as image
+ */
+ exportChart(chartId, format = 'png') {
+ const chart = this.charts.get(chartId);
+ if (!chart) {
+ throw new Error('Chart not found');
+ }
+
+ const dataURL = chart.toBase64Image(format, 1.0);
+ return dataURL;
+ }
+
+ /**
+ * Get chart statistics
+ */
+ getChartStatistics(chartId) {
+ const chart = this.charts.get(chartId);
+ if (!chart) {
+ return null;
+ }
+
+ const data = chart.data.datasets[0].data;
+ const stats = {
+ min: Math.min(...data),
+ max: Math.max(...data),
+ mean: data.reduce((sum, val) => sum + val, 0) / data.length,
+ count: data.length
+ };
+
+ return stats;
+ }
+
+ /**
+ * Get numeric columns from data
+ */
+ getNumericColumns(data) {
+ if (!data || data.length === 0) return [];
+
+ const headers = Object.keys(data[0]);
+ return headers.filter(header => {
+ const values = data.map(row => row[header]);
+ return values.every(val => !isNaN(parseFloat(val)));
+ });
+ }
+
+ /**
+ * Get categorical columns from data
+ */
+ getCategoricalColumns(data) {
+ if (!data || data.length === 0) return [];
+
+ const headers = Object.keys(data[0]);
+ return headers.filter(header => {
+ const values = data.map(row => row[header]);
+ return values.some(val => isNaN(parseFloat(val)));
+ });
+ }
+
+ /**
+ * Get default X axis for chart type
+ */
+ getDefaultXAxis(data, chartType) {
+ const headers = Object.keys(data[0]);
+
+ // For scatter plots, prefer numeric columns
+ if (chartType === 'scatter') {
+ const numericColumns = this.getNumericColumns(data);
+ return numericColumns.length > 0 ? numericColumns[0] : headers[0];
+ }
+
+ // For other charts, prefer categorical columns
+ const categoricalColumns = this.getCategoricalColumns(data);
+ return categoricalColumns.length > 0 ? categoricalColumns[0] : headers[0];
+ }
+
+ /**
+ * Get default Y axis for chart type
+ */
+ getDefaultYAxis(data, chartType) {
+ const headers = Object.keys(data[0]);
+
+ // Prefer numeric columns for Y axis
+ const numericColumns = this.getNumericColumns(data);
+ return numericColumns.length > 0 ? numericColumns[0] : headers[0];
+ }
+
+ /**
+ * Generate colors for chart
+ */
+ generateColors(count, alpha = 1.0) {
+ const colors = [];
+ for (let i = 0; i < count; i++) {
+ const hue = (i * 137.508) % 360; // Golden angle
+ colors.push(this.hslToHex(hue, 70, 50));
+ }
+ return colors;
+ }
+
+ /**
+ * Convert HSL to Hex
+ */
+ hslToHex(h, s, l) {
+ l /= 100;
+ const a = s * Math.min(l, 1 - l) / 100;
+ const f = n => {
+ const k = (n + h / 30) % 12;
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
+ return Math.round(255 * color).toString(16).padStart(2, '0');
+ };
+ return `#${f(0)}${f(8)}${f(4)}`;
+ }
+
+ /**
+ * Get theme colors
+ */
+ getThemeColors(theme) {
+ // Get colors from CSS custom properties
+ const root = document.documentElement;
+ return {
+ primary: getComputedStyle(root).getPropertyValue('--primary-color').trim(),
+ secondary: getComputedStyle(root).getPropertyValue('--secondary-color').trim(),
+ success: getComputedStyle(root).getPropertyValue('--success-color').trim(),
+ danger: getComputedStyle(root).getPropertyValue('--danger-color').trim(),
+ warning: getComputedStyle(root).getPropertyValue('--warning-color').trim(),
+ info: getComputedStyle(root).getPropertyValue('--info-color').trim(),
+ text: getComputedStyle(root).getPropertyValue('--text-primary').trim(),
+ bgSecondary: getComputedStyle(root).getPropertyValue('--bg-secondary').trim(),
+ border: getComputedStyle(root).getPropertyValue('--border-color').trim()
+ };
+ }
+
+ /**
+ * Get current theme
+ */
+ getCurrentTheme() {
+ return document.documentElement.getAttribute('data-theme') || 'light';
+ }
+
+ /**
+ * Clear all charts
+ */
+ clearCharts() {
+ this.charts.forEach(chart => {
+ if (chart && chart.destroy) {
+ chart.destroy();
+ }
+ });
+ this.charts.clear();
+ this.currentChart = null;
+ }
+
+ /**
+ * Get chart by ID
+ */
+ getChart(chartId) {
+ return this.charts.get(chartId);
+ }
+
+ /**
+ * Get all charts
+ */
+ getAllCharts() {
+ return Array.from(this.charts.values());
+ }
+
+ /**
+ * Remove chart
+ */
+ removeChart(chartId) {
+ const chart = this.charts.get(chartId);
+ if (chart) {
+ chart.destroy();
+ this.charts.delete(chartId);
+ if (this.currentChart && this.currentChart.id === chartId) {
+ this.currentChart = null;
+ }
+ }
+ }
+
+ /**
+ * Setup responsive chart behavior
+ */
+ setupResponsiveChart() {
+ // Chart.js handles responsiveness automatically with responsive: true
+ // But we can add additional responsive behavior here if needed
+ window.addEventListener('resize', () => {
+ this.resizeCharts();
+ });
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Visualizer;
+} else {
+ window.Visualizer = Visualizer;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/formatters.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/formatters.js
new file mode 100644
index 00000000000..772c79ba2e8
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/formatters.js
@@ -0,0 +1,552 @@
+/**
+ * Formatting Utilities
+ *
+ * Data formatting functions for the Excel Analyzer Web application.
+ */
+
+class Formatters {
+ /**
+ * Format number with locale
+ */
+ formatNumber(num, options = {}) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return options.defaultValue || '0';
+ }
+
+ const defaultOptions = {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 2,
+ useGrouping: true,
+ locale: 'en-US'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ return Number(num).toLocaleString(config.locale, {
+ minimumFractionDigits: config.minimumFractionDigits,
+ maximumFractionDigits: config.maximumFractionDigits,
+ useGrouping: config.useGrouping
+ });
+ }
+
+ /**
+ * Format currency
+ */
+ formatCurrency(num, options = {}) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return options.defaultValue || '$0.00';
+ }
+
+ const defaultOptions = {
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ locale: 'en-US'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ return Number(num).toLocaleString(config.locale, {
+ style: 'currency',
+ currency: config.currency,
+ minimumFractionDigits: config.minimumFractionDigits,
+ maximumFractionDigits: config.maximumFractionDigits
+ });
+ }
+
+ /**
+ * Format percentage
+ */
+ formatPercentage(num, options = {}) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return options.defaultValue || '0%';
+ }
+
+ const defaultOptions = {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 2,
+ locale: 'en-US'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ return Number(num).toLocaleString(config.locale, {
+ style: 'percent',
+ minimumFractionDigits: config.minimumFractionDigits,
+ maximumFractionDigits: config.maximumFractionDigits
+ });
+ }
+
+ /**
+ * Format date
+ */
+ formatDate(date, options = {}) {
+ if (!date) {
+ return options.defaultValue || '';
+ }
+
+ const defaultOptions = {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ locale: 'en-US'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ try {
+ const d = new Date(date);
+ if (isNaN(d.getTime())) {
+ return options.defaultValue || '';
+ }
+
+ return d.toLocaleDateString(config.locale, {
+ year: config.year,
+ month: config.month,
+ day: config.day,
+ hour: config.hour,
+ minute: config.minute,
+ second: config.second
+ });
+ } catch (error) {
+ return options.defaultValue || '';
+ }
+ }
+
+ /**
+ * Format time
+ */
+ formatTime(date, options = {}) {
+ if (!date) {
+ return options.defaultValue || '';
+ }
+
+ const defaultOptions = {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ locale: 'en-US'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ try {
+ const d = new Date(date);
+ if (isNaN(d.getTime())) {
+ return options.defaultValue || '';
+ }
+
+ return d.toLocaleTimeString(config.locale, {
+ hour: config.hour,
+ minute: config.minute,
+ second: config.second
+ });
+ } catch (error) {
+ return options.defaultValue || '';
+ }
+ }
+
+ /**
+ * Format file size
+ */
+ formatFileSize(bytes, options = {}) {
+ if (bytes === 0) {
+ return options.defaultValue || '0 Bytes';
+ }
+
+ const defaultOptions = {
+ decimals: 2,
+ units: ['Bytes', 'KB', 'MB', 'GB', 'TB']
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ if (bytes === 0) return '0 ' + config.units[0];
+
+ const k = 1024;
+ const dm = config.decimals < 0 ? 0 : config.decimals;
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + config.units[i];
+ }
+
+ /**
+ * Format duration
+ */
+ formatDuration(milliseconds, options = {}) {
+ if (!milliseconds || milliseconds <= 0) {
+ return options.defaultValue || '0s';
+ }
+
+ const defaultOptions = {
+ showDays: true,
+ showHours: true,
+ showMinutes: true,
+ showSeconds: true,
+ showMilliseconds: false
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ const ms = milliseconds % 1000;
+ const seconds = Math.floor((milliseconds / 1000) % 60);
+ const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+ const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24));
+
+ const parts = [];
+
+ if (config.showDays && days > 0) {
+ parts.push(`${days}d`);
+ }
+
+ if (config.showHours && (hours > 0 || parts.length > 0)) {
+ parts.push(`${hours}h`);
+ }
+
+ if (config.showMinutes && (minutes > 0 || parts.length > 0)) {
+ parts.push(`${minutes}m`);
+ }
+
+ if (config.showSeconds && (seconds > 0 || parts.length > 0)) {
+ parts.push(`${seconds}s`);
+ }
+
+ if (config.showMilliseconds && ms > 0) {
+ parts.push(`${ms}ms`);
+ }
+
+ return parts.join(' ') || '0s';
+ }
+
+ /**
+ * Format large numbers with abbreviations
+ */
+ formatLargeNumber(num, options = {}) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return options.defaultValue || '0';
+ }
+
+ const defaultOptions = {
+ decimals: 1,
+ abbreviations: {
+ 1e3: 'K',
+ 1e6: 'M',
+ 1e9: 'B',
+ 1e12: 'T'
+ }
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ const absNum = Math.abs(num);
+
+ for (const [threshold, abbr] of Object.entries(config.abbreviations).sort((a, b) => b[0] - a[0])) {
+ if (absNum >= threshold) {
+ const value = num / threshold;
+ const decimals = absNum >= threshold * 10 ? 0 : config.decimals;
+ return value.toFixed(decimals) + abbr;
+ }
+ }
+
+ return this.formatNumber(num, { maximumFractionDigits: config.decimals });
+ }
+
+ /**
+ * Format text case
+ */
+ formatTextCase(text, format = 'title') {
+ if (!text) return '';
+
+ switch (format.toLowerCase()) {
+ case 'upper':
+ return text.toUpperCase();
+ case 'lower':
+ return text.toLowerCase();
+ case 'title':
+ return text.replace(/\w\S*/g, (txt) => {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ });
+ case 'sentence':
+ return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
+ case 'camel':
+ return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
+ return index === 0 ? word.toLowerCase() : word.toUpperCase();
+ }).replace(/\s+/g, '');
+ case 'pascal':
+ return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => {
+ return word.toUpperCase();
+ }).replace(/\s+/g, '');
+ case 'kebab':
+ return text.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+ case 'snake':
+ return text.replace(/([a-z])([A-Z])/g, '$1_$2').replace(/\s+/g, '_').toLowerCase();
+ default:
+ return text;
+ }
+ }
+
+ /**
+ * Format phone number
+ */
+ formatPhoneNumber(phone, options = {}) {
+ if (!phone) return '';
+
+ const defaultOptions = {
+ format: 'US', // US, International, etc.
+ separator: '-'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ // Remove all non-digit characters
+ const digits = phone.replace(/\D/g, '');
+
+ if (config.format === 'US') {
+ if (digits.length === 10) {
+ return digits.replace(/(\d{3})(\d{3})(\d{4})/, `($1) $2${config.separator}$3`);
+ } else if (digits.length === 11 && digits[0] === '1') {
+ return digits.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/, `+$1 ($2) $3${config.separator}$4`);
+ }
+ }
+
+ // Default format
+ return digits;
+ }
+
+ /**
+ * Format postal code
+ */
+ formatPostalCode(code, options = {}) {
+ if (!code) return '';
+
+ const defaultOptions = {
+ format: 'US' // US, Canada, etc.
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ const cleanCode = code.replace(/\s+/g, '').toUpperCase();
+
+ if (config.format === 'US') {
+ // US ZIP code format: 12345 or 12345-6789
+ if (cleanCode.length === 5) {
+ return cleanCode;
+ } else if (cleanCode.length === 9) {
+ return cleanCode.replace(/(\d{5})(\d{4})/, '$1-$2');
+ }
+ } else if (config.format === 'Canada') {
+ // Canadian postal code format: A1A 1A1
+ if (cleanCode.length === 6) {
+ return cleanCode.replace(/(.{3})(.{3})/, '$1 $2');
+ }
+ }
+
+ return cleanCode;
+ }
+
+ /**
+ * Format percentage change
+ */
+ formatPercentageChange(oldValue, newValue, options = {}) {
+ if (oldValue === null || newValue === null || oldValue === 0) {
+ return options.defaultValue || '0%';
+ }
+
+ const change = ((newValue - oldValue) / Math.abs(oldValue)) * 100;
+ const formattedChange = this.formatPercentage(Math.abs(change) / 100, options);
+
+ if (newValue > oldValue) {
+ return `+${formattedChange}`;
+ } else if (newValue < oldValue) {
+ return `-${formattedChange}`;
+ } else {
+ return '0%';
+ }
+ }
+
+ /**
+ * Format data size
+ */
+ formatDataSize(bytes, options = {}) {
+ return this.formatFileSize(bytes, options);
+ }
+
+ /**
+ * Format memory size
+ */
+ formatMemorySize(bytes, options = {}) {
+ const defaultOptions = {
+ units: ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
+ };
+
+ return this.formatFileSize(bytes, { ...defaultOptions, ...options });
+ }
+
+ /**
+ * Format speed
+ */
+ formatSpeed(bytesPerSecond, options = {}) {
+ const defaultOptions = {
+ timeUnit: 's' // s, m, h
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ let unit = '/s';
+ let speed = bytesPerSecond;
+
+ switch (config.timeUnit) {
+ case 'm':
+ speed = bytesPerSecond * 60;
+ unit = '/m';
+ break;
+ case 'h':
+ speed = bytesPerSecond * 3600;
+ unit = '/h';
+ break;
+ }
+
+ return this.formatDataSize(speed, { ...config, defaultValue: '0 B' }) + unit;
+ }
+
+ /**
+ * Format ratio
+ */
+ formatRatio(numerator, denominator, options = {}) {
+ if (denominator === 0) {
+ return options.defaultValue || '0:1';
+ }
+
+ const defaultOptions = {
+ decimals: 2
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ const ratio = numerator / denominator;
+ return this.formatNumber(ratio, { ...config, defaultValue: '0' }) + ':1';
+ }
+
+ /**
+ * Format score
+ */
+ formatScore(score, maxScore = 100, options = {}) {
+ if (score === null || score === undefined) {
+ return options.defaultValue || '0';
+ }
+
+ const defaultOptions = {
+ showPercentage: true,
+ showFraction: false,
+ maxDecimals: 2
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ let result = '';
+
+ if (config.showFraction) {
+ result += `${score}/${maxScore}`;
+ }
+
+ if (config.showPercentage) {
+ const percentage = (score / maxScore) * 100;
+ const formattedPercentage = this.formatNumber(percentage, {
+ maximumFractionDigits: config.maxDecimals
+ });
+
+ if (result) {
+ result += ' ';
+ }
+ result += `(${formattedPercentage}%)`;
+ }
+
+ return result || this.formatNumber(score, { maximumFractionDigits: config.maxDecimals });
+ }
+
+ /**
+ * Format progress
+ */
+ formatProgress(current, total, options = {}) {
+ if (total === 0) {
+ return options.defaultValue || '0%';
+ }
+
+ const percentage = (current / total) * 100;
+ return this.formatPercentage(percentage / 100, options);
+ }
+
+ /**
+ * Format version
+ */
+ formatVersion(version, options = {}) {
+ if (!version) return '';
+
+ const defaultOptions = {
+ padZeros: true,
+ maxParts: 4
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ // Remove all non-numeric and non-dot characters except hyphens
+ const cleanVersion = version.replace(/[^\d.-]/g, '');
+
+ // Split by dots and hyphens
+ const parts = cleanVersion.split(/[.-]/).filter(part => part.length > 0);
+
+ if (config.padZeros) {
+ // Pad each part to at least 2 digits
+ const paddedParts = parts.map(part => part.padStart(2, '0'));
+ return paddedParts.slice(0, config.maxParts).join('.');
+ }
+
+ return parts.slice(0, config.maxParts).join('.');
+ }
+
+ /**
+ * Format scientific notation
+ */
+ formatScientific(num, options = {}) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return options.defaultValue || '0';
+ }
+
+ const defaultOptions = {
+ precision: 2
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ return Number(num).toExponential(config.precision);
+ }
+
+ /**
+ * Format boolean
+ */
+ formatBoolean(value, options = {}) {
+ const defaultOptions = {
+ trueLabel: 'Yes',
+ falseLabel: 'No',
+ nullLabel: 'N/A'
+ };
+
+ const config = { ...defaultOptions, ...options };
+
+ if (value === true || value === 'true' || value === 1) {
+ return config.trueLabel;
+ } else if (value === false || value === 'false' || value === 0) {
+ return config.falseLabel;
+ } else {
+ return config.nullLabel;
+ }
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Formatters;
+} else {
+ window.Formatters = Formatters;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/helpers.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/helpers.js
new file mode 100644
index 00000000000..505e3d055a5
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/helpers.js
@@ -0,0 +1,391 @@
+/**
+ * Helper Utilities
+ *
+ * Common utility functions for the Excel Analyzer Web application.
+ */
+
+class Helpers {
+ /**
+ * Deep clone an object
+ */
+ deepClone(obj) {
+ if (obj === null || typeof obj !== 'object') {
+ return obj;
+ }
+
+ if (obj instanceof Date) {
+ return new Date(obj.getTime());
+ }
+
+ if (obj instanceof Array) {
+ return obj.map(item => this.deepClone(item));
+ }
+
+ if (typeof obj === 'object') {
+ const clonedObj = {};
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ clonedObj[key] = this.deepClone(obj[key]);
+ }
+ }
+ return clonedObj;
+ }
+ }
+
+ /**
+ * Generate unique ID
+ */
+ generateId(prefix = 'id') {
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ /**
+ * Format number with commas and decimal places
+ */
+ formatNumber(num, decimals = 2) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return '0';
+ }
+
+ return Number(num).toLocaleString('en-US', {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals
+ });
+ }
+
+ /**
+ * Format currency
+ */
+ formatCurrency(num, currency = 'USD', decimals = 2) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return '$0.00';
+ }
+
+ return Number(num).toLocaleString('en-US', {
+ style: 'currency',
+ currency: currency,
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals
+ });
+ }
+
+ /**
+ * Format percentage
+ */
+ formatPercentage(num, decimals = 2) {
+ if (num === null || num === undefined || isNaN(num)) {
+ return '0%';
+ }
+
+ return Number(num).toLocaleString('en-US', {
+ style: 'percent',
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals
+ });
+ }
+
+ /**
+ * Format date
+ */
+ formatDate(date, format = 'YYYY-MM-DD') {
+ if (!date) return '';
+
+ const d = new Date(date);
+ if (isNaN(d.getTime())) return '';
+
+ const year = d.getFullYear();
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ const hours = String(d.getHours()).padStart(2, '0');
+ const minutes = String(d.getMinutes()).padStart(2, '0');
+ const seconds = String(d.getSeconds()).padStart(2, '0');
+
+ return format
+ .replace('YYYY', year)
+ .replace('MM', month)
+ .replace('DD', day)
+ .replace('HH', hours)
+ .replace('mm', minutes)
+ .replace('ss', seconds);
+ }
+
+ /**
+ * Debounce function
+ */
+ debounce(func, wait, immediate = false) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ timeout = null;
+ if (!immediate) func.apply(this, args);
+ };
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(this, args);
+ };
+ }
+
+ /**
+ * Throttle function
+ */
+ throttle(func, limit) {
+ let inThrottle;
+ return function(...args) {
+ if (!inThrottle) {
+ func.apply(this, args);
+ inThrottle = true;
+ setTimeout(() => inThrottle = false, limit);
+ }
+ };
+ }
+
+ /**
+ * Check if value is empty
+ */
+ isEmpty(value) {
+ if (value === null || value === undefined) return true;
+ if (typeof value === 'string') return value.trim() === '';
+ if (Array.isArray(value)) return value.length === 0;
+ if (typeof value === 'object') return Object.keys(value).length === 0;
+ return false;
+ }
+
+ /**
+ * Get object size in bytes
+ */
+ getObjectSize(obj) {
+ const objStr = JSON.stringify(obj);
+ return new Blob([objStr]).size;
+ }
+
+ /**
+ * Capitalize first letter
+ */
+ capitalize(str) {
+ if (!str) return '';
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
+ }
+
+ /**
+ * Truncate string
+ */
+ truncate(str, maxLength = 50, suffix = '...') {
+ if (!str || str.length <= maxLength) return str;
+ return str.substring(0, maxLength - suffix.length) + suffix;
+ }
+
+ /**
+ * Get file extension
+ */
+ getFileExtension(filename) {
+ if (!filename) return '';
+ return filename.split('.').pop().toLowerCase();
+ }
+
+ /**
+ * Check if file is image
+ */
+ isImageFile(filename) {
+ const ext = this.getFileExtension(filename);
+ return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'].includes(ext);
+ }
+
+ /**
+ * Check if file is Excel
+ */
+ isExcelFile(filename) {
+ const ext = this.getFileExtension(filename);
+ return ['xlsx', 'xls'].includes(ext);
+ }
+
+ /**
+ * Get random color
+ */
+ getRandomColor() {
+ const letters = '0123456789ABCDEF';
+ let color = '#';
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+ }
+
+ /**
+ * Generate color palette
+ */
+ generateColorPalette(count = 5) {
+ const colors = [];
+ for (let i = 0; i < count; i++) {
+ const hue = (i * 137.508) % 360; // Golden angle
+ colors.push(this.hslToHex(hue, 70, 50));
+ }
+ return colors;
+ }
+
+ /**
+ * Convert HSL to Hex
+ */
+ hslToHex(h, s, l) {
+ l /= 100;
+ const a = s * Math.min(l, 1 - l) / 100;
+ const f = n => {
+ const k = (n + h / 30) % 12;
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
+ return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
+ };
+ return `#${f(0)}${f(8)}${f(4)}`;
+ }
+
+ /**
+ * Shuffle array
+ */
+ shuffleArray(array) {
+ const arr = [...array];
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+ }
+
+ /**
+ * Get unique values from array
+ */
+ getUniqueValues(array) {
+ return [...new Set(array)];
+ }
+
+ /**
+ * Group array by property
+ */
+ groupBy(array, key) {
+ return array.reduce((groups, item) => {
+ const groupKey = item[key];
+ groups[groupKey] = groups[groupKey] || [];
+ groups[groupKey].push(item);
+ return groups;
+ }, {});
+ }
+
+ /**
+ * Sort array by property
+ */
+ sortBy(array, key, direction = 'asc') {
+ return [...array].sort((a, b) => {
+ const aVal = a[key];
+ const bVal = b[key];
+
+ if (aVal < bVal) return direction === 'asc' ? -1 : 1;
+ if (aVal > bVal) return direction === 'asc' ? 1 : -1;
+ return 0;
+ });
+ }
+
+ /**
+ * Search in array of objects
+ */
+ searchInArray(array, searchTerm, keys) {
+ if (!searchTerm) return array;
+
+ const term = searchTerm.toLowerCase();
+ return array.filter(item => {
+ return keys.some(key => {
+ const value = item[key];
+ return value && value.toString().toLowerCase().includes(term);
+ });
+ });
+ }
+
+ /**
+ * Download file
+ */
+ downloadFile(content, filename, mimeType = 'text/plain') {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+
+ /**
+ * Copy text to clipboard
+ */
+ copyToClipboard(text) {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text);
+ } else {
+ // Fallback for older browsers
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ } catch (err) {
+ console.error('Failed to copy text: ', err);
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ }
+ }
+
+ /**
+ * Validate email
+ */
+ isValidEmail(email) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ }
+
+ /**
+ * Validate URL
+ */
+ isValidUrl(string) {
+ try {
+ new URL(string);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ /**
+ * Get URL parameters
+ */
+ getUrlParams(url = window.location.href) {
+ const params = {};
+ const urlObj = new URL(url);
+ urlObj.searchParams.forEach((value, key) => {
+ params[key] = value;
+ });
+ return params;
+ }
+
+ /**
+ * Set URL parameters
+ */
+ setUrlParams(params) {
+ const url = new URL(window.location.href);
+ Object.keys(params).forEach(key => {
+ if (params[key] !== null && params[key] !== undefined) {
+ url.searchParams.set(key, params[key]);
+ } else {
+ url.searchParams.delete(key);
+ }
+ });
+ window.history.replaceState({}, '', url.toString());
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Helpers;
+} else {
+ window.Helpers = Helpers;
+}
\ No newline at end of file
diff --git a/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/validators.js b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/validators.js
new file mode 100644
index 00000000000..416d8058a44
--- /dev/null
+++ b/apps/web-roo-code/src/components/excel-analyzer/src/js/utils/validators.js
@@ -0,0 +1,444 @@
+/**
+ * Validation Utilities
+ *
+ * Input validation functions for the Excel Analyzer Web application.
+ */
+
+class Validators {
+ /**
+ * Validate file format
+ */
+ validateFileFormat(file) {
+ const allowedTypes = [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel'
+ ];
+
+ if (!file) {
+ return { isValid: false, error: 'No file selected' };
+ }
+
+ if (!allowedTypes.includes(file.type)) {
+ return {
+ isValid: false,
+ error: 'Invalid file type. Please upload an Excel file (.xlsx or .xls)'
+ };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate file size
+ */
+ validateFileSize(file, maxSize = 50 * 1024 * 1024) {
+ if (!file) {
+ return { isValid: false, error: 'No file selected' };
+ }
+
+ if (file.size > maxSize) {
+ const maxSizeMB = Math.round(maxSize / (1024 * 1024));
+ return {
+ isValid: false,
+ error: `File too large. Maximum file size is ${maxSizeMB}MB`
+ };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate email address
+ */
+ validateEmail(email) {
+ if (!email) {
+ return { isValid: false, error: 'Email is required' };
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ return { isValid: false, error: 'Invalid email format' };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate URL
+ */
+ validateUrl(url) {
+ if (!url) {
+ return { isValid: false, error: 'URL is required' };
+ }
+
+ try {
+ new URL(url);
+ return { isValid: true };
+ } catch (error) {
+ return { isValid: false, error: 'Invalid URL format' };
+ }
+ }
+
+ /**
+ * Validate number range
+ */
+ validateNumberRange(value, min = null, max = null) {
+ if (value === null || value === undefined || value === '') {
+ return { isValid: false, error: 'Number is required' };
+ }
+
+ const num = Number(value);
+ if (isNaN(num)) {
+ return { isValid: false, error: 'Value must be a number' };
+ }
+
+ if (min !== null && num < min) {
+ return { isValid: false, error: `Value must be at least ${min}` };
+ }
+
+ if (max !== null && num > max) {
+ return { isValid: false, error: `Value must be at most ${max}` };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate string length
+ */
+ validateStringLength(str, minLength = 1, maxLength = null) {
+ if (!str) {
+ return { isValid: false, error: 'String is required' };
+ }
+
+ if (str.length < minLength) {
+ return { isValid: false, error: `String must be at least ${minLength} characters long` };
+ }
+
+ if (maxLength && str.length > maxLength) {
+ return { isValid: false, error: `String must be at most ${maxLength} characters long` };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate data structure
+ */
+ validateDataStructure(data) {
+ if (!data || !Array.isArray(data)) {
+ return { isValid: false, error: 'Invalid data structure' };
+ }
+
+ if (data.length === 0) {
+ return { isValid: false, error: 'No data available' };
+ }
+
+ const headers = Object.keys(data[0]);
+ if (headers.length === 0) {
+ return { isValid: false, error: 'No columns found in data' };
+ }
+
+ // Check for consistent structure
+ for (let i = 1; i < data.length; i++) {
+ const rowHeaders = Object.keys(data[i]);
+ if (rowHeaders.length !== headers.length) {
+ return { isValid: false, error: `Inconsistent data structure at row ${i + 1}` };
+ }
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate chart configuration
+ */
+ validateChartConfig(config) {
+ const requiredFields = ['type', 'data'];
+ const errors = [];
+
+ for (const field of requiredFields) {
+ if (!config[field]) {
+ errors.push(`Missing required field: ${field}`);
+ }
+ }
+
+ if (config.type && !['bar', 'line', 'pie', 'scatter', 'doughnut'].includes(config.type)) {
+ errors.push('Invalid chart type');
+ }
+
+ if (config.data && !Array.isArray(config.data)) {
+ errors.push('Chart data must be an array');
+ }
+
+ if (errors.length > 0) {
+ return { isValid: false, error: errors.join(', ') };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate export settings
+ */
+ validateExportSettings(settings) {
+ const errors = [];
+
+ if (!settings) {
+ return { isValid: false, error: 'Export settings are required' };
+ }
+
+ const validFormats = ['pdf', 'excel', 'csv', 'json'];
+ if (settings.format && !validFormats.includes(settings.format)) {
+ errors.push('Invalid export format');
+ }
+
+ if (settings.includeCharts !== undefined && typeof settings.includeCharts !== 'boolean') {
+ errors.push('includeCharts must be boolean');
+ }
+
+ if (settings.includeData !== undefined && typeof settings.includeData !== 'boolean') {
+ errors.push('includeData must be boolean');
+ }
+
+ if (errors.length > 0) {
+ return { isValid: false, error: errors.join(', ') };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate dashboard configuration
+ */
+ validateDashboardConfig(config) {
+ const errors = [];
+
+ if (!config) {
+ return { isValid: false, error: 'Dashboard configuration is required' };
+ }
+
+ if (config.widgets && !Array.isArray(config.widgets)) {
+ errors.push('Widgets must be an array');
+ }
+
+ if (config.layout && typeof config.layout !== 'string') {
+ errors.push('Layout must be a string');
+ }
+
+ if (errors.length > 0) {
+ return { isValid: false, error: errors.join(', ') };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate settings object
+ */
+ validateSettings(settings) {
+ const errors = [];
+
+ if (!settings || typeof settings !== 'object') {
+ return { isValid: false, error: 'Settings must be an object' };
+ }
+
+ // Validate specific settings
+ if (settings.maxFileSize && !this.validateNumberRange(settings.maxFileSize, 1024).isValid) {
+ errors.push('maxFileSize must be at least 1024 bytes');
+ }
+
+ if (settings.maxRows && !this.validateNumberRange(settings.maxRows, 1, 100000).isValid) {
+ errors.push('maxRows must be between 1 and 100000');
+ }
+
+ if (settings.maxColumns && !this.validateNumberRange(settings.maxColumns, 1, 1000).isValid) {
+ errors.push('maxColumns must be between 1 and 1000');
+ }
+
+ if (settings.outlierThreshold && !this.validateNumberRange(settings.outlierThreshold, 1).isValid) {
+ errors.push('outlierThreshold must be at least 1');
+ }
+
+ if (settings.theme && !['light', 'dark'].includes(settings.theme)) {
+ errors.push('theme must be either "light" or "dark"');
+ }
+
+ if (errors.length > 0) {
+ return { isValid: false, error: errors.join(', ') };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate color value
+ */
+ validateColor(color) {
+ if (!color) {
+ return { isValid: false, error: 'Color is required' };
+ }
+
+ // Check for hex color
+ const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
+ if (hexRegex.test(color)) {
+ return { isValid: true };
+ }
+
+ // Check for RGB color
+ const rgbRegex = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/;
+ if (rgbRegex.test(color)) {
+ const matches = color.match(rgbRegex);
+ const r = parseInt(matches[1]);
+ const g = parseInt(matches[2]);
+ const b = parseInt(matches[3]);
+
+ if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
+ return { isValid: true };
+ }
+ }
+
+ // Check for named colors
+ const namedColors = [
+ 'red', 'blue', 'green', 'yellow', 'orange', 'purple', 'pink', 'brown',
+ 'black', 'white', 'gray', 'grey', 'cyan', 'magenta', 'lime', 'navy',
+ 'maroon', 'olive', 'teal', 'silver', 'aqua', 'fuchsia'
+ ];
+
+ if (namedColors.includes(color.toLowerCase())) {
+ return { isValid: true };
+ }
+
+ return { isValid: false, error: 'Invalid color format' };
+ }
+
+ /**
+ * Validate date range
+ */
+ validateDateRange(startDate, endDate) {
+ if (!startDate || !endDate) {
+ return { isValid: false, error: 'Both start and end dates are required' };
+ }
+
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+ return { isValid: false, error: 'Invalid date format' };
+ }
+
+ if (start > end) {
+ return { isValid: false, error: 'Start date must be before end date' };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate array of items
+ */
+ validateArray(items, validatorFn, minItems = 0, maxItems = null) {
+ if (!Array.isArray(items)) {
+ return { isValid: false, error: 'Must be an array' };
+ }
+
+ if (items.length < minItems) {
+ return { isValid: false, error: `Must have at least ${minItems} items` };
+ }
+
+ if (maxItems && items.length > maxItems) {
+ return { isValid: false, error: `Must have at most ${maxItems} items` };
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ const result = validatorFn(items[i]);
+ if (!result.isValid) {
+ return { isValid: false, error: `Item ${i + 1}: ${result.error}` };
+ }
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate object properties
+ */
+ validateObject(obj, rules) {
+ if (!obj || typeof obj !== 'object') {
+ return { isValid: false, error: 'Must be an object' };
+ }
+
+ for (const [key, validator] of Object.entries(rules)) {
+ if (obj[key] !== undefined) {
+ const result = validator(obj[key]);
+ if (!result.isValid) {
+ return { isValid: false, error: `${key}: ${result.error}` };
+ }
+ }
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate required field
+ */
+ validateRequired(value, fieldName = 'Field') {
+ if (value === null || value === undefined || value === '') {
+ return { isValid: false, error: `${fieldName} is required` };
+ }
+
+ return { isValid: true };
+ }
+
+ /**
+ * Validate against custom function
+ */
+ validateCustom(value, validatorFn, errorMessage = 'Validation failed') {
+ try {
+ const result = validatorFn(value);
+ if (result === true) {
+ return { isValid: true };
+ } else if (typeof result === 'string') {
+ return { isValid: false, error: result };
+ } else {
+ return { isValid: false, error: errorMessage };
+ }
+ } catch (error) {
+ return { isValid: false, error: error.message || errorMessage };
+ }
+ }
+
+ /**
+ * Batch validation
+ */
+ validateBatch(validations) {
+ const results = [];
+ let isValid = true;
+
+ for (const validation of validations) {
+ const result = validation.validator(validation.value, ...validation.args);
+ results.push({
+ ...result,
+ field: validation.field
+ });
+
+ if (!result.isValid) {
+ isValid = false;
+ }
+ }
+
+ return {
+ isValid,
+ results,
+ errors: results.filter(r => !r.isValid).map(r => `${r.field}: ${r.error}`)
+ };
+ }
+}
+
+// Export for use in main.js
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Validators;
+} else {
+ window.Validators = Validators;
+}
\ No newline at end of file