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 + + + + + + + + + + + + + + + + +
+ +
+
+
+

+ + Upload Excel File +

+
+
+
+
+ +
Drag and drop your Excel file here
+

or

+ + +
+
+ + + + + +
+
+
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 + ? ` +
+
Latest Chart Snapshot
+
+ Chart snapshot +
+
+ ` + : ` +
+
Latest Chart Snapshot
+
+ No chart rendered yet. +
+
+ `; + + const columns = (structure?.columns || []) + .slice(0, 8) + .map((c) => `
  • ${c.name}${c.type}
  • `) + .join(''); + + this.container.innerHTML = ` +
    +
    +
    +
    Dataset Summary
    +
    +

    Rows: ${stats.rows}

    +

    Columns: ${stats.columns}

    +

    Missing: ${stats.missing}

    +

    Quality: ${stats.quality}%

    +
    +
    +
    +
    +
    +
    Columns
    +
    +
      + ${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 = ` + + `; + } + } + + 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 = ` +
    +
    +

    Application Settings

    +
    + + +
    +
    + +
    + +
    + +
    + ${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