diff --git a/dashboard/.env b/dashboard/.env new file mode 100644 index 0000000..b446580 --- /dev/null +++ b/dashboard/.env @@ -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 diff --git a/dashboard/CHATBOT_README.md b/dashboard/CHATBOT_README.md new file mode 100644 index 0000000..efee617 --- /dev/null +++ b/dashboard/CHATBOT_README.md @@ -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 diff --git a/dashboard/Chatbot.js b/dashboard/Chatbot.js new file mode 100644 index 0000000..eba315b --- /dev/null +++ b/dashboard/Chatbot.js @@ -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 ( +
setIsOpen(true)}> +
+ πŸ’¬ +
+ Chat Assistant +
+ ); + } + + return ( +
+
+

CityPulse Assistant

+ +
+ +
+ {messages.map((message) => ( +
+
+ {message.type === 'bot' ? 'πŸ€–' : 'πŸ‘€'} +
+
+
{message.content}
+
+ {formatTime(message.timestamp)} +
+
+
+ ))} + + {isLoading && ( +
+
πŸ€–
+
+
+
+ + + +
+
+
+
+ )} +
+
+ +
+ {quickActions.map((action, index) => ( + + ))} +
+ +
+ setInputValue(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask me anything about CityPulse..." + disabled={isLoading} + className="chatbot-input" + /> + +
+
+ ); +} + +// Export for use in other modules +window.Chatbot = Chatbot; diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..0667440 --- /dev/null +++ b/dashboard/README.md @@ -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. diff --git a/dashboard/app.js b/dashboard/app.js index 0f489c0..87b9dfc 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -603,6 +603,9 @@ const cycleStatus = async (reportId) => { ) : null}
+ {/* Chatbot */} + + diff --git a/dashboard/index.html b/dashboard/index.html index f49fa2b..5cda182 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -28,5 +28,6 @@ + \ No newline at end of file diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..1def54d --- /dev/null +++ b/dashboard/package.json @@ -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" +} diff --git a/dashboard/replace-env-vars.js b/dashboard/replace-env-vars.js new file mode 100644 index 0000000..2a6094a --- /dev/null +++ b/dashboard/replace-env-vars.js @@ -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'); diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt new file mode 100644 index 0000000..a0d011e --- /dev/null +++ b/dashboard/requirements.txt @@ -0,0 +1,3 @@ +flask==2.3.3 +flask-cors==4.0.0 +python-dotenv==1.0.0 diff --git a/dashboard/server.py b/dashboard/server.py new file mode 100644 index 0000000..edba42e --- /dev/null +++ b/dashboard/server.py @@ -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) diff --git a/dashboard/styles.css b/dashboard/styles.css index 5e66cfb..8be9914 100644 --- a/dashboard/styles.css +++ b/dashboard/styles.css @@ -437,4 +437,279 @@ button.chip[aria-pressed="true"]{ } /* accessibility tweaks */ -.chip, .btn{font-family:inherit} \ No newline at end of file +.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; + } +} \ No newline at end of file diff --git a/dashboard/test.html b/dashboard/test.html new file mode 100644 index 0000000..af15f2d --- /dev/null +++ b/dashboard/test.html @@ -0,0 +1,192 @@ + + + + + + Dashboard Test + + + +
+

FixMate Dashboard - Test Page

+ +
+

πŸ—ΊοΈ Map Initialization Test

+

Testing if the Leaflet map initializes properly without the "Map container not found" error.

+
Testing...
+
+ +
+

πŸ”§ Backend Connection Test

+

Testing connection to the Python backend server.

+
Testing...
+
+ +
+

πŸ“Š Data Loading Test

+

Testing if ticket data loads successfully from the backend.

+
Testing...
+
+ +
+

🎨 UI Components Test

+

Testing if all UI components render correctly (filters, tickets list, etc.).

+
Testing...
+
+ +
+

πŸ“± Responsive Design Test

+

Testing if the dashboard works on different screen sizes.

+
Testing...
+
+ +
+

πŸš€ Full Dashboard Preview

+

Live preview of the complete dashboard (if all tests pass).

+ +
+
+ + + +