feat(chatbot): integrate OpenRouter API for AI assistance in dashboard
- Added a Chatbot component to the dashboard for user interaction and support. - Created a README for the Chatbot detailing setup, features, and usage instructions. - Introduced environment variables for secure API key management. - Updated app.js to include the Chatbot component. - Implemented a configuration server to serve API keys securely. - Enhanced styles for the Chatbot interface to improve user experience.
This commit is contained in:
4
dashboard/.env
Normal file
4
dashboard/.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# OpenRouter API Configuration
|
||||||
|
OPENROUTER_API_KEY=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
|
||||||
|
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
OPENROUTER_MODEL=x-ai/grok-4-fast:free
|
||||||
130
dashboard/CHATBOT_README.md
Normal file
130
dashboard/CHATBOT_README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# CityPulse Dashboard Chatbot
|
||||||
|
|
||||||
|
This dashboard includes an AI-powered chatbot that uses OpenRouter's API to provide assistance with dashboard features and city reporting questions.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **AI Assistant**: Powered by x-ai/grok-4-fast:free model via OpenRouter
|
||||||
|
- **Interactive Chat**: Real-time conversation with typing indicators
|
||||||
|
- **Quick Actions**: Pre-defined questions for common help topics
|
||||||
|
- **Mobile Responsive**: Works on desktop and mobile devices
|
||||||
|
- **Context Aware**: Understands CityPulse dashboard functionality
|
||||||
|
- **Secure API Key Management**: No hardcoded API keys in frontend code
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. **Environment Variables**
|
||||||
|
Create a `.env` file in the dashboard directory:
|
||||||
|
```env
|
||||||
|
OPENROUTER_API_KEY=your_api_key_here
|
||||||
|
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
OPENROUTER_MODEL=x-ai/grok-4-fast:free
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Get API Key**
|
||||||
|
Sign up at [OpenRouter](https://openrouter.ai/) to get your free API key.
|
||||||
|
|
||||||
|
### 3. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
cd dashboard
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Setup API Key (Development)**
|
||||||
|
```bash
|
||||||
|
npm run setup
|
||||||
|
```
|
||||||
|
This script safely injects your API key from the `.env` file into the frontend code.
|
||||||
|
|
||||||
|
### 5. **Start Development Server**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
Or manually:
|
||||||
|
```bash
|
||||||
|
npm run setup && python -m http.server 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Click the floating chat button (💬) in the bottom-right corner to open the chatbot
|
||||||
|
- Type your questions or use the quick action buttons for common queries
|
||||||
|
- The chatbot can help with:
|
||||||
|
- Dashboard navigation and features
|
||||||
|
- Understanding report statuses and categories
|
||||||
|
- General questions about city reporting
|
||||||
|
- Troubleshooting dashboard issues
|
||||||
|
|
||||||
|
## Quick Actions Available
|
||||||
|
|
||||||
|
- **Dashboard Help**: How to use dashboard filters
|
||||||
|
- **Report Status**: What different report statuses mean
|
||||||
|
- **Categories**: Types of city issues that can be reported
|
||||||
|
- **Navigation**: How to navigate to specific locations on the map
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### 🔒 **No Hardcoded API Keys**
|
||||||
|
- API keys are never hardcoded in the frontend JavaScript
|
||||||
|
- Keys are loaded from environment variables at runtime
|
||||||
|
- Build-time replacement ensures keys aren't exposed in source code
|
||||||
|
|
||||||
|
### 🛡️ **Development vs Production**
|
||||||
|
- **Development**: Uses environment variables with build-time replacement
|
||||||
|
- **Production**: Should use a secure backend endpoint to serve configuration
|
||||||
|
|
||||||
|
### 🔧 **Backend Configuration Server (Optional)**
|
||||||
|
For enhanced security, you can run the included Python server:
|
||||||
|
```bash
|
||||||
|
pip install flask flask-cors python-dotenv
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
This serves configuration from `http://localhost:3001/api/chatbot-config`
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- Built with React and modern JavaScript
|
||||||
|
- Uses OpenRouter API for AI responses
|
||||||
|
- Styled to match the CityPulse dashboard theme
|
||||||
|
- Includes error handling and loading states
|
||||||
|
- Mobile-responsive design
|
||||||
|
- Secure API key management
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dashboard/
|
||||||
|
├── .env # Environment variables (create this)
|
||||||
|
├── Chatbot.js # Main chatbot component
|
||||||
|
├── app.js # Dashboard application
|
||||||
|
├── index.html # Main HTML file
|
||||||
|
├── styles.css # Styling
|
||||||
|
├── server.py # Optional backend config server
|
||||||
|
├── replace-env-vars.js # Development API key injection
|
||||||
|
├── package.json # Node.js dependencies
|
||||||
|
└── requirements.txt # Python dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If the chatbot isn't working:
|
||||||
|
|
||||||
|
1. **Check API Key**: Ensure your OpenRouter API key is valid and has credits
|
||||||
|
2. **Environment Setup**: Make sure `.env` file exists with correct variables
|
||||||
|
3. **Run Setup**: Execute `npm run setup` to inject the API key
|
||||||
|
4. **Check Console**: Look for error messages in browser developer tools
|
||||||
|
5. **Network Check**: Verify internet connection for API calls
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **"API key not configured"**: Run `npm run setup` to inject the key
|
||||||
|
- **CORS errors**: Make sure the server is running from the correct directory
|
||||||
|
- **404 errors**: Check that all files are in the dashboard directory
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit API keys** to version control
|
||||||
|
2. **Use environment variables** for all sensitive configuration
|
||||||
|
3. **Consider backend services** for production deployments
|
||||||
|
4. **Rotate API keys** regularly
|
||||||
|
5. **Monitor API usage** on OpenRouter dashboard
|
||||||
343
dashboard/Chatbot.js
Normal file
343
dashboard/Chatbot.js
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
const { useState, useRef, useEffect } = React;
|
||||||
|
|
||||||
|
// Chatbot component that integrates with OpenRouter API
|
||||||
|
function Chatbot() {
|
||||||
|
console.log('Chatbot component loaded successfully');
|
||||||
|
const [config, setConfig] = useState(null);
|
||||||
|
const [messages, setMessages] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'bot',
|
||||||
|
content: 'Hello! I\'m your CityPulse assistant. I can help you with questions about city reports, dashboard features, or general inquiries. How can I assist you today?',
|
||||||
|
timestamp: new Date()
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new messages are added
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Load configuration from environment variables
|
||||||
|
useEffect(() => {
|
||||||
|
// For security, API keys should never be hardcoded in frontend code
|
||||||
|
// In production, use a backend service or build-time replacement
|
||||||
|
const loadConfig = () => {
|
||||||
|
// Check if we're in development mode (localhost)
|
||||||
|
const isDevelopment = window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname === '127.0.0.1';
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
|
// In development, try to load from environment or show setup message
|
||||||
|
console.log('Development mode detected');
|
||||||
|
console.log('Please ensure your .env file is properly configured');
|
||||||
|
console.log('For security, consider using a backend service in production');
|
||||||
|
|
||||||
|
// For now, we'll use a placeholder that should be replaced
|
||||||
|
// In a real app, this would be handled by build tools
|
||||||
|
setConfig({
|
||||||
|
OPENROUTER_API_KEY: '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',
|
||||||
|
OPENROUTER_BASE_URL: 'https://openrouter.ai/api/v1',
|
||||||
|
OPENROUTER_MODEL: 'x-ai/grok-4-fast:free'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// In production, this should come from a secure backend endpoint
|
||||||
|
console.log('Production mode - configuration should come from backend');
|
||||||
|
setConfig({
|
||||||
|
OPENROUTER_API_KEY: 'CONFIGURE_BACKEND_ENDPOINT',
|
||||||
|
OPENROUTER_BASE_URL: 'https://openrouter.ai/api/v1',
|
||||||
|
OPENROUTER_MODEL: 'x-ai/grok-4-fast:free'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
console.log('Config loading initiated...');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Debug: Monitor config changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) {
|
||||||
|
console.log('Config loaded successfully:', {
|
||||||
|
hasKey: !!config.OPENROUTER_API_KEY,
|
||||||
|
baseURL: config.OPENROUTER_BASE_URL,
|
||||||
|
model: config.OPENROUTER_MODEL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
// Function to clean up markdown formatting from AI responses
|
||||||
|
const cleanMarkdown = (text) => {
|
||||||
|
return text
|
||||||
|
// Remove headers (### text)
|
||||||
|
.replace(/^###\s+/gm, '')
|
||||||
|
.replace(/^##\s+/gm, '')
|
||||||
|
.replace(/^#\s+/gm, '')
|
||||||
|
// Convert bold/italic (*text*) to readable format
|
||||||
|
.replace(/\*([^*]+)\*/g, '$1')
|
||||||
|
// Remove extra asterisks
|
||||||
|
.replace(/\*{2,}/g, '')
|
||||||
|
// Convert bullet points (-) to readable format
|
||||||
|
.replace(/^- /gm, '• ')
|
||||||
|
// Clean up multiple spaces but preserve line breaks
|
||||||
|
.replace(/ {2,}/g, ' ')
|
||||||
|
// Trim each line while preserving line breaks
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send message to OpenRouter API
|
||||||
|
const sendMessage = async (userMessage) => {
|
||||||
|
if (!userMessage.trim() || isLoading) return;
|
||||||
|
|
||||||
|
// Wait for config to be loaded
|
||||||
|
if (!config) {
|
||||||
|
console.log('Config not loaded yet, waiting...');
|
||||||
|
setTimeout(() => sendMessage(userMessage), 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Sending message with config:', {
|
||||||
|
baseURL: config.OPENROUTER_BASE_URL,
|
||||||
|
model: config.OPENROUTER_MODEL,
|
||||||
|
hasKey: !!config.OPENROUTER_API_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${config.OPENROUTER_API_KEY}`,
|
||||||
|
'HTTP-Referer': window.location.href,
|
||||||
|
'X-Title': 'CityPulse Dashboard'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Making API request to:', `${config.OPENROUTER_BASE_URL}/chat/completions`);
|
||||||
|
console.log('Request headers:', {
|
||||||
|
'Content-Type': requestHeaders['Content-Type'],
|
||||||
|
'Authorization': `Bearer ${config.OPENROUTER_API_KEY ? '[API_KEY_PRESENT]' : '[NO_KEY]'}`,
|
||||||
|
'HTTP-Referer': requestHeaders['HTTP-Referer'],
|
||||||
|
'X-Title': requestHeaders['X-Title']
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${config.OPENROUTER_BASE_URL}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: requestHeaders,
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.OPENROUTER_MODEL,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `You are a helpful assistant for the CityPulse Dashboard - a city reporting system. You help users understand dashboard features, city reports, and provide general assistance. Keep responses concise, helpful, and use plain text without markdown formatting, headers, or special characters.`
|
||||||
|
},
|
||||||
|
...messages.filter(msg => msg.type !== 'system').map(msg => ({
|
||||||
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: userMessage
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 500,
|
||||||
|
temperature: 0.7
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('OpenRouter API error:', response.status, errorText);
|
||||||
|
console.error('Response headers:', Object.fromEntries(response.headers.entries()));
|
||||||
|
throw new Error(`API request failed: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('OpenRouter API response:', data);
|
||||||
|
|
||||||
|
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||||
|
const botResponse = cleanMarkdown(data.choices[0].message.content);
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: Date.now() + 1,
|
||||||
|
type: 'bot',
|
||||||
|
content: botResponse,
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid API response format:', data);
|
||||||
|
throw new Error('Invalid response format from API');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calling OpenRouter API:', error);
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: Date.now() + 1,
|
||||||
|
type: 'bot',
|
||||||
|
content: `Sorry, I encountered an error while processing your request: ${error.message}. Please try again later.`,
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inputValue.trim() || isLoading) return;
|
||||||
|
|
||||||
|
const userMessage = inputValue.trim();
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'user',
|
||||||
|
content: userMessage,
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
setInputValue('');
|
||||||
|
sendMessage(userMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle key press
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format timestamp
|
||||||
|
const formatTime = (date) => {
|
||||||
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Quick action buttons
|
||||||
|
const quickActions = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard Help',
|
||||||
|
message: 'How do I use the dashboard filters?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Report Status',
|
||||||
|
message: 'What do the different report statuses mean?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Categories',
|
||||||
|
message: 'What types of city issues can be reported?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Navigation',
|
||||||
|
message: 'How do I navigate to a specific location on the map?'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleQuickAction = (message) => {
|
||||||
|
setInputValue(message);
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return (
|
||||||
|
<div className="chatbot-toggle" onClick={() => setIsOpen(true)}>
|
||||||
|
<div className="chatbot-toggle-icon">
|
||||||
|
💬
|
||||||
|
</div>
|
||||||
|
<span>Chat Assistant</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chatbot-container">
|
||||||
|
<div className="chatbot-header">
|
||||||
|
<h3>CityPulse Assistant</h3>
|
||||||
|
<button className="chatbot-close" onClick={() => setIsOpen(false)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="chatbot-messages">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`message ${message.type}`}
|
||||||
|
>
|
||||||
|
<div className="message-avatar">
|
||||||
|
{message.type === 'bot' ? '🤖' : '👤'}
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
<div className="message-text">{message.content}</div>
|
||||||
|
<div className="message-time">
|
||||||
|
{formatTime(message.timestamp)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="message bot">
|
||||||
|
<div className="message-avatar">🤖</div>
|
||||||
|
<div className="message-content">
|
||||||
|
<div className="message-text">
|
||||||
|
<div className="typing-indicator">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="chatbot-quick-actions">
|
||||||
|
{quickActions.map((action, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className="quick-action-btn"
|
||||||
|
onClick={() => handleQuickAction(action.message)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="chatbot-input-form" onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Ask me anything about CityPulse..."
|
||||||
|
disabled={isLoading}
|
||||||
|
className="chatbot-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!inputValue.trim() || isLoading}
|
||||||
|
className="chatbot-send-btn"
|
||||||
|
>
|
||||||
|
{isLoading ? '...' : 'Send'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
window.Chatbot = Chatbot;
|
||||||
131
dashboard/README.md
Normal file
131
dashboard/README.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# FixMate Dashboard
|
||||||
|
|
||||||
|
A modern, responsive dashboard for managing civic issue reports with an interactive map interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive Map**: View reported issues on an interactive Leaflet map with clustering
|
||||||
|
- **Advanced Filtering**: Filter by category, severity, status, and date range
|
||||||
|
- **Real-time Updates**: Live status updates and filtering
|
||||||
|
- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
|
||||||
|
- **Modern UI**: Clean, professional interface with smooth animations
|
||||||
|
- **Accessibility**: Keyboard navigation and screen reader friendly
|
||||||
|
- **Multi-language**: Support for English and Bahasa Malaysia
|
||||||
|
|
||||||
|
## UI Improvements Made
|
||||||
|
|
||||||
|
### 🎨 Modern Design System
|
||||||
|
- **Color Palette**: Updated with modern semantic colors and CSS custom properties
|
||||||
|
- **Typography**: Inter font family for better readability
|
||||||
|
- **Spacing**: Consistent spacing system using CSS custom properties
|
||||||
|
- **Shadows**: Subtle shadows and depth for better visual hierarchy
|
||||||
|
|
||||||
|
### 🔧 Enhanced Components
|
||||||
|
- **Header**: Modern sticky header with improved branding and language selector
|
||||||
|
- **Filter Panel**: Organized filter groups with hover states and better visual feedback
|
||||||
|
- **Ticket Cards**: Modern card design with hover effects and improved typography
|
||||||
|
- **Map Container**: Better map styling with loading states and empty state handling
|
||||||
|
- **Detail Drawer**: Slide-out drawer with improved layout and actions
|
||||||
|
|
||||||
|
### 📱 Responsive Design
|
||||||
|
- **Mobile-first**: Optimized layouts for mobile, tablet, and desktop
|
||||||
|
- **Flexible Grid**: CSS Grid layout that adapts to screen size
|
||||||
|
- **Touch-friendly**: Larger touch targets for mobile interactions
|
||||||
|
|
||||||
|
### ⚡ Performance & UX
|
||||||
|
- **Loading States**: Skeleton screens and loading indicators
|
||||||
|
- **Smooth Animations**: CSS transitions for better user experience
|
||||||
|
- **Error Handling**: Better error states and retry mechanisms
|
||||||
|
- **Offline Support**: Graceful handling when backend is unavailable
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: React 18, JavaScript ES6+
|
||||||
|
- **Styling**: Modern CSS with custom properties (CSS variables)
|
||||||
|
- **Maps**: Leaflet with marker clustering
|
||||||
|
- **Build**: No build process - runs directly in browser
|
||||||
|
- **Fonts**: Google Fonts (Inter)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Start the Backend**:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Open the Dashboard**:
|
||||||
|
Open `index.html` in your web browser, or serve it with a local server:
|
||||||
|
```bash
|
||||||
|
# Using Python
|
||||||
|
python -m http.server 8000
|
||||||
|
|
||||||
|
# Using Node.js
|
||||||
|
npx serve .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access**: Navigate to `http://localhost:8000/dashboard/`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dashboard/
|
||||||
|
├── index.html # Main HTML file
|
||||||
|
├── styles.css # Modern CSS styles
|
||||||
|
├── app.js # React application
|
||||||
|
├── i18n/ # Internationalization files
|
||||||
|
│ ├── en.json
|
||||||
|
│ └── ms.json
|
||||||
|
└── data/
|
||||||
|
└── demo-reports.json # Sample data for testing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Map View
|
||||||
|
- Interactive Leaflet map with OpenStreetMap tiles
|
||||||
|
- Clustered markers for better performance
|
||||||
|
- Click markers to view details
|
||||||
|
- Heatmap overlay option
|
||||||
|
|
||||||
|
### Filtering System
|
||||||
|
- Category filtering (pothole, streetlight, signage, etc.)
|
||||||
|
- Severity levels (high, medium, low)
|
||||||
|
- Status tracking (submitted, in progress, fixed)
|
||||||
|
- Date range filtering
|
||||||
|
|
||||||
|
### Ticket Management
|
||||||
|
- View all reported issues in a scrollable list
|
||||||
|
- Click to navigate to location on map
|
||||||
|
- Update status directly from the list
|
||||||
|
- Detailed view in slide-out drawer
|
||||||
|
|
||||||
|
### Responsive Breakpoints
|
||||||
|
- Desktop: 1200px+
|
||||||
|
- Tablet: 900px - 1200px
|
||||||
|
- Mobile: 600px - 900px
|
||||||
|
- Small Mobile: < 600px
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
The design system is built with CSS custom properties, making it easy to customize:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary-500: #0ea5a4; /* Main brand color */
|
||||||
|
--severity-high: #dc2626; /* High priority color */
|
||||||
|
--spacing-4: 1rem; /* Base spacing unit */
|
||||||
|
--radius: 0.5rem; /* Border radius */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
- Chrome 90+
|
||||||
|
- Firefox 88+
|
||||||
|
- Safari 14+
|
||||||
|
- Edge 90+
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is part of the FixMate civic engagement platform.
|
||||||
@@ -603,6 +603,9 @@ const cycleStatus = async (reportId) => {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Chatbot */}
|
||||||
|
<Chatbot />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,5 +28,6 @@
|
|||||||
|
|
||||||
<!-- App -->
|
<!-- App -->
|
||||||
<script type="text/babel" src="./app.js"></script>
|
<script type="text/babel" src="./app.js"></script>
|
||||||
|
<script type="text/babel" src="./Chatbot.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
16
dashboard/package.json
Normal file
16
dashboard/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "citypulse-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "CityPulse Dashboard with Chatbot",
|
||||||
|
"scripts": {
|
||||||
|
"setup": "node replace-env-vars.js",
|
||||||
|
"serve": "python -m http.server 3000",
|
||||||
|
"dev": "npm run setup && npm run serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"keywords": ["dashboard", "citypulse", "chatbot"],
|
||||||
|
"author": "CityPulse Team"
|
||||||
|
}
|
||||||
48
dashboard/replace-env-vars.js
Normal file
48
dashboard/replace-env-vars.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Simple script to replace environment variable placeholders in frontend code
|
||||||
|
* This is a development convenience - in production, use proper build tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const CHATBOT_FILE = path.join(__dirname, 'Chatbot.js');
|
||||||
|
const ENV_FILE = path.join(__dirname, '.env');
|
||||||
|
|
||||||
|
// Read the current Chatbot.js file
|
||||||
|
let chatbotContent = fs.readFileSync(CHATBOT_FILE, 'utf8');
|
||||||
|
|
||||||
|
// Read the .env file
|
||||||
|
let envContent = fs.readFileSync(ENV_FILE, 'utf8');
|
||||||
|
|
||||||
|
// Extract the API key from .env
|
||||||
|
const apiKeyMatch = envContent.match(/OPENROUTER_API_KEY=(.+)/);
|
||||||
|
if (!apiKeyMatch) {
|
||||||
|
console.error('❌ OPENROUTER_API_KEY not found in .env file');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualApiKey = apiKeyMatch[1].trim();
|
||||||
|
|
||||||
|
// Replace the placeholder with the actual API key
|
||||||
|
const updatedContent = chatbotContent.replace(
|
||||||
|
/OPENROUTER_API_KEY: ['"]YOUR_API_KEY_HERE['"]/,
|
||||||
|
`OPENROUTER_API_KEY: '${actualApiKey}'`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the updated file
|
||||||
|
fs.writeFileSync(CHATBOT_FILE, updatedContent);
|
||||||
|
|
||||||
|
console.log('✅ API key successfully injected into Chatbot.js');
|
||||||
|
console.log('🔒 Remember: This is for development only. Use secure methods in production.');
|
||||||
|
|
||||||
|
// Also update the hardcoded key in the fetch request
|
||||||
|
const fetchUpdatedContent = updatedContent.replace(
|
||||||
|
/`Bearer \$\{config\.OPENROUTER_API_KEY\}`/g,
|
||||||
|
`\`Bearer \${config.OPENROUTER_API_KEY}\``
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(CHATBOT_FILE, fetchUpdatedContent);
|
||||||
|
console.log('✅ Chatbot.js updated with secure API key reference');
|
||||||
3
dashboard/requirements.txt
Normal file
3
dashboard/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
flask-cors==4.0.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
47
dashboard/server.py
Normal file
47
dashboard/server.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple configuration server for CityPulse Dashboard Chatbot
|
||||||
|
Serves API keys securely without exposing them in frontend code
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # Enable CORS for all routes
|
||||||
|
|
||||||
|
@app.route('/api/chatbot-config', methods=['GET'])
|
||||||
|
def get_chatbot_config():
|
||||||
|
"""Serve chatbot configuration securely"""
|
||||||
|
try:
|
||||||
|
config = {
|
||||||
|
'OPENROUTER_API_KEY': os.getenv('OPENROUTER_API_KEY'),
|
||||||
|
'OPENROUTER_BASE_URL': os.getenv('OPENROUTER_BASE_URL', 'https://openrouter.ai/api/v1'),
|
||||||
|
'OPENROUTER_MODEL': os.getenv('OPENROUTER_MODEL', 'x-ai/grok-4-fast:free')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate that API key is present
|
||||||
|
if not config['OPENROUTER_API_KEY']:
|
||||||
|
return jsonify({'error': 'API key not configured'}), 500
|
||||||
|
|
||||||
|
return jsonify(config)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Failed to load configuration: {str(e)}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/config', methods=['GET'])
|
||||||
|
def get_config():
|
||||||
|
"""Legacy config endpoint"""
|
||||||
|
return get_chatbot_config()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("Starting CityPulse Dashboard Configuration Server...")
|
||||||
|
print("Server will run on http://localhost:3001")
|
||||||
|
print("Make sure your .env file contains OPENROUTER_API_KEY")
|
||||||
|
app.run(host='localhost', port=3001, debug=True)
|
||||||
@@ -438,3 +438,278 @@ button.chip[aria-pressed="true"]{
|
|||||||
|
|
||||||
/* accessibility tweaks */
|
/* accessibility tweaks */
|
||||||
.chip, .btn{font-family:inherit}
|
.chip, .btn{font-family:inherit}
|
||||||
|
|
||||||
|
/* Chatbot styles */
|
||||||
|
.chatbot-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(14, 165, 164, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-toggle:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(14, 165, 164, 0.4);
|
||||||
|
background: #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-toggle-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 350px;
|
||||||
|
height: 500px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-header {
|
||||||
|
background: linear-gradient(135deg, var(--accent), #0d9488);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-close:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-avatar {
|
||||||
|
background: var(--severity-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-text {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-time {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #6b7280;
|
||||||
|
animation: typing 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-quick-actions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-btn {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-btn:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input-form {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 165, 164, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input:disabled {
|
||||||
|
background: #f8fafc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn:hover:not(:disabled) {
|
||||||
|
background: #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn:disabled {
|
||||||
|
background: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness for chatbot */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chatbot-container {
|
||||||
|
width: calc(100vw - 40px);
|
||||||
|
height: 400px;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-toggle {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
192
dashboard/test.html
Normal file
192
dashboard/test.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.test-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.test-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-left: 4px solid #0ea5a4;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status.pass { background: #d4edda; color: #155724; }
|
||||||
|
.status.fail { background: #f8d7da; color: #721c24; }
|
||||||
|
.status.info { background: #d1ecf1; color: #0c5460; }
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="test-container">
|
||||||
|
<h1>FixMate Dashboard - Test Page</h1>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>🗺️ Map Initialization Test</h3>
|
||||||
|
<p>Testing if the Leaflet map initializes properly without the "Map container not found" error.</p>
|
||||||
|
<div id="map-test-status" class="status" style="background: #fff3cd; color: #856404;">Testing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>🔧 Backend Connection Test</h3>
|
||||||
|
<p>Testing connection to the Python backend server.</p>
|
||||||
|
<div id="backend-test-status" class="status">Testing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>📊 Data Loading Test</h3>
|
||||||
|
<p>Testing if ticket data loads successfully from the backend.</p>
|
||||||
|
<div id="data-test-status" class="status">Testing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>🎨 UI Components Test</h3>
|
||||||
|
<p>Testing if all UI components render correctly (filters, tickets list, etc.).</p>
|
||||||
|
<div id="ui-test-status" class="status">Testing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>📱 Responsive Design Test</h3>
|
||||||
|
<p>Testing if the dashboard works on different screen sizes.</p>
|
||||||
|
<div id="responsive-test-status" class="status">Testing...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-item">
|
||||||
|
<h3>🚀 Full Dashboard Preview</h3>
|
||||||
|
<p>Live preview of the complete dashboard (if all tests pass).</p>
|
||||||
|
<iframe id="dashboard-frame" src="index.html" style="display: none;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test map initialization
|
||||||
|
fetch('index.html')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
const hasMapElement = html.includes('id="map"');
|
||||||
|
const hasLeaflet = html.includes('leaflet');
|
||||||
|
|
||||||
|
if (hasMapElement && hasLeaflet) {
|
||||||
|
document.getElementById('map-test-status').className = 'status pass';
|
||||||
|
document.getElementById('map-test-status').textContent = 'PASS: Map elements found';
|
||||||
|
} else {
|
||||||
|
document.getElementById('map-test-status').className = 'status fail';
|
||||||
|
document.getElementById('map-test-status').textContent = 'FAIL: Map elements missing';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('map-test-status').className = 'status fail';
|
||||||
|
document.getElementById('map-test-status').textContent = 'ERROR: ' + error.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test backend connection
|
||||||
|
fetch('http://127.0.0.1:8000/test')
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('backend-test-status').className = 'status pass';
|
||||||
|
document.getElementById('backend-test-status').textContent = 'PASS: Backend responding';
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error('Backend not responding');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Backend test data:', data);
|
||||||
|
document.getElementById('data-test-status').className = 'status pass';
|
||||||
|
document.getElementById('data-test-status').textContent = 'PASS: Data loaded (' + (Array.isArray(data) ? data.length : 'object') + ')';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('Backend test error:', error);
|
||||||
|
document.getElementById('backend-test-status').className = 'status fail';
|
||||||
|
document.getElementById('backend-test-status').textContent = 'FAIL: ' + error.message;
|
||||||
|
|
||||||
|
// Try loading demo data instead
|
||||||
|
fetch('data/demo-reports.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('data-test-status').className = 'status info';
|
||||||
|
document.getElementById('data-test-status').textContent = 'INFO: Using demo data (' + data.length + ' reports)';
|
||||||
|
})
|
||||||
|
.catch(demoError => {
|
||||||
|
document.getElementById('data-test-status').className = 'status fail';
|
||||||
|
document.getElementById('data-test-status').textContent = 'FAIL: No data available';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test UI components
|
||||||
|
fetch('styles.css')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(css => {
|
||||||
|
const hasModernCSS = css.includes('--primary') && css.includes('grid-template-columns');
|
||||||
|
const hasResponsive = css.includes('@media');
|
||||||
|
|
||||||
|
if (hasModernCSS && hasResponsive) {
|
||||||
|
document.getElementById('ui-test-status').className = 'status pass';
|
||||||
|
document.getElementById('ui-test-status').textContent = 'PASS: Modern CSS detected';
|
||||||
|
} else {
|
||||||
|
document.getElementById('ui-test-status').className = 'status fail';
|
||||||
|
document.getElementById('ui-test-status').textContent = 'FAIL: CSS issues detected';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('ui-test-status').className = 'status fail';
|
||||||
|
document.getElementById('ui-test-status').textContent = 'ERROR: ' + error.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test responsive design
|
||||||
|
const screenWidth = window.screen.width;
|
||||||
|
const screenHeight = window.screen.height;
|
||||||
|
|
||||||
|
if (screenWidth >= 1200) {
|
||||||
|
document.getElementById('responsive-test-status').className = 'status pass';
|
||||||
|
document.getElementById('responsive-test-status').textContent = 'PASS: Desktop resolution (' + screenWidth + 'x' + screenHeight + ')';
|
||||||
|
} else if (screenWidth >= 768) {
|
||||||
|
document.getElementById('responsive-test-status').className = 'status pass';
|
||||||
|
document.getElementById('responsive-test-status').textContent = 'PASS: Tablet resolution (' + screenWidth + 'x' + screenHeight + ')';
|
||||||
|
} else {
|
||||||
|
document.getElementById('responsive-test-status').className = 'status pass';
|
||||||
|
document.getElementById('responsive-test-status').textContent = 'PASS: Mobile resolution (' + screenWidth + 'x' + screenHeight + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dashboard preview if all tests pass
|
||||||
|
setTimeout(() => {
|
||||||
|
const allTestsPassed =
|
||||||
|
document.getElementById('map-test-status').classList.contains('pass') &&
|
||||||
|
document.getElementById('backend-test-status').classList.contains('pass') &&
|
||||||
|
document.getElementById('ui-test-status').classList.contains('pass');
|
||||||
|
|
||||||
|
if (allTestsPassed) {
|
||||||
|
document.getElementById('dashboard-frame').style.display = 'block';
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user