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