Compare commits

..

10 Commits

Author SHA1 Message Date
d5d8f2b6d3 auth changes 2025-12-24 11:12:21 +05:30
8932c3d502 added knowledge base 2025-12-24 11:05:39 +05:30
ab3bf2038b feat: improve responsiveness and refactor auth components
- Fix responsiveness at md (768px) and lg (1024px) breakpoints across all pages
- Reduce forgot-password-modal.tsx from 472 to 319 lines by extracting components
- Extract OTPInput and modal icons into separate files for better code organization
- Optimize card sizes and text scaling for lg (1024px) breakpoint
- Adjust login page layout: smaller card and reduced text sizes at lg breakpoint
- Improve responsive typography, spacing, and touch targets across all components
- Update all pages (login, sign-up, dashboard) with proper mobile-first responsive design
2025-12-24 11:02:55 +05:30
e812486fe0 changes code quality 2025-12-24 10:40:44 +05:30
6c38cc160a Authentication changes 2025-12-24 10:33:04 +05:30
6008490446 Added Agent flow 2025-12-24 09:50:26 +05:30
9e9d0e09fa added 2025-12-24 08:39:26 +05:30
0e517b09e1 Added Agent designs 2025-12-23 20:21:05 +05:30
52d4aaa88b feat(auth): implement new login page with animated auth card and forgot password modal
- Add new LoginPage with responsive 2-column layout
- Create reusable AuthCard components (AuthCard, AuthInput, AuthButton)
- Implement animated AuthFormCard for Sign In/Register toggle
- Add ForgotPasswordModal with OTP verification flow
- Add background decorations (logo glow, waves, gradients)
- Remove old sign-in page, set LoginPage as root route
- Add SVG type declarations for Vite
2025-12-23 20:04:38 +05:30
5fa95db946 Checking 2025-12-23 15:41:41 +05:30
63 changed files with 4388 additions and 641 deletions

36
.editorconfig Normal file
View File

@ -0,0 +1,36 @@
# EditorConfig is awesome: https://EditorConfig.org
# Top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
# TypeScript and JavaScript files
[*.{ts,tsx,js,jsx}]
indent_size = 4
max_line_length = 120
# JSON files
[*.json]
indent_size = 2
# YAML files
[*.{yml,yaml}]
indent_size = 2
# Markdown files
[*.md]
trim_trailing_whitespace = false
max_line_length = off
# Package files
[{package.json,package-lock.json}]
indent_size = 2

View File

@ -1,2 +0,0 @@
# API Configuration
# VITE_API_BASE_URL=http://localhost:3000

43
.gitattributes vendored Normal file
View File

@ -0,0 +1,43 @@
# Auto detect text files and perform LF normalization
* text=auto
# Source code files
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# Configuration files
*.config.js text eol=lf
*.config.ts text eol=lf
.editorconfig text eol=lf
.gitignore text eol=lf
.gitattributes text eol=lf
# Shell scripts
*.sh text eol=lf
# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
# Archives
*.zip binary
*.tar binary
*.gz binary

99
.gitignore vendored
View File

@ -1,5 +1,16 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Production builds
dist/
dist-ssr/
build/
*.local
# Logs # Logs
logs logs/
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
@ -7,18 +18,88 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules # Environment variables
dist .env
dist-ssr .env.local
*.local .env.development.local
.env.test.local
.env.production.local
.env*.local
# Editor directories and files # Testing
.vscode/* coverage/
!.vscode/extensions.json .nyc_output/
.idea *.lcov
.jest/
# IDE and Editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store .DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# OS files
Thumbs.db
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Desktop.ini
# Temporary files
*.tmp
*.temp
.cache/
.parcel-cache/
.turbo/
# TypeScript
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# Storybook build outputs
storybook-static/
# Vite
.vite/
# Debug
.vscode-test/
# Local Netlify folder
.netlify
# Sentry
.sentryclirc
# Documentation files (internal only, not for GitHub)
GUIDELINES.md
CONTRIBUTING.md
# Lock files (optional - uncomment if you want to ignore)
# package-lock.json
# yarn.lock
# pnpm-lock.yaml

View File

@ -1,292 +0,0 @@
# AgenticIQ Coding Standards and Best Practices
## 1. Introduction and Purpose
This document establishes comprehensive coding standards and best practices for all developers working on the AgenticIQ enterprise application. Following these guidelines ensures code consistency, maintainability, and scalability across the entire development team.
### 1.1 Document Objectives
- Establish uniform coding standards across all development teams
- Reduce technical debt and improve code quality
- Facilitate easier onboarding of new team members
- Enable efficient code reviews and collaboration
- Ensure application performs optimally across all devices
### 1.2 Technology Stack
AgenticIQ is built using modern enterprise technologies. All code must align with the architectural patterns and technology constraints defined for this application.
## 2. General Coding Standards
### 2.1 Code Formatting Rules
1. Use 4 spaces for indentation (no tabs)
2. Maximum line length: 120 characters
3. Use UTF-8 encoding for all source files
4. End files with a single newline character
5. Remove trailing whitespace from all lines
6. Use consistent brace style (opening brace on same line for JS/TS, new line for C#)
### 2.2 Code Organization Principles
- Single Responsibility Principle (SRP): Each file/class/function should have one purpose
- DRY (Don't Repeat Yourself): Extract common logic into reusable functions/components
- KISS (Keep It Simple): Avoid over-engineering; prefer clarity over cleverness
- YAGNI (You Aren't Gonna Need It): Don't add functionality until it's required
### 2.3 File Size Guidelines
- Maximum file length: 400 lines (excluding comments)
- Maximum function/method length: 50 lines
- Maximum class length: 300 lines
- If limits are exceeded, refactor into smaller units
## 3. Naming Conventions
Consistent naming is critical for code readability and maintainability. All naming must be meaningful, descriptive, and follow the patterns defined below.
### 3.1 General Naming Rules
1. Use meaningful, descriptive names that reveal intent
2. Avoid single-letter names except for loop counters (i, j, k)
3. Avoid abbreviations unless universally understood (e.g., URL, HTTP, ID)
4. Use American English spelling consistently
5. Boolean variables should indicate true/false (isActive, hasPermission, canEdit)
### 3.2 Naming Convention Reference Table
| Element | Convention | Example | Notes |
| --------------- | -------------- | --------------------- | --------------------------- |
| Class/Interface | PascalCase | UserService, IRepository | Prefix I for interfaces (C#) |
| Function/Method | camelCase | getUserById, calculateTotal | Start with verb |
| Variable | camelCase | userName, orderItems | Descriptive nouns |
| Constant | SCREAMING_SNAKE| MAX_RETRY_COUNT | All uppercase |
| Private Field | _camelCase | _userRepository | Underscore prefix |
| Component | PascalCase | UserDashboard.tsx | Match filename |
| CSS Class | kebab-case | user-profile-card | BEM methodology |
| File (General) | kebab-case | user-service.ts | Lowercase |
| Database Table | PascalCase Singular | UserProfile, OrderItem | Match entity name |
| API Endpoint | kebab-case plural | /api/user-profiles | RESTful conventions |
### 3.3 Function Naming Patterns
Function names should clearly describe the action performed. Use consistent verb prefixes to indicate the operation type.
| Prefix | Purpose | Examples |
| ---------- | ------------------ | ---------------------------------------------- |
| get | Retrieve data | getUserById(), getOrderList(), getActiveUsers()|
| set | Assign value | setUserName(), setOrderStatus() |
| create | Create new entity | createUser(), createOrder(), createReport() |
| update | Modify existing | updateUserProfile(), updateOrderQuantity() |
| delete/remove | Remove data | deleteUser(), removeCartItem() |
| is/has/can | Boolean checks | isActive(), hasPermission(), canEdit() |
| validate | Check validity | validateEmail(), validateUserInput() |
| calculate | Compute values | calculateTotal(), calculateDiscount() |
| handle | Event handling | handleSubmit(), handleClick(), handleError() |
| fetch/load | Async data load | fetchUserData(), loadConfiguration() |
| render | UI rendering | renderHeader(), renderUserList() |
| format/parse | Data transformation | formatDate(), parseJson(), formatCurrency() |
## 4. Function Design Guidelines
### 4.1 Function Structure Best Practices
1. Single Purpose: Each function should do exactly one thing well
2. Parameter Limit: Maximum 4 parameters; use objects for more
3. Pure Functions: Prefer functions without side effects when possible
4. Early Returns: Use guard clauses to handle edge cases first
5. Avoid Deep Nesting: Maximum 3 levels of nesting; refactor if deeper
### 4.2 Function Signature Guidelines
- Return Type: Always specify explicit return types in TypeScript
- Parameter Types: Use strong typing; avoid `any`
- Default Values: Provide sensible defaults for optional parameters
- Async Functions: Return `Promise<T>` with proper typing
### 4.3 Function Documentation Template
Every public function must include JSDoc/XML documentation with:
- `@description` - Brief explanation of what the function does
- `@param {Type} name` - Description of each parameter
- `@returns {Type}` - Description of the return value
- `@throws {ErrorType}` - Exceptions that may be thrown
- `@example` - Usage example (for complex functions)
## 5. Responsive Design & Screen Optimization
### 5.1 Breakpoint Standards
| Device | Breakpoint | CSS Variable | Target Devices |
| -------------- | ---------------- | --------------- | ------------------- |
| Mobile S | < 375px | --screen-xs | Small phones |
| Mobile M | 375px - 424px | --screen-sm | Standard phones |
| Mobile L | 425px - 767px | --screen-md | Large phones |
| Tablet | 768px - 1023px | --screen-lg | iPad, Tablets |
| Laptop | 1024px - 1439px | --screen-xl | Laptops, small desktops |
| Desktop | 1440px - 1919px | --screen-xxl | Standard monitors |
| Large Display | ≥ 1920px | --screen-xxxl | Large/4K monitors |
### 5.2 Mobile-First Development
1. Start with mobile layout and progressively enhance for larger screens
2. Use min-width media queries to add complexity as screen size increases
3. Design touch-friendly interfaces with minimum 44px touch targets
4. Optimize images for mobile using srcset and responsive images
5. Test on actual devices, not just browser developer tools
### 5.3 Layout Guidelines
- Flexbox: Use for one-dimensional layouts (rows or columns)
- CSS Grid: Use for two-dimensional layouts and complex designs
- Container Queries: Use for component-level responsiveness
- Relative Units: Use rem/em for typography, % for widths, vh/vw for viewports
- Avoid fixed pixel widths for containers; prefer max-width constraints
### 5.4 Typography Scaling
| Element | Mobile | Tablet | Desktop |
| ------- | ------------- | ------------- | ------------- |
| H1 | 1.75rem (28px)| 2rem (32px) | 2.5rem (40px) |
| H2 | 1.5rem (24px) | 1.75rem (28px)| 2rem (32px) |
| H3 | 1.25rem (20px)| 1.375rem (22px)| 1.5rem (24px)|
| Body | 0.875rem (14px)| 0.9375rem (15px)| 1rem (16px) |
| Caption | 0.75rem (12px)| 0.8125rem (13px)| 0.875rem (14px)|
## 6. Code Architecture Principles
### 6.1 Folder Structure Standards
Maintain consistent folder organization across all modules:
- `/src/components`: Reusable UI components
- `/src/pages`: Route-level page components
- `/src/hooks`: Custom React hooks
- `/src/services`: API calls and external integrations
- `/src/utils`: Utility functions and helpers
- `/src/types`: TypeScript type definitions
- `/src/constants`: Application constants and enums
- `/src/styles`: Global styles and theme configuration
### 6.2 Component Design Patterns
1. Container/Presentational Pattern: Separate logic from presentation
2. Compound Components: Create flexible, composable component APIs
3. Render Props/HOCs: Share behavior between components
4. Custom Hooks: Extract reusable stateful logic
5. Context Pattern: Share state across component tree
### 6.3 State Management Guidelines
- Local State: Use `useState` for component-specific state
- Server State: Use React Query/TanStack Query for API data
- Global State: Use Zustand/Redux for app-wide state
- Form State: Use React Hook Form for complex forms
## 7. Error Handling Best Practices
### 7.1 Error Handling Principles
1. Fail Fast: Detect and report errors as early as possible
2. Be Specific: Use specific error types, not generic exceptions
3. Log Appropriately: Include context but exclude sensitive data
4. User-Friendly Messages: Show helpful messages, not stack traces
5. Recover Gracefully: Implement fallback behavior where possible
### 7.2 Error Categories
| Error Type | Handling Strategy | Example |
| -------------- | ------------------------------ | ------------------------ |
| Validation | Show inline field errors | Invalid email format |
| Network | Retry with exponential backoff | API timeout |
| Authentication | Redirect to login | Token expired |
| Authorization | Show access denied message | 403 Forbidden |
| Not Found | Show 404 page or fallback | Resource not found |
| Server Error | Show error page with support contact | 500 Internal Error |
### 7.3 Try-Catch Guidelines
- Catch specific errors, not generic Exception/Error
- Never swallow exceptions silently without logging
- Use finally blocks for cleanup operations
- Avoid try-catch for flow control
- Re-throw with additional context when appropriate
## 8. Performance Optimization
### 8.1 Frontend Performance
1. Code Splitting: Use dynamic imports for route-level splitting
2. Lazy Loading: Defer loading of below-the-fold content
3. Image Optimization: Use WebP format, responsive images, lazy loading
4. Bundle Analysis: Regularly analyze and optimize bundle size
5. Caching: Implement proper caching headers and service workers
6. Memoization: Use `useMemo`, `useCallback`, `React.memo` appropriately
### 8.2 React Performance Tips
- Use virtualization for long lists (react-window, react-virtualized)
- Avoid inline function definitions in render
- Use stable keys for list items
- Avoid unnecessary re-renders with proper state structure
- Profile with React DevTools before optimizing
### 8.3 Performance Metrics Targets
| Metric | Target | Measurement |
| ------------------------ | --------------- | -------------------- |
| First Contentful Paint (FCP) | < 1.8 seconds | Lighthouse |
| Largest Contentful Paint (LCP) | < 2.5 seconds | Lighthouse |
| Cumulative Layout Shift (CLS) | < 0.1 | Lighthouse |
| Time to Interactive (TTI) | < 3.8 seconds | Lighthouse |
| Bundle Size (main) | < 250KB gzipped | Webpack analyzer |
## 9. Security Guidelines
### 9.1 Input Validation & Sanitization
1. Validate all inputs on both client and server side
2. Sanitize user inputs to prevent XSS attacks
3. Use parameterized queries to prevent SQL injection
4. Implement rate limiting on API endpoints
5. Validate file uploads (type, size, content)
### 9.2 Authentication & Authorization
- Never store sensitive data in localStorage/sessionStorage
- Use HTTP-only, Secure cookies for session tokens
- Implement proper CORS configuration
- Use CSRF tokens for state-changing operations
- Always verify authorization on the server
### 9.3 Data Protection
- Never log sensitive information (passwords, tokens, PII)
- Use environment variables for secrets
- Encrypt sensitive data at rest and in transit
- Implement proper error messages (no stack traces in production)
- Follow principle of least privilege
## 10. Documentation Standards
### 10.1 Code Comments Guidelines
- WHY, not WHAT: Explain the reasoning, not what the code does
- Self-Documenting Code: Use clear names; add comments only when necessary
- Update Comments: Keep comments in sync with code changes
- TODO Comments: Include ticket number, e.g., `// TODO(JIRA-123): Fix edge case`
- Avoid obvious comments
### 10.2 README Requirements
Every project/module must have a README.md with:
- Project overview and purpose
- Prerequisites and dependencies
- Installation and setup instructions
- Environment configuration
- Available scripts and commands
- Contributing guidelines
### 10.3 API Documentation
All APIs must be documented using OpenAPI/Swagger specification with endpoint descriptions, request/response schemas, authentication requirements, error codes and responses, and usage examples.
## 11. Version Control Guidelines
### 11.1 Git Commit Message Format
Follow the Conventional Commits specification: `<type>(<scope>): <subject>`
| Type | Description |
| ------ | --------------------------------------------- |
| feat | New feature for the user |
| fix | Bug fix for the user |
| docs | Documentation only changes |
| style | Formatting, missing semicolons (no code change) |
| refactor | Code change that neither fixes a bug nor adds a feature |
| test | Adding or updating tests |
| chore | Build process or auxiliary tool changes |
### 11.2 Branching Strategy
- `main`: Production-ready code only
- `develop`: Integration branch for features
- `feature/JIRA-XXX-description`: Feature branches
- `bugfix/JIRA-XXX-description`: Bug fix branches
- `hotfix/JIRA-XXX-description`: Production hotfixes
- `release/vX.X.X`: Release preparation branches
## 12. Testing Standards
### 12.1 Testing Pyramid
- Unit Tests (70%): Test individual functions/components in isolation
- Integration Tests (20%): Test component interactions and API integrations
- E2E Tests (10%): Test critical user journeys end-to-end
### 12.2 Test Naming Convention
Use the pattern: describe what is being tested + under what conditions + expected outcome.
- Example: `getUserById returns user object when valid ID is provided`
- Example: `calculateTotal throws error when items array is empty`
### 12.3 Code Coverage Requirements
- Minimum line coverage: 80%
- Minimum branch coverage: 75%
- Critical paths: 100% coverage required
- New code must maintain or improve coverage

View File

@ -125,12 +125,19 @@ All functions include JSDoc comments with:
## 🤝 Contributing ## 🤝 Contributing
Follow the AgenticIQ Enterprise Coding Guidelines when contributing: We welcome contributions! Please read our [Contributing Guide](CONTRIBUTING.md) for details on:
- Git workflow and branching strategy
- Commit message format (Conventional Commits)
- Code standards and guidelines
- Pull request process
Quick checklist:
1. Use conventional commits (`feat:`, `fix:`, `docs:`, etc.) 1. Use conventional commits (`feat:`, `fix:`, `docs:`, etc.)
2. Follow branching strategy (`feature/`, `bugfix/`, `hotfix/`) 2. Follow branching strategy (`feature/`, `bugfix/`, `hotfix/`)
3. Maintain code quality standards 3. Maintain code quality standards (see `GUIDELINES.md`)
4. Add tests for new features 4. Add tests for new features
5. Ensure all checks pass before submitting PR
## 📄 License ## 📄 License

49
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@tanstack/router-devtools": "^1.143.2", "@tanstack/router-devtools": "^1.143.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -3012,6 +3013,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3614,6 +3642,21 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -4147,6 +4190,12 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -17,6 +17,7 @@
"@tanstack/router-devtools": "^1.143.2", "@tanstack/router-devtools": "^1.143.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

3
public/Agent/add.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25 1.125C8.25 0.50368 7.74632 0 7.125 0C6.50368 0 6 0.50368 6 1.125V6H1.125C0.50368 6 0 6.50368 0 7.125C0 7.74632 0.50368 8.25 1.125 8.25H6V13.125C6 13.7463 6.50368 14.25 7.125 14.25C7.74632 14.25 8.25 13.7463 8.25 13.125V8.25H13.125C13.7463 8.25 14.25 7.74632 14.25 7.125C14.25 6.50368 13.7463 6 13.125 6H8.25V1.125Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

10
public/Agent/agent.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="140" height="141" viewBox="0 0 140 141" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse opacity="0.1" cx="64" cy="136" rx="46.5" ry="4.5" fill="#2B2A7F"/>
<path d="M15.6504 73.3227C15.6504 74.4272 16.5458 75.3227 17.6504 75.3227C18.755 75.3227 19.6504 74.4272 19.6504 73.3227H17.6504H15.6504ZM113.85 73.3227H111.85C111.85 98.783 91.2107 119.423 65.7504 119.423V121.423V123.423C93.4199 123.423 115.85 100.992 115.85 73.3227H113.85ZM17.6504 73.3227H19.6504C19.6504 47.8623 40.2901 27.2227 65.7504 27.2227V25.2227V23.2227C38.0809 23.2227 15.6504 45.6532 15.6504 73.3227H17.6504ZM65.7504 25.2227V27.2227C91.2107 27.2227 111.85 47.8623 111.85 73.3227H113.85H115.85C115.85 45.6532 93.4199 23.2227 65.7504 23.2227V25.2227Z" fill="black"/>
<rect x="11.25" y="63.2539" width="16" height="20" rx="4" fill="#F7F8FF" stroke="#0033FF" stroke-width="4"/>
<rect x="104.25" y="63.2539" width="16" height="20" rx="4" fill="#F7F8FF" stroke="#0033FF" stroke-width="4"/>
<rect x="73.25" y="116.254" width="9" height="14" rx="4.5" transform="rotate(90 73.25 116.254)" fill="white" stroke="#0033FF" stroke-width="4"/>
<path d="M64.8779 54.123C67.5621 54.123 70.0705 54.6241 72.4023 55.627C74.7341 56.6297 76.7763 58.0073 78.5283 59.7598C80.2808 61.5118 81.6583 63.5528 82.6611 65.8818C83.6639 68.2105 84.165 70.7196 84.165 73.4082C84.165 78.4051 82.8151 83.0208 80.1143 87.2549C77.4132 91.4887 73.991 94.896 69.8496 97.4766C69.5408 97.6388 69.2317 97.7281 68.9229 97.7441C68.614 97.7606 68.3292 97.6917 68.0693 97.5371C67.8101 97.3829 67.5912 97.1834 67.4121 96.9395C67.2329 96.6952 67.1304 96.398 67.1055 96.0488L66.9209 92.6924H64.8809C59.5112 92.6924 54.9542 90.821 51.2109 87.0781C47.4676 83.3358 45.5958 78.7808 45.5957 73.4131C45.5957 68.0447 47.4671 63.4869 51.21 59.7412C54.9532 55.9957 59.5093 54.1231 64.8779 54.123ZM64.8809 57.7393C60.5016 57.7393 56.7951 59.2557 53.7617 62.2891C50.7284 65.3224 49.2119 69.0289 49.2119 73.4082C49.212 77.7873 50.7286 81.4931 53.7617 84.5264C56.7951 87.5597 60.5016 89.0771 64.8809 89.0771H70.9072V92.3311C73.7597 89.9205 76.0803 87.0984 77.8682 83.8643C79.656 80.6301 80.5498 77.1445 80.5498 73.4082C80.5498 69.0289 79.0324 65.3224 75.999 62.2891C72.9658 59.2559 69.2599 57.7393 64.8809 57.7393Z" fill="black"/>
<path d="M64.8779 54.123C67.5621 54.123 70.0705 54.6241 72.4023 55.627C74.7341 56.6297 76.7763 58.0073 78.5283 59.7598C80.2808 61.5118 81.6583 63.5528 82.6611 65.8818C83.6639 68.2105 84.165 70.7196 84.165 73.4082C84.165 78.4051 82.8151 83.0208 80.1143 87.2549C77.4132 91.4887 73.991 94.896 69.8496 97.4766C69.5408 97.6388 69.2317 97.7281 68.9229 97.7441C68.614 97.7606 68.3292 97.6917 68.0693 97.5371C67.8101 97.3829 67.5912 97.1834 67.4121 96.9395C67.2329 96.6952 67.1304 96.398 67.1055 96.0488L66.9209 92.6924H64.8809C59.5112 92.6924 54.9542 90.821 51.2109 87.0781C47.4676 83.3358 45.5958 78.7808 45.5957 73.4131C45.5957 68.0447 47.4671 63.4869 51.21 59.7412C54.9532 55.9957 59.5093 54.1231 64.8779 54.123ZM64.8809 57.7393C60.5016 57.7393 56.7951 59.2557 53.7617 62.2891C50.7284 65.3224 49.2119 69.0289 49.2119 73.4082C49.212 77.7873 50.7286 81.4931 53.7617 84.5264C56.7951 87.5597 60.5016 89.0771 64.8809 89.0771H70.9072V92.3311C73.7597 89.9205 76.0803 87.0984 77.8682 83.8643C79.656 80.6301 80.5498 77.1445 80.5498 73.4082C80.5498 69.0289 79.0324 65.3224 75.999 62.2891C72.9658 59.2559 69.2599 57.7393 64.8809 57.7393Z" stroke="black"/>
<path d="M65.75 82.5999C65.3375 82.5999 64.9844 82.4531 64.6908 82.1594C64.3969 81.8656 64.25 81.5124 64.25 81.0999C64.25 80.6874 64.3969 80.3343 64.6908 80.0407C64.9844 79.7468 65.3375 79.5999 65.75 79.5999C66.1625 79.5999 66.5156 79.7468 66.8092 80.0407C67.1031 80.3343 67.25 80.6874 67.25 81.0999C67.25 81.5124 67.1031 81.8656 66.8092 82.1594C66.5156 82.4531 66.1625 82.5999 65.75 82.5999ZM64.3848 77.6962V66.2539H67.1152V77.6962H64.3848Z" fill="#0033FF"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.91667 12.25C2.59583 12.25 2.32118 12.1358 2.09271 11.9073C1.86424 11.6788 1.75 11.4042 1.75 11.0833V2.91667C1.75 2.59583 1.86424 2.32118 2.09271 2.09271C2.32118 1.86424 2.59583 1.75 2.91667 1.75H6.41667C6.58194 1.75 6.72049 1.8059 6.83229 1.91771C6.9441 2.02951 7 2.16806 7 2.33333C7 2.49861 6.9441 2.63715 6.83229 2.74896C6.72049 2.86076 6.58194 2.91667 6.41667 2.91667H2.91667V11.0833H11.0833V7.58333C11.0833 7.41806 11.1392 7.27951 11.251 7.16771C11.3628 7.0559 11.5014 7 11.6667 7C11.8319 7 11.9705 7.0559 12.0823 7.16771C12.1941 7.27951 12.25 7.41806 12.25 7.58333V11.0833C12.25 11.4042 12.1358 11.6788 11.9073 11.9073C11.6788 12.1358 11.4042 12.25 11.0833 12.25H2.91667ZM11.0833 3.73333L6.06667 8.75C5.95972 8.85694 5.82361 8.91042 5.65833 8.91042C5.49306 8.91042 5.35694 8.85694 5.25 8.75C5.14306 8.64306 5.08958 8.50694 5.08958 8.34167C5.08958 8.17639 5.14306 8.04028 5.25 7.93333L10.2667 2.91667H8.75C8.58472 2.91667 8.44618 2.86076 8.33437 2.74896C8.22257 2.63715 8.16667 2.49861 8.16667 2.33333C8.16667 2.16806 8.22257 2.02951 8.33437 1.91771C8.44618 1.8059 8.58472 1.75 8.75 1.75H11.6667C11.8319 1.75 11.9705 1.8059 12.0823 1.91771C12.1941 2.02951 12.25 2.16806 12.25 2.33333V5.25C12.25 5.41528 12.1941 5.55382 12.0823 5.66563C11.9705 5.77743 11.8319 5.83333 11.6667 5.83333C11.5014 5.83333 11.3628 5.77743 11.251 5.66563C11.1392 5.55382 11.0833 5.41528 11.0833 5.25V3.73333Z" fill="#0033FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
public/Agent/search.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2.41602C14.1881 2.41602 17.5838 5.81101 17.584 9.99902C17.584 14.1872 14.1882 17.583 10 17.583C5.81199 17.5828 2.41699 14.1871 2.41699 9.99902C2.41717 5.81112 5.8121 2.41619 10 2.41602Z" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.333 18.334L15.833 15.834" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -0,0 +1,19 @@
<svg width="63" height="85" viewBox="0 0 63 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1_27468" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="63" height="85">
<path d="M0 0H63V69C63 77.8366 55.8366 85 47 85H0V0Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1_27468)">
<path d="M63.7743 1L17.5518 25.5215C11.753 28.5978 7.95639 34.7136 7.58789 41.5719L7.58789 85.5834H63.6215L63.7743 1Z" fill="url(#paint0_linear_1_27468)" fill-opacity="0.25"/>
<path d="M63.7743 13.1812L29.4387 34.6055C23.4302 38.3033 19.821 44.9802 19.9728 52.1178V85.5834L63.6215 85.5834L63.7743 13.1812Z" fill="url(#paint1_linear_1_27468)" fill-opacity="0.25"/>
</g>
<defs>
<linearGradient id="paint0_linear_1_27468" x1="13.5464" y1="72.2922" x2="70.9508" y2="28.0665" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="1" stop-color="#001C8D"/>
</linearGradient>
<linearGradient id="paint1_linear_1_27468" x1="13.5464" y1="72.2922" x2="70.9508" y2="28.0665" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="1" stop-color="#001C8D"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,19 @@
<svg width="59" height="85" viewBox="0 0 59 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1_27478" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="59" height="85">
<path d="M0 0H59V69C59 77.8366 51.8366 85 43 85H0V0Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1_27478)">
<path d="M59.7743 1L13.5518 25.5215C7.75305 28.5978 3.95639 34.7136 3.5879 41.5719L3.58789 85.5834H59.6215L59.7743 1Z" fill="url(#paint0_linear_1_27478)" fill-opacity="0.25"/>
<path d="M59.7743 13.1812L25.4387 34.6055C19.4302 38.3033 15.821 44.9802 15.9728 52.1178V85.5834L59.6215 85.5834L59.7743 13.1812Z" fill="url(#paint1_linear_1_27478)" fill-opacity="0.25"/>
</g>
<defs>
<linearGradient id="paint0_linear_1_27478" x1="-2.00685" y1="74.0206" x2="73.6108" y2="-2.37143" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB8C6"/>
<stop offset="1" stop-color="#596AC3"/>
</linearGradient>
<linearGradient id="paint1_linear_1_27478" x1="-2.00685" y1="74.0206" x2="73.6108" y2="-2.37143" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB8C6"/>
<stop offset="1" stop-color="#596AC3"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,19 @@
<svg width="59" height="85" viewBox="0 0 59 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1_27498" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="59" height="85">
<path d="M0 0H59V69C59 77.8366 51.8366 85 43 85H0V0Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1_27498)">
<path d="M59.7743 1L13.5518 25.5215C7.75305 28.5978 3.95639 34.7136 3.5879 41.5719L3.58789 85.5834H59.6215L59.7743 1Z" fill="url(#paint0_linear_1_27498)" fill-opacity="0.25"/>
<path d="M59.7743 13.1812L25.4387 34.6055C19.4302 38.3033 15.821 44.9802 15.9728 52.1178V85.5834L59.6215 85.5834L59.7743 13.1812Z" fill="url(#paint1_linear_1_27498)" fill-opacity="0.25"/>
</g>
<defs>
<linearGradient id="paint0_linear_1_27498" x1="3.99975" y1="79.5" x2="47.9644" y2="11.1068" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0CDFF"/>
<stop offset="1" stop-color="#0020A0"/>
</linearGradient>
<linearGradient id="paint1_linear_1_27498" x1="3.99975" y1="79.5" x2="47.9644" y2="11.1068" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0CDFF"/>
<stop offset="1" stop-color="#0020A0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,19 @@
<svg width="59" height="85" viewBox="0 0 59 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1_27498" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="59" height="85">
<path d="M0 0H59V69C59 77.8366 51.8366 85 43 85H0V0Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1_27498)">
<path d="M59.7743 1L13.5518 25.5215C7.75305 28.5978 3.95639 34.7136 3.5879 41.5719L3.58789 85.5834H59.6215L59.7743 1Z" fill="url(#paint0_linear_1_27498)" fill-opacity="0.25"/>
<path d="M59.7743 13.1812L25.4387 34.6055C19.4302 38.3033 15.821 44.9802 15.9728 52.1178V85.5834L59.6215 85.5834L59.7743 13.1812Z" fill="url(#paint1_linear_1_27498)" fill-opacity="0.25"/>
</g>
<defs>
<linearGradient id="paint0_linear_1_27498" x1="3.99975" y1="79.5" x2="47.9644" y2="11.1068" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0CDFF"/>
<stop offset="1" stop-color="#0020A0"/>
</linearGradient>
<linearGradient id="paint1_linear_1_27498" x1="3.99975" y1="79.5" x2="47.9644" y2="11.1068" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0CDFF"/>
<stop offset="1" stop-color="#0020A0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,46 @@
# Images Directory
This directory contains all image assets for the AgenticIQ application, organized by category.
## Folder Structure
```
images/
├── logo/ # Brand logos and identity assets
├── icons/ # UI icons and symbols
├── auth/ # Authentication-related images (social login icons, etc.)
├── backgrounds/ # Background images and decorative elements
└── ui/ # General UI component images
```
## Usage Guidelines
### Importing Images in Components
```typescript
// Example: Importing a logo
import agenticiqLogo from '@/assets/images/logo/agenticiq-logo.png';
// Example: Importing an icon
import googleIcon from '@/assets/images/auth/google-icon.svg';
```
### File Naming Convention
- Use **kebab-case** for all filenames (e.g., `agenticiq-logo.png`, `google-icon.svg`)
- Be descriptive and avoid abbreviations
- Include size/resolution suffix if multiple versions exist (e.g., `logo-256x256.png`)
### Image Formats
- **Logos**: PNG (with transparency) or SVG
- **Icons**: SVG (preferred) or PNG
- **Photos**: WebP (preferred) or JPG
- **Backgrounds**: WebP or PNG
### Optimization
- Optimize images before adding them to this directory
- Use appropriate formats for the use case
- Consider responsive images for different screen densities

View File

@ -0,0 +1,35 @@
# Authentication Assets
This directory contains images related to authentication and social login.
## Files to Add
### Social Login Icons
- `google-icon.svg` - Google sign-in button icon
- `azure-icon.svg` - Microsoft Azure sign-in button icon
- `github-icon.svg` - GitHub sign-in icon (if needed)
- `facebook-icon.svg` - Facebook sign-in icon (if needed)
### Authentication UI
- `auth-background.svg` - Background pattern for auth pages (if needed)
- `lock-icon.svg` - Security/lock icon
- `user-icon.svg` - User profile icon
## Usage Example
```typescript
import googleIcon from '@/assets/images/auth/google-icon.svg';
import azureIcon from '@/assets/images/auth/azure-icon.svg';
<button>
<img src={googleIcon} alt="Google" className="w-6 h-6" />
<span>Sign In with Google</span>
</button>
```
## Icon Specifications
- **Size**: 24x24px or 32x32px for social login buttons
- **Format**: SVG preferred for scalability
- **Style**: Match brand guidelines for each provider

View File

@ -0,0 +1,32 @@
# Background Assets
This directory contains background images and decorative elements.
## Files to Add
### Decorative Elements
- `gradient-blob-1.svg` - Decorative gradient shape (if needed as image)
- `gradient-blob-2.svg` - Additional decorative element
- `pattern-overlay.svg` - Pattern overlay (if needed)
### Background Images
- `auth-background.jpg` or `.webp` - Full background image for auth pages (if needed)
- `dashboard-background.svg` - Dashboard background pattern
## Usage Example
```typescript
import backgroundPattern from '@/assets/images/backgrounds/pattern-overlay.svg';
<div
style={{ backgroundImage: `url(${backgroundPattern})` }}
className="absolute inset-0 opacity-20"
/>
```
## Notes
- Most decorative backgrounds are created with CSS gradients (as in login-page.tsx)
- Only add image files if CSS gradients cannot achieve the desired effect
- Optimize all background images for performance

View File

@ -0,0 +1,10 @@
<svg width="411" height="449" viewBox="0 0 411 449" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-36.0004 0L317.405 172.143C376.454 203.681 411.924 260.629 410.432 321.505L410.432 614H-36.0004V0Z" fill="url(#paint0_linear_11_38052)"/>
<defs>
<linearGradient id="paint0_linear_11_38052" x1="227.514" y1="-4.05561" x2="223.001" y2="663.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="0.615385" stop-color="#0033FF"/>
<stop offset="0.904576" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@ -0,0 +1,9 @@
<svg width="527" height="593" viewBox="0 0 527 593" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L417.142 203.263C486.841 240.503 528.707 307.746 526.947 379.628L526.947 725H0V0Z" fill="url(#paint0_linear_11_38051)"/>
<defs>
<linearGradient id="paint0_linear_11_38051" x1="243.5" y1="60.5" x2="263" y2="927" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="0.796851" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@ -0,0 +1,12 @@
<svg width="405" height="243" viewBox="0 0 405 243" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-18.8586 -296.056C-171.92 -235.336 -241.739 -70.7007 -174.805 71.6671C-107.871 214.035 70.4706 280.224 223.532 219.504C376.592 158.784 446.412 -5.85137 379.478 -148.219C312.544 -290.587 134.202 -356.776 -18.8586 -296.056Z" fill="url(#paint0_radial_11_38040)"/>
<defs>
<radialGradient id="paint0_radial_11_38040" cx="0" cy="0" r="1" gradientTransform="matrix(93.0029 -317.795 343.872 89.0633 102.336 -38.276)" gradientUnits="userSpaceOnUse">
<stop stop-color="#C8FAFF"/>
<stop offset="0.33" stop-color="#00CEE0"/>
<stop offset="0.705031" stop-color="#6172F3"/>
<stop offset="0.816395" stop-color="#C6D7FE"/>
<stop offset="1" stop-color="#F5F8FF"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1,9 @@
<svg width="302" height="397" viewBox="0 0 302 397" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M181.364 -142.056C28.3031 -81.3358 -41.5164 83.2993 25.4178 225.667C92.352 368.035 270.693 434.224 423.754 373.504C576.815 312.784 646.635 148.149 579.7 5.78083C512.766 -136.587 334.425 -202.776 181.364 -142.056Z" fill="url(#paint0_radial_11_38047)"/>
<defs>
<radialGradient id="paint0_radial_11_38047" cx="0" cy="0" r="1" gradientTransform="matrix(140.333 -462.353 501.033 134.576 255.229 260.282)" gradientUnits="userSpaceOnUse">
<stop stop-color="#00CEE0"/>
<stop offset="0.864679" stop-color="#0033FF"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@ -0,0 +1,33 @@
# Icon Assets
This directory contains UI icons and symbols used throughout the application.
## Files to Add
### Navigation Icons
- `home-icon.svg`
- `dashboard-icon.svg`
- `settings-icon.svg`
### Action Icons
- `eye-icon.svg` - Show/hide password
- `eye-close-icon.svg` - Hide password
- `search-icon.svg`
- `plus-icon.svg`
- `edit-icon.svg`
- `delete-icon.svg`
### Status Icons
- `success-icon.svg`
- `error-icon.svg`
- `warning-icon.svg`
- `info-icon.svg`
## Usage Example
```typescript
import eyeIcon from '@/assets/images/icons/eye-icon.svg';
<img src={eyeIcon} alt="Show password" className="w-5 h-5" />
```

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -0,0 +1,27 @@
# Logo Assets
This directory contains brand logos and identity assets for AgenticIQ.
## Files to Add
### Primary Logo
- `agenticiq-logo.png` or `agenticiq-logo.svg` - Main AgenticIQ logo
- `agenticiq-logo-white.png` - White variant for dark backgrounds
- `agenticiq-logo-dark.png` - Dark variant for light backgrounds
### Logo Variants
- `agenticiq-logo-horizontal.svg` - Horizontal layout variant
- `agenticiq-logo-vertical.svg` - Vertical/stacked layout variant
- `agenticiq-icon.svg` - Icon-only version (favicon, app icon)
### Trademark Badge
- `trademark-badge.svg` - TM badge component (if separate from logo)
## Usage Example
```typescript
import agenticiqLogo from '@/assets/images/logo/agenticiq-logo.svg';
<img src={agenticiqLogo} alt="AgenticIQ Logo" />
```

View File

@ -0,0 +1,31 @@
# UI Component Assets
This directory contains images used in UI components throughout the application.
## Files to Add
### Profile & User
- `default-avatar.png` - Default user avatar placeholder
- `profile-placeholder.svg` - Profile image placeholder
### Dashboard Components
- `metric-card-icon.svg` - Icon for metric cards
- `empty-state-illustration.svg` - Empty state illustrations
### General UI
- `loading-spinner.svg` - Loading animation (if not CSS-based)
- `error-illustration.svg` - Error state illustration
- `success-illustration.svg` - Success state illustration
## Usage Example
```typescript
import defaultAvatar from '@/assets/images/ui/default-avatar.png';
<img
src={defaultAvatar}
alt="User avatar"
className="w-10 h-10 rounded-full"
/>
```

View File

@ -0,0 +1,220 @@
/**
* AuthButton Component
* @description Reusable button components for authentication forms.
* Includes primary action button and social login buttons.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
/**
* Props for AuthButton component
*/
interface AuthButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button content */
children: ReactNode;
/** Loading state */
isLoading?: boolean;
/** Full width button */
fullWidth?: boolean;
}
/**
* Props for SocialButton component
*/
interface SocialButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button text */
children: ReactNode;
/** Icon element to show before text */
icon: ReactNode;
}
/**
* Props for TextButton component
*/
interface TextButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button text */
children: ReactNode;
}
/**
* Google icon for social login
* @returns {JSX.Element} Google SVG icon
*/
export function GoogleIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.56 12.25C22.56 11.47 22.49 10.72 22.36 10H12V14.26H17.92C17.66 15.63 16.88 16.79 15.71 17.57V20.34H19.28C21.36 18.42 22.56 15.6 22.56 12.25Z"
fill="#4285F4"
/>
<path
d="M12 23C14.97 23 17.46 22.02 19.28 20.34L15.71 17.57C14.73 18.23 13.48 18.63 12 18.63C9.13999 18.63 6.70999 16.7 5.83999 14.1H2.17999V16.94C3.98999 20.53 7.69999 23 12 23Z"
fill="#34A853"
/>
<path
d="M5.84 14.1C5.62 13.44 5.49 12.73 5.49 12C5.49 11.27 5.62 10.56 5.84 9.9V7.06H2.18C1.43 8.55 1 10.22 1 12C1 13.78 1.43 15.45 2.18 16.94L5.84 14.1Z"
fill="#FBBC05"
/>
<path
d="M12 5.38C13.62 5.38 15.06 5.94 16.21 7.02L19.36 3.87C17.45 2.09 14.97 1 12 1C7.69999 1 3.98999 3.47 2.17999 7.06L5.83999 9.9C6.70999 7.3 9.13999 5.38 12 5.38Z"
fill="#EA4335"
/>
</svg>
);
}
/**
* Azure icon for social login
* @returns {JSX.Element} Azure SVG icon
*/
export function AzureIcon(): JSX.Element {
return (
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5L7.667 2.875L14.375 0L14.833 11.5L7.667 20.125L0 11.5Z" fill="#0078D4" />
<path d="M8.625 8.625L14.375 0L23 5.75V17.25L14.375 23L8.625 8.625Z" fill="#0078D4" />
<path d="M8.625 8.625L14.375 11.5L14.375 23L8.625 8.625Z" fill="#0050A4" />
<path d="M14.375 11.5L23 5.75V17.25L14.375 11.5Z" fill="#0050A4" />
</svg>
);
}
/**
* AuthButton component
* @description Primary action button with cyan/teal background.
* Used for main form submissions (Sign In, Sign Up, etc.).
* @param {AuthButtonProps} props - Component props
* @returns {JSX.Element} AuthButton element
*/
export function AuthButton({
children,
isLoading = false,
fullWidth = true,
disabled,
className = '',
...buttonProps
}: AuthButtonProps): JSX.Element {
return (
<button
type="submit"
disabled={disabled || isLoading}
className={`
flex items-center justify-center
min-h-[44px] h-11 md:h-[52px]
px-4 md:px-[18px] py-2.5 md:py-[13px]
bg-[#00E2E0]
border border-[rgba(255,255,255,0.2)]
rounded-lg md:rounded-[12px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-sm md:text-base md:text-[16px] font-semibold leading-5 md:leading-6 text-black
cursor-pointer
hover:bg-[#00D4D2]
active:bg-[#00C6C4]
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-150
${fullWidth ? 'w-full' : ''}
${className}
`}
{...buttonProps}
>
{isLoading ? (
<span className="flex items-center gap-2">
<svg
className="animate-spin h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Loading...
</span>
) : (
children
)}
</button>
);
}
/**
* SocialButton component
* @description Social login button with white background.
* Used for Google, Azure, and other OAuth providers.
* @param {SocialButtonProps} props - Component props
* @returns {JSX.Element} SocialButton element
*/
export function SocialButton({
children,
icon,
className = '',
...buttonProps
}: SocialButtonProps): JSX.Element {
return (
<button
type="button"
className={`
flex items-center justify-center gap-2 md:gap-3
px-3 md:px-4 py-2 md:py-2.5
bg-white
rounded-lg md:rounded-xl
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-xs md:text-sm font-semibold leading-5 md:leading-6 text-black
cursor-pointer
hover:bg-gray-50
active:bg-gray-100
transition-colors duration-200
min-h-[44px]
${className}
`}
{...buttonProps}
>
<span className="shrink-0 w-6 h-6 flex items-center justify-center">
{icon}
</span>
{children}
</button>
);
}
/**
* TextButton component
* @description Text-only button for secondary actions (e.g., "Forgot password?").
* @param {TextButtonProps} props - Component props
* @returns {JSX.Element} TextButton element
*/
export function TextButton({
children,
className = '',
...buttonProps
}: TextButtonProps): JSX.Element {
return (
<button
type="button"
className={`
text-sm font-medium leading-5
text-[rgba(255,255,255,0.75)]
cursor-pointer
hover:text-white
hover:underline
transition-colors duration-200
${className}
`}
{...buttonProps}
>
{children}
</button>
);
}

View File

@ -0,0 +1,108 @@
/**
* AuthCard Component
* @description Reusable authentication card with gradient background.
* Can be used for Sign In, Sign Up, Forgot Password, and other auth screens.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { type ReactNode } from 'react';
/**
* Props for AuthCard component
*/
interface AuthCardProps {
/** Child elements to render inside the card */
children: ReactNode;
/** Additional CSS classes for customization */
className?: string;
/** Card variant - 'signin' has standard padding, 'register' has compact padding */
variant?: 'signin' | 'register';
}
/**
* Props for AuthCardHeader component
*/
interface AuthCardHeaderProps {
/** Main title text */
title: string;
/** Subtitle/description text */
subtitle?: string;
}
/**
* Props for AuthCardContent component
*/
interface AuthCardContentProps {
/** Child elements (form fields, etc.) */
children: ReactNode;
/** Additional CSS classes */
className?: string;
}
/**
* AuthCard component
* @description Main container with gradient background (teal to dark blue).
* Provides the card structure for authentication screens.
* No border to avoid dark line artifacts.
* @param {AuthCardProps} props - Component props
* @returns {JSX.Element} AuthCard element
*/
export function AuthCard({ children, className = '', variant = 'signin' }: AuthCardProps): JSX.Element {
const isRegister = variant === 'register';
return (
<div
className={`
flex flex-col items-center gap-5 md:gap-6 lg:gap-6 xl:gap-8
w-full max-w-[520px]
rounded-2xl md:rounded-3xl lg:rounded-2xl xl:rounded-[32px]
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
${isRegister ? 'px-6 py-8 md:px-8 md:py-12 lg:px-8 lg:py-10 xl:px-[42px] xl:py-[64px]' : 'px-6 py-8 md:px-10 md:py-12 lg:px-10 lg:py-12 xl:px-[60px] xl:py-[80px]'}
${className}
`}
style={{
background: 'linear-gradient(180deg, #00b8b7 0%, #001c8e 100%)',
}}
>
{children}
</div>
);
}
/**
* AuthCardHeader component
* @description Header section with title and optional subtitle.
* @param {AuthCardHeaderProps} props - Component props
* @returns {JSX.Element} AuthCardHeader element
*/
export function AuthCardHeader({ title, subtitle }: AuthCardHeaderProps): JSX.Element {
return (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col items-center gap-2 w-full text-center">
<h2 className="text-2xl md:text-2xl lg:text-2xl xl:text-3xl 2xl:text-[30px] font-semibold leading-tight md:leading-tight lg:leading-tight xl:leading-[38px] text-white w-full">
{title}
</h2>
{subtitle && (
<p className="text-sm md:text-base lg:text-sm xl:text-lg 2xl:text-[18px] font-medium leading-5 md:leading-5 lg:leading-5 xl:leading-6 text-[rgba(255,255,255,0.75)]">
{subtitle}
</p>
)}
</div>
</div>
);
}
/**
* AuthCardContent component
* @description Content section for form fields and actions.
* @param {AuthCardContentProps} props - Component props
* @returns {JSX.Element} AuthCardContent element
*/
export function AuthCardContent({ children, className = '' }: AuthCardContentProps): JSX.Element {
return (
<div className={`flex flex-col items-center gap-6 w-full rounded-xl ${className}`}>
{children}
</div>
);
}

View File

@ -0,0 +1,333 @@
/**
* AuthFormCard Component
* @description Unified authentication form with animated transitions between Sign In and Register.
* Uses framer-motion for smooth expand/collapse animations.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState, type FormEvent } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
AuthCard,
AuthCardHeader,
AuthCardContent,
AuthInput,
AuthButton,
SocialButton,
TextButton,
GoogleIcon,
AzureIcon,
} from './index';
/** Animation timing constants (in milliseconds) */
const ANIMATION_TIMING = {
/** Duration for simulating form submission */
FORM_SUBMIT_DELAY: 1000,
/** Duration for name fields expand/collapse */
NAME_FIELDS_DURATION: 0.2,
/** Duration for forgot password link animation */
FORGOT_PASSWORD_DURATION: 0.15,
/** Opacity fade delay */
OPACITY_DELAY: 0.05,
/** Opacity fade duration */
OPACITY_DURATION: 0.15,
/** Margin animation bottom value */
NAME_FIELDS_MARGIN: 16,
/** Margin animation top value */
FORGOT_PASSWORD_MARGIN: 8,
/** Layout animation duration */
LAYOUT_DURATION: 0.3,
} as const;
/**
* Auth form mode type
*/
type AuthMode = 'signin' | 'register';
/**
* Props for AuthFormCard component
*/
interface AuthFormCardProps {
/** Initial mode (signin or register) */
initialMode?: AuthMode;
/** Callback when sign in is submitted */
onSignIn?: (email: string, password: string) => void;
/** Callback when register is submitted */
onRegister?: (data: RegisterData) => void;
/** Callback for Google sign in */
onGoogleAuth?: () => void;
/** Callback for Azure sign in */
onAzureAuth?: () => void;
/** Callback for forgot password */
onForgotPassword?: () => void;
}
/**
* Register form data interface
*/
interface RegisterData {
firstName: string;
lastName: string;
email: string;
password: string;
}
/**
* Spring transition for smooth physics-based animations
* Using 'as const' to ensure literal types for framer-motion
*/
const springTransition = {
type: 'spring' as const,
stiffness: 500,
damping: 30,
mass: 1,
};
/**
* Animation variants for the collapsible name fields row
*/
const nameFieldsVariants = {
hidden: {
height: 0,
opacity: 0,
marginBottom: 0,
transition: {
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
opacity: { duration: 0.1 },
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
},
},
visible: {
height: 'auto',
opacity: 1,
marginBottom: ANIMATION_TIMING.NAME_FIELDS_MARGIN,
transition: {
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
},
},
};
/**
* Animation variants for forgot password link
*/
const forgotPasswordVariants = {
hidden: {
height: 0,
opacity: 0,
marginTop: 0,
transition: {
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
opacity: { duration: 0.1 },
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
},
},
visible: {
height: 'auto',
opacity: 1,
marginTop: ANIMATION_TIMING.FORGOT_PASSWORD_MARGIN,
transition: {
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
},
},
};
/**
* AuthFormCard component
* @description Unified authentication card with animated transitions between Sign In and Register modes.
* Features smooth expand/collapse animations for form fields using framer-motion.
* @param {AuthFormCardProps} props - Component props
* @returns {JSX.Element} AuthFormCard element
*/
export function AuthFormCard({
initialMode = 'signin',
onSignIn,
onRegister,
onGoogleAuth,
onAzureAuth,
onForgotPassword,
}: AuthFormCardProps): JSX.Element {
const [mode, setMode] = useState<AuthMode>(initialMode);
const isRegister = mode === 'register';
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
/**
* Handle form submission based on current mode
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
setIsLoading(true);
if (isRegister) {
onRegister?.({ firstName, lastName, email, password });
} else {
onSignIn?.(email, password);
}
setTimeout(() => setIsLoading(false), ANIMATION_TIMING.FORM_SUBMIT_DELAY);
};
/**
* Toggle between Sign In and Register modes
* Clears all form fields when switching modes
*/
const toggleMode = (): void => {
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
setFirstName('');
setLastName('');
setEmail('');
setPassword('');
};
return (
<motion.div
layout
transition={{ layout: { ...springTransition, duration: ANIMATION_TIMING.LAYOUT_DURATION } }}
className="w-full max-w-[460px] lg:max-w-[440px] xl:max-w-[520px]"
style={{ willChange: 'transform' }}
>
<AuthCard variant={isRegister ? 'register' : 'signin'}>
{/* Header Section */}
<AuthCardHeader
title={isRegister ? 'Create Your Account' : 'Sign In to Your Account'}
subtitle={
isRegister
? 'Welcome! Please enter your information.'
: 'Welcome back. Please enter your credentials.'
}
/>
{/* Form Content */}
<AuthCardContent>
<form onSubmit={handleSubmit} className="flex flex-col gap-3 md:gap-4 lg:gap-3 xl:gap-4 w-full">
{/* Collapsible Name Fields Row */}
<AnimatePresence initial={false}>
{isRegister && (
<motion.div
key="name-fields"
variants={nameFieldsVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="overflow-hidden"
style={{ willChange: 'height, opacity' }}
>
<div className="flex gap-3 w-full">
<div className="flex-1 min-w-0">
<AuthInput
label="First Name"
type="text"
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
isRequired
autoComplete="given-name"
/>
</div>
<div className="flex-1 min-w-0">
<AuthInput
label="Last Name"
type="text"
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
autoComplete="family-name"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Email Input */}
<motion.div layout transition={springTransition}>
<AuthInput
label="Email"
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
isRequired
autoComplete={isRegister ? 'email' : 'username'}
/>
</motion.div>
{/* Password Input */}
<motion.div layout transition={springTransition}>
<AuthInput
label="Password"
type="password"
placeholder={isRegister ? 'Create a password' : 'Enter your password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
isRequired
autoComplete={isRegister ? 'new-password' : 'current-password'}
/>
</motion.div>
{/* Forgot Password Link - Collapses in Register mode */}
<AnimatePresence initial={false}>
{!isRegister && (
<motion.div
key="forgot-password"
variants={forgotPasswordVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="overflow-hidden flex justify-end w-full"
style={{ willChange: 'height, opacity' }}
>
<TextButton onClick={onForgotPassword}>
Forgot your password?
</TextButton>
</motion.div>
)}
</AnimatePresence>
{/* Submit Button */}
<AuthButton isLoading={isLoading}>
{isRegister ? 'Create Account' : 'Sign In'}
</AuthButton>
</form>
{/* Social Login Buttons */}
<div className="flex gap-3 w-full">
<SocialButton
icon={<GoogleIcon />}
onClick={onGoogleAuth}
className="flex-1"
>
{isRegister ? 'Sign Up with Google' : 'Sign In with Google'}
</SocialButton>
<SocialButton
icon={<AzureIcon />}
onClick={onAzureAuth}
className="flex-1"
>
{isRegister ? 'Sign Up with Azure' : 'Sign In with Azure'}
</SocialButton>
</div>
</AuthCardContent>
{/* Footer Section */}
<div className="flex items-baseline justify-center gap-1 w-full">
<span className="text-sm font-normal leading-5 text-[rgba(255,255,255,0.5)]">
{isRegister ? 'Already have an account?' : "Don't have an account?"}
</span>
<TextButton onClick={toggleMode}>
{isRegister ? 'Sign In' : 'Register here'}
</TextButton>
</div>
</AuthCard>
</motion.div>
);
}

View File

@ -0,0 +1,168 @@
/**
* AuthInput Component
* @description Reusable input field for authentication forms.
* Features semi-transparent background, white text, and password visibility toggle.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { forwardRef, useState, type InputHTMLAttributes } from 'react';
/**
* Props for AuthInput component
*/
interface AuthInputProps extends InputHTMLAttributes<HTMLInputElement> {
/** Label text for the input */
label: string;
/** Whether the field is required */
isRequired?: boolean;
/** Error message to display */
error?: string;
/** Additional container classes */
containerClassName?: string;
}
/**
* Eye closed icon for password visibility toggle
* @returns {JSX.Element} Eye closed SVG icon
*/
function EyeClosedIcon(): JSX.Element {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 2.5L17.5 17.5M8.82 8.82a1.667 1.667 0 002.36 2.36M7.5 14.167A7.5 7.5 0 0110 13.75c2.917 0 5.5 1.25 7.5 3.75a10.833 10.833 0 00-2.358-2.358M12.5 5.833A7.5 7.5 0 0110 6.25C7.083 6.25 4.5 7.5 2.5 10a10.833 10.833 0 002.358 2.358"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/**
* Eye open icon for password visibility toggle
* @returns {JSX.Element} Eye open SVG icon
*/
function EyeOpenIcon(): JSX.Element {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4.167c-4.167 0-7.5 3.333-7.5 5.833s3.333 5.833 7.5 5.833 7.5-3.333 7.5-5.833-3.333-5.833-7.5-5.833z"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/**
* AuthInput component
* @description Styled input field with label, required indicator, and password toggle.
* Semi-transparent background with white text for dark gradient cards.
* @param {AuthInputProps} props - Component props
* @returns {JSX.Element} AuthInput element
*/
export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
function AuthInput(
{
label,
isRequired = false,
error,
containerClassName = '',
type = 'text',
placeholder,
...inputProps
},
ref
): JSX.Element {
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === 'password';
const inputType = isPasswordType && showPassword ? 'text' : type;
const togglePasswordVisibility = (): void => {
setShowPassword((prev) => !prev);
};
return (
<div className={`flex flex-col gap-1.5 w-full ${containerClassName}`}>
{/* Label */}
<label className="text-sm font-medium leading-5 text-white">
{label}
{isRequired && <span className="text-[#ff3434] ml-0.5">*</span>}
</label>
{/* Input Container */}
<div
className={`
flex items-center gap-2
w-full px-4 py-2.5
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.05)]
rounded-xl
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
overflow-hidden
focus-within:border-[rgba(255,255,255,0.3)]
transition-colors duration-200
${error ? 'border-[#ff3434]' : ''}
min-w-0
`}
>
<input
ref={ref}
type={inputType}
className="
flex-1 min-w-0
bg-transparent
text-sm md:text-base lg:text-sm xl:text-base font-medium leading-5 md:leading-6 lg:leading-5 xl:leading-6
text-white
placeholder:text-[rgba(255,255,255,0.5)]
outline-none
border-none
"
placeholder={placeholder}
{...inputProps}
/>
{/* Password Toggle Icon */}
{isPasswordType && (
<button
type="button"
onClick={togglePasswordVisibility}
className="shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOpenIcon /> : <EyeClosedIcon />}
</button>
)}
</div>
{/* Error Message */}
{error && (
<p className="text-sm text-[#ff3434] mt-1">{error}</p>
)}
</div>
);
}
);

View File

@ -0,0 +1,319 @@
/**
* ForgotPasswordModal Component
* @description Modal dialog for password recovery with two steps:
* 1. Email input (forgot password)
* 2. OTP verification (verify your email)
* Features smooth animated transitions between steps.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState, type FormEvent } from 'react';
import { motion, AnimatePresence, type Easing } from 'framer-motion';
import { AuthInput, AuthButton } from './index';
import { OTPInput } from './otp-input';
import { CloseIcon, ArrowLeftIcon } from './modal-icons';
/** Animation timing constants (in milliseconds) */
const ANIMATION_TIMING = {
MODAL_RESET_DELAY: 250,
EMAIL_SUBMIT_DELAY: 600,
OTP_VERIFY_DELAY: 800,
TRANSITION_DURATION: 0.25,
LAYOUT_DURATION: 0.3,
HEADER_DURATION: 0.2,
OTP_LENGTH: 6,
} as const;
/**
* Modal step type - determines which view to show
*/
type ModalStep = 'email' | 'verify';
/**
* Props for ForgotPasswordModal component
*/
interface ForgotPasswordModalProps {
/** Whether the modal is visible */
isOpen: boolean;
/** Callback when modal should close */
onClose: () => void;
/** Callback when email is submitted */
onSubmit?: (email: string) => void;
/** Callback when OTP is verified */
onVerify?: (otp: string) => void;
}
/**
* Smooth easing curve for animations (cubic-bezier)
* Material Design standard easing
*/
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
/**
* ForgotPasswordModal component
* @description Modal with smooth transitions between email input and OTP verification.
* Features animated backdrop, scale/fade modal entrance, and sliding form content.
* @param {ForgotPasswordModalProps} props - Component props
* @returns {JSX.Element} ForgotPasswordModal element
*/
export function ForgotPasswordModal({
isOpen,
onClose,
onSubmit,
onVerify,
}: ForgotPasswordModalProps): JSX.Element {
const [step, setStep] = useState<ModalStep>('email');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
const [isLoading, setIsLoading] = useState(false);
/**
* Handle modal close and reset state
*/
const handleClose = (): void => {
onClose();
setTimeout(() => {
setStep('email');
setEmail('');
setOtp(['', '', '', '', '', '']);
setIsLoading(false);
}, ANIMATION_TIMING.MODAL_RESET_DELAY);
};
/**
* Handle email form submission
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleEmailSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
setIsLoading(true);
onSubmit?.(email);
setTimeout(() => {
setIsLoading(false);
setStep('verify');
}, ANIMATION_TIMING.EMAIL_SUBMIT_DELAY);
};
/**
* Handle OTP verification form submission
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleVerifySubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const otpString = otp.join('');
if (otpString.length !== ANIMATION_TIMING.OTP_LENGTH) return;
setIsLoading(true);
onVerify?.(otpString);
setTimeout(() => {
setIsLoading(false);
handleClose();
}, ANIMATION_TIMING.OTP_VERIFY_DELAY);
};
/**
* Handle resend code request
*/
const handleResendCode = (): void => {
// TODO(AUTH-007): Implement resend OTP API call
onSubmit?.(email);
};
/**
* Handle backdrop click to close modal
*/
const handleBackdropClick = (): void => handleClose();
/**
* Prevent modal card clicks from closing modal
* @param {React.MouseEvent} event - Mouse event
*/
const handleModalClick = (event: React.MouseEvent): void => event.stopPropagation();
const isEmailStep = step === 'email';
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
/>
{/* Modal Container */}
<motion.div
key="modal-container"
initial={{ opacity: 0, scale: 0.96, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 10 }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
>
{/* Modal Card */}
<motion.div
layout
transition={{ duration: ANIMATION_TIMING.LAYOUT_DURATION, ease: smoothEasing }}
onClick={handleModalClick}
className="
flex flex-col items-center gap-6 md:gap-7 lg:gap-8
w-full max-w-[520px]
p-6 md:p-8 lg:p-[42px]
rounded-2xl md:rounded-3xl lg:rounded-[32px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
mx-4
"
style={{
background: 'linear-gradient(180deg, #00B8B7 0%, #001C8E 100%)',
}}
>
{/* Header with Close Button */}
<div className="flex items-start justify-between w-full gap-4">
{/* Title Section */}
<div className="flex-1 flex flex-col gap-[6px] items-center text-center">
<AnimatePresence mode="wait">
<motion.div
key={isEmailStep ? 'email-header' : 'verify-header'}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
>
<h2 className="text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white">
{isEmailStep ? 'Forgot your password' : 'Verify Your Email'}
</h2>
<p className={isEmailStep
? "text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white"
: "text-base md:text-base lg:text-lg xl:text-[18px] font-medium leading-5 md:leading-5 lg:leading-6 text-[rgba(255,255,255,0.75)] mt-1"
}>
{isEmailStep ? 'and continue' : 'Enter the 6-Digit Verification Code'}
</p>
</motion.div>
</AnimatePresence>
</div>
{/* Close Button */}
<button
type="button"
onClick={handleClose}
className="
flex items-center justify-center
w-8 h-8 md:w-10 md:h-10 rounded-full
bg-[rgba(255,255,255,0.1)]
hover:bg-[rgba(255,255,255,0.2)]
transition-colors duration-200
cursor-pointer shrink-0 min-w-[44px] min-h-[44px]
"
aria-label="Close modal"
>
<CloseIcon />
</button>
</div>
{/* Form Content */}
<div className="flex flex-col gap-6 items-center w-full">
<AnimatePresence mode="wait">
{isEmailStep ? (
<motion.form
key="email-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onSubmit={handleEmailSubmit}
className="flex flex-col gap-6 w-full"
>
<AuthInput
label="Email"
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
isRequired
autoComplete="email"
/>
<AuthButton isLoading={isLoading}>
Submit
</AuthButton>
</motion.form>
) : (
<motion.form
key="otp-form"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onSubmit={handleVerifySubmit}
className="flex flex-col gap-6 w-full items-center"
>
<OTPInput otp={otp} setOtp={setOtp} />
<AuthButton
isLoading={isLoading}
disabled={otp.join('').length !== ANIMATION_TIMING.OTP_LENGTH}
fullWidth
>
Continue
</AuthButton>
</motion.form>
)}
</AnimatePresence>
</div>
{/* Footer */}
<AnimatePresence mode="wait">
{isEmailStep ? (
<motion.button
key="back-btn"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
type="button"
onClick={handleClose}
className="
flex items-center justify-center gap-1.5
text-sm font-semibold text-white
cursor-pointer hover:opacity-80
transition-opacity duration-200
min-h-[44px]
"
>
<ArrowLeftIcon />
<span>Back to Sign In</span>
</motion.button>
) : (
<motion.div
key="resend-code"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
className="flex flex-col sm:flex-row items-center justify-center gap-1 text-center sm:text-left"
>
<span className="text-xs sm:text-sm font-normal text-[rgba(255,255,255,0.5)]">
Didn't you receive any code?
</span>
<button
type="button"
onClick={handleResendCode}
className="text-xs sm:text-sm font-semibold text-white cursor-pointer hover:underline min-h-[44px]"
>
Resend Code
</button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,14 @@
/**
* Auth Components Barrel Export
* @description Exports all authentication-related components.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
export { AuthCard, AuthCardHeader, AuthCardContent } from './auth-card';
export { AuthInput } from './auth-input';
export { AuthButton, SocialButton, TextButton, GoogleIcon, AzureIcon } from './auth-button';
export { AuthFormCard } from './auth-form-card';
export { ForgotPasswordModal } from './forgot-password-modal';
export { OTPInput } from './otp-input';
export { CloseIcon, ArrowLeftIcon } from './modal-icons';

View File

@ -0,0 +1,32 @@
/**
* Modal Icons Component
* @description Icon components for modal dialogs
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
/**
* Close icon component
* @description Renders an X icon for closing the modal
* @returns {JSX.Element} Close icon SVG
*/
export function CloseIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Left arrow icon component
* @description Renders a left-pointing arrow for back navigation
* @returns {JSX.Element} Arrow left icon SVG
*/
export function ArrowLeftIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}

View File

@ -0,0 +1,139 @@
/**
* OTP Input Component
* @description Six individual input boxes for OTP entry with auto-focus,
* backspace navigation, and paste support.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useRef, useEffect, type KeyboardEvent, type ClipboardEvent } from 'react';
import { motion, type Easing } from 'framer-motion';
/** Animation timing constants */
const ANIMATION_TIMING = {
OTP_AUTOFOCUS_DELAY: 300,
TRANSITION_DURATION: 0.25,
OTP_STAGGER_DELAY: 0.04,
OTP_LENGTH: 6,
OTP_MAX_INDEX: 5,
} as const;
/**
* Props for OTPInput component
*/
interface OTPInputProps {
/** Current OTP values array */
otp: string[];
/** Setter function for OTP state */
setOtp: React.Dispatch<React.SetStateAction<string[]>>;
}
/**
* Smooth easing curve for animations
*/
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
/**
* OTP Input component
* @description Six individual input boxes for OTP entry with auto-focus,
* backspace navigation, and paste support.
* @param {OTPInputProps} props - Component props
* @returns {JSX.Element} OTP input element
*/
export function OTPInput({ otp, setOtp }: OTPInputProps): JSX.Element {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
const timer = setTimeout(() => {
inputRefs.current[0]?.focus();
}, ANIMATION_TIMING.OTP_AUTOFOCUS_DELAY);
return () => clearTimeout(timer);
}, []);
/**
* Handle input value change
* @param {number} index - Input index
* @param {string} value - New input value
*/
const handleChange = (index: number, value: string): void => {
if (value.length > 1) value = value.slice(-1);
if (value && !/^\d$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < ANIMATION_TIMING.OTP_MAX_INDEX) {
inputRefs.current[index + 1]?.focus();
}
};
/**
* Handle keyboard navigation
* @param {KeyboardEvent<HTMLInputElement>} event - Keyboard event
* @param {number} index - Current input index
*/
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>, index: number): void => {
if (event.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
/**
* Handle paste event for OTP auto-fill
* @param {ClipboardEvent<HTMLInputElement>} event - Clipboard event
*/
const handlePaste = (event: ClipboardEvent<HTMLInputElement>): void => {
event.preventDefault();
const pastedData = event.clipboardData
.getData('text')
.replace(/\D/g, '')
.slice(0, ANIMATION_TIMING.OTP_LENGTH);
const newOtp = [...otp];
for (let i = 0; i < pastedData.length; i++) {
newOtp[i] = pastedData[i];
}
setOtp(newOtp);
const nextIndex = Math.min(pastedData.length, ANIMATION_TIMING.OTP_MAX_INDEX);
inputRefs.current[nextIndex]?.focus();
};
return (
<div className="flex gap-2 md:gap-3 items-center justify-center w-full">
{otp.map((digit, index) => (
<motion.input
key={index}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: ANIMATION_TIMING.TRANSITION_DURATION,
delay: index * ANIMATION_TIMING.OTP_STAGGER_DELAY,
ease: smoothEasing,
}}
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
className="
w-10 h-10 md:w-[44px] md:h-[44px]
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.1)]
rounded-lg md:rounded-[8px]
text-center text-white text-base md:text-[18px] font-medium
outline-none
focus:border-[rgba(255,255,255,0.4)]
focus:bg-[rgba(255,255,255,0.2)]
transition-all duration-200
flex-1 max-w-[44px]
"
placeholder="•"
aria-label={`Digit ${index + 1}`}
/>
))}
</div>
);
}

View File

@ -7,8 +7,11 @@
* AgentCard component props * AgentCard component props
*/ */
interface AgentCardProps { interface AgentCardProps {
/** Agent title/name */
title: string; title: string;
/** Array of tag labels */
tags: string[]; tags: string[];
/** Agent description text */
description: string; description: string;
} }
@ -16,9 +19,13 @@ interface AgentCardProps {
* AgentCard component * AgentCard component
* @description Card showing agent details with tags * @description Card showing agent details with tags
* @param props - Component props * @param props - Component props
* @returns AgentCard element * @returns {JSX.Element} AgentCard element
*/ */
export function AgentCard({ title, tags, description }: AgentCardProps) { export function AgentCard({
title,
tags,
description,
}: AgentCardProps): JSX.Element {
// Define your colors in order // Define your colors in order
const tagColors = [ const tagColors = [
"bg-[#E5E8F4]", // 1st tag (index 0) "bg-[#E5E8F4]", // 1st tag (index 0)
@ -27,22 +34,26 @@ export function AgentCard({ title, tags, description }: AgentCardProps) {
]; ];
return ( return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover"> <div className="bg-white rounded-xl p-4 md:p-6 shadow-sm border border-gray-100 card-hover">
<h3 className="text-lg font-semibold text-gray-900 mb-3">{title}</h3> <h3 className="text-[1.25rem] md:text-[1.375rem] lg:text-[1.5rem] font-semibold text-gray-900 mb-2 md:mb-3 leading-tight">
{title}
</h3>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-3 md:mb-4">
{tags.map((tag, index) => ( {tags.map((tag, index) => (
<span <span
key={index} key={index}
/* Use the index to pick the color; fall back to the first color if index > 2 */ /* Use the index to pick the color; fall back to the first color if index > 2 */
className={`${tagColors[index] || tagColors[0]} px-3 py-1 text-xs font-medium text-gray-700 border border-gray-200`} className={`${tagColors[index] || tagColors[0]} px-2.5 py-1 md:px-3 text-[0.75rem] md:text-[0.8125rem] lg:text-[0.875rem] font-medium text-gray-700 border border-gray-200 rounded`}
> >
{tag} {tag}
</span> </span>
))} ))}
</div> </div>
<p className="text-sm text-gray-600 leading-relaxed">{description}</p> <p className="text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] text-gray-600 leading-relaxed">
{description}
</p>
</div> </div>
); );
} }

View File

@ -0,0 +1,121 @@
/**
* Agents Section Component
* @description Displays agents list with tabs and pagination
*/
import { useState, useMemo, useEffect } from 'react';
import { AgentCard } from './agent-card';
import { Pagination } from './pagination';
import type { AgentData } from '@/constants/agents';
/**
* Agents section component props
*/
interface AgentsSectionProps {
/**
* List of agents to display
*/
agents: AgentData[];
/**
* Number of items per page
*/
itemsPerPage?: number;
}
/**
* Agents section component
* @description Displays agents with tabs and pagination
* @param props - Component props
* @returns {JSX.Element} AgentsSection element
*/
export function AgentsSection({
agents,
itemsPerPage = 6,
}: AgentsSectionProps): JSX.Element {
const [activeTab, setActiveTab] = useState<'all' | 'my'>('all');
const [currentPage, setCurrentPage] = useState(1);
/**
* Calculate pagination values
*/
const paginationData = useMemo(() => {
const totalItems = agents.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedAgents = agents.slice(startIndex, endIndex);
return {
totalPages,
paginatedAgents,
};
}, [agents, currentPage, itemsPerPage]);
/**
* Reset pagination when tab changes
*/
useEffect(() => {
setCurrentPage(1);
}, [activeTab]);
/**
* Handle page change
* @description Updates the current page number
* @param page - New page number (1-indexed)
*/
const handlePageChange = (page: number): void => {
setCurrentPage(page);
};
return (
<div className="bg-white rounded-xl md:rounded-2xl shadow-sm border border-gray-200 p-4 md:p-6 pb-20 md:pb-24">
{/* Tabs */}
<div className="flex gap-2 md:gap-4 mb-4 md:mb-6">
<button
onClick={() => setActiveTab('all')}
className={`px-4 py-2 md:px-6 md:py-2 rounded-lg text-[0.875rem] md:text-[1rem] font-medium transition-colors min-h-[44px] ${
activeTab === 'all'
? 'bg-black text-white'
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
}`}
>
All Agents
</button>
<button
onClick={() => setActiveTab('my')}
className={`px-4 py-2 md:px-6 md:py-2 rounded-lg text-[0.875rem] md:text-[1rem] font-medium transition-colors min-h-[44px] ${
activeTab === 'my'
? 'bg-black text-white'
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
}`}
>
My Agents
</button>
</div>
{/* Agent Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 mb-4 md:mb-6">
{paginationData.paginatedAgents.map((agent, index) => (
<AgentCard
key={`${agent.title}-${index}`}
title={agent.title}
tags={agent.tags}
description={agent.description}
/>
))}
</div>
{/* Fixed Pagination at Bottom */}
{paginationData.totalPages > 1 && (
<div className="fixed bottom-0 left-0 right-0 md:left-[88px] lg:left-[88px] md:mx-6 lg:mx-8 md:rounded-2xl bg-white border-t border-gray-200 px-4 py-3 md:px-6 lg:px-10 md:py-4 flex justify-end z-40 shadow-lg">
<Pagination
currentPage={currentPage}
totalPages={paginationData.totalPages}
onPageChange={handlePageChange}
/>
</div>
)}
</div>
);
}

View File

@ -6,3 +6,5 @@
export { Sidebar } from './sidebar'; export { Sidebar } from './sidebar';
export { MetricCard } from './metric-card'; export { MetricCard } from './metric-card';
export { AgentCard } from './agent-card'; export { AgentCard } from './agent-card';
export { Pagination } from './pagination';
export { AgentsSection } from './agents-section';

View File

@ -1,72 +1,92 @@
/** /**
* Metric Card Component * Metric card component props
* @description Displays metric information with gradient background
*/
/**
* MetricCard component props
*/ */
interface MetricCardProps { interface MetricCardProps {
/** Card title text */
title: string; title: string;
/** Card subtitle text */
subtitle: string; subtitle: string;
/** Display value */
value: string; value: string;
/** Gradient color scheme */
gradient: 'blue-teal' | 'pink-purple' | 'orange-yellow' | 'blue-purple'; gradient: 'blue-teal' | 'pink-purple' | 'orange-yellow' | 'blue-purple';
/** Optional image URL for card decoration */
imageUrl?: string;
} }
/**
* Gradient style type
*/
type GradientStyle = {
backgroundImage?: string;
background?: string;
};
/** /**
* MetricCard component * MetricCard component
* @description Card showing metric with gradient background and premium styling * @description Displays a metric card with gradient background and optional image
* @param props - Component props * @param props - Component props
* @returns MetricCard element * @returns MetricCard element
*/ */
export function MetricCard({ title, subtitle, value, gradient }: MetricCardProps) { export function MetricCard({
const gradients = { title,
'blue-teal': ' bg-gradient-to-br from-[#001B2F] via-[#0626A8] to-[#039A98] text-white ', subtitle,
'pink-purple': 'bg-gradient-to-br from-[#FF6B8B] via-[#944D8F] to-[#51459E]', value,
'orange-yellow': 'bg-gradient-to-br from-[#FF9D42] to-[#F17E31]', gradient,
'blue-purple': 'bg-gradient-to-br from-[#2D5BFF] to-[#8E34FF]', imageUrl,
}: MetricCardProps): JSX.Element {
const gradientStyles: Record<MetricCardProps['gradient'], GradientStyle> = {
'blue-teal': {
backgroundImage: 'linear-gradient(16.03674192372833deg, rgba(0, 27, 47, 1) 10.549%, rgba(6, 38, 168, 1) 30.146%, rgba(3, 154, 152, 1) 77.418%)',
},
'pink-purple': {
background: 'linear-gradient(180deg, #ff708c 0%, #001377 100%)',
},
'orange-yellow': {
background: 'linear-gradient(180deg, #f3696e 0%, #f8a902 100%)',
},
'blue-purple': {
background: 'linear-gradient(180deg, #c336ff 0%, #0033ff 100%)',
},
}; };
// return ( const imageUrls: Record<MetricCardProps['gradient'], string> = {
// <div className={`${gradients[gradient]} rounded-[24px] p-6 text-white card-hover cursor-pointer relative overflow-hidden h-[120px] group transition-all duration-300`}> 'blue-teal': imageUrl || 'https://www.figma.com/api/mcp/asset/57e6c8be-018b-41d3-a71d-d59c115b529c',
// {/* Right side decorative vertical bar */} 'pink-purple': imageUrl || 'https://www.figma.com/api/mcp/asset/ccb9eb9b-4d2e-417d-b727-8dcd1586344a',
// <div className="absolute right-0 top-0 bottom-0 w-[28%] bg-white/10 backdrop-blur-[2px]" /> 'orange-yellow': imageUrl || 'https://www.figma.com/api/mcp/asset/63c3ed6f-400a-431e-b3a7-43b6ff3d4eee',
'blue-purple': imageUrl || 'https://www.figma.com/api/mcp/asset/e5d86ccf-d5ce-4b5e-823a-a01acb631af7',
};
// <div className="flex justify-between items-center h-full relative z-10 font-['Poppins']">
// <div className="flex flex-col justify-center">
// <p className="text-[11px] font-semibold tracking-wider opacity-90 uppercase mb-0.5">
// {title}
// </p>
// <h3 className="text-[22px] font-bold leading-tight">
// {subtitle}
// </h3>
// </div>
// <div className="flex items-center justify-center min-w-[60px]">
// <span className="text-[48px] font-bold tracking-tighter leading-none">
// {value}
// </span>
// </div>
// </div>
// </div>
// );
return ( return (
<div className={`p-6 h-[110px] rounded-2xl ${gradients[gradient]} w-full `}> <div className="w-full h-full">
<div className="flex items-center justify-between w-full "> <div
<div className="flex flex-col"> className="flex flex-col justify-center min-h-[100px] sm:min-h-[110px] md:min-h-[120px] lg:min-h-[120px] p-4 sm:p-5 md:p-6 relative rounded-xl md:rounded-2xl overflow-hidden w-full h-full"
<span className="text-[12px] font-medium text-white uppercase tracking-wider"> style={gradientStyles[gradient]}
>
{/* Text Content Container */}
<div className="flex items-center justify-between w-full z-10 text-white gap-2 sm:gap-3 md:gap-4">
<div className="flex flex-col items-start flex-1 min-w-0">
<p className="font-medium text-[0.625rem] sm:text-[0.6875rem] md:text-[0.75rem] lg:text-[0.875rem] uppercase opacity-80 leading-tight">
{title} {title}
</span> </p>
<span className="text-[18px] font-bold mt-1 text-white"> <p className="capitalize font-semibold text-[0.875rem] sm:text-[1rem] md:text-[1.125rem] lg:text-[1.25rem] truncate w-full mt-0.5 sm:mt-1">
{subtitle} {subtitle}
</span> </p>
</div> </div>
<div className="text-5xl font-bold text-white"> <p className="capitalize font-semibold text-[1.5rem] sm:text-[1.75rem] md:text-[2rem] lg:text-[2rem] ml-2 sm:ml-3 md:ml-4 flex-shrink-0">
{value} {value}
</p>
</div>
{/* Image Container - Fixed to the right */}
<div className="absolute right-0 bottom-0 flex items-center h-full w-12 sm:w-16 md:w-20 pointer-events-none opacity-40 sm:opacity-50">
<img
alt=""
className="object-contain h-full w-full"
src={imageUrls[gradient]}
/>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -0,0 +1,177 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
/**
* Pagination component props
*/
interface PaginationProps {
/** Current active page number (1-indexed) */
currentPage: number;
/** Total number of pages */
totalPages: number;
/** Callback when page changes */
onPageChange: (page: number) => void;
}
/**
* Pagination component
* @description Displays pagination controls with page numbers and navigation arrows
* @param props - Component props
* @returns {JSX.Element} Pagination element
*/
export function Pagination({
currentPage,
totalPages,
onPageChange,
}: PaginationProps): JSX.Element {
/**
* Generate page numbers to display
* @description Calculates which page numbers to show based on current page and total pages
* @returns {Array<number | 'ellipsis'>} Array of page numbers and ellipsis markers
*/
const getPageNumbers = (): (number | 'ellipsis')[] => {
const pages: (number | 'ellipsis')[] = [];
const maxVisible = 8;
if (totalPages <= maxVisible) {
// Show all pages if total is less than max visible
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (currentPage <= 3) {
// Near the beginning: show 1, 2, 3, ..., last
pages.push(2, 3);
if (totalPages > 4) {
pages.push('ellipsis');
}
pages.push(totalPages);
} else if (currentPage >= totalPages - 2) {
// Near the end: show 1, ..., last-2, last-1, last
pages.push('ellipsis');
pages.push(totalPages - 2, totalPages - 1, totalPages);
} else {
// In the middle: show 1, ..., current-1, current, current+1, ..., last
pages.push('ellipsis');
pages.push(currentPage - 1, currentPage, currentPage + 1);
if (currentPage + 1 < totalPages - 1) {
pages.push('ellipsis');
}
pages.push(totalPages);
}
}
return pages;
};
/**
* Handle previous page navigation
* @description Navigates to the previous page if available
*/
const handlePrevious = (): void => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
/**
* Handle next page navigation
* @description Navigates to the next page if available
*/
const handleNext = (): void => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
/**
* Handle page number click
* @description Navigates to the specified page if valid
* @param page - Page number to navigate to
*/
const handlePageClick = (page: number): void => {
if (page !== currentPage && page >= 1 && page <= totalPages) {
onPageChange(page);
}
};
const pageNumbers = getPageNumbers();
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
return (
<div className="flex gap-2 md:gap-3 items-center justify-end rounded-[6px]">
{/* Previous Button */}
<button
onClick={handlePrevious}
disabled={isFirstPage}
className={`w-11 h-11 md:w-9 md:h-9 rounded-[6px] flex items-center justify-center transition-colors min-h-[44px] min-w-[44px] md:min-h-9 md:min-w-9 ${
isFirstPage
? 'bg-white border border-[#e1e4ef] cursor-not-allowed opacity-60'
: 'bg-white border border-[#e1e4ef] hover:bg-gray-50 cursor-pointer'
}`}
aria-label="Previous page"
>
<ChevronLeft className="w-4 h-4 text-[#4A4F5C]" />
</button>
{/* Page Numbers */}
{pageNumbers.map((page, index) => {
if (page === 'ellipsis') {
return (
<div
key={`ellipsis-${index}`}
className="w-11 h-11 md:w-9 md:h-9 rounded-[6px] bg-white border border-[rgba(0,51,255,0.25)] flex items-center justify-center min-h-[44px] min-w-[44px] md:min-h-9 md:min-w-9"
>
<span className="text-[0.75rem] md:text-[0.75rem] font-black text-[#03f] tracking-[1.44px] leading-none">
...
</span>
</div>
);
}
const isActive = page === currentPage;
return (
<button
key={page}
onClick={() => handlePageClick(page)}
className={`w-11 h-11 md:w-9 md:h-9 rounded-[6px] flex items-center justify-center transition-colors min-h-[44px] min-w-[44px] md:min-h-9 md:min-w-9 ${
isActive
? 'bg-[#03f] border border-[#03f]'
: 'bg-white border border-[rgba(0,51,255,0.25)] hover:bg-[#F5F8FF]'
}`}
aria-label={`Go to page ${page}`}
aria-current={isActive ? 'page' : undefined}
>
<span
className={`text-[0.875rem] md:text-[0.75rem] font-medium leading-none ${
isActive ? 'text-white' : 'text-[#03f]'
}`}
>
{page}
</span>
</button>
);
})}
{/* Next Button */}
<button
onClick={handleNext}
disabled={isLastPage}
className={`w-11 h-11 md:w-9 md:h-9 rounded-[6px] flex items-center justify-center transition-colors min-h-[44px] min-w-[44px] md:min-h-9 md:min-w-9 ${
isLastPage
? 'bg-white border border-[#e1e4ef] cursor-not-allowed opacity-60'
: 'bg-white border border-[#e1e4ef] hover:bg-gray-50 cursor-pointer'
}`}
aria-label="Next page"
>
<ChevronRight className="w-4 h-4 text-[#4A4F5C]" />
</button>
</div>
);
}

View File

@ -1,58 +1,65 @@
/**
* Dashboard Sidebar Component
* @description Vertical navigation sidebar with icons
*/
import { Home, Settings } from 'lucide-react'; import { Home, Settings } from 'lucide-react';
import { Link } from '@tanstack/react-router';
import { APP_PATHS } from '@/routes';
/** /**
* Sidebar component * Sidebar component
* @description Left sidebar navigation with icon menu * @description Left sidebar navigation with icon menu
* @returns Sidebar element * @returns {JSX.Element} Sidebar element
*/ */
export function Sidebar() { export function Sidebar(): JSX.Element {
return ( return (
<aside className="fixed left-6 top-24 bottom-6 w-16 bg-gradient-to-b from-[#00A1A0] to-[#00166D] flex flex-col items-center py-6 gap-6 z-50 rounded-2xl shadow-xl"> <aside className="fixed left-4 top-20 bottom-4 w-14 md:left-6 md:top-24 md:bottom-6 md:w-16 bg-gradient-to-b from-[#00A1A0] to-[#00166D] flex flex-col items-center py-4 md:py-6 gap-4 md:gap-6 z-50 rounded-xl md:rounded-2xl shadow-xl hidden md:flex">
{/* Navigation Icons */} {/* Navigation Icons */}
<nav className="flex flex-col items-center gap-5 flex-1 w-full"> <nav className="flex flex-col items-center gap-3 sm:gap-4 md:gap-5 flex-1 w-full">
{/* Home - Active State */} {/* Home - Active State */}
<button className="w-[42px] h-[42px] bg-white/20 rounded-xl flex items-center justify-center text-white shadow-inner transition-all hover:bg-white/30" aria-label="Home"> <Link
<Home className="w-5 h-5 text-white" /> to={APP_PATHS.dashboard}
</button> className="sidebar-icon-v3 hover:bg-white/10"
activeProps={{ className: 'sidebar-icon-v3 bg-white/20 ring-1 ring-white/40' }}
aria-label="Home"
>
<Home className="w-4 h-4 md:w-5 md:h-5 text-white" />
</Link>
<button className="sidebar-icon-v3" aria-label="Agent"> <Link
<img src="/Agent.svg" alt="Agent" className="w-[42px] h-[42px] " /> to={APP_PATHS.agent}
</button> className="sidebar-icon-v3 hover:bg-white/10"
activeProps={{ className: 'sidebar-icon-v3 bg-white/20 ring-1 ring-white/40' }}
aria-label="Agent"
>
<img src="/Agent.svg" alt="Agent" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</Link>
<button className="sidebar-icon-v3" aria-label="KB"> <button className="sidebar-icon-v3" aria-label="KB">
<img src="/KB.svg" alt="KB" className="w-[42px] h-[42px] " /> <img src="/KB.svg" alt="KB" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="Models"> <button className="sidebar-icon-v3" aria-label="Models">
<img src="/Models.svg" alt="Models" className="w-[42px] h-[42px] " /> <img src="/Models.svg" alt="Models" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="Workflow"> <button className="sidebar-icon-v3" aria-label="Workflow">
<img src="/Workflow.svg" alt="Workflow" className="w-[42px] h-[42px] " /> <img src="/Workflow.svg" alt="Workflow" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="Nav Item"> <button className="sidebar-icon-v3" aria-label="Nav Item">
<img src="/Nav item.svg" alt="Nav Item" className="w-[42px] h-[42px] " /> <img src="/Nav item.svg" alt="Nav Item" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="LLM"> <button className="sidebar-icon-v3" aria-label="LLM">
<img src="/LLM.svg" alt="LLM" className="w-[42px] h-[42px] " /> <img src="/LLM.svg" alt="LLM" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="FT"> <button className="sidebar-icon-v3" aria-label="FT">
<img src="/FT.svg" alt="FT" className="w-[42px] h-[42px] " /> <img src="/FT.svg" alt="FT" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="Task Watcher"> <button className="sidebar-icon-v3" aria-label="Task Watcher">
<img src="/Task watacher.svg" alt="Task Watcher" className="w-[42px] h-[42px] " /> <img src="/Task watacher.svg" alt="Task Watcher" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
<button className="sidebar-icon-v3" aria-label="Optimization"> <button className="sidebar-icon-v3" aria-label="Optimization">
<img src="/Optimization.svg" alt="Optimization" className="w-[42px] h-[42px] " /> <img src="/Optimization.svg" alt="Optimization" className="w-9 h-9 md:w-[42px] md:h-[42px]" />
</button> </button>
</nav> </nav>
{/* Bottom Icon */} {/* Bottom Icon */}
<div className="flex flex-col gap-4 mt-auto"> <div className="flex flex-col gap-3 md:gap-4 mt-auto">
<button className="sidebar-icon-v3" aria-label="Settings"> <button className="sidebar-icon-v3" aria-label="Settings">
<Settings className="w-5 h-5 text-white" /> <Settings className="w-4 h-4 md:w-5 md:h-5 text-white" />
</button> </button>
</div> </div>
</aside> </aside>

View File

@ -0,0 +1,35 @@
/**
* Chat Agent Component
* @description Fixed chat agent button positioned at bottom right corner
*/
// Chat icon from Figma design
const chatIcon = 'https://www.figma.com/api/mcp/asset/34915bcc-807b-45a6-82a2-2806ff0d1d6b';
/**
* ChatAgent component
* @description Floating chat agent button matching Figma design exactly
* @returns ChatAgent element
*/
export function ChatAgent() {
return (
<button
className="fixed bottom-0 right-0 flex items-center justify-center gap-2 md:gap-3 px-4 py-3 md:px-8 md:py-6 rounded-tl-xl md:rounded-tl-3xl text-white shadow-[-4px_-3px_10px_0px_rgba(123,64,129,0.25)] transition-all hover:opacity-90 z-50 min-h-[44px]"
style={{
backgroundImage:
'linear-gradient(13.793546193113684deg, rgba(0, 27, 47, 1) 10.549%, rgba(6, 38, 168, 1) 30.146%, rgba(3, 154, 152, 1) 77.418%)',
}}
aria-label="Chat Agent"
>
<img
src={chatIcon}
alt="Chat"
className="w-5 h-5 md:w-6 md:h-6 shrink-0"
/>
<span className="text-[0.875rem] md:text-[1.25rem] font-medium leading-tight whitespace-nowrap text-center hidden md:inline">
Chat Agent
</span>
</button>
);
}

View File

@ -3,8 +3,7 @@
* @description Global top header with logo, notifications, theme toggle, and profile * @description Global top header with logo, notifications, theme toggle, and profile
*/ */
import { useState } from 'react'; import { Bell, ChevronDown, Moon, Sun } from 'lucide-react';
import { Bell, Moon, Sun, ChevronDown } from 'lucide-react';
/** /**
* Header component * Header component
@ -12,62 +11,37 @@ import { Bell, Moon, Sun, ChevronDown } from 'lucide-react';
* @returns Header element * @returns Header element
*/ */
export function Header() { export function Header() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return ( return (
<header className="fixed top-0 mt-[24px] mb-[16px] left-0 right-0 h-16 z-[100] backdrop-blur-md flex items-center justify-between px-8"> <header className="sticky top-0 z-[100] flex items-center justify-between px-4 py-4 md:px-6 lg:px-10 lg:pt-6 bg-gradient-to-r from-[#C8FAFF] via-[#E4F2FF] to-[#C6D7FE]">
{/* Logo */} <div className="flex items-end gap-2">
<div className="flex items-center">
<img <img
src="/Logo.png" src="/Logo.png"
alt="AgenticIQ Logo" alt="AgenticIQ Logo"
width={168} width={160}
height={60} height={54}
className="object-contain" className="object-contain w-24 h-auto md:w-32 lg:w-40"
/> />
</div> </div>
{/* Right Actions */} <div className="flex items-center gap-2 md:gap-3 rounded-xl md:rounded-2xl border border-[#E6EAF5] bg-white px-2 py-1.5 md:px-3 md:py-2 shadow-sm">
<div className="bg-white rounded-[20px] p-2 flex items-center gap-2 shadow-sm border border-gray-100/50"> <button className="flex h-11 w-11 md:h-10 md:w-10 items-center justify-center rounded-full border border-[#E6EAF5] bg-white text-[#1A1A1A] transition hover:bg-[#F4F7FF] min-h-[44px] min-w-[44px]">
{/* Bell Notification */} <Bell className="h-4 w-4 md:h-5 md:w-5" strokeWidth={1.6} />
<button className="p-2.5 bg-[#F4F9FF] hover:bg-[#E9F3FF] rounded-xl transition-all duration-200">
<Bell className="w-5 h-5 text-[#1A1A1A]" strokeWidth={1.5} />
</button> </button>
<button className="flex h-11 w-11 md:h-10 md:w-10 items-center justify-center rounded-full bg-[#0A4BFF] text-white shadow-md shadow-blue-500/30 transition hover:bg-[#0033FF] min-h-[44px] min-w-[44px]">
{/* Theme Toggles */} <Sun className="h-4 w-4 md:h-5 md:w-5" strokeWidth={1.6} />
<div className="flex items-center gap-1.5 px-1">
<button
onClick={() => setTheme('light')}
className={`p-2.5 rounded-full transition-all duration-300 ${
theme === 'light'
? 'bg-[#0033FF] text-white shadow-lg shadow-blue-500/40 scale-105'
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
}`}
>
<Sun className="w-5 h-5" strokeWidth={theme === 'light' ? 2 : 1.5} />
</button> </button>
<button <button className="hidden md:flex h-10 w-10 items-center justify-center rounded-full border border-[#E6EAF5] bg-white text-[#1A1A1A] transition hover:bg-[#F4F7FF]">
onClick={() => setTheme('dark')} <Moon className="h-5 w-5" strokeWidth={1.6} />
className={`p-2.5 rounded-full transition-all duration-300 ${
theme === 'dark'
? 'bg-[#0052FF] text-white shadow-lg shadow-blue-500/40 scale-105'
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
}`}
>
<Moon className="w-5 h-5" strokeWidth={theme === 'dark' ? 2 : 1.5} />
</button> </button>
</div> <div className="flex items-center gap-1.5 md:gap-2 pl-0.5 md:pl-1 pr-1 md:pr-2">
<div className="h-9 w-9 md:h-10 md:w-10 overflow-hidden rounded-full border-2 border-white shadow-sm ring-1 ring-gray-100">
{/* Profile Section */}
<div className="flex items-center gap-2 pl-1 pr-2">
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-white shadow-sm ring-1 ring-gray-100">
<img <img
src="/profile.svg" src="/profile.svg"
alt="User Avatar" alt="User Avatar"
className="w-full h-full" className="h-full w-full"
/> />
</div> </div>
<ChevronDown className="w-4 h-4 text-gray-900" strokeWidth={2.5} /> <ChevronDown className="h-3.5 w-3.5 md:h-4 md:w-4 text-gray-900" strokeWidth={2.5} />
</div> </div>
</div> </div>
</header> </header>

View File

@ -5,4 +5,5 @@
export { RootLayout } from './root-layout'; export { RootLayout } from './root-layout';
export { Header } from './header'; export { Header } from './header';
export { ChatAgent } from './chat-agent';

View File

@ -6,6 +6,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Sidebar } from '@/components/dashboard/sidebar'; import { Sidebar } from '@/components/dashboard/sidebar';
import { Header } from './header'; import { Header } from './header';
import { ChatAgent } from './chat-agent';
interface RootLayoutProps { interface RootLayoutProps {
children: ReactNode; children: ReactNode;
@ -14,15 +15,16 @@ interface RootLayoutProps {
/** /**
* RootLayout component * RootLayout component
* @description Wraps all pages with consistent sidebar and header layout * @description Wraps all pages with consistent sidebar and header layout
* @param children - Page content to render * @param props - Component props
* @returns RootLayout element * @param props.children - Page content to render
* @returns {JSX.Element} RootLayout element
*/ */
export function RootLayout({ children }: RootLayoutProps) { export function RootLayout({ children }: RootLayoutProps): JSX.Element {
return ( return (
<div <div
className="min-h-screen" className="min-h-screen"
style={{ style={{
backgroundColor: '#F5F8FF', backgroundColor: '#f7f8ff',
backgroundImage: ` backgroundImage: `
radial-gradient(at 0% 0%, #C8FAFF 0px, transparent 50%), radial-gradient(at 0% 0%, #C8FAFF 0px, transparent 50%),
radial-gradient(at 100% 0%, #C6D7FE 0px, transparent 50%), radial-gradient(at 100% 0%, #C6D7FE 0px, transparent 50%),
@ -38,9 +40,12 @@ export function RootLayout({ children }: RootLayoutProps) {
<Sidebar /> <Sidebar />
{/* Main Content Area */} {/* Main Content Area */}
<main className="ml-24 pt-16 min-h-screen"> <main className="min-h-screen md:ml-[88px] lg:ml-[88px]">
{children} {children}
</main> </main>
{/* Chat Agent Button - Fixed at bottom right */}
<ChatAgent />
</div> </div>
); );
} }

View File

@ -0,0 +1,320 @@
/**
* Add Tool Dialog Component
* @description Dialog for adding and configuring tools (API/MCP)
*/
import { useState, useMemo, useEffect } from 'react';
import { ChevronDown } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
} from './dialog';
import { useClickOutside } from '@/hooks';
import type { Tool } from '@/types';
/**
* AddToolDialog component props
*/
interface AddToolDialogProps {
/** Whether the dialog is open */
open: boolean;
/** Callback when dialog open state changes */
onOpenChange: (open: boolean) => void;
/** Callback when a new tool is added */
onAddTool?: (tool: Omit<Tool, 'id'>) => void;
/** Callback when an existing tool is updated */
onUpdateTool?: (toolId: string, tool: Omit<Tool, 'id'>) => void;
/** Tool being edited, if any */
editingTool?: Tool | null;
}
// Tool options for the dropdown with their display names and colors
const toolOptions = [
{ name: 'Slack', displayName: 'Slack', color: '#ff0080' },
{ name: 'GitHub', displayName: 'GitHub', color: '#8fbc24' },
{ name: 'Search', displayName: 'Search', color: '#0033FF' },
{ name: 'Weather', displayName: 'Weather', color: '#03f' },
{ name: 'Calculator', displayName: 'Calculator', color: '#03f' },
{ name: 'Translator', displayName: 'Translator', color: '#03f' },
];
const DROPDOWN_MENU_CLASS =
'absolute bg-white border border-[#0033FF] border-solid flex flex-col items-start left-0 rounded-bl-[8px] rounded-br-[8px] shadow-[0px_4px_8px_0px_#EEF1F7] top-[46px] w-full z-10';
const SAVE_BUTTON_CLASS =
'bg-[#03f] flex flex-1 items-center justify-center px-[42px] py-[14px] rounded-[12px] hover:bg-[#002BCC] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed';
const SAVE_BUTTON_TEXT_CLASS = 'font-semibold leading-[18px] not-italic text-[16px] text-center text-white';
/**
* Get initial state for the dialog
* @description Determines initial state based on editing tool
* @param editingTool - Tool being edited, if any
* @returns Initial state with selected type and tool
*/
const getInitialState = (
editingTool: Tool | null | undefined
): {
selectedType: 'API' | 'MCP';
selectedTool: typeof toolOptions[0] | null;
} => {
if (editingTool) {
const toolOption = toolOptions.find((opt) => opt.displayName === editingTool.name);
return {
selectedType: editingTool.type as 'API' | 'MCP',
selectedTool: toolOption || null,
};
}
return {
selectedType: 'API' as const,
selectedTool: null,
};
};
/**
* AddToolDialog component
* @description Modal dialog for adding and configuring tools with API/MCP selection
* @param props - Component props
* @returns {JSX.Element} AddToolDialog element
*/
export function AddToolDialog({
open,
onOpenChange,
onAddTool,
onUpdateTool,
editingTool,
}: AddToolDialogProps): JSX.Element {
const initialState = useMemo(() => getInitialState(editingTool), [editingTool]);
const [selectedType, setSelectedType] = useState<'API' | 'MCP'>(initialState.selectedType);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [selectedTool, setSelectedTool] = useState<typeof toolOptions[0] | null>(initialState.selectedTool);
// Sync state when dialog opens or editingTool changes
useEffect(() => {
if (open) {
setSelectedType(initialState.selectedType);
setSelectedTool(initialState.selectedTool);
setIsDropdownOpen(false);
} else {
setIsDropdownOpen(false);
setSelectedTool(null);
}
}, [open, initialState.selectedType, initialState.selectedTool]);
const dropdownRef = useClickOutside<HTMLDivElement>(() => setIsDropdownOpen(false), isDropdownOpen);
/**
* Handle tool selection from dropdown
* @description Sets the selected tool and closes dropdown
* @param tool - Selected tool option
*/
const handleToolSelect = (tool: typeof toolOptions[0]): void => {
setSelectedTool(tool);
setIsDropdownOpen(false);
};
/**
* Toggle dropdown open/close state
* @description Switches dropdown visibility
*/
const handleToggleDropdown = (): void => {
setIsDropdownOpen((prev) => !prev);
};
/**
* Handle save action
* @description Saves the tool configuration (add or update)
*/
const handleSave = (): void => {
if (!selectedTool) return;
const toolData = {
name: selectedTool.displayName,
type: selectedType,
color: selectedTool.color,
};
if (editingTool && onUpdateTool) {
onUpdateTool(editingTool.id, toolData);
} else if (onAddTool) {
onAddTool(toolData);
}
setSelectedTool(null);
setIsDropdownOpen(false);
onOpenChange(false);
};
/**
* Handle dialog close
* @description Closes dialog and resets state
*/
const handleClose = (): void => {
setIsDropdownOpen(false);
setSelectedTool(null);
onOpenChange(false);
};
/**
* Get radio button class based on selection state
* @description Returns CSS classes for radio button styling
* @param type - Radio button type (API or MCP)
* @returns CSS class string
*/
const radioButtonClass = (type: 'API' | 'MCP'): string =>
`relative size-5 rounded-full border-2 flex items-center justify-center transition-all ${
selectedType === type ? 'border-[#0033FF]' : 'border-[#E5E8F4]'
}`;
const dropdownButtonBaseClass =
'bg-white border border-solid flex gap-3 h-[46px] items-center justify-between px-4 py-[15px] rounded-tl-[8px] rounded-tr-[8px] shadow-[0px_4px_8px_0px_#EEF1F7] w-full cursor-pointer transition-colors';
const dropdownButtonClass = `${dropdownButtonBaseClass} ${
isDropdownOpen
? 'border-[#0033FF] shadow-[0px_4px_8px_0px_rgba(0,51,255,0.25)]'
: 'border-[#E5E8F4] hover:border-[#0033FF]'
}`;
const dropdownItemBaseClass =
'w-full px-4 py-[13px] text-left font-normal text-sm leading-[20px] transition-colors border-b border-[#E5E8F4] last:border-b-0';
/**
* Get dropdown item class based on selection state
* @description Returns CSS classes for dropdown item styling
* @param isSelected - Whether the item is selected
* @returns CSS class string
*/
const dropdownItemClass = (isSelected: boolean): string =>
`${dropdownItemBaseClass} ${
isSelected
? 'bg-[rgba(0,51,255,0.1)] text-[#0033FF]'
: 'text-black hover:bg-[rgba(0,51,255,0.05)]'
}`;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[720px]">
<DialogHeader>
<div className="flex flex-col gap-0.5">
<DialogTitle className="font-semibold text-[22px] text-black leading-normal">
{editingTool ? 'Update Tool' : 'Add Tool'}
</DialogTitle>
<DialogDescription className="font-normal text-base text-[rgba(0,0,0,0.75)]">
Quick easy smart configuration utility
</DialogDescription>
</div>
<DialogClose onClick={handleClose} />
</DialogHeader>
<div className="px-10 pt-6 pb-8">
<div className="flex flex-col gap-6">
{/* API/MCP Radio Button Selection */}
<div className="flex gap-4 items-start">
{/* API Radio Button */}
<label className="flex gap-1 items-center cursor-pointer group">
<input
type="radio"
name="toolType"
value="API"
checked={selectedType === 'API'}
onChange={() => setSelectedType('API')}
className="sr-only"
/>
<div className="relative size-5 flex items-center justify-center">
<div className={radioButtonClass('API')}>
{selectedType === 'API' && (
<div className="size-2.5 rounded-full bg-[#0033FF]" />
)}
</div>
</div>
<span className="font-medium text-sm text-black leading-[1.6] select-none">
API
</span>
</label>
{/* MCP Radio Button */}
<label className="flex gap-1 items-center cursor-pointer group">
<input
type="radio"
name="toolType"
value="MCP"
checked={selectedType === 'MCP'}
onChange={() => setSelectedType('MCP')}
className="sr-only"
/>
<div className="relative size-5 flex items-center justify-center">
<div className={radioButtonClass('MCP')}>
{selectedType === 'MCP' && (
<div className="size-2.5 rounded-full bg-[#0033FF]" />
)}
</div>
</div>
<span className="font-medium text-sm text-black leading-[1.6] select-none">
MCP
</span>
</label>
</div>
{/* Configure Tools Dropdown */}
<div className="flex flex-col gap-2.5 items-start relative w-full">
<label className="font-medium text-sm text-black leading-[1.6]">
Configure tools
</label>
<div className="relative w-full">
<div
onClick={handleToggleDropdown}
className={dropdownButtonClass}
>
<span className="flex-1 font-normal text-sm text-black text-left tracking-[0.2px]">
{selectedTool?.displayName || 'Select'}
</span>
<div className="flex items-center justify-center relative shrink-0">
<ChevronDown
className={`size-5 transition-transform ${
isDropdownOpen ? 'rotate-180' : ''
}`}
/>
</div>
</div>
{/* Dropdown Menu */}
{isDropdownOpen && (
<div
ref={dropdownRef}
className={DROPDOWN_MENU_CLASS}
>
{toolOptions.map((tool) => (
<button
key={tool.name}
type="button"
onClick={() => handleToolSelect(tool)}
className={dropdownItemClass(selectedTool?.name === tool.name)}
>
{tool.displayName}
</button>
))}
</div>
)}
</div>
</div>
{/* Save Button */}
<div className="flex h-[46px] items-center justify-center w-full">
<button
type="button"
onClick={handleSave}
disabled={!selectedTool}
className={SAVE_BUTTON_CLASS}
>
<span className={SAVE_BUTTON_TEXT_CLASS}>
{editingTool ? 'Update' : 'Save'}
</span>
</button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,131 @@
/**
* Configure Knowledge Base Dialog Component
* @description Dialog for managing knowledge base collections
*/
import { useEscapeKey, useBodyScrollLock } from '@/hooks';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
} from './dialog';
/**
* ConfigureKnowledgeBaseDialog component props
*/
interface ConfigureKnowledgeBaseDialogProps {
/** Whether the dialog is open */
open: boolean;
/** Callback when dialog open state changes */
onOpenChange: (open: boolean) => void;
/** Callback when Add Knowledge Base button is clicked */
onAddKnowledgeBase?: () => void;
}
/**
* ConfigureKnowledgeBaseDialog component
* @description Modal dialog for managing knowledge base collections with empty state
* @param props - Component props
* @returns {JSX.Element} ConfigureKnowledgeBaseDialog element
*/
export function ConfigureKnowledgeBaseDialog({
open,
onOpenChange,
onAddKnowledgeBase,
}: ConfigureKnowledgeBaseDialogProps): JSX.Element {
useEscapeKey(() => onOpenChange(false), open);
useBodyScrollLock(open);
/**
* Handle Add Knowledge Base button click
* @description Triggers the onAddKnowledgeBase callback and closes dialog
*/
const handleAddKnowledgeBase = (): void => {
if (onAddKnowledgeBase) {
onAddKnowledgeBase();
}
onOpenChange(false);
};
/**
* Handle keyboard events on Add Knowledge Base button
* @description Handles Enter and Space key presses
* @param e - Keyboard event
*/
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAddKnowledgeBase();
}
};
// Icon from Figma design - database icon with exclamation
const emptyStateIcon = 'https://www.figma.com/api/mcp/asset/31344752-2e10-42c5-9678-5d2cb0b3dadb';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[620px]">
<DialogHeader>
<div className="flex flex-col gap-0.5">
<DialogTitle className="font-semibold text-[22px] text-black leading-normal">
Add Knowledge Base
</DialogTitle>
<DialogDescription className="font-normal text-sm text-[rgba(0,0,0,0.75)] max-w-[484px]">
Manage your document collections and knowledge sources
</DialogDescription>
</div>
<DialogClose onClick={() => onOpenChange(false)} />
</DialogHeader>
<div className="px-8 pt-6 pb-8">
<div className="flex flex-col gap-8 items-end relative shrink-0">
{/* Empty State Content */}
<div className="flex flex-col items-center justify-center relative shrink-0 w-full">
<div className="flex flex-col gap-4 items-center relative shrink-0">
{/* Database Icon */}
<div className="h-[61px] relative shrink-0 w-[59px]">
<img
alt="Database icon"
className="block max-w-none size-full"
src={emptyStateIcon}
/>
</div>
{/* Empty State Messages */}
<div className="flex flex-col gap-[11px] items-center not-italic relative shrink-0 text-center">
<div className="flex flex-col font-semibold justify-center leading-normal min-w-full relative shrink-0 text-[#252526] text-base tracking-[0.25px] w-[min-content]">
<p className="leading-normal whitespace-pre-wrap">
No knowledge bases found
</p>
</div>
<p className="font-normal leading-normal relative shrink-0 text-[#6d6b70] text-sm w-[266px] whitespace-pre-wrap">
Get started by adding your first knowledge base
</p>
</div>
</div>
</div>
{/* Add Knowledge Base Button */}
<div className="flex items-center justify-center relative shrink-0 w-full">
<button
type="button"
onClick={handleAddKnowledgeBase}
onKeyDown={handleKeyDown}
className="bg-[#03f] flex flex-1 items-center justify-center px-[42px] py-[14px] relative rounded-[12px] hover:bg-[#002BCC] active:bg-[#0025AA] transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-[#0033FF] focus:ring-offset-2"
aria-label="Add Knowledge Base"
>
<p className="capitalize font-medium leading-[18px] not-italic relative shrink-0 text-sm text-center text-white">
Add Knowledge base
</p>
</button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,269 @@
/**
* Configure Memory Dialog Component
* @description Dialog for configuring memory settings (max messages stored)
*/
import { useState, useMemo, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
} from './dialog';
import { useEscapeKey, useBodyScrollLock } from '@/hooks';
/**
* ConfigureMemoryDialog component props
*/
interface ConfigureMemoryDialogProps {
/** Whether the dialog is open */
open: boolean;
/** Callback when dialog open state changes */
onOpenChange: (open: boolean) => void;
/** Initial memory value (default: 2) */
initialValue?: number;
/** Callback when memory value is saved */
onSave?: (value: number) => void;
}
/**
* Minimum memory value
*/
const MIN_VALUE = 2;
/**
* Maximum memory value
*/
const MAX_VALUE = 10;
/**
* Clamp value between min and max
* @description Ensures value is within bounds
* @param value - Value to clamp
* @param min - Minimum value
* @param max - Maximum value
* @returns Clamped value
*/
const clampValue = (value: number, min: number, max: number): number => {
return Math.max(min, Math.min(max, value));
};
/**
* Calculate percentage for slider
* @description Calculates percentage position for slider thumb
* @param value - Current value
* @param min - Minimum value
* @param max - Maximum value
* @returns Percentage (0-100)
*/
const calculatePercentage = (value: number, min: number, max: number): number => {
return ((value - min) / (max - min)) * 100;
};
const SLIDER_GRADIENT = 'linear-gradient(270deg, #03F 0%, #00E2E0 100%)';
const MEMORY_CARD_CLASS =
'bg-[rgba(0,0,0,0.05)] border border-[#e5e8f4] border-solid flex flex-col h-[128px] items-start p-4 relative rounded-[12px] w-full';
const MEMORY_LABEL_CLASS =
'font-medium leading-[1.6] not-italic relative shrink-0 text-[#171d25] text-sm w-full whitespace-pre-wrap';
const MIN_LABEL_WRAPPER_CLASS = 'flex flex-col items-center justify-center relative shrink-0 w-[7px]';
const MIN_LABEL_CLASS =
'font-medium leading-4 not-italic relative shrink-0 text-xs text-black w-full whitespace-pre-wrap';
const VALUE_DISPLAY_CLASS =
'bg-white border border-[rgba(0,0,0,0.1)] border-solid flex items-center justify-end px-2 py-1.5 relative rounded-[6px] shrink-0';
const VALUE_TEXT_CLASS = 'font-normal leading-[1.6] not-italic relative shrink-0 text-xs text-black';
const SAVE_BUTTON_CLASS =
'bg-[#03f] flex flex-1 items-center justify-center px-[42px] py-[14px] relative rounded-[12px] hover:bg-[#002BCC] active:bg-[#0025AA] transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-[#0033FF] focus:ring-offset-2';
const BUTTON_TEXT_CLASS = 'font-semibold leading-[18px] not-italic relative shrink-0 text-sm text-center text-white';
const FILLED_TRACK_CLASS = 'absolute left-0 top-0 h-full rounded-[50px] transition-all duration-200';
const SLIDER_INPUT_CLASS = 'absolute top-[-5px] left-0 w-full h-2 opacity-0 cursor-pointer z-30 appearance-none';
const SLIDER_THUMB_CLASS =
'absolute top-[-5px] size-[18px] bg-white rounded-full border-2 border-[#0033FF] shadow-md pointer-events-none transition-all duration-200 z-20';
const MAX_LABEL_CLASS = 'font-medium leading-4 not-italic relative shrink-0 text-xs text-black';
/**
* ConfigureMemoryDialog component
* @description Modal dialog for configuring memory settings with slider
* @param props - Component props
* @returns {JSX.Element} ConfigureMemoryDialog element
*/
export function ConfigureMemoryDialog({
open,
onOpenChange,
initialValue = MIN_VALUE,
onSave,
}: ConfigureMemoryDialogProps): JSX.Element {
const clampedInitial = useMemo(
() => clampValue(initialValue, MIN_VALUE, MAX_VALUE),
[initialValue]
);
const [memoryValue, setMemoryValue] = useState(clampedInitial);
useEffect(() => {
if (open) {
setMemoryValue(clampedInitial);
}
}, [open, clampedInitial]);
useEscapeKey(() => onOpenChange(false), open);
useBodyScrollLock(open);
/**
* Handle save action
* @description Saves the memory configuration and closes dialog
*/
const handleSave = (): void => {
if (onSave && memoryValue >= MIN_VALUE && memoryValue <= MAX_VALUE) {
onSave(memoryValue);
}
onOpenChange(false);
};
/**
* Handle keyboard events on save button
* @description Handles Enter and Space key presses
* @param e - Keyboard event
*/
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSave();
}
};
/**
* Handle slider value change
* @description Updates memory value when slider moves
* @param e - Change event from slider input
*/
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setMemoryValue(Number(e.target.value));
};
const percentage = useMemo(
() => calculatePercentage(memoryValue, MIN_VALUE, MAX_VALUE),
[memoryValue]
);
const sliderThumbStyle = useMemo(
() => ({
left: `clamp(0px, calc(${percentage}% - 9px), calc(100% - 18px))`,
}),
[percentage]
);
const filledTrackStyle = useMemo(
() => ({
width: `${percentage}%`,
background: SLIDER_GRADIENT,
}),
[percentage]
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[630px]">
<DialogHeader>
<div className="flex flex-col gap-0.5">
<DialogTitle className="font-semibold text-[22px] text-black leading-normal">
Configure Memory
</DialogTitle>
<DialogDescription className="font-normal text-sm text-[rgba(0,0,0,0.75)] max-w-[484px]">
Defines the maximum number of messages the agent can retain in short-term memory
</DialogDescription>
</div>
<DialogClose onClick={onOpenChange.bind(null, false)} />
</DialogHeader>
<div className="pl-[40px] pr-[32px] pt-8 pb-8">
<div className="flex flex-col gap-8 items-end">
{/* Memory Configuration Card */}
<div className={MEMORY_CARD_CLASS}>
<div className="flex flex-col gap-4 items-start overflow-clip relative shrink-0 w-full">
<p className={MEMORY_LABEL_CLASS}>
Max message stored as Short-term Memory
<span className="text-[#ef3125]">*</span>
</p>
{/* Slider Container */}
<div className="flex flex-col items-start relative shrink-0 w-full">
<div className="flex gap-1.5 items-center relative shrink-0 w-full">
{/* Min Value Label */}
<div className={MIN_LABEL_WRAPPER_CLASS}>
<p className={MIN_LABEL_CLASS}>
{MIN_VALUE}
</p>
</div>
{/* Slider Track */}
<div className="flex-1 relative h-2 bg-[rgba(0,0,0,0.1)] rounded-[50px]">
{/* Filled Track */}
<div
className={FILLED_TRACK_CLASS}
style={filledTrackStyle}
/>
{/* Slider Input (transparent overlay) */}
<input
type="range"
min={MIN_VALUE}
max={MAX_VALUE}
step="1"
value={memoryValue}
onChange={handleSliderChange}
className={SLIDER_INPUT_CLASS}
style={{
background: 'transparent',
WebkitAppearance: 'none',
MozAppearance: 'none',
}}
/>
{/* Custom Thumb */}
<div
className={SLIDER_THUMB_CLASS}
style={sliderThumbStyle}
/>
</div>
{/* Max Value Label */}
<div className="flex flex-col items-center justify-center relative shrink-0">
<p className={MAX_LABEL_CLASS}>
{MAX_VALUE}
</p>
</div>
</div>
</div>
{/* Current Value Display */}
<div className={VALUE_DISPLAY_CLASS}>
<p className={VALUE_TEXT_CLASS}>
{memoryValue} Messages
</p>
</div>
</div>
</div>
{/* Create Button */}
<div className="flex items-center justify-center relative shrink-0 w-full">
<button
type="button"
onClick={handleSave}
onKeyDown={handleKeyDown}
className={SAVE_BUTTON_CLASS}
aria-label="Save memory configuration"
>
<p className={BUTTON_TEXT_CLASS}>
Create
</p>
</button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,162 @@
/**
* Dialog component
* @description Reusable modal/dialog component with backdrop
*/
import * as React from 'react';
import { cn } from '@/lib/utils';
interface DialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
className?: string;
}
interface DialogContentProps {
children: React.ReactNode;
className?: string;
}
interface DialogHeaderProps {
children: React.ReactNode;
className?: string;
}
interface DialogTitleProps {
children: React.ReactNode;
className?: string;
}
interface DialogDescriptionProps {
children: React.ReactNode;
className?: string;
}
interface DialogCloseProps {
className?: string;
onClick?: () => void;
}
/**
* Dialog component
* @description Modal dialog with backdrop blur
*/
export function Dialog({ open, onOpenChange, children, className }: DialogProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/35 backdrop-blur-[17.5px]"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
{/* Dialog Content */}
<div className={cn('relative z-50', className)}>{children}</div>
</div>
);
}
/**
* DialogContent component
* @description Container for dialog content
*/
export function DialogContent({ children, className }: DialogContentProps) {
return (
<div
className={cn(
'bg-white rounded-[24px] shadow-lg w-full max-w-[720px]',
className
)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
);
}
/**
* DialogHeader component
* @description Header section of dialog
*/
export function DialogHeader({ children, className }: DialogHeaderProps) {
return (
<div
className={cn(
'bg-[#EEF1F7] border-b border-[#E5E8F4] px-8 py-6 rounded-tl-[24px] rounded-tr-[24px] flex items-center justify-between',
className
)}
>
{children}
</div>
);
}
/**
* DialogTitle component
* @description Title section of dialog header
*/
export function DialogTitle({ children, className }: DialogTitleProps) {
return (
<div className={cn('flex flex-col gap-0.5', className)}>{children}</div>
);
}
/**
* DialogDescription component
* @description Description/subtitle in dialog header
*/
export function DialogDescription({
children,
className,
}: DialogDescriptionProps) {
return (
<p
className={cn(
'font-["Poppins:Regular",sans-serif] text-base text-[rgba(0,0,0,0.75)]',
className
)}
>
{children}
</p>
);
}
/**
* DialogClose component
* @description Close button for dialog
*/
export function DialogClose({ className, onClick }: DialogCloseProps) {
const closeIcon = 'https://www.figma.com/api/mcp/asset/d151f987-7858-49a2-85dd-eac10b6bf0b0';
const closeCircleBg = 'https://www.figma.com/api/mcp/asset/3702e7bd-24a9-4380-83be-0347e18baa40';
return (
<button
type="button"
onClick={onClick}
className={cn(
'relative size-[30px] flex items-center justify-center cursor-pointer',
className
)}
aria-label="Close dialog"
>
<div className="absolute inset-0">
<img
src={closeCircleBg}
alt=""
className="block max-w-none size-full"
/>
</div>
<div className="relative ml-[3px] mt-[3.5px] size-6">
<img
src={closeIcon}
alt=""
className="block max-w-none size-full"
/>
</div>
</button>
);
}

View File

@ -0,0 +1,67 @@
/**
* Tool Chip Component
* @description Displays a tool chip with name, close button, and redirect button
*/
import { X, ArrowRight } from 'lucide-react';
import type { Tool } from '@/types';
/**
* ToolChip component props
*/
interface ToolChipProps {
/** Tool object to display */
tool: Tool;
/** Callback when delete button is clicked */
onDelete: (id: string) => void;
/** Callback when redirect button is clicked */
onRedirect: (id: string) => void;
}
/**
* ToolChip component
* @description Chip component for displaying tools with delete and redirect actions
* Matches Figma design with exact styling
* @param tool - The tool object containing id, name, and color
* @param onDelete - Callback function called when delete button is clicked, receives tool id
* @param onRedirect - Callback function called when redirect button is clicked, receives tool id
* @returns {JSX.Element} A chip component displaying the tool with action buttons
*/
export function ToolChip({ tool, onDelete, onRedirect }: ToolChipProps): JSX.Element {
return (
<div className="bg-white border border-[#eef1f7] border-solid flex gap-8 h-[32px] items-center px-3 py-[5px] relative rounded-[6px] shrink-0">
<div className="flex items-center relative shrink-0">
<p
className="font-normal leading-[1.5] not-italic relative shrink-0 text-[14px]"
style={{ color: tool.color }}
>
{tool.name}
</p>
</div>
<div className="flex gap-1 items-center justify-end leading-[0] relative shrink-0">
{/* Close Button - matches Figma design */}
<button
type="button"
onClick={() => onDelete(tool.id)}
className="relative size-[18px] flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity group"
aria-label={`Delete ${tool.name}`}
>
<div className="absolute inset-0 size-[18px] rounded-full bg-[rgba(0,0,0,0.08)] group-hover:bg-[rgba(0,0,0,0.12)] transition-colors" />
<X className="absolute size-[14.4px] text-black ml-[1.8px] mt-[2.1px]" strokeWidth={2} />
</button>
{/* Redirect Button - matches Figma design */}
<button
type="button"
onClick={() => onRedirect(tool.id)}
className="relative size-[16px] flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity group"
aria-label={`Configure ${tool.name}`}
>
<div className="absolute inset-0 size-[16px] rounded-full bg-[rgba(0,0,0,0.08)] group-hover:bg-[rgba(0,0,0,0.12)] transition-colors" />
<ArrowRight className="absolute size-[12.8px] text-black ml-[1.78px] mt-[1.78px]" strokeWidth={2} />
</button>
</div>
</div>
);
}

95
src/constants/agents.ts Normal file
View File

@ -0,0 +1,95 @@
/**
* Agents Data Constants
* @description Mock data for agents list
*/
/**
* Agent data interface
*/
export interface AgentData {
title: string;
tags: string[];
description: string;
}
/**
* Mock agents data
*/
export const MOCK_AGENTS: AgentData[] = [
{
title: 'Customer Support Agent',
tags: ['GPT-4', 'Chat Interface'],
description: 'Automates customer service inquiries with intelligent escalation and context preservation.',
},
{
title: 'Document Analyst',
tags: ['Claude-3', 'PDF Processing', 'Knowledge Graph'],
description: 'Analyzes documents to extract insights and generate comprehensive reports automatically.',
},
{
title: 'Sales Qualifier',
tags: ['Lead Scoring', 'CRM Integration', 'Email Autom...'],
description: 'Qualifies prospects, schedules meetings, and manages follow-up communications with tracking.',
},
{
title: 'Customer Support Agent',
tags: ['GPT-4', 'Chat Interface'],
description: 'Automates customer service inquiries with intelligent escalation and context preservation.',
},
{
title: 'Document Analyst',
tags: ['Claude-3', 'PDF Processing', 'Knowledge Graph'],
description: 'Analyzes documents to extract insights and generate comprehensive reports automatically.',
},
{
title: 'Sales Qualifier',
tags: ['Lead Scoring', 'CRM Integration', 'Email Autom...'],
description: 'Qualifies prospects, schedules meetings, and manages follow-up communications with tracking.',
},
{
title: 'Data Analyst Agent',
tags: ['GPT-4', 'Data Processing'],
description: 'Processes and analyzes large datasets to generate insights and visualizations.',
},
{
title: 'Content Writer',
tags: ['Claude-3', 'Content Generation'],
description: 'Generates high-quality content for blogs, articles, and marketing materials.',
},
{
title: 'Code Reviewer',
tags: ['GPT-4', 'Code Analysis'],
description: 'Reviews code for quality, security, and best practices automatically.',
},
{
title: 'Email Assistant',
tags: ['GPT-4', 'Email Management'],
description: 'Manages and responds to emails intelligently with context awareness.',
},
{
title: 'Meeting Scheduler',
tags: ['Calendar Integration', 'AI Scheduling'],
description: 'Schedules meetings by understanding natural language requests and availability.',
},
{
title: 'Research Assistant',
tags: ['Claude-3', 'Web Research'],
description: 'Conducts comprehensive research on topics and compiles detailed reports.',
},
{
title: 'Translation Agent',
tags: ['Multi-language', 'GPT-4'],
description: 'Translates content between multiple languages with high accuracy.',
},
{
title: 'Quality Assurance',
tags: ['Testing', 'Automation'],
description: 'Automates quality assurance processes and generates test reports.',
},
{
title: 'Financial Analyst',
tags: ['Data Analysis', 'Reporting'],
description: 'Analyzes financial data and generates comprehensive reports and insights.',
},
];

View File

@ -3,7 +3,7 @@
* @description Reusable hooks following AgenticIQ best practices * @description Reusable hooks following AgenticIQ best practices
*/ */
import { useState, useEffect } from 'react' import { useState, useEffect, useRef, type RefObject } from 'react'
/** /**
* Hook for managing media queries * Hook for managing media queries
@ -56,3 +56,78 @@ export function useDebounce<T>(value: T, delay: number): T {
return debouncedValue return debouncedValue
} }
/**
* Hook for handling click outside events
* @description Closes dropdown/modal when clicking outside the element
* @param handler - Callback function to execute when click outside occurs
* @param enabled - Whether the hook is enabled
* @returns Ref to attach to the element
* @example
* const ref = useClickOutside(() => setIsOpen(false), isOpen)
*/
export function useClickOutside<T extends HTMLElement>(
handler: () => void,
enabled: boolean = true
): RefObject<T> {
const ref = useRef<T>(null)
useEffect(() => {
if (!enabled) return
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
handler()
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [handler, enabled])
return ref
}
/**
* Hook for handling escape key press
* @description Executes callback when Escape key is pressed
* @param handler - Callback function to execute
* @param enabled - Whether the hook is enabled
* @example
* useEscapeKey(() => onClose(), isOpen)
*/
export function useEscapeKey(handler: () => void, enabled: boolean = true): void {
useEffect(() => {
if (!enabled) return
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handler()
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [handler, enabled])
}
/**
* Hook for locking/unlocking body scroll
* @description Prevents body scroll when modal/dialog is open
* @param locked - Whether body scroll should be locked
* @example
* useBodyScrollLock(isModalOpen)
*/
export function useBodyScrollLock(locked: boolean): void {
useEffect(() => {
if (locked) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [locked])
}

View File

@ -3,7 +3,7 @@
@layer base { @layer base {
body { body {
font-family: 'Poppins', sans-serif; font-family: 'Poppins', sans-serif;
font-size: 12px; font-size: 0.875rem; /* 14px - Base body text size */
} }
:root { :root {
@ -59,6 +59,22 @@
} }
.sidebar-icon-v3 { .sidebar-icon-v3 {
@apply w-[42px] h-[42px] flex items-center justify-center text-white hover:bg-white/10 rounded-xl transition-all duration-300; @apply w-9 h-9 md:w-[42px] md:h-[42px] flex items-center justify-center text-white hover:bg-white/10 rounded-xl transition-all duration-300 min-h-[44px] min-w-[44px] md:min-h-[42px] md:min-w-[42px];
}
/* Global Font Utilities */
.font-regular {
font-family: 'Poppins', sans-serif;
font-weight: 400;
}
.font-medium {
font-family: 'Poppins', sans-serif;
font-weight: 500;
}
.font-semibold {
font-family: 'Poppins', sans-serif;
font-weight: 600;
} }
} }

402
src/pages/agent-create.tsx Normal file
View File

@ -0,0 +1,402 @@
import { useState } from 'react';
import { Link } from '@tanstack/react-router';
import { Info, ChevronDown } from 'lucide-react';
import { APP_PATHS } from '@/routes';
import { AddToolDialog } from '@/components/ui/add-tool-dialog';
import { ConfigureMemoryDialog } from '@/components/ui/configure-memory-dialog';
import { ConfigureKnowledgeBaseDialog } from '@/components/ui/configure-knowledge-base-dialog';
import { ToolChip } from '@/components/ui/tool-chip';
import type { Tool } from '@/types';
/**
* Toggle Switch Component
* @description A toggle switch that visually represents on/off state
* @param enabled - Whether the toggle is enabled
* @returns {JSX.Element} ToggleSwitch element
*/
function ToggleSwitch({ enabled }: { enabled: boolean }): JSX.Element {
return (
<div
className={`relative h-5 w-[35px] rounded-full transition-colors duration-200 ${
enabled ? 'bg-[#0033FF]' : 'bg-gray-300'
}`}
>
<div
className={`absolute top-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200 ${
enabled ? 'translate-x-[15px]' : 'translate-x-0.5'
}`}
/>
</div>
);
}
/**
* AgentCreatePage component
* @description Page for creating a new agent with configuration options
* @returns {JSX.Element} Agent creation form page
*/
export function AgentCreatePage(): JSX.Element {
const [enableKb, setEnableKb] = useState(false);
const [enableMemory, setEnableMemory] = useState(true);
const [isToolDialogOpen, setIsToolDialogOpen] = useState(false);
const [isMemoryDialogOpen, setIsMemoryDialogOpen] = useState(false);
const [isKnowledgeBaseDialogOpen, setIsKnowledgeBaseDialogOpen] = useState(false);
const [memoryValue, setMemoryValue] = useState(2);
const [editingTool, setEditingTool] = useState<Tool | null>(null);
const [tools, setTools] = useState<Tool[]>([
{ id: '1', name: 'Slack', type: 'API', color: '#ff0080' },
{ id: '2', name: 'GitHub', type: 'MCP', color: '#8fbc24' },
]);
/**
* Handle adding a new tool
* @description Adds a new tool to the agent configuration
* @param toolData - Tool data without ID
*/
const handleAddTool = (toolData: Omit<Tool, 'id'>): void => {
const newTool: Tool = {
...toolData,
id: Date.now().toString(),
};
setTools((prev) => [...prev, newTool]);
};
/**
* Handle updating an existing tool
* @description Updates tool data for the specified tool ID
* @param toolId - ID of the tool to update
* @param toolData - Updated tool data without ID
*/
const handleUpdateTool = (
toolId: string,
toolData: Omit<Tool, 'id'>
): void => {
setTools((prev) =>
prev.map((tool) =>
tool.id === toolId ? { ...tool, ...toolData } : tool
)
);
setEditingTool(null);
};
/**
* Handle deleting a tool
* @description Removes a tool from the agent configuration
* @param id - ID of the tool to delete
*/
const handleDeleteTool = (id: string): void => {
setTools((prev) => prev.filter((tool) => tool.id !== id));
};
/**
* Handle redirecting to tool configuration
* @description Opens the tool dialog for editing the specified tool
* @param id - ID of the tool to configure
*/
const handleRedirectTool = (id: string): void => {
const tool = tools.find((t) => t.id === id);
if (tool) {
setEditingTool(tool);
setIsToolDialogOpen(true);
}
};
/**
* Handle dialog close state change
* @description Manages dialog open/close state and resets editing tool
* @param open - Whether the dialog should be open
*/
const handleDialogClose = (open: boolean): void => {
setIsToolDialogOpen(open);
if (!open) {
setEditingTool(null);
}
};
return (
<div className="min-h-screen px-4 py-6 md:px-8 lg:px-[50px]">
<div className="mb-6">
<h1 className="font-semibold text-[20px] text-black leading-normal">
Add Agent
</h1>
</div>
<div className="flex flex-col gap-6">
{/* Main Form Card */}
<div className="bg-white border border-[rgba(0,0,0,0.1)] rounded-[24px] p-8 shadow-md">
<div className="grid gap-6 lg:grid-cols-2">
{/* Left Column */}
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
Name<span className="text-[#ef3125]">*</span>
</label>
<input
className="w-full h-[46px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 text-sm text-[rgba(0,0,0,0.75)] shadow-[0px_2px_4px_0px_#EEF1F7] outline-none focus:border-[#0033FF] tracking-[0.2px]"
placeholder="Input here"
type="text"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
Agent Description
<span className="text-[#ef3125]">*</span>
</label>
<textarea
rows={4}
className="w-full h-[135px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 text-sm text-[rgba(0,0,0,0.75)] shadow-[0px_2px_4px_0px_#EEF1F7] outline-none focus:border-[#0033FF] tracking-[0.2px] resize-none"
placeholder="Lorem Ipsum is simply dummy text"
name="agentDescription"
/>
</div>
<div className="grid gap-8 md:grid-cols-2">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
LLM Provider
<span className="text-[#ef3125]">*</span>
</label>
<div className="flex items-center justify-between h-[46px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 shadow-[0px_2px_4px_0px_#EEF1F7]">
<span className="text-sm text-[rgba(0,0,0,0.75)] tracking-[0.2px]">
Select
</span>
<ChevronDown className="shrink-0 size-5 text-[rgba(0,0,0,0.75)]" />
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
LLM Model
<span className="text-[#ef3125]">*</span>
</label>
<div className="flex items-center justify-between h-[46px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 shadow-[0px_2px_4px_0px_#EEF1F7]">
<span className="text-sm text-[rgba(0,0,0,0.75)] tracking-[0.2px]">
Select
</span>
<ChevronDown className="shrink-0 size-5 text-[rgba(0,0,0,0.75)]" />
</div>
</div>
</div>
</div>
{/* Right Column */}
<div className="flex flex-col gap-6 border border-[rgba(0,0,0,0.15)] rounded-[24px] p-8">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
Agent Role<span className="text-[#ef3125]">*</span>
</label>
<input
className="w-full h-[46px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 text-sm text-[rgba(0,0,0,0.75)] shadow-[0px_2px_4px_0px_#EEF1F7] outline-none focus:border-[#0033FF] tracking-[0.2px]"
placeholder="Input here"
type="text"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm text-black leading-[1.6]">
Prompt<span className="text-[#ef3125]">*</span>
</label>
<input
className="w-full h-[46px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 text-sm text-[rgba(0,0,0,0.75)] shadow-[0px_2px_4px_0px_#EEF1F7] outline-none focus:border-[#0033FF] tracking-[0.2px]"
placeholder="Your Prompt"
/>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<label className="font-medium text-sm text-black leading-[1.6]">
Instruction & Suggestion
<span className="text-[#ef3125]">*</span>
</label>
<button
className="bg-[rgba(0,51,255,0.06)] border border-[#333] rounded-md px-3 py-1.5 text-xs font-medium text-[#333] hover:bg-[rgba(0,51,255,0.1)] transition-colors"
>
Improve Instruction
</button>
</div>
<textarea
rows={3}
className="w-full h-[78px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-3 text-sm text-[rgba(0,0,0,0.75)] shadow-[0px_2px_4px_0px_#EEF1F7] outline-none focus:border-[#0033FF] tracking-[0.2px] resize-none"
placeholder="Lorem Ipsum is simply dummy text"
/>
</div>
</div>
</div>
</div>
{/* Features Card */}
<div className="bg-white border border-[rgba(0,0,0,0.1)] rounded-[24px] p-8 shadow-md">
<div className="flex flex-col gap-6">
<h2 className="font-semibold text-base text-black leading-[1.6]">
Features
</h2>
<div className="grid gap-6 md:grid-cols-3">
{/* Knowledge Base */}
<div className="flex flex-col gap-2">
<div
className={`flex items-center justify-between h-[56px] rounded-lg px-4 py-[13px] transition-all ${
enableKb
? 'border border-[rgba(0,51,255,0.25)] shadow-[0px_2px_12px_0px_rgba(0,51,255,0.5)]'
: 'border border-[#E5E8F4] bg-white shadow-[0px_2px_4px_0px_#EEF1F7]'
}`}
>
<div className="flex items-center gap-0.5">
<span className="font-medium text-sm text-black leading-[1.5]">
Knowledge Base
</span>
<Info className="size-4 text-black" />
</div>
<button
onClick={() => setEnableKb((v) => !v)}
className="relative shrink-0"
aria-pressed={enableKb}
>
<ToggleSwitch enabled={enableKb} />
</button>
</div>
{enableKb && (
<div className="flex items-center justify-between">
<button
onClick={() => setIsKnowledgeBaseDialogOpen(true)}
className="flex items-center gap-0.5 text-xs font-medium text-[#03f] leading-[1.5] hover:underline"
>
Configure
<img src="/Agent/redirect.svg" alt="Redirect" className="size-3.5 text-[#03f]" />
</button>
<div className="bg-white h-[22px] rounded px-1.5 py-0.5" />
</div>
)}
</div>
{/* Memory */}
<div className="flex flex-col gap-2">
<div
className={`flex items-center justify-between h-[56px] rounded-lg px-4 py-[13px] transition-all ${
enableMemory
? 'border border-[rgba(0,51,255,0.25)] shadow-[0px_2px_12px_0px_rgba(0,51,255,0.5)]'
: 'border border-[#E5E8F4] bg-white shadow-[0px_2px_4px_0px_#EEF1F7]'
}`}
>
<div className="flex items-center gap-0.5">
<span className="font-medium text-sm text-black leading-[1.5]">
Memory
</span>
<Info className="size-4 text-black" />
</div>
<button
onClick={() => setEnableMemory((v) => !v)}
className="relative shrink-0"
aria-pressed={enableMemory}
>
<ToggleSwitch enabled={enableMemory} />
</button>
</div>
{enableMemory && (
<div className="flex items-center justify-between">
<button
onClick={() => setIsMemoryDialogOpen(true)}
className="flex items-center gap-0.5 text-xs font-medium text-[#03f] leading-[1.5] hover:underline"
>
Configure
<img src="/Agent/redirect.svg" alt="Redirect" className="size-3.5 text-[#03f]" />
</button>
<span className="bg-white border border-[#e5e8f4] border-solid flex items-center px-1.5 py-0.5 rounded text-xs font-normal text-black text-center">
{memoryValue} Messages
</span>
</div>
)}
</div>
{/* Tool Configuration */}
<div className="flex items-center justify-between h-[56px] rounded-lg border border-[#E5E8F4] bg-white px-4 py-[13px] shadow-[0px_2px_8px_0px_#EEF1F7]">
<div className="flex flex-1 gap-0.5 items-center">
<span className="font-medium text-sm text-black leading-[1.6]">
Tool Configuration
</span>
<Info className="size-4 text-black" />
</div>
<button
onClick={() => {
setEditingTool(null);
setIsToolDialogOpen(true);
}}
className="bg-white border border-black border-solid flex items-center justify-center gap-1.5 px-3 py-[3px] rounded-[6px] hover:bg-gray-50 transition-colors"
>
<div className="relative size-6 flex items-center justify-center">
<img
src="/Agent/add.svg"
alt="Add"
className="size-3.5 filter brightness-0"
/>
</div>
<span className="font-medium text-sm text-black leading-[1.5] text-left">
Add
</span>
</button>
</div>
</div>
{/* Divider */}
<div className="h-px bg-[#E5E8F4] w-full" />
{/* Tools Display Section */}
<div className="bg-[rgba(229,232,244,0.25)] border border-[#e5e8f4] border-solid flex flex-col items-start justify-center p-6 relative rounded-[12px] shrink-0 w-full">
<div className="flex gap-3 items-start relative shrink-0 flex-wrap">
{tools.map((tool) => (
<ToolChip
key={tool.id}
tool={tool}
onDelete={handleDeleteTool}
onRedirect={handleRedirectTool}
/>
))}
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2 justify-center font-semibold">
<Link
to={APP_PATHS.agent}
className="bg-white border border-[rgba(0,0,0,0.5)] rounded-xl px-[42px] py-[19px] w-[182px] flex items-center justify-center hover:bg-gray-50 transition-colors"
>
<span className="font-medium text-sm text-black text-center leading-[18px]">
Cancel
</span>
</Link>
<button
className="bg-[#03f] border border-[#03f] rounded-xl px-[42px] py-[19px] w-[182px] flex items-center justify-center hover:bg-[#002BCC] transition-colors"
>
<span className="font-medium text-sm text-white text-center leading-[18px]">
Create
</span>
</button>
</div>
</div>
{/* Add Tool Dialog */}
<AddToolDialog
open={isToolDialogOpen}
onOpenChange={handleDialogClose}
onAddTool={handleAddTool}
onUpdateTool={handleUpdateTool}
editingTool={editingTool}
/>
{/* Configure Memory Dialog */}
<ConfigureMemoryDialog
open={isMemoryDialogOpen}
onOpenChange={setIsMemoryDialogOpen}
initialValue={memoryValue}
onSave={setMemoryValue}
/>
{/* Configure Knowledge Base Dialog */}
<ConfigureKnowledgeBaseDialog
open={isKnowledgeBaseDialogOpen}
onOpenChange={setIsKnowledgeBaseDialogOpen}
onAddKnowledgeBase={() => {
// Handle add knowledge base action
console.log('Add Knowledge Base clicked');
}}
/>
</div>
);
}

72
src/pages/agent.tsx Normal file
View File

@ -0,0 +1,72 @@
import { Link } from '@tanstack/react-router';
import { APP_PATHS } from '@/routes';
const searchIcon =
'https://www.figma.com/api/mcp/asset/379327bd-47f2-4ebc-a8e0-c0004928d27b';
const emptyStateIcon = '/Agent/agent.svg';
const addIcon = '/Agent/add.svg';
/**
* AgentPage component
* @description Displays the agent management page with search and empty state
* @returns {JSX.Element} AgentPage element
*/
export function AgentPage(): JSX.Element {
return (
<div className="px-4 py-6 md:px-6 md:py-8 lg:px-10 lg:pt-10 lg:pb-16">
<div className="flex flex-col gap-6 md:gap-8">
<div className="space-y-2">
<h1 className="text-[1.75rem] md:text-[2rem] lg:text-[2.5rem] font-semibold text-[#1B1B1B] leading-tight">
Agent
</h1>
<p className="max-w-3xl text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] text-[#444750]">
Intelligent Agents that automate workflows and enhance decision-making
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex-1 w-full max-w-2xl">
<div className="flex items-center gap-3 rounded-xl border border-[#E1E4EF] bg-white px-3 py-2.5 md:px-4 md:py-3 shadow-sm">
<img
src={searchIcon}
alt="Search"
className="h-4 w-4 md:h-5 md:w-5 opacity-70"
/>
<input
type="text"
placeholder="Search Agent"
className="w-full border-none bg-transparent text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] text-[#6F7282] placeholder:text-[#9DA3B0] focus:outline-none"
/>
</div>
</div>
</div>
<div className="flex min-h-[400px] md:min-h-[540px] items-center justify-center">
<div className="flex flex-col items-center gap-4 md:gap-6 text-center px-4">
<img
src={emptyStateIcon}
alt="No agent"
className="h-24 w-24 md:h-32 md:w-32 lg:h-36 lg:w-36"
/>
<div className="space-y-2">
<h2 className="text-[1.5rem] md:text-[1.75rem] lg:text-[2rem] font-semibold text-[#252526]">
No Agent Found
</h2>
<p className="text-[0.75rem] md:text-[0.8125rem] lg:text-[0.875rem] text-[#6D6B70]">
Get started by adding your first Agent
</p>
</div>
<Link
to={APP_PATHS.agentCreate}
className="inline-flex items-center gap-2 rounded-xl bg-[#0033FF] px-4 py-2.5 md:px-5 md:py-3 text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] font-semibold text-white shadow-lg shadow-blue-500/25 transition hover:bg-[#002BCC] min-h-[44px]"
>
<img src={addIcon} alt="Add" className="h-4 w-4 md:h-5 md:w-5" />
Add Agent
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -3,147 +3,84 @@
* @description Main dashboard page with metrics and agent cards * @description Main dashboard page with metrics and agent cards
*/ */
import { useState } from 'react';
import { MetricCard } from '@/components/dashboard/metric-card'; import { MetricCard } from '@/components/dashboard/metric-card';
import { AgentCard } from '@/components/dashboard/agent-card'; import { AgentsSection } from '@/components/dashboard/agents-section';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Search, Plus } from 'lucide-react'; import { Search, Plus } from 'lucide-react';
import { MOCK_AGENTS } from '@/constants/agents';
/** /**
* Dashboard component * Dashboard component
* @description Main dashboard page with all sections * @description Main dashboard page with all sections
* @returns Dashboard element * @returns {JSX.Element} Dashboard element
*/ */
export function Dashboard() { export function Dashboard(): JSX.Element {
const [activeTab, setActiveTab] = useState<'all' | 'my'>('all');
const agents = [
{
title: 'Customer Support Agent',
tags: ['GPT-4', 'Chat Interface'],
description: 'Automates customer service inquiries with intelligent escalation and context preservation.',
},
{
title: 'Document Analyst',
tags: ['Claude-3', 'PDF Processing', 'Knowledge Graph'],
description: 'Analyzes documents to extract insights and generate comprehensive reports automatically.',
},
{
title: 'Sales Qualifier',
tags: ['Lead Scoring', 'CRM Integration', 'Email Autom...'],
description: 'Qualifies prospects, schedules meetings, and manages follow-up communications with tracking.',
},
{
title: 'Customer Support Agent',
tags: ['GPT-4', 'Chat Interface'],
description: 'Automates customer service inquiries with intelligent escalation and context preservation.',
},
{
title: 'Document Analyst',
tags: ['Claude-3', 'PDF Processing', 'Knowledge Graph'],
description: 'Analyzes documents to extract insights and generate comprehensive reports automatically.',
},
{
title: 'Sales Qualifier',
tags: ['Lead Scoring', 'CRM Integration', 'Email Autom...'],
description: 'Qualifies prospects, schedules meetings, and manages follow-up communications with tracking.',
},
];
return ( return (
<div className="px-8 pt-8"> <div className="px-4 py-4 md:px-6 lg:px-10 lg:pt-6 lg:pb-10">
{/* Content Header: Welcome + Search/Create */} {/* Content Header: Welcome + Search/Create */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"> <div className="mb-4 md:mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div> <div className="space-y-2">
<h1 className="text-[22px] font-semibold text-gray-900 mb-2"> <h1 className="text-[1.75rem] md:text-[2rem] lg:text-[2.5rem] font-semibold text-[#1A1A1A] leading-tight">
Welcome, Vijay! 👋 Welcome, Vijay! 👋🏻
</h1> </h1>
<p className="text-gray-500 max-w-2xl text-[16px]"> <p className="max-w-2xl text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] text-[#5B6170]">
Monitor and manage your intelligent agents, knowledge bases, and automated workflows from a centralized dashboard Manage Agents, Workflows, Tools and Knowledge Base
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex flex-col md:flex-row items-stretch md:items-center gap-3 w-full md:w-auto">
<div className="relative group"> <div className="relative flex-1 md:flex-initial">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#9AA1B5]" />
<input <input
type="text" type="text"
placeholder="Search" placeholder="Search"
className="pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 w-64 shadow-sm transition-all" className="h-11 w-full md:w-[300px] lg:w-[340px] rounded-lg border border-[#E1E4EF] bg-white pl-10 pr-4 text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] text-[#4A4F5C] outline-none transition focus:border-[#0033FF] focus:ring-2 focus:ring-[#0033FF]/15 min-h-[44px]"
/> />
</div> </div>
<Button className="bg-[#0033FF] hover:bg-[#0042CC] text-white px-6 py-2.5 rounded-xl font-semibold flex items-center gap-2 shadow-lg shadow-blue-500/20 active:scale-95 transition-all h-auto"> <Button className="flex h-11 items-center justify-center gap-2 rounded-lg border border-[#0033FF] bg-white px-4 md:px-5 text-[0.875rem] md:text-[0.9375rem] lg:text-[1rem] font-medium text-[#0033FF] shadow-[0_0_2px_rgba(0,51,255,0.25),0_3px_8px_rgba(0,51,255,0.12)] transition hover:bg-[#F5F8FF] min-h-[44px]">
<Plus className="w-5 h-5" /> <Plus className="h-4 w-4" />
Create Agent Create Agent
</Button> </Button>
</div> </div>
</div> </div>
{/* Metrics Grid */} {/* Metrics Grid */}
<div className="bg-[#F3F8FF]/80 backdrop-blur-sm rounded-[32px] p-6 mb-8 border border-white/40 shadow-sm"> <div className="mb-4 md:mb-7 h-full bg-[rgba(247,248,255,0.82)] border border-solid border-white content-stretch flex flex-col items-start justify-center p-4 md:p-6 relative rounded-xl md:rounded-2xl lg:rounded-3xl">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="content-stretch grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 items-stretch relative shrink-0 w-full">
<MetricCard <MetricCard
title="ACTIVE AGENTS" title="ACTIVE AGENTS"
subtitle="Agents" subtitle="Agents"
value="03" value="03"
gradient="blue-teal" gradient="blue-teal"
imageUrl="/Dashboard/Card1.svg"
/> />
<MetricCard <MetricCard
title="KNOWLEDGE BASES" title="KNOWLEDGE BASES"
subtitle="Knowledge Bases" subtitle="Knowledge Bases"
value="02" value="02"
gradient="pink-purple" gradient="pink-purple"
imageUrl="/Dashboard/Card2.svg"
/> />
<MetricCard <MetricCard
title="ACTIVE WORKFLOWS" title="ACTIVE WORKFLOWS"
subtitle="Workflows" subtitle="Workflows"
value="01" value="01"
gradient="orange-yellow" gradient="orange-yellow"
imageUrl="/Dashboard/Card3.svg"
/> />
<MetricCard <MetricCard
title="CONNECTED TOOLS" title="CONNECTED TOOLS"
subtitle="Tools" subtitle="Tools"
value="01" value="01"
gradient="blue-purple" gradient="blue-purple"
imageUrl="/Dashboard/Card4.svg"
/> />
</div> </div>
</div> </div>
{/* Agents Section */} {/* Agents Section */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6"> <div className="mt-4 md:mt-6">
{/* Tabs */} <AgentsSection agents={MOCK_AGENTS} />
<div className="flex gap-4 mb-6">
<button
onClick={() => setActiveTab('all')}
className={`px-6 py-2 rounded-lg text font-medium transition-colors ${activeTab === 'all'
? 'bg-black text-white'
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
}`}
>
All Agents
</button>
<button
onClick={() => setActiveTab('my')}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${activeTab === 'my'
? 'bg-black text-white'
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
}`}
>
My Agents
</button>
</div>
{/* Agent Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{agents.map((agent, index) => (
<AgentCard
key={index}
title={agent.title}
tags={agent.tags}
description={agent.description}
/>
))}
</div>
</div> </div>
</div> </div>
); );

229
src/pages/login-page.tsx Normal file
View File

@ -0,0 +1,229 @@
/**
* Login Page Component
* @description Main authentication page with 2-column responsive layout.
* Left column displays branding/marketing content, right column contains auth form.
* Supports both Sign In and Register modes with animated transitions.
* Includes Forgot Password modal with OTP verification.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState } from 'react';
import agenticiqLogo from '@/assets/images/logo/AgenticIQLogo.svg';
import topRightGlow from '@/assets/images/backgrounds/top-right-glow.svg';
import bottomLeftWave1 from '@/assets/images/backgrounds/bottom-left-wave1.svg';
import bottomLeftWave2 from '@/assets/images/backgrounds/bottom-left-wave2.svg';
import logoGlowBg from '@/assets/images/backgrounds/logo-glow-bg.svg';
import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
/**
* Register form data interface
*/
interface RegisterFormData {
firstName: string;
lastName: string;
email: string;
password: string;
}
/**
* LoginPage component
* @description Full-page login layout with responsive 2-column grid structure.
* Mobile-first design that stacks columns on smaller screens.
* @returns {JSX.Element} LoginPage element
*/
export function LoginPage(): JSX.Element {
const [showForgotPwd, setShowForgotPwd] = useState(false);
/**
* Handle sign in form submission
* @param {string} email - User email address
* @param {string} password - User password
*/
const handleSignIn = (email: string, password: string): void => {
// TODO(AUTH-001): Implement actual sign in API call
void Promise.resolve({ email, password });
};
/**
* Handle register form submission
* @param {RegisterFormData} data - Registration form data
*/
const handleRegister = (data: RegisterFormData): void => {
// TODO(AUTH-002): Implement actual registration API call
void Promise.resolve(data);
};
/**
* Handle Google OAuth authentication
*/
const handleGoogleAuth = (): void => {
// TODO(AUTH-003): Implement Google OAuth flow
};
/**
* Handle Azure OAuth authentication
*/
const handleAzureAuth = (): void => {
// TODO(AUTH-004): Implement Azure OAuth flow
};
/**
* Open forgot password modal
*/
const handleForgotPasswordClick = (): void => {
setShowForgotPwd(true);
};
/**
* Close forgot password modal
*/
const handleForgotPasswordClose = (): void => {
setShowForgotPwd(false);
};
/**
* Handle forgot password email submission
* @param {string} email - Email for password recovery
*/
const handleForgotPasswordSubmit = (email: string): void => {
// TODO(AUTH-005): Implement password recovery API call
void Promise.resolve(email);
};
/**
* Handle OTP verification submission
* @param {string} otp - 6-digit OTP code
*/
const handleOtpVerify = (otp: string): void => {
// TODO(AUTH-006): Implement OTP verification API call
void Promise.resolve(otp);
};
return (
<div className="login-page">
<div className="relative min-h-screen w-full overflow-hidden bg-white">
{/* Decorative Background Elements */}
<BackgroundDecorations />
{/* AgenticIQ Logo */}
<div className="absolute left-4 top-4 md:left-8 lg:left-12 xl:left-[80px] md:top-8 lg:top-8 xl:top-[54px] z-20">
<img
src={agenticiqLogo}
alt="AgenticIQ Logo"
className="h-16 w-auto md:h-24 lg:h-20 xl:h-[117px]"
/>
</div>
{/* Left Side Text Content */}
<LeftColumnText />
{/* Main Content Grid */}
<div className="relative z-10 grid min-h-screen grid-cols-1 md:grid-cols-1 lg:grid-cols-[1fr_480px] xl:grid-cols-[1fr_600px] 2xl:grid-cols-[1fr_620px]">
{/* Left Column - Branding/Marketing Content */}
<LeftColumn />
{/* Right Column - Auth Form Card */}
<div className="flex items-center justify-center md:justify-center lg:justify-start px-4 py-8 md:px-6 md:py-12 lg:px-4 lg:py-12 xl:px-0 xl:py-16">
<AuthFormCard
initialMode="signin"
onSignIn={handleSignIn}
onRegister={handleRegister}
onGoogleAuth={handleGoogleAuth}
onAzureAuth={handleAzureAuth}
onForgotPassword={handleForgotPasswordClick}
/>
</div>
</div>
</div>
{/* Forgot Password Modal */}
<ForgotPasswordModal
isOpen={showForgotPwd}
onClose={handleForgotPasswordClose}
onSubmit={handleForgotPasswordSubmit}
onVerify={handleOtpVerify}
/>
</div>
);
}
/**
* BackgroundDecorations component
* @description Renders decorative background images matching Figma design.
* All images are purely decorative and hidden from screen readers.
* @returns {JSX.Element} Background decoration elements
*/
function BackgroundDecorations(): JSX.Element {
return (
<div className="pointer-events-none absolute inset-0 z-0" aria-hidden="true">
{/* Logo glow background */}
<img
src={logoGlowBg}
alt=""
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08] hidden md:block"
style={{ width: '550px', height: 'auto', maxHeight: '350px' }}
/>
{/* Top-right glow decoration */}
<img
src={topRightGlow}
alt=""
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '515px', height: 'auto', maxHeight: '614px' }}
/>
{/* Bottom-left wave decoration 1 */}
<img
src={bottomLeftWave1}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '559px', height: 'auto', maxHeight: '613px' }}
/>
{/* Bottom-left wave decoration 2 */}
<img
src={bottomLeftWave2}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
/>
</div>
);
}
/**
* LeftColumnText component
* @description Left side text content with heading and description.
* Positioned absolutely to match Figma design specifications.
* @returns {JSX.Element} Text elements positioned as per Figma
*/
function LeftColumnText(): JSX.Element {
return (
<>
<h1
className="hidden lg:block absolute text-2xl md:text-3xl lg:text-2xl xl:text-[36px] font-semibold text-black leading-tight lg:leading-tight xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-64 lg:w-80 xl:left-[110px] xl:top-[348px] xl:w-[513px]"
>
Engineering the Future with Intelligent Agents
</h1>
<p
className="hidden lg:block absolute text-sm md:text-base lg:text-sm xl:text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-relaxed lg:leading-relaxed xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-96 lg:w-80 xl:left-[110px] xl:top-[482px] xl:w-[607px]"
>
Deploy intelligent agents that automate complex workflows, enhance decision-making, and scale your operations. Built for enterprise reliability with seamless integration capabilities.
</p>
</>
);
}
/**
* LeftColumn component
* @description Empty container for left side content in the grid layout.
* Text content is positioned absolutely on the page level for precise control.
* @returns {JSX.Element} Left column container
*/
function LeftColumn(): JSX.Element {
return (
<div className="hidden md:hidden lg:flex flex-col justify-center px-6 py-12 md:px-12 lg:px-8 xl:px-20 2xl:px-28">
{/* Text content is positioned absolutely on page level */}
</div>
);
}

View File

@ -1,53 +0,0 @@
/**
* Sign In Page
* @description Basic sign-in form for existing users.
*/
import { Link } from '@tanstack/react-router';
export function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-12">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<div className="mb-6 text-center">
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-10 w-auto" />
<h1 className="mt-4 text-2xl font-semibold text-gray-900">Welcome back</h1>
<p className="mt-1 text-sm text-gray-500">Sign in to continue to your dashboard.</p>
</div>
<form className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
placeholder="you@example.com"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full rounded-lg bg-[#0033FF] px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC]"
>
Sign In
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
New here?{' '}
<Link to="/signup" className="font-semibold text-[#0033FF] hover:underline">
Create an account
</Link>
</p>
</div>
</div>
);
}

View File

@ -7,12 +7,12 @@ import { Link } from '@tanstack/react-router';
export function SignUpPage() { export function SignUpPage() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-12"> <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-8 sm:py-12">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl"> <div className="w-full max-w-md rounded-xl md:rounded-2xl bg-white p-6 sm:p-8 shadow-xl">
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-10 w-auto" /> <img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-8 w-auto sm:h-10" />
<h1 className="mt-4 text-2xl font-semibold text-gray-900">Create your account</h1> <h1 className="mt-4 text-xl sm:text-2xl font-semibold text-gray-900">Create your account</h1>
<p className="mt-1 text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p> <p className="mt-1 text-xs sm:text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
</div> </div>
<form className="space-y-4"> <form className="space-y-4">
@ -20,7 +20,7 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Full name</label> <label className="mb-1 block text-sm font-medium text-gray-700">Full name</label>
<input <input
type="text" type="text"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="Jane Doe" placeholder="Jane Doe"
/> />
</div> </div>
@ -28,7 +28,7 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label> <label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
<input <input
type="email" type="email"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="you@example.com" placeholder="you@example.com"
/> />
</div> </div>
@ -36,19 +36,19 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label> <label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
<input <input
type="password" type="password"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="••••••••" placeholder="••••••••"
/> />
</div> </div>
<button <button
type="submit" type="submit"
className="w-full rounded-lg bg-[#0033FF] px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC]" className="w-full rounded-lg bg-[#0033FF] px-4 py-3 sm:py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC] min-h-[44px]"
> >
Sign Up Sign Up
</button> </button>
</form> </form>
<p className="mt-4 text-center text-sm text-gray-600"> <p className="mt-4 text-center text-xs sm:text-sm text-gray-600">
Already have an account?{' '} Already have an account?{' '}
<Link to="/" className="font-semibold text-[#0033FF] hover:underline"> <Link to="/" className="font-semibold text-[#0033FF] hover:underline">
Sign in Sign in

View File

@ -5,9 +5,19 @@
import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router'; import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router';
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout';
import { AgentPage } from '@/pages/agent';
import { AgentCreatePage } from '@/pages/agent-create';
import { Dashboard } from '@/pages/dashboard'; import { Dashboard } from '@/pages/dashboard';
import { SignInPage } from '@/pages/sign-in';
import { SignUpPage } from '@/pages/sign-up'; import { SignUpPage } from '@/pages/sign-up';
import { LoginPage } from '@/pages/login-page';
export const APP_PATHS = {
signIn: '/',
signUp: '/signup',
dashboard: '/dashboard',
agent: '/agent',
agentCreate: '/agent/create',
} as const;
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: () => <Outlet />, component: () => <Outlet />,
@ -29,27 +39,57 @@ const appRoute = createRoute({
), ),
}); });
const signInRoute = createRoute({ const loginRoute = createRoute({
getParentRoute: () => publicRoute, getParentRoute: () => publicRoute,
path: '/', path: '/',
component: SignInPage, component: LoginPage,
}); });
const signUpRoute = createRoute({ const signUpRoute = createRoute({
getParentRoute: () => publicRoute, getParentRoute: () => publicRoute,
path: '/signup', path: APP_PATHS.signUp,
component: SignUpPage, component: SignUpPage,
}); });
const dashboardRoute = createRoute({ const dashboardRoute = createRoute({
getParentRoute: () => appRoute, getParentRoute: () => appRoute,
path: '/dashboard', path: APP_PATHS.dashboard,
component: Dashboard, component: Dashboard,
}); });
const agentRoute = createRoute({
getParentRoute: () => appRoute,
path: APP_PATHS.agent,
component: AgentPage,
});
const agentCreateRoute = createRoute({
getParentRoute: () => appRoute,
path: APP_PATHS.agentCreate,
component: AgentCreatePage,
});
const notFoundRoute = createRoute({
getParentRoute: () => rootRoute,
path: '*',
component: () => (
<div className="px-8 pt-8">
<div className="rounded-2xl border border-gray-200 bg-white/90 p-8 shadow-sm">
<h1 className="text-xl font-semibold text-gray-900">Page not found</h1>
<p className="mt-2 text-sm text-gray-600">The page you requested does not exist.</p>
</div>
</div>
),
});
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
publicRoute.addChildren([signInRoute, signUpRoute]), publicRoute.addChildren([loginRoute, signUpRoute]),
appRoute.addChildren([dashboardRoute]), appRoute.addChildren([
dashboardRoute,
agentRoute,
agentCreateRoute,
notFoundRoute,
]),
]); ]);
export const router = createRouter({ export const router = createRouter({

View File

@ -25,3 +25,10 @@ export interface ApiError {
} }
export type LoadingState = 'idle' | 'loading' | 'success' | 'error' export type LoadingState = 'idle' | 'loading' | 'success' | 'error'
export interface Tool {
id: string
name: string
type: 'API' | 'MCP'
color: string // Color for the tool name (e.g., '#ff0080' for Slack, '#8fbc24' for GitHub)
}

14
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="vite/client" />
/**
* SVG Module Declaration
* @description Allows importing SVG files as modules in TypeScript.
* Supports both default export (URL string) and ReactComponent export.
*/
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
@ -24,9 +25,7 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Path Aliases */ /* Path Aliases */
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {