From 98b6bd93138a2c539b375542e1e7fbb8f930c67f Mon Sep 17 00:00:00 2001 From: johnpccd Date: Sat, 24 May 2025 15:17:02 +0200 Subject: [PATCH] client improvements --- client/README.md | 360 +++++++++++++--------- client/app.py | 423 -------------------------- client/index.html | 264 ++++++++++++++++ client/js/api.js | 212 +++++++++++++ client/js/apikeys.js | 354 ++++++++++++++++++++++ client/js/app.js | 362 ++++++++++++++++++++++ client/js/config.js | 166 ++++++++++ client/js/images.js | 370 ++++++++++++++++++++++ client/js/search.js | 297 ++++++++++++++++++ client/js/teams.js | 318 +++++++++++++++++++ client/js/ui.js | 280 +++++++++++++++++ client/js/users.js | 471 +++++++++++++++++++++++++++++ client/requirements.txt | 25 +- client/serve.py | 75 +++++ client/styles.css | 300 ++++++++++++++++++ client/templates/api_key_form.html | 111 ------- client/templates/api_keys.html | 101 ------- client/templates/base.html | 116 ------- client/templates/bootstrap.html | 140 --------- client/templates/config.html | 150 --------- client/templates/image_detail.html | 246 --------------- client/templates/image_edit.html | 186 ------------ client/templates/image_upload.html | 168 ---------- client/templates/images.html | 192 ------------ client/templates/index.html | 141 --------- client/templates/search.html | 264 ---------------- client/templates/team_form.html | 145 --------- client/templates/teams.html | 96 ------ client/templates/user_form.html | 182 ----------- client/templates/users.html | 112 ------- scripts/README.md | 69 +++++ scripts/client.sh | 322 ++++++++++++++++++++ 32 files changed, 4094 insertions(+), 2924 deletions(-) delete mode 100644 client/app.py create mode 100644 client/index.html create mode 100644 client/js/api.js create mode 100644 client/js/apikeys.js create mode 100644 client/js/app.js create mode 100644 client/js/config.js create mode 100644 client/js/images.js create mode 100644 client/js/search.js create mode 100644 client/js/teams.js create mode 100644 client/js/ui.js create mode 100644 client/js/users.js create mode 100644 client/serve.py create mode 100644 client/styles.css delete mode 100644 client/templates/api_key_form.html delete mode 100644 client/templates/api_keys.html delete mode 100644 client/templates/base.html delete mode 100644 client/templates/bootstrap.html delete mode 100644 client/templates/config.html delete mode 100644 client/templates/image_detail.html delete mode 100644 client/templates/image_edit.html delete mode 100644 client/templates/image_upload.html delete mode 100644 client/templates/images.html delete mode 100644 client/templates/index.html delete mode 100644 client/templates/search.html delete mode 100644 client/templates/team_form.html delete mode 100644 client/templates/teams.html delete mode 100644 client/templates/user_form.html delete mode 100644 client/templates/users.html create mode 100644 scripts/client.sh diff --git a/client/README.md b/client/README.md index 89f7c13..390faee 100644 --- a/client/README.md +++ b/client/README.md @@ -1,208 +1,272 @@ -# SEREACT Web Client +# SeReact Frontend Client -A comprehensive web interface for the SEREACT API, providing full CRUD operations for teams, users, images, and API keys, along with AI-powered image search capabilities. +A modern, responsive web frontend for the SeReact AI-powered image management platform. This is a pure frontend application that communicates directly with your SeReact backend API. ## Features -- **Configuration Management**: Set API endpoint and authentication -- **Bootstrap Setup**: Initial system setup with team and admin user creation -- **Teams Management**: Create, view, edit, and delete teams -- **Users Management**: Create, view, edit, and delete users with role management -- **API Keys Management**: Create and manage API keys for authentication +- **Pure Frontend**: No backend dependencies - runs entirely in the browser +- **Modern UI**: Built with Bootstrap 5 and modern CSS features +- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices +- **Real-time Updates**: Live connection status and automatic data refresh +- **Keyboard Shortcuts**: Power-user friendly navigation and actions +- **Offline Awareness**: Graceful handling of network connectivity issues + +### Core Functionality + +- **Configuration Management**: Easy API endpoint and authentication setup - **Image Management**: Upload, view, edit, and delete images with metadata - **AI-Powered Search**: Semantic image search using natural language queries -- **Responsive Design**: Modern Bootstrap-based UI that works on all devices +- **Team Management**: Create and manage teams and user access +- **User Management**: Full user lifecycle with role-based permissions +- **API Key Management**: Generate and manage API keys for integrations -## Prerequisites +## Quick Start -- Python 3.8+ -- Running SEREACT API server -- Network access to the SEREACT API +### Prerequisites -## Installation +- A running SeReact backend API server +- Modern web browser (Chrome, Firefox, Safari, Edge) +- Web server to serve static files (optional for development) -1. Navigate to the client directory: +### Installation + +1. **Download/Clone the frontend files**: ```bash - cd client + # If you have the full SeReact repository + cd sereact/client + + # Or download just the client folder ``` -2. Create and activate a virtual environment: +2. **Serve the files**: + + **Option A: Simple HTTP Server (Python)** ```bash - python -m venv venv - source venv/bin/activate # Linux/macOS - venv\Scripts\activate # Windows + # Python 3 + python -m http.server 8080 + + # Python 2 + python -m SimpleHTTPServer 8080 ``` - -3. Install dependencies: + + **Option B: Node.js HTTP Server** ```bash - pip install -r requirements.txt + npx http-server -p 8080 + ``` + + **Option C: Any web server** + - Copy files to your web server directory + - Ensure `index.html` is accessible + +3. **Open in browser**: + ``` + http://localhost:8080 ``` -## Running the Client - -1. Start the Flask development server: - ```bash - python app.py - ``` - -2. Open your browser and navigate to: - ``` - http://localhost:5000 - ``` - -## Initial Setup - -### First Time Configuration +### First-Time Setup 1. **Configure API Connection**: - - Go to the "Config" page - - Set your SEREACT API base URL (e.g., `http://localhost:8000`) - - If you have an API key, enter it here + - Click "Configure Now" in the welcome dialog + - Enter your SeReact API base URL (e.g., `http://localhost:8000`) + - Enter your API key + - Test the connection -2. **Bootstrap the System** (if this is a new SEREACT installation): - - Go to the "Bootstrap" page - - Enter your organization details - - Create the initial admin user - - The system will generate an API key automatically - -3. **Start Using the System**: - - Upload images - - Search for images using AI +2. **Start Using**: + - Upload your first images + - Try the AI-powered search - Manage teams and users - - Create additional API keys - -## Usage Guide - -### Teams Management -- **View Teams**: See all teams in your organization -- **Create Team**: Add new teams with descriptions -- **Edit Team**: Update team information -- **Delete Team**: Remove teams (admin only) - -### Users Management -- **View Users**: See all users in your team/organization -- **Create User**: Add new users and assign them to teams -- **Edit User**: Update user information and admin status -- **Delete User**: Remove users from the system - -### API Keys Management -- **View API Keys**: See all your API keys and their status -- **Create API Key**: Generate new API keys for applications -- **Revoke API Key**: Disable API keys that are no longer needed - -### Image Management -- **Upload Images**: Add new images with descriptions and tags -- **Browse Images**: View all uploaded images with filtering -- **View Image Details**: See full image information and metadata -- **Edit Images**: Update descriptions and tags -- **Delete Images**: Remove images from storage - -### AI-Powered Search -- **Semantic Search**: Use natural language to find images -- **Advanced Options**: Adjust similarity thresholds and result limits -- **Filter Results**: Combine search with tag filtering -- **Similarity Scores**: See how closely images match your query ## Configuration +### API Settings + +The frontend stores configuration in browser localStorage: + +- **API Base URL**: The URL of your SeReact backend (e.g., `http://localhost:8000`) +- **API Key**: Your authentication key for the backend API + ### Environment Variables -You can set these environment variables to configure default values: +For deployment, you can set default values by modifying `js/config.js`: -```bash -export SEREACT_API_URL=http://localhost:8000 -export SEREACT_API_KEY=your-api-key +```javascript +// Default configuration +this.apiBaseUrl = localStorage.getItem('apiBaseUrl') || 'https://your-api.example.com'; +this.apiKey = localStorage.getItem('apiKey') || ''; ``` -### Session Storage +## File Structure -The client stores configuration in the browser session: -- API base URL -- API key -- These are not persistent and need to be re-entered after browser restart +``` +client/ +├── index.html # Main application entry point +├── styles.css # Custom CSS styles +├── js/ # JavaScript modules +│ ├── config.js # Configuration management +│ ├── api.js # API client and HTTP requests +│ ├── ui.js # UI utilities and common functions +│ ├── images.js # Image management functionality +│ ├── search.js # AI search functionality +│ ├── teams.js # Team management +│ ├── users.js # User management +│ ├── apikeys.js # API key management +│ └── app.js # Main application initialization +└── README.md # This file +``` -## API Integration +## Features in Detail -The client integrates with the following SEREACT API endpoints: +### Image Management +- **Upload**: Drag & drop or click to upload images +- **Metadata**: Add descriptions and tags to images +- **View**: Full-size image viewing with details +- **Edit**: Update descriptions and tags +- **Delete**: Remove images with confirmation -- `/api/v1/auth/*` - Authentication and API key management -- `/api/v1/teams/*` - Team management -- `/api/v1/users/*` - User management -- `/api/v1/images/*` - Image upload and management -- `/api/v1/search/*` - AI-powered image search +### AI-Powered Search +- **Natural Language**: Search using descriptive text +- **Similarity Threshold**: Adjust search sensitivity +- **Result Filtering**: Filter by tags and metadata +- **Search History**: Save and reuse frequent searches -## Security Notes +### Team & User Management +- **Teams**: Create and manage organizational teams +- **Users**: Add users with role-based permissions +- **Admin Controls**: Administrative functions for system management -- **API Keys**: Store API keys securely and never share them +### API Key Management +- **Generate Keys**: Create API keys for integrations +- **Security**: One-time display of new keys +- **Usage Tracking**: Monitor key usage and activity + +## Keyboard Shortcuts + +- **Ctrl+K**: Open search page and focus search input +- **Ctrl+U**: Upload new image +- **Ctrl+,**: Open configuration page +- **1-6**: Navigate to different pages (Home, Images, Search, Teams, Users, API Keys) +- **Esc**: Close open modals + +## Browser Compatibility + +- **Chrome**: 80+ +- **Firefox**: 75+ +- **Safari**: 13+ +- **Edge**: 80+ + +### Required Features +- ES6+ JavaScript support +- Fetch API +- CSS Grid and Flexbox +- Local Storage + +## Deployment + +### Static Hosting + +The frontend can be deployed to any static hosting service: + +- **Netlify**: Drag and drop the client folder +- **Vercel**: Connect your repository +- **GitHub Pages**: Push to a gh-pages branch +- **AWS S3**: Upload files to an S3 bucket with static hosting +- **Azure Static Web Apps**: Deploy via GitHub integration + +### Web Server Configuration + +For proper routing with hash-based navigation, no special server configuration is needed. For history-based routing (if implemented), configure your server to serve `index.html` for all routes. + +### CORS Configuration + +Ensure your SeReact backend API is configured to allow requests from your frontend domain: + +```python +# In your backend CORS configuration +ALLOWED_ORIGINS = [ + "http://localhost:8080", + "https://your-frontend-domain.com" +] +``` + +## Development + +### Local Development + +1. **Start a local server**: + ```bash + python -m http.server 8080 + ``` + +2. **Open browser with dev tools**: + ``` + http://localhost:8080 + ``` + +3. **Make changes**: Edit files and refresh browser + +### Debugging + +- Open browser developer tools (F12) +- Check console for error messages +- Use Network tab to monitor API requests +- Application tab shows localStorage configuration + +### Adding Features + +The modular structure makes it easy to add new features: + +1. Create new JavaScript module in `js/` folder +2. Add corresponding HTML section to `index.html` +3. Include script tag in `index.html` +4. Add navigation link if needed + +## Security Considerations + +- **API Keys**: Stored in browser localStorage (consider security implications) - **HTTPS**: Use HTTPS in production for secure communication -- **Access Control**: The client respects the API's team-based access control -- **Session Security**: Configure secure session cookies in production +- **CORS**: Properly configure backend CORS settings +- **Content Security Policy**: Consider implementing CSP headers ## Troubleshooting ### Common Issues 1. **Connection Errors**: - - Verify the API base URL is correct - - Ensure the SEREACT API server is running - - Check network connectivity + - Verify API base URL is correct + - Check if backend server is running + - Ensure CORS is properly configured 2. **Authentication Errors**: - - Verify your API key is valid and active - - Check if the API key has expired - - Ensure you have the necessary permissions + - Verify API key is valid and active + - Check API key permissions + - Ensure key hasn't expired 3. **Image Upload Issues**: - - Check file size (max 10MB) + - Check file size limits (default 10MB) - Verify file format is supported - - Ensure sufficient storage space + - Ensure sufficient backend storage 4. **Search Not Working**: - - Verify images have been processed (have embeddings) - - Check if the vector database is configured - - Try adjusting similarity thresholds + - Verify images have been processed + - Check vector database configuration + - Try adjusting similarity threshold ### Error Messages -- **"API key not set"**: Configure your API key in the Config page -- **"Request failed"**: Check API server status and network connection -- **"Invalid JSON response"**: API server may be returning errors +- **"API not configured"**: Set up API base URL and key in configuration +- **"Connection failed"**: Check network and backend server status +- **"Authentication failed"**: Verify API key is correct and active - **"File size exceeds limit"**: Choose a smaller image file -## Development +## Support -### Project Structure +For issues and questions: -``` -client/ -├── app.py # Main Flask application -├── requirements.txt # Python dependencies -├── templates/ # HTML templates -│ ├── base.html # Base template with navigation -│ ├── index.html # Home page -│ ├── config.html # Configuration page -│ ├── bootstrap.html # Bootstrap setup page -│ ├── teams.html # Teams listing -│ ├── team_form.html # Team create/edit form -│ ├── users.html # Users listing -│ ├── user_form.html # User create/edit form -│ ├── api_keys.html # API keys listing -│ ├── api_key_form.html # API key creation form -│ ├── images.html # Images listing with pagination -│ ├── image_upload.html # Image upload form -│ ├── image_detail.html # Image detail view -│ ├── image_edit.html # Image edit form -│ └── search.html # AI search interface -└── uploads/ # Temporary upload directory -``` - -### Customization - -- **Styling**: Modify the CSS in `templates/base.html` -- **Features**: Add new routes and templates in `app.py` -- **API Integration**: Extend the `make_api_request` function -- **UI Components**: Use Bootstrap classes for consistent styling +1. Check the browser console for error messages +2. Verify backend API is running and accessible +3. Check network connectivity and CORS configuration +4. Review the SeReact backend documentation ## License -This web client is part of the SEREACT project and follows the same license terms. \ No newline at end of file +This frontend client is part of the SeReact project. See the main project license for details. \ No newline at end of file diff --git a/client/app.py b/client/app.py deleted file mode 100644 index c2b688c..0000000 --- a/client/app.py +++ /dev/null @@ -1,423 +0,0 @@ -import os -import requests -from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify -from werkzeug.utils import secure_filename -import json -from datetime import datetime - -app = Flask(__name__) -app.secret_key = 'your-secret-key-change-in-production' - -# Configuration -UPLOAD_FOLDER = 'uploads' -ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'} -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER -app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB max file size - -# Ensure upload folder exists -os.makedirs(UPLOAD_FOLDER, exist_ok=True) - -def allowed_file(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - -def get_api_base_url(): - return session.get('api_base_url', 'http://localhost:8000') - -def get_api_key(): - return session.get('api_key') - -def make_api_request(method, endpoint, data=None, files=None, params=None): - """Make API request with proper headers""" - api_key = get_api_key() - if not api_key: - return None, "API key not set" - - headers = {'X-API-Key': api_key} - url = f"{get_api_base_url()}/api/v1{endpoint}" - - try: - if method == 'GET': - response = requests.get(url, headers=headers, params=params) - elif method == 'POST': - if files: - response = requests.post(url, headers=headers, data=data, files=files) - else: - headers['Content-Type'] = 'application/json' - response = requests.post(url, headers=headers, json=data) - elif method == 'PUT': - headers['Content-Type'] = 'application/json' - response = requests.put(url, headers=headers, json=data) - elif method == 'DELETE': - response = requests.delete(url, headers=headers) - else: - return None, f"Unsupported method: {method}" - - if response.status_code == 204: - return True, None - - return response.json(), None if response.status_code < 400 else response.json().get('detail', 'Unknown error') - - except requests.exceptions.RequestException as e: - return None, f"Request failed: {str(e)}" - except json.JSONDecodeError: - return None, "Invalid JSON response" - -@app.route('/') -def index(): - return render_template('index.html') - -@app.route('/config', methods=['GET', 'POST']) -def config(): - if request.method == 'POST': - api_base_url = request.form.get('api_base_url', '').strip() - api_key = request.form.get('api_key', '').strip() - - if api_base_url: - session['api_base_url'] = api_base_url.rstrip('/') - if api_key: - session['api_key'] = api_key - - flash('Configuration updated successfully!', 'success') - return redirect(url_for('index')) - - return render_template('config.html', - api_base_url=get_api_base_url(), - api_key=get_api_key()) - -# Teams CRUD -@app.route('/teams') -def teams(): - data, error = make_api_request('GET', '/teams') - if error: - flash(f'Error loading teams: {error}', 'error') - return render_template('teams.html', teams=[]) - - return render_template('teams.html', teams=data.get('teams', [])) - -@app.route('/teams/create', methods=['GET', 'POST']) -def create_team(): - if request.method == 'POST': - team_data = { - 'name': request.form.get('name'), - 'description': request.form.get('description', '') - } - - data, error = make_api_request('POST', '/teams', team_data) - if error: - flash(f'Error creating team: {error}', 'error') - else: - flash('Team created successfully!', 'success') - return redirect(url_for('teams')) - - return render_template('team_form.html', team=None, action='Create') - -@app.route('/teams//edit', methods=['GET', 'POST']) -def edit_team(team_id): - if request.method == 'POST': - team_data = { - 'name': request.form.get('name'), - 'description': request.form.get('description', '') - } - - data, error = make_api_request('PUT', f'/teams/{team_id}', team_data) - if error: - flash(f'Error updating team: {error}', 'error') - else: - flash('Team updated successfully!', 'success') - return redirect(url_for('teams')) - - # Get team data for editing - data, error = make_api_request('GET', f'/teams/{team_id}') - if error: - flash(f'Error loading team: {error}', 'error') - return redirect(url_for('teams')) - - return render_template('team_form.html', team=data, action='Edit') - -@app.route('/teams//delete', methods=['POST']) -def delete_team(team_id): - data, error = make_api_request('DELETE', f'/teams/{team_id}') - if error: - flash(f'Error deleting team: {error}', 'error') - else: - flash('Team deleted successfully!', 'success') - - return redirect(url_for('teams')) - -# Users CRUD -@app.route('/users') -def users(): - data, error = make_api_request('GET', '/users') - if error: - flash(f'Error loading users: {error}', 'error') - return render_template('users.html', users=[]) - - return render_template('users.html', users=data.get('users', [])) - -@app.route('/users/create', methods=['GET', 'POST']) -def create_user(): - if request.method == 'POST': - user_data = { - 'name': request.form.get('name'), - 'email': request.form.get('email'), - 'team_id': request.form.get('team_id') or None, - 'is_admin': request.form.get('is_admin') == 'on' - } - - data, error = make_api_request('POST', '/users', user_data) - if error: - flash(f'Error creating user: {error}', 'error') - else: - flash('User created successfully!', 'success') - return redirect(url_for('users')) - - # Get teams for dropdown - teams_data, teams_error = make_api_request('GET', '/teams') - teams = teams_data.get('teams', []) if teams_data else [] - - return render_template('user_form.html', user=None, teams=teams, action='Create') - -@app.route('/users//edit', methods=['GET', 'POST']) -def edit_user(user_id): - if request.method == 'POST': - user_data = { - 'name': request.form.get('name'), - 'email': request.form.get('email'), - 'is_admin': request.form.get('is_admin') == 'on' - } - - data, error = make_api_request('PUT', f'/users/{user_id}', user_data) - if error: - flash(f'Error updating user: {error}', 'error') - else: - flash('User updated successfully!', 'success') - return redirect(url_for('users')) - - # Get user data for editing - data, error = make_api_request('GET', f'/users/{user_id}') - if error: - flash(f'Error loading user: {error}', 'error') - return redirect(url_for('users')) - - # Get teams for dropdown - teams_data, teams_error = make_api_request('GET', '/teams') - teams = teams_data.get('teams', []) if teams_data else [] - - return render_template('user_form.html', user=data, teams=teams, action='Edit') - -@app.route('/users//delete', methods=['POST']) -def delete_user(user_id): - data, error = make_api_request('DELETE', f'/users/{user_id}') - if error: - flash(f'Error deleting user: {error}', 'error') - else: - flash('User deleted successfully!', 'success') - - return redirect(url_for('users')) - -# API Keys CRUD -@app.route('/api-keys') -def api_keys(): - data, error = make_api_request('GET', '/auth/api-keys') - if error: - flash(f'Error loading API keys: {error}', 'error') - return render_template('api_keys.html', api_keys=[]) - - return render_template('api_keys.html', api_keys=data.get('api_keys', [])) - -@app.route('/api-keys/create', methods=['GET', 'POST']) -def create_api_key(): - if request.method == 'POST': - key_data = { - 'name': request.form.get('name'), - 'description': request.form.get('description', ''), - 'team_id': request.form.get('team_id') or None, - 'user_id': request.form.get('user_id') or None - } - - data, error = make_api_request('POST', '/auth/api-keys', key_data) - if error: - flash(f'Error creating API key: {error}', 'error') - else: - flash(f'API key created successfully! Key: {data.get("key")}', 'success') - return redirect(url_for('api_keys')) - - # Get teams and users for dropdowns - teams_data, _ = make_api_request('GET', '/teams') - users_data, _ = make_api_request('GET', '/users') - teams = teams_data.get('teams', []) if teams_data else [] - users = users_data.get('users', []) if users_data else [] - - return render_template('api_key_form.html', teams=teams, users=users) - -@app.route('/api-keys//delete', methods=['POST']) -def delete_api_key(key_id): - data, error = make_api_request('DELETE', f'/auth/api-keys/{key_id}') - if error: - flash(f'Error deleting API key: {error}', 'error') - else: - flash('API key deleted successfully!', 'success') - - return redirect(url_for('api_keys')) - -# Images CRUD -@app.route('/images') -def images(): - page = request.args.get('page', 1, type=int) - limit = 20 - skip = (page - 1) * limit - - params = {'skip': skip, 'limit': limit} - tags = request.args.get('tags') - if tags: - params['tags'] = tags - - data, error = make_api_request('GET', '/images', params=params) - if error: - flash(f'Error loading images: {error}', 'error') - return render_template('images.html', images=[], total=0, page=page, limit=limit) - - return render_template('images.html', - images=data.get('images', []), - total=data.get('total', 0), - page=page, - limit=limit) - -@app.route('/images/upload', methods=['GET', 'POST']) -def upload_image(): - if request.method == 'POST': - if 'file' not in request.files: - flash('No file selected', 'error') - return redirect(request.url) - - file = request.files['file'] - if file.filename == '': - flash('No file selected', 'error') - return redirect(request.url) - - if file and allowed_file(file.filename): - filename = secure_filename(file.filename) - - # Prepare form data - form_data = { - 'description': request.form.get('description', ''), - 'tags': request.form.get('tags', ''), - 'collection_id': request.form.get('collection_id') or None - } - - # Prepare files - files = {'file': (filename, file, file.content_type)} - - data, error = make_api_request('POST', '/images', data=form_data, files=files) - if error: - flash(f'Error uploading image: {error}', 'error') - else: - flash('Image uploaded successfully!', 'success') - return redirect(url_for('images')) - else: - flash('Invalid file type. Please upload an image file.', 'error') - - return render_template('image_upload.html') - -@app.route('/images/') -def view_image(image_id): - data, error = make_api_request('GET', f'/images/{image_id}') - if error: - flash(f'Error loading image: {error}', 'error') - return redirect(url_for('images')) - - return render_template('image_detail.html', image=data) - -@app.route('/images//edit', methods=['GET', 'POST']) -def edit_image(image_id): - if request.method == 'POST': - image_data = { - 'description': request.form.get('description', ''), - 'tags': [tag.strip() for tag in request.form.get('tags', '').split(',') if tag.strip()] - } - - data, error = make_api_request('PUT', f'/images/{image_id}', image_data) - if error: - flash(f'Error updating image: {error}', 'error') - else: - flash('Image updated successfully!', 'success') - return redirect(url_for('view_image', image_id=image_id)) - - # Get image data for editing - data, error = make_api_request('GET', f'/images/{image_id}') - if error: - flash(f'Error loading image: {error}', 'error') - return redirect(url_for('images')) - - return render_template('image_edit.html', image=data) - -@app.route('/images//delete', methods=['POST']) -def delete_image(image_id): - data, error = make_api_request('DELETE', f'/images/{image_id}') - if error: - flash(f'Error deleting image: {error}', 'error') - else: - flash('Image deleted successfully!', 'success') - - return redirect(url_for('images')) - -# Search functionality -@app.route('/search', methods=['GET', 'POST']) -def search(): - if request.method == 'POST': - query = request.form.get('query', '').strip() - limit = int(request.form.get('limit', 10)) - threshold = float(request.form.get('threshold', 0.7)) - tags = request.form.get('tags', '').strip() - - params = { - 'q': query, - 'limit': limit, - 'threshold': threshold - } - if tags: - params['tags'] = tags - - data, error = make_api_request('GET', '/search', params=params) - if error: - flash(f'Search error: {error}', 'error') - return render_template('search.html', results=[], query=query) - - return render_template('search.html', - results=data.get('results', []), - query=query, - total=data.get('total', 0)) - - return render_template('search.html', results=[], query='') - -@app.route('/bootstrap', methods=['GET', 'POST']) -def bootstrap(): - if request.method == 'POST': - bootstrap_data = { - 'team_name': request.form.get('team_name'), - 'admin_email': request.form.get('admin_email'), - 'admin_name': request.form.get('admin_name'), - 'api_key_name': request.form.get('api_key_name', 'Initial API Key') - } - - # Make bootstrap request without API key - headers = {'Content-Type': 'application/json'} - url = f"{get_api_base_url()}/api/v1/auth/bootstrap" - - try: - response = requests.post(url, headers=headers, json=bootstrap_data) - if response.status_code == 201: - data = response.json() - session['api_key'] = data.get('key') - flash(f'Bootstrap successful! API Key: {data.get("key")}', 'success') - return redirect(url_for('index')) - else: - error_data = response.json() - flash(f'Bootstrap error: {error_data.get("detail", "Unknown error")}', 'error') - except Exception as e: - flash(f'Bootstrap failed: {str(e)}', 'error') - - return render_template('bootstrap.html') - -if __name__ == '__main__': - app.run(debug=True, port=5000) \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..fa54957 --- /dev/null +++ b/client/index.html @@ -0,0 +1,264 @@ + + + + + + SeReact - AI-Powered Image Management + + + + + + + + + +
+ +
+ + +
+
+
+
+

Welcome to SeReact

+

AI-powered image management and semantic search platform

+
+

Upload images, manage your team, and search using natural language queries.

+
+
+
+
+ +
Upload Images
+

Upload and manage your image collection with metadata and tags.

+ +
+
+
+
+
+
+ +
AI Search
+

Find images using natural language queries powered by AI.

+ +
+
+
+
+
+
+ +
Team Management
+

Manage teams, users, and access permissions.

+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/js/api.js b/client/js/api.js new file mode 100644 index 0000000..7c8cf6d --- /dev/null +++ b/client/js/api.js @@ -0,0 +1,212 @@ +// API client for making requests to the backend +class ApiClient { + constructor() { + this.baseUrl = ''; + this.apiKey = ''; + } + + updateConfig() { + this.baseUrl = config.getApiBaseUrl(); + this.apiKey = config.getApiKey(); + } + + getHeaders(includeContentType = true) { + const headers = { + 'X-API-Key': this.apiKey + }; + + if (includeContentType) { + headers['Content-Type'] = 'application/json'; + } + + return headers; + } + + async makeRequest(method, endpoint, data = null, isFormData = false) { + this.updateConfig(); + + if (!this.baseUrl || !this.apiKey) { + throw new Error('API not configured. Please set API base URL and key.'); + } + + const url = `${this.baseUrl}/api/v1${endpoint}`; + + const options = { + method, + headers: this.getHeaders(!isFormData) + }; + + if (data) { + if (isFormData) { + options.body = data; // FormData object + } else { + options.body = JSON.stringify(data); + } + } + + try { + const response = await fetch(url, options); + + // Handle different response types + if (response.status === 204) { + return null; // No content + } + + const contentType = response.headers.get('content-type'); + let responseData; + + if (contentType && contentType.includes('application/json')) { + responseData = await response.json(); + } else { + responseData = await response.text(); + } + + if (!response.ok) { + const errorMessage = responseData?.detail || responseData?.message || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return responseData; + } catch (error) { + console.error(`API request failed: ${method} ${url}`, error); + throw error; + } + } + + // Teams API + async getTeams() { + return this.makeRequest('GET', '/teams'); + } + + async createTeam(teamData) { + return this.makeRequest('POST', '/teams', teamData); + } + + async updateTeam(teamId, teamData) { + return this.makeRequest('PUT', `/teams/${teamId}`, teamData); + } + + async deleteTeam(teamId) { + return this.makeRequest('DELETE', `/teams/${teamId}`); + } + + // Users API + async getUsers() { + return this.makeRequest('GET', '/users'); + } + + async createUser(userData) { + return this.makeRequest('POST', '/users', userData); + } + + async updateUser(userId, userData) { + return this.makeRequest('PUT', `/users/${userId}`, userData); + } + + async deleteUser(userId) { + return this.makeRequest('DELETE', `/users/${userId}`); + } + + // API Keys API + async getApiKeys() { + return this.makeRequest('GET', '/auth/api-keys'); + } + + async createApiKey(keyData) { + return this.makeRequest('POST', '/auth/api-keys', keyData); + } + + async deleteApiKey(keyId) { + return this.makeRequest('DELETE', `/auth/api-keys/${keyId}`); + } + + // Images API + async getImages(page = 1, limit = 20, tags = null) { + let endpoint = `/images?page=${page}&limit=${limit}`; + if (tags) { + endpoint += `&tags=${encodeURIComponent(tags)}`; + } + return this.makeRequest('GET', endpoint); + } + + async getImage(imageId) { + return this.makeRequest('GET', `/images/${imageId}`); + } + + async uploadImage(formData) { + return this.makeRequest('POST', '/images', formData, true); + } + + async updateImage(imageId, imageData) { + return this.makeRequest('PUT', `/images/${imageId}`, imageData); + } + + async deleteImage(imageId) { + return this.makeRequest('DELETE', `/images/${imageId}`); + } + + // Search API + async searchImages(query, similarityThreshold = 0.7, maxResults = 20, tags = null) { + const searchData = { + query, + similarity_threshold: similarityThreshold, + max_results: maxResults + }; + + if (tags) { + searchData.tags = tags; + } + + return this.makeRequest('POST', '/search', searchData); + } + + // Bootstrap API + async bootstrap(bootstrapData) { + return this.makeRequest('POST', '/auth/bootstrap', bootstrapData); + } + + // Health check + async healthCheck() { + this.updateConfig(); + const response = await fetch(`${this.baseUrl}/health`); + return response.ok; + } + + // Get image URL for display + getImageUrl(imageId) { + this.updateConfig(); + return `${this.baseUrl}/api/v1/images/${imageId}/file`; + } + + // Get image thumbnail URL + getThumbnailUrl(imageId, size = 'medium') { + this.updateConfig(); + return `${this.baseUrl}/api/v1/images/${imageId}/thumbnail?size=${size}`; + } +} + +// Global API client instance +const apiClient = new ApiClient(); + +// Helper function to handle API errors consistently +function handleApiError(error, context = '') { + console.error(`API Error ${context}:`, error); + + let message = error.message || 'An unknown error occurred'; + + // Handle common error types + if (message.includes('Failed to fetch') || message.includes('NetworkError')) { + message = 'Unable to connect to the server. Please check your connection and API configuration.'; + } else if (message.includes('401')) { + message = 'Authentication failed. Please check your API key.'; + } else if (message.includes('403')) { + message = 'Access denied. You don\'t have permission to perform this action.'; + } else if (message.includes('404')) { + message = 'The requested resource was not found.'; + } else if (message.includes('500')) { + message = 'Server error. Please try again later.'; + } + + showAlert(message, 'danger'); + return message; +} \ No newline at end of file diff --git a/client/js/apikeys.js b/client/js/apikeys.js new file mode 100644 index 0000000..3e9d9f3 --- /dev/null +++ b/client/js/apikeys.js @@ -0,0 +1,354 @@ +// API Key management functionality + +// Load API keys +async function loadApiKeys() { + if (!config.isConfigured()) { + showAlert('Please configure your API settings first.', 'warning'); + return; + } + + const container = document.getElementById('apiKeysContainer'); + container.innerHTML = '
Loading API keys...
'; + + try { + const apiKeys = await apiClient.getApiKeys(); + displayApiKeys(apiKeys); + } catch (error) { + handleApiError(error, 'loading API keys'); + container.innerHTML = '
Failed to load API keys
'; + } +} + +// Display API keys +function displayApiKeys(apiKeys) { + const container = document.getElementById('apiKeysContainer'); + + if (!apiKeys || apiKeys.length === 0) { + container.innerHTML = ` +
+ +

No API keys found

+

Create your first API key to get started!

+ +
+ `; + return; + } + + const apiKeysHtml = ` +
+ + + + + + + + + + + + + ${apiKeys.map(key => ` + + + + + + + + + `).join('')} + +
NameKeyStatusCreatedLast UsedActions
+
+ + ${escapeHtml(key.name)} +
+
+
+ + ${key.key ? key.key.substring(0, 8) + '...' : 'Hidden'} + + +
+
+ + ${key.is_active ? 'Active' : 'Inactive'} + + ${formatDate(key.created_at)} + ${key.last_used_at ? formatDate(key.last_used_at) : 'Never'} + +
+ + +
+
+
+ `; + + container.innerHTML = apiKeysHtml; +} + +// Show create API key modal +function showCreateApiKeyModal() { + const modalBody = ` +
+
+ + +
Choose a descriptive name to identify this API key
+
+
+ + +
+
+ + Important: The API key will only be shown once after creation. + Make sure to copy and store it securely. +
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('createApiKeyModal', 'Create New API Key', modalBody, modalFooter); + + // Handle form submission + document.getElementById('createApiKeyForm').addEventListener('submit', (e) => { + e.preventDefault(); + createApiKey(); + }); + + modal.show(); + + // Focus on name field + setTimeout(() => document.getElementById('apiKeyName').focus(), 100); +} + +// Create API key +async function createApiKey() { + const name = document.getElementById('apiKeyName').value.trim(); + const description = document.getElementById('apiKeyDescription').value.trim(); + + if (!name) { + showAlert('API key name is required', 'danger'); + return; + } + + const createButton = document.querySelector('#createApiKeyModal .btn-primary'); + setLoadingState(createButton); + + try { + const result = await apiClient.createApiKey({ + name, + description + }); + + // Close the create modal + bootstrap.Modal.getInstance(document.getElementById('createApiKeyModal')).hide(); + removeModal('createApiKeyModal'); + + // Show the new API key + showNewApiKeyModal(result); + + // Refresh the list + loadApiKeys(); + + } catch (error) { + handleApiError(error, 'creating API key'); + } finally { + setLoadingState(createButton, false); + } +} + +// Show new API key modal +function showNewApiKeyModal(apiKeyData) { + const modalBody = ` +
+ + API Key Created Successfully! +
+ +
+ +

${escapeHtml(apiKeyData.name)}

+
+ +
+ +
+ + +
+
+ +
+ + Important: This is the only time you'll see this API key. + Make sure to copy and store it securely before closing this dialog. +
+ `; + + const modalFooter = ` + + `; + + const modal = createModal('newApiKeyModal', 'New API Key Created', modalBody, modalFooter); + modal.show(); + + // Auto-select the API key text + setTimeout(() => { + const input = document.getElementById('newApiKeyValue'); + input.select(); + input.focus(); + }, 100); +} + +// Copy API key to clipboard +async function copyApiKey(key) { + if (!key) { + showAlert('No API key to copy', 'warning'); + return; + } + + try { + await copyToClipboard(key); + } catch (error) { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = key; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + showAlert('API key copied to clipboard!', 'success'); + } +} + +// View API key details +async function viewApiKey(keyId) { + try { + const apiKeys = await apiClient.getApiKeys(); + const apiKey = apiKeys.find(k => k.id === keyId); + + if (!apiKey) { + showAlert('API key not found', 'danger'); + return; + } + + const modalBody = ` +
+
+
${escapeHtml(apiKey.name)}
+

${escapeHtml(apiKey.description || 'No description')}

+
+
+
+
Key Information
+

Created: ${formatDate(apiKey.created_at)}

+

Last Used: ${apiKey.last_used_at ? formatDate(apiKey.last_used_at) : 'Never'}

+

ID: ${apiKey.id}

+
+
+
Status & Security
+

Status: + + ${apiKey.is_active ? 'Active' : 'Inactive'} + +

+

Key Preview: ${apiKey.key ? apiKey.key.substring(0, 8) + '...' : 'Hidden'}

+
+
+ + ${apiKey.last_used_at ? ` +
+
Usage Statistics
+
+ + This API key was last used on ${formatDate(apiKey.last_used_at)} +
+
+ ` : ` +
+
+ + This API key has never been used +
+
+ `} +
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('viewApiKeyModal', 'API Key Details', modalBody, modalFooter); + modal.show(); + + } catch (error) { + handleApiError(error, 'loading API key details'); + } +} + +// Delete API key +function deleteApiKey(keyId) { + confirmAction('Are you sure you want to delete this API key? This action cannot be undone and will immediately revoke access for any applications using this key.', async () => { + try { + await apiClient.deleteApiKey(keyId); + showAlert('API key deleted successfully!', 'success'); + loadApiKeys(); + + // Close any open modals + const modals = ['viewApiKeyModal', 'newApiKeyModal']; + modals.forEach(modalId => { + const modalElement = document.getElementById(modalId); + if (modalElement) { + bootstrap.Modal.getInstance(modalElement)?.hide(); + removeModal(modalId); + } + }); + + } catch (error) { + handleApiError(error, 'deleting API key'); + } + }); +} + +// Generate API key usage report +function generateUsageReport() { + showAlert('Usage report functionality would be implemented here', 'info'); +} + +// Bulk operations for API keys +function bulkDeleteApiKeys() { + showAlert('Bulk operations functionality would be implemented here', 'info'); +} \ No newline at end of file diff --git a/client/js/app.js b/client/js/app.js new file mode 100644 index 0000000..3231d7e --- /dev/null +++ b/client/js/app.js @@ -0,0 +1,362 @@ +// Main application initialization and global functionality + +// Application state +const app = { + initialized: false, + currentPage: 'home', + version: '1.0.0' +}; + +// Initialize the application +document.addEventListener('DOMContentLoaded', () => { + console.log('SeReact Frontend v' + app.version + ' - Initializing...'); + + // Initialize configuration + initializeApp(); + + // Set up global event listeners + setupGlobalEventListeners(); + + // Check initial configuration state + checkInitialConfiguration(); + + app.initialized = true; + console.log('SeReact Frontend - Initialization complete'); +}); + +// Initialize the application +function initializeApp() { + // Update navigation state based on configuration + updateNavigationState(); + + // Initialize page routing + initializeRouting(); + + // Set up periodic health checks + setupHealthChecks(); + + // Initialize keyboard shortcuts + setupKeyboardShortcuts(); +} + +// Set up global event listeners +function setupGlobalEventListeners() { + // Handle online/offline status + window.addEventListener('online', () => { + showAlert('Connection restored', 'success'); + updateConnectionStatus(true); + }); + + window.addEventListener('offline', () => { + showAlert('Connection lost - working offline', 'warning', true); + updateConnectionStatus(false); + }); + + // Handle visibility changes (tab switching) + document.addEventListener('visibilitychange', () => { + if (!document.hidden && config.isConfigured()) { + // Refresh current page data when tab becomes visible + refreshCurrentPageData(); + } + }); + + // Handle window resize + window.addEventListener('resize', debounce(() => { + handleWindowResize(); + }, 250)); + + // Handle beforeunload for unsaved changes warning + window.addEventListener('beforeunload', (e) => { + if (hasUnsavedChanges()) { + e.preventDefault(); + e.returnValue = ''; + } + }); +} + +// Check initial configuration +function checkInitialConfiguration() { + if (!config.isConfigured()) { + // Show welcome message for first-time users + setTimeout(() => { + showWelcomeMessage(); + }, 1000); + } else { + // Test connection on startup + setTimeout(() => { + testConnectionSilently(); + }, 500); + } +} + +// Show welcome message for new users +function showWelcomeMessage() { + const modalBody = ` +
+ +

Welcome to SeReact!

+

AI-powered image management and semantic search platform

+
+ +
+
+
+
+ +
Configure API
+

Set up your API connection to get started

+
+
+
+
+
+
+ +
Upload Images
+

Start building your image collection

+
+
+
+
+ +
+

Ready to begin? Let's configure your API connection first.

+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('welcomeModal', 'Welcome to SeReact', modalBody, modalFooter); + modal.show(); +} + +// Test connection silently (without showing alerts) +async function testConnectionSilently() { + try { + const isHealthy = await apiClient.healthCheck(); + updateConnectionStatus(isHealthy); + } catch (error) { + updateConnectionStatus(false); + } +} + +// Update connection status indicator +function updateConnectionStatus(isOnline) { + // Add a connection status indicator to the navbar + let statusIndicator = document.getElementById('connectionStatus'); + + if (!statusIndicator) { + statusIndicator = document.createElement('div'); + statusIndicator.id = 'connectionStatus'; + statusIndicator.className = 'nav-item'; + + const navbar = document.querySelector('.navbar-nav'); + if (navbar) { + navbar.appendChild(statusIndicator); + } + } + + statusIndicator.innerHTML = ` + + + + `; +} + +// Initialize routing +function initializeRouting() { + // Handle hash changes for deep linking + window.addEventListener('hashchange', handleRouteChange); + + // Handle initial route + handleRouteChange(); +} + +// Handle route changes +function handleRouteChange() { + const hash = window.location.hash.substring(1); + const route = hash || 'home'; + + // Validate route + const validRoutes = ['home', 'config', 'images', 'search', 'teams', 'users', 'api-keys']; + + if (validRoutes.includes(route)) { + showPage(route); + } else { + // Invalid route, redirect to home + window.location.hash = 'home'; + } +} + +// Set up periodic health checks +function setupHealthChecks() { + if (!config.isConfigured()) return; + + // Check health every 5 minutes + setInterval(async () => { + if (config.isConfigured() && navigator.onLine) { + try { + const isHealthy = await apiClient.healthCheck(); + updateConnectionStatus(isHealthy); + } catch (error) { + updateConnectionStatus(false); + } + } + }, 5 * 60 * 1000); +} + +// Set up keyboard shortcuts +function setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Only handle shortcuts when not in input fields + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + return; + } + + // Ctrl/Cmd + shortcuts + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'k': + e.preventDefault(); + showPage('search'); + setTimeout(() => { + const searchInput = document.getElementById('searchQuery'); + if (searchInput) searchInput.focus(); + }, 100); + break; + case 'u': + e.preventDefault(); + if (app.currentPage === 'images') { + showUploadModal(); + } else { + showPage('images'); + setTimeout(() => showUploadModal(), 100); + } + break; + case ',': + e.preventDefault(); + showPage('config'); + break; + } + } + + // Number shortcuts for navigation + if (e.key >= '1' && e.key <= '6' && !e.ctrlKey && !e.metaKey && !e.altKey) { + const pages = ['home', 'images', 'search', 'teams', 'users', 'api-keys']; + const pageIndex = parseInt(e.key) - 1; + if (pages[pageIndex]) { + e.preventDefault(); + showPage(pages[pageIndex]); + } + } + + // Escape key to close modals + if (e.key === 'Escape') { + const openModals = document.querySelectorAll('.modal.show'); + openModals.forEach(modal => { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + }); + } + }); +} + +// Refresh current page data +function refreshCurrentPageData() { + switch (app.currentPage) { + case 'images': + loadImages(currentPage); + break; + case 'teams': + loadTeams(); + break; + case 'users': + loadUsers(); + break; + case 'api-keys': + loadApiKeys(); + break; + } +} + +// Handle window resize +function handleWindowResize() { + // Reinitialize tooltips and popovers after resize + initializeTooltips(); + initializePopovers(); + + // Adjust image grid if on images page + if (app.currentPage === 'images') { + // Force reflow of image grid + const imageGrid = document.querySelector('.image-grid'); + if (imageGrid) { + imageGrid.style.display = 'none'; + imageGrid.offsetHeight; // Trigger reflow + imageGrid.style.display = 'grid'; + } + } +} + +// Check for unsaved changes +function hasUnsavedChanges() { + // Check if any forms have been modified + const forms = document.querySelectorAll('form'); + for (const form of forms) { + if (form.dataset.modified === 'true') { + return true; + } + } + return false; +} + +// Mark form as modified +function markFormAsModified(formElement) { + formElement.dataset.modified = 'true'; +} + +// Mark form as saved +function markFormAsSaved(formElement) { + formElement.dataset.modified = 'false'; +} + +// Global error handler +window.addEventListener('error', (e) => { + console.error('Global error:', e.error); + + // Don't show alerts for network errors or script loading errors + if (e.error && !e.error.message.includes('Loading')) { + showAlert('An unexpected error occurred. Please refresh the page if problems persist.', 'danger'); + } +}); + +// Global unhandled promise rejection handler +window.addEventListener('unhandledrejection', (e) => { + console.error('Unhandled promise rejection:', e.reason); + + // Don't show alerts for network-related rejections + if (e.reason && !e.reason.message?.includes('fetch')) { + showAlert('An unexpected error occurred. Please try again.', 'danger'); + } + + // Prevent the default browser behavior + e.preventDefault(); +}); + +// Export app object for debugging +window.SeReactApp = app; + +// Add helpful console messages +console.log('%cSeReact Frontend', 'color: #0d6efd; font-size: 24px; font-weight: bold;'); +console.log('%cVersion: ' + app.version, 'color: #6c757d; font-size: 14px;'); +console.log('%cKeyboard Shortcuts:', 'color: #198754; font-size: 16px; font-weight: bold;'); +console.log('%c Ctrl+K: Search', 'color: #6c757d;'); +console.log('%c Ctrl+U: Upload Image', 'color: #6c757d;'); +console.log('%c Ctrl+,: Settings', 'color: #6c757d;'); +console.log('%c 1-6: Navigate to pages', 'color: #6c757d;'); +console.log('%c Esc: Close modals', 'color: #6c757d;'); \ No newline at end of file diff --git a/client/js/config.js b/client/js/config.js new file mode 100644 index 0000000..9a76762 --- /dev/null +++ b/client/js/config.js @@ -0,0 +1,166 @@ +// Configuration management +class Config { + constructor() { + this.apiBaseUrl = localStorage.getItem('apiBaseUrl') || 'http://localhost:8000'; + this.apiKey = localStorage.getItem('apiKey') || ''; + } + + setApiBaseUrl(url) { + this.apiBaseUrl = url.replace(/\/$/, ''); // Remove trailing slash + localStorage.setItem('apiBaseUrl', this.apiBaseUrl); + } + + setApiKey(key) { + this.apiKey = key; + localStorage.setItem('apiKey', this.apiKey); + } + + getApiBaseUrl() { + return this.apiBaseUrl; + } + + getApiKey() { + return this.apiKey; + } + + isConfigured() { + return this.apiBaseUrl && this.apiKey; + } + + clear() { + this.apiBaseUrl = ''; + this.apiKey = ''; + localStorage.removeItem('apiBaseUrl'); + localStorage.removeItem('apiKey'); + } +} + +// Global config instance +const config = new Config(); + +// Initialize configuration form +function initializeConfigForm() { + const form = document.getElementById('configForm'); + const apiBaseUrlInput = document.getElementById('apiBaseUrl'); + const apiKeyInput = document.getElementById('apiKey'); + + // Load current values + apiBaseUrlInput.value = config.getApiBaseUrl(); + apiKeyInput.value = config.getApiKey(); + + // Handle form submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + + const newBaseUrl = apiBaseUrlInput.value.trim(); + const newApiKey = apiKeyInput.value.trim(); + + if (!newBaseUrl) { + showAlert('API Base URL is required', 'danger'); + return; + } + + config.setApiBaseUrl(newBaseUrl); + if (newApiKey) { + config.setApiKey(newApiKey); + } + + showAlert('Configuration saved successfully!', 'success'); + + // Update navigation state + updateNavigationState(); + }); +} + +// Test API connection +async function testConnection() { + const apiBaseUrl = document.getElementById('apiBaseUrl').value.trim(); + const apiKey = document.getElementById('apiKey').value.trim(); + + if (!apiBaseUrl) { + showAlert('API Base URL is required', 'danger'); + return; + } + + if (!apiKey) { + showAlert('API Key is required', 'danger'); + return; + } + + const testButton = document.querySelector('button[onclick="testConnection()"]'); + const originalText = testButton.innerHTML; + testButton.innerHTML = ' Testing...'; + testButton.disabled = true; + + try { + // Test health endpoint first + const healthResponse = await fetch(`${apiBaseUrl}/health`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!healthResponse.ok) { + throw new Error(`Health check failed with status ${healthResponse.status}`); + } + + // Test API authentication + const authResponse = await fetch(`${apiBaseUrl}/api/v1/teams`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + } + }); + + if (authResponse.status === 401) { + throw new Error('Authentication failed - Invalid API key'); + } else if (authResponse.status === 403) { + throw new Error('Access forbidden - API key lacks permissions'); + } else if (!authResponse.ok) { + throw new Error(`API request failed with status ${authResponse.status}`); + } + + showAlert('API connection successful! Backend is up and running.', 'success'); + + // Save the working configuration + config.setApiBaseUrl(apiBaseUrl); + config.setApiKey(apiKey); + + // Update form values + document.getElementById('apiBaseUrl').value = apiBaseUrl; + document.getElementById('apiKey').value = apiKey; + + } catch (error) { + console.error('Connection test failed:', error); + showAlert(`Connection test failed: ${error.message}`, 'danger'); + } finally { + testButton.innerHTML = originalText; + testButton.disabled = false; + } +} + +// Update navigation state based on configuration +function updateNavigationState() { + const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); + const isConfigured = config.isConfigured(); + + navLinks.forEach(link => { + const onclick = link.getAttribute('onclick'); + if (onclick && onclick.includes('showPage') && !onclick.includes('config') && !onclick.includes('home')) { + if (isConfigured) { + link.classList.remove('disabled'); + link.style.opacity = '1'; + } else { + link.classList.add('disabled'); + link.style.opacity = '0.5'; + } + } + }); + + // Show configuration warning if not configured + if (!isConfigured && window.location.hash !== '#config') { + showAlert('Please configure your API settings first.', 'warning', true); + } +} \ No newline at end of file diff --git a/client/js/images.js b/client/js/images.js new file mode 100644 index 0000000..205b25e --- /dev/null +++ b/client/js/images.js @@ -0,0 +1,370 @@ +// Image management functionality + +let currentPage = 1; +let totalPages = 1; + +// Load images with pagination +async function loadImages(page = 1, tags = null) { + if (!config.isConfigured()) { + showAlert('Please configure your API settings first.', 'warning'); + return; + } + + const container = document.getElementById('imagesContainer'); + container.innerHTML = '
Loading images...
'; + + try { + const response = await apiClient.getImages(page, 20, tags); + currentPage = page; + totalPages = Math.ceil(response.total / response.limit); + + displayImages(response.images); + displayPagination(response); + } catch (error) { + handleApiError(error, 'loading images'); + container.innerHTML = '
Failed to load images
'; + } +} + +// Display images in grid +function displayImages(images) { + const container = document.getElementById('imagesContainer'); + + if (!images || images.length === 0) { + container.innerHTML = ` +
+ +

No images found

+

Upload your first image to get started!

+ +
+ `; + return; + } + + const imagesHtml = images.map(image => ` +
+ ${escapeHtml(image.description || 'Image')} +
+
${escapeHtml(truncateText(image.description || 'Untitled', 50))}
+

+ ${formatDate(image.created_at)} +

+ ${image.tags && image.tags.length > 0 ? ` +
+ ${image.tags.map(tag => `${escapeHtml(tag)}`).join('')} +
+ ` : ''} +
+ + + +
+
+
+ `).join(''); + + container.innerHTML = `
${imagesHtml}
`; +} + +// Display pagination +function displayPagination(response) { + const container = document.getElementById('imagesContainer'); + + if (totalPages <= 1) return; + + const paginationHtml = ` + + `; + + container.insertAdjacentHTML('beforeend', paginationHtml); +} + +// Show upload modal +function showUploadModal() { + const modalBody = ` +
+
+ +
+ +
Drag & drop an image here
+

or click to select a file

+ +
+ +
+
+ + +
+
+ + +
e.g., nature, landscape, sunset
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('uploadModal', 'Upload Image', modalBody, modalFooter); + + // Setup file input and drag & drop + const uploadArea = document.getElementById('uploadArea'); + const fileInput = document.getElementById('imageFile'); + + uploadArea.addEventListener('click', () => fileInput.click()); + + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleFileSelection(e.target.files[0]); + } + }); + + setupFileDropZone(uploadArea, (files) => { + if (files.length > 0) { + handleFileSelection(files[0]); + } + }); + + modal.show(); +} + +// Handle file selection +async function handleFileSelection(file) { + try { + validateFile(file); + + const preview = await createImagePreview(file); + const previewContainer = document.getElementById('imagePreview'); + const previewImg = document.getElementById('previewImg'); + + previewImg.src = preview; + previewContainer.style.display = 'block'; + + // Store file for upload + document.getElementById('uploadForm').selectedFile = file; + + } catch (error) { + showAlert(error.message, 'danger'); + } +} + +// Upload image +async function uploadImage() { + const form = document.getElementById('uploadForm'); + const file = form.selectedFile; + + if (!file) { + showAlert('Please select an image file', 'danger'); + return; + } + + const description = document.getElementById('imageDescription').value.trim(); + const tagsInput = document.getElementById('imageTags').value.trim(); + const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : []; + + const uploadButton = document.querySelector('#uploadModal .btn-primary'); + setLoadingState(uploadButton); + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('description', description); + if (tags.length > 0) { + formData.append('tags', JSON.stringify(tags)); + } + + await apiClient.uploadImage(formData); + + showAlert('Image uploaded successfully!', 'success'); + + // Close modal and refresh images + bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide(); + removeModal('uploadModal'); + loadImages(currentPage); + + } catch (error) { + handleApiError(error, 'uploading image'); + } finally { + setLoadingState(uploadButton, false); + } +} + +// View image details +async function viewImage(imageId) { + try { + const image = await apiClient.getImage(imageId); + + const modalBody = ` +
+ +
+
+
+
Description
+

${escapeHtml(image.description || 'No description')}

+
+
+
Details
+

Created: ${formatDate(image.created_at)}

+

Size: ${formatFileSize(image.file_size)}

+

Dimensions: ${image.width} × ${image.height}

+
+
+ ${image.tags && image.tags.length > 0 ? ` +
+
Tags
+ ${image.tags.map(tag => `${escapeHtml(tag)}`).join('')} +
+ ` : ''} + `; + + const modalFooter = ` + + + + `; + + const modal = createModal('viewImageModal', 'Image Details', modalBody, modalFooter); + modal.show(); + + } catch (error) { + handleApiError(error, 'loading image details'); + } +} + +// Edit image +async function editImage(imageId) { + try { + const image = await apiClient.getImage(imageId); + + const modalBody = ` +
+
+ +
+
+ + +
+
+ + +
Enter tags separated by commas
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('editImageModal', 'Edit Image', modalBody, modalFooter); + modal.show(); + + } catch (error) { + handleApiError(error, 'loading image for editing'); + } +} + +// Save image changes +async function saveImageChanges(imageId) { + const description = document.getElementById('editDescription').value.trim(); + const tagsInput = document.getElementById('editTags').value.trim(); + const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : []; + + const saveButton = document.querySelector('#editImageModal .btn-primary'); + setLoadingState(saveButton); + + try { + await apiClient.updateImage(imageId, { + description, + tags + }); + + showAlert('Image updated successfully!', 'success'); + + // Close modal and refresh images + bootstrap.Modal.getInstance(document.getElementById('editImageModal')).hide(); + removeModal('editImageModal'); + loadImages(currentPage); + + } catch (error) { + handleApiError(error, 'updating image'); + } finally { + setLoadingState(saveButton, false); + } +} + +// Delete image +function deleteImage(imageId) { + confirmAction('Are you sure you want to delete this image? This action cannot be undone.', async () => { + try { + await apiClient.deleteImage(imageId); + showAlert('Image deleted successfully!', 'success'); + loadImages(currentPage); + + // Close any open modals + const modals = ['viewImageModal', 'editImageModal']; + modals.forEach(modalId => { + const modalElement = document.getElementById(modalId); + if (modalElement) { + bootstrap.Modal.getInstance(modalElement)?.hide(); + removeModal(modalId); + } + }); + + } catch (error) { + handleApiError(error, 'deleting image'); + } + }); +} \ No newline at end of file diff --git a/client/js/search.js b/client/js/search.js new file mode 100644 index 0000000..1dfb1a4 --- /dev/null +++ b/client/js/search.js @@ -0,0 +1,297 @@ +// AI-powered image search functionality + +// Initialize search form +function initializeSearchForm() { + const form = document.getElementById('searchForm'); + const thresholdSlider = document.getElementById('similarityThreshold'); + const thresholdValue = document.getElementById('thresholdValue'); + + // Update threshold display + thresholdSlider.addEventListener('input', (e) => { + thresholdValue.textContent = e.target.value; + }); + + // Handle form submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + performSearch(); + }); +} + +// Perform search +async function performSearch() { + if (!config.isConfigured()) { + showAlert('Please configure your API settings first.', 'warning'); + return; + } + + const query = document.getElementById('searchQuery').value.trim(); + const threshold = parseFloat(document.getElementById('similarityThreshold').value); + const maxResults = parseInt(document.getElementById('maxResults').value); + + if (!query) { + showAlert('Please enter a search query', 'danger'); + return; + } + + const resultsContainer = document.getElementById('searchResults'); + resultsContainer.innerHTML = ` +
+
+

Searching for "${escapeHtml(query)}"...

+
+ `; + + try { + const results = await apiClient.searchImages(query, threshold, maxResults); + displaySearchResults(results, query); + } catch (error) { + handleApiError(error, 'searching images'); + resultsContainer.innerHTML = ` +
+ + Search failed. Please try again. +
+ `; + } +} + +// Display search results +function displaySearchResults(results, query) { + const container = document.getElementById('searchResults'); + + if (!results || results.length === 0) { + container.innerHTML = ` +
+ +

No results found

+

Try adjusting your search query or similarity threshold.

+
+ +
+
+ `; + return; + } + + const resultsHtml = ` +
+

Search Results

+

Found ${results.length} images matching "${escapeHtml(query)}"

+
+
+ ${results.map(result => ` +
+
+
+ ${escapeHtml(result.image.description || 'Image')} +
+ + ${Math.round(result.similarity * 100)}% match + +
+
+
+
${escapeHtml(truncateText(result.image.description || 'Untitled', 60))}
+

+ ${formatDate(result.image.created_at)} +

+ ${result.image.tags && result.image.tags.length > 0 ? ` +
+ ${result.image.tags.slice(0, 3).map(tag => + `${escapeHtml(tag)}` + ).join('')} + ${result.image.tags.length > 3 ? + `+${result.image.tags.length - 3}` : '' + } +
+ ` : ''} +
+
+ + +
+ + Similarity: ${(result.similarity * 100).toFixed(1)}% + +
+
+
+
+ `).join('')} +
+ `; + + container.innerHTML = resultsHtml; + + // Add search refinement options + addSearchRefinementOptions(query, results); +} + +// Add search refinement options +function addSearchRefinementOptions(query, results) { + const container = document.getElementById('searchResults'); + + // Extract common tags from results + const allTags = results.flatMap(result => result.image.tags || []); + const tagCounts = {}; + allTags.forEach(tag => { + tagCounts[tag] = (tagCounts[tag] || 0) + 1; + }); + + const popularTags = Object.entries(tagCounts) + .sort(([,a], [,b]) => b - a) + .slice(0, 8) + .map(([tag]) => tag); + + if (popularTags.length > 0) { + const refinementHtml = ` +
+
Refine your search
+

Popular tags in these results:

+
+ ${popularTags.map(tag => ` + + `).join('')} +
+
+ `; + + container.insertAdjacentHTML('beforeend', refinementHtml); + } + + // Add export/share options + const actionsHtml = ` +
+
+ + + +
+
+ `; + + container.insertAdjacentHTML('beforeend', actionsHtml); +} + +// Refine search with tag +function refineSearchWithTag(tag) { + const currentQuery = document.getElementById('searchQuery').value.trim(); + const newQuery = currentQuery ? `${currentQuery} ${tag}` : tag; + + document.getElementById('searchQuery').value = newQuery; + performSearch(); +} + +// Export search results +function exportSearchResults(query) { + // This would typically generate a CSV or JSON export + showAlert('Export functionality would be implemented here', 'info'); +} + +// Share search results +async function shareSearchResults(query) { + const url = `${window.location.origin}${window.location.pathname}#search`; + const text = `Check out these AI search results for "${query}"`; + + if (navigator.share) { + try { + await navigator.share({ + title: 'SeReact Search Results', + text: text, + url: url + }); + } catch (error) { + console.log('Error sharing:', error); + copyToClipboard(url); + } + } else { + copyToClipboard(url); + } +} + +// Save search +function saveSearch(query) { + const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]'); + + if (!savedSearches.includes(query)) { + savedSearches.push(query); + localStorage.setItem('savedSearches', JSON.stringify(savedSearches)); + showAlert('Search saved successfully!', 'success'); + updateSavedSearches(); + } else { + showAlert('This search is already saved', 'info'); + } +} + +// Update saved searches display +function updateSavedSearches() { + const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]'); + + if (savedSearches.length === 0) return; + + const searchForm = document.getElementById('searchForm'); + const existingSaved = document.getElementById('savedSearches'); + + if (existingSaved) { + existingSaved.remove(); + } + + const savedHtml = ` +
+
Saved Searches
+
+ ${savedSearches.map(search => ` + + + `).join('')} +
+
+ `; + + searchForm.insertAdjacentHTML('afterend', savedHtml); +} + +// Load saved search +function loadSavedSearch(query) { + document.getElementById('searchQuery').value = query; + performSearch(); +} + +// Remove saved search +function removeSavedSearch(query) { + const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]'); + const filtered = savedSearches.filter(search => search !== query); + localStorage.setItem('savedSearches', JSON.stringify(filtered)); + updateSavedSearches(); + showAlert('Saved search removed', 'info'); +} + +// Initialize saved searches on page load +document.addEventListener('DOMContentLoaded', () => { + // Add a small delay to ensure the search form is loaded + setTimeout(updateSavedSearches, 100); +}); \ No newline at end of file diff --git a/client/js/teams.js b/client/js/teams.js new file mode 100644 index 0000000..ae29786 --- /dev/null +++ b/client/js/teams.js @@ -0,0 +1,318 @@ +// Team management functionality + +// Load teams +async function loadTeams() { + if (!config.isConfigured()) { + showAlert('Please configure your API settings first.', 'warning'); + return; + } + + const container = document.getElementById('teamsContainer'); + container.innerHTML = '
Loading teams...
'; + + try { + const teams = await apiClient.getTeams(); + displayTeams(teams); + } catch (error) { + handleApiError(error, 'loading teams'); + container.innerHTML = '
Failed to load teams
'; + } +} + +// Display teams +function displayTeams(teams) { + const container = document.getElementById('teamsContainer'); + + if (!teams || teams.length === 0) { + container.innerHTML = ` +
+ +

No teams found

+

Create your first team to get started!

+ +
+ `; + return; + } + + const teamsHtml = ` +
+ ${teams.map(team => ` +
+
+
+
+ + ${escapeHtml(team.name)} +
+

${escapeHtml(team.description || 'No description')}

+
+ Created: ${formatDate(team.created_at)} +
+
+ +
+
+ `).join('')} +
+ `; + + container.innerHTML = teamsHtml; +} + +// Show create team modal +function showCreateTeamModal() { + const modalBody = ` +
+
+ + +
+
+ + +
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('createTeamModal', 'Create New Team', modalBody, modalFooter); + + // Handle form submission + document.getElementById('createTeamForm').addEventListener('submit', (e) => { + e.preventDefault(); + createTeam(); + }); + + modal.show(); + + // Focus on name field + setTimeout(() => document.getElementById('teamName').focus(), 100); +} + +// Create team +async function createTeam() { + const name = document.getElementById('teamName').value.trim(); + const description = document.getElementById('teamDescription').value.trim(); + + if (!name) { + showAlert('Team name is required', 'danger'); + return; + } + + const createButton = document.querySelector('#createTeamModal .btn-primary'); + setLoadingState(createButton); + + try { + await apiClient.createTeam({ + name, + description + }); + + showAlert('Team created successfully!', 'success'); + + // Close modal and refresh teams + bootstrap.Modal.getInstance(document.getElementById('createTeamModal')).hide(); + removeModal('createTeamModal'); + loadTeams(); + + } catch (error) { + handleApiError(error, 'creating team'); + } finally { + setLoadingState(createButton, false); + } +} + +// View team details +async function viewTeam(teamId) { + try { + const teams = await apiClient.getTeams(); + const team = teams.find(t => t.id === teamId); + + if (!team) { + showAlert('Team not found', 'danger'); + return; + } + + const modalBody = ` +
+
+
${escapeHtml(team.name)}
+

${escapeHtml(team.description || 'No description')}

+
+
+
+
Team Information
+

Created: ${formatDate(team.created_at)}

+

ID: ${team.id}

+
+
+
Statistics
+

Members: Loading...

+

Status: Active

+
+
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('viewTeamModal', 'Team Details', modalBody, modalFooter); + modal.show(); + + // Load team member count + loadTeamMemberCount(teamId); + + } catch (error) { + handleApiError(error, 'loading team details'); + } +} + +// Load team member count +async function loadTeamMemberCount(teamId) { + try { + const users = await apiClient.getUsers(); + const teamMembers = users.filter(user => user.team_id === teamId); + document.getElementById('teamMemberCount').textContent = teamMembers.length; + } catch (error) { + document.getElementById('teamMemberCount').textContent = 'Error loading'; + } +} + +// Edit team +async function editTeam(teamId) { + try { + const teams = await apiClient.getTeams(); + const team = teams.find(t => t.id === teamId); + + if (!team) { + showAlert('Team not found', 'danger'); + return; + } + + const modalBody = ` +
+
+ + +
+
+ + +
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('editTeamModal', 'Edit Team', modalBody, modalFooter); + + // Handle form submission + document.getElementById('editTeamForm').addEventListener('submit', (e) => { + e.preventDefault(); + saveTeamChanges(teamId); + }); + + modal.show(); + + } catch (error) { + handleApiError(error, 'loading team for editing'); + } +} + +// Save team changes +async function saveTeamChanges(teamId) { + const name = document.getElementById('editTeamName').value.trim(); + const description = document.getElementById('editTeamDescription').value.trim(); + + if (!name) { + showAlert('Team name is required', 'danger'); + return; + } + + const saveButton = document.querySelector('#editTeamModal .btn-primary'); + setLoadingState(saveButton); + + try { + await apiClient.updateTeam(teamId, { + name, + description + }); + + showAlert('Team updated successfully!', 'success'); + + // Close modal and refresh teams + bootstrap.Modal.getInstance(document.getElementById('editTeamModal')).hide(); + removeModal('editTeamModal'); + loadTeams(); + + // Close view modal if open + const viewModal = document.getElementById('viewTeamModal'); + if (viewModal) { + bootstrap.Modal.getInstance(viewModal)?.hide(); + removeModal('viewTeamModal'); + } + + } catch (error) { + handleApiError(error, 'updating team'); + } finally { + setLoadingState(saveButton, false); + } +} + +// Delete team +function deleteTeam(teamId) { + confirmAction('Are you sure you want to delete this team? This action cannot be undone and will affect all team members.', async () => { + try { + await apiClient.deleteTeam(teamId); + showAlert('Team deleted successfully!', 'success'); + loadTeams(); + + // Close any open modals + const modals = ['viewTeamModal', 'editTeamModal']; + modals.forEach(modalId => { + const modalElement = document.getElementById(modalId); + if (modalElement) { + bootstrap.Modal.getInstance(modalElement)?.hide(); + removeModal(modalId); + } + }); + + } catch (error) { + handleApiError(error, 'deleting team'); + } + }); +} \ No newline at end of file diff --git a/client/js/ui.js b/client/js/ui.js new file mode 100644 index 0000000..1a4b80a --- /dev/null +++ b/client/js/ui.js @@ -0,0 +1,280 @@ +// UI utilities and common functions + +// Page navigation +function showPage(pageId) { + // Hide all pages + const pages = document.querySelectorAll('.page'); + pages.forEach(page => { + page.style.display = 'none'; + }); + + // Show the selected page + const targetPage = document.getElementById(pageId + 'Page'); + if (targetPage) { + targetPage.style.display = 'block'; + + // Update URL hash + window.location.hash = pageId; + + // Update navigation active state + updateNavActiveState(pageId); + + // Load page data if needed + loadPageData(pageId); + } +} + +// Update navigation active state +function updateNavActiveState(activePageId) { + const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); + navLinks.forEach(link => { + link.classList.remove('active'); + const onclick = link.getAttribute('onclick'); + if (onclick && onclick.includes(`'${activePageId}'`)) { + link.classList.add('active'); + } + }); +} + +// Load page-specific data +function loadPageData(pageId) { + switch (pageId) { + case 'config': + initializeConfigForm(); + break; + case 'images': + loadImages(); + break; + case 'teams': + loadTeams(); + break; + case 'users': + loadUsers(); + break; + case 'api-keys': + loadApiKeys(); + break; + case 'search': + initializeSearchForm(); + break; + } +} + +// Alert system +function showAlert(message, type = 'info', persistent = false) { + const alertContainer = document.getElementById('alertContainer'); + + const alertId = 'alert-' + Date.now(); + const alertHtml = ` + + `; + + alertContainer.insertAdjacentHTML('beforeend', alertHtml); + + // Auto-dismiss non-persistent alerts + if (!persistent) { + setTimeout(() => { + const alertElement = document.getElementById(alertId); + if (alertElement) { + const bsAlert = new bootstrap.Alert(alertElement); + bsAlert.close(); + } + }, 5000); + } +} + +// Clear all alerts +function clearAlerts() { + const alertContainer = document.getElementById('alertContainer'); + alertContainer.innerHTML = ''; +} + +// Modal utilities +function createModal(id, title, body, footer = '') { + const modalHtml = ` + + `; + + const modalContainer = document.getElementById('modalContainer'); + modalContainer.insertAdjacentHTML('beforeend', modalHtml); + + return new bootstrap.Modal(document.getElementById(id)); +} + +// Remove modal from DOM +function removeModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.remove(); + } +} + +// Loading state utilities +function setLoadingState(element, loading = true) { + if (loading) { + element.disabled = true; + element.dataset.originalText = element.innerHTML; + element.innerHTML = ' Loading...'; + } else { + element.disabled = false; + element.innerHTML = element.dataset.originalText || element.innerHTML; + } +} + +// Format date for display +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); +} + +// Format file size +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// Truncate text +function truncateText(text, maxLength = 100) { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; +} + +// Escape HTML +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Debounce function +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Confirm dialog +function confirmAction(message, callback) { + if (confirm(message)) { + callback(); + } +} + +// Copy to clipboard +async function copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + showAlert('Copied to clipboard!', 'success'); + } catch (err) { + console.error('Failed to copy: ', err); + showAlert('Failed to copy to clipboard', 'danger'); + } +} + +// Initialize tooltips +function initializeTooltips() { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); +} + +// Initialize popovers +function initializePopovers() { + const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); +} + +// Handle file drag and drop +function setupFileDropZone(element, callback) { + element.addEventListener('dragover', (e) => { + e.preventDefault(); + element.classList.add('dragover'); + }); + + element.addEventListener('dragleave', (e) => { + e.preventDefault(); + element.classList.remove('dragover'); + }); + + element.addEventListener('drop', (e) => { + e.preventDefault(); + element.classList.remove('dragover'); + + const files = Array.from(e.dataTransfer.files); + callback(files); + }); +} + +// Validate file type and size +function validateFile(file, allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], maxSize = 10 * 1024 * 1024) { + if (!allowedTypes.includes(file.type)) { + throw new Error(`File type ${file.type} is not allowed. Allowed types: ${allowedTypes.join(', ')}`); + } + + if (file.size > maxSize) { + throw new Error(`File size ${formatFileSize(file.size)} exceeds maximum allowed size of ${formatFileSize(maxSize)}`); + } + + return true; +} + +// Create image preview +function createImagePreview(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +// Handle hash changes for navigation +window.addEventListener('hashchange', () => { + const hash = window.location.hash.substring(1); + if (hash) { + showPage(hash); + } +}); + +// Initialize page on load +document.addEventListener('DOMContentLoaded', () => { + // Initialize tooltips and popovers + initializeTooltips(); + initializePopovers(); + + // Update navigation state + updateNavigationState(); + + // Show initial page + const hash = window.location.hash.substring(1); + const initialPage = hash || 'home'; + showPage(initialPage); +}); \ No newline at end of file diff --git a/client/js/users.js b/client/js/users.js new file mode 100644 index 0000000..cedd9c2 --- /dev/null +++ b/client/js/users.js @@ -0,0 +1,471 @@ +// User management functionality + +// Load users +async function loadUsers() { + if (!config.isConfigured()) { + showAlert('Please configure your API settings first.', 'warning'); + return; + } + + const container = document.getElementById('usersContainer'); + container.innerHTML = '
Loading users...
'; + + try { + const users = await apiClient.getUsers(); + displayUsers(users); + } catch (error) { + handleApiError(error, 'loading users'); + container.innerHTML = '
Failed to load users
'; + } +} + +// Display users +function displayUsers(users) { + const container = document.getElementById('usersContainer'); + + if (!users || users.length === 0) { + container.innerHTML = ` +
+ +

No users found

+

Create your first user to get started!

+ +
+ `; + return; + } + + const usersHtml = ` +
+ + + + + + + + + + + + + ${users.map(user => ` + + + + + + + + + `).join('')} + +
NameEmailTeamRoleCreatedActions
+
+
+ ${user.name.charAt(0).toUpperCase()} +
+ ${escapeHtml(user.name)} +
+
${escapeHtml(user.email)} + + Loading... + + + + ${user.is_admin ? 'Admin' : 'User'} + + ${formatDate(user.created_at)} +
+ + + +
+
+
+ `; + + container.innerHTML = usersHtml; + + // Load team names + loadTeamNames(); +} + +// Load team names for display +async function loadTeamNames() { + try { + const teams = await apiClient.getTeams(); + teams.forEach(team => { + const teamBadges = document.querySelectorAll(`#team-${team.id}`); + teamBadges.forEach(badge => { + badge.textContent = team.name; + }); + }); + } catch (error) { + console.error('Failed to load team names:', error); + } +} + +// Show create user modal +async function showCreateUserModal() { + try { + const teams = await apiClient.getTeams(); + + const modalBody = ` +
+
+ + +
+
+ + +
+
+ + +
Minimum 8 characters
+
+
+ + +
+
+
+ + +
Admins can manage teams, users, and system settings
+
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('createUserModal', 'Create New User', modalBody, modalFooter); + + // Handle form submission + document.getElementById('createUserForm').addEventListener('submit', (e) => { + e.preventDefault(); + createUser(); + }); + + modal.show(); + + // Focus on name field + setTimeout(() => document.getElementById('userName').focus(), 100); + + } catch (error) { + handleApiError(error, 'loading teams for user creation'); + } +} + +// Create user +async function createUser() { + const name = document.getElementById('userName').value.trim(); + const email = document.getElementById('userEmail').value.trim(); + const password = document.getElementById('userPassword').value; + const teamId = document.getElementById('userTeam').value; + const isAdmin = document.getElementById('userIsAdmin').checked; + + if (!name || !email || !password || !teamId) { + showAlert('All fields are required', 'danger'); + return; + } + + if (password.length < 8) { + showAlert('Password must be at least 8 characters long', 'danger'); + return; + } + + const createButton = document.querySelector('#createUserModal .btn-primary'); + setLoadingState(createButton); + + try { + await apiClient.createUser({ + name, + email, + password, + team_id: teamId, + is_admin: isAdmin + }); + + showAlert('User created successfully!', 'success'); + + // Close modal and refresh users + bootstrap.Modal.getInstance(document.getElementById('createUserModal')).hide(); + removeModal('createUserModal'); + loadUsers(); + + } catch (error) { + handleApiError(error, 'creating user'); + } finally { + setLoadingState(createButton, false); + } +} + +// View user details +async function viewUser(userId) { + try { + const users = await apiClient.getUsers(); + const user = users.find(u => u.id === userId); + + if (!user) { + showAlert('User not found', 'danger'); + return; + } + + const teams = await apiClient.getTeams(); + const userTeam = teams.find(t => t.id === user.team_id); + + const modalBody = ` +
+
+
+
+ ${user.name.charAt(0).toUpperCase()} +
+
${escapeHtml(user.name)}
+

${escapeHtml(user.email)}

+ + ${user.is_admin ? 'Administrator' : 'User'} + +
+
+
+
+
User Information
+

Team: ${userTeam ? escapeHtml(userTeam.name) : 'Unknown'}

+

Created: ${formatDate(user.created_at)}

+

ID: ${user.id}

+
+
+
Permissions
+

Admin Access: ${user.is_admin ? 'Yes' : 'No'}

+

Status: Active

+
+
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('viewUserModal', 'User Details', modalBody, modalFooter); + modal.show(); + + } catch (error) { + handleApiError(error, 'loading user details'); + } +} + +// Edit user +async function editUser(userId) { + try { + const users = await apiClient.getUsers(); + const user = users.find(u => u.id === userId); + + if (!user) { + showAlert('User not found', 'danger'); + return; + } + + const teams = await apiClient.getTeams(); + + const modalBody = ` +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
Leave blank to keep current password
+
+
+ `; + + const modalFooter = ` + + + `; + + const modal = createModal('editUserModal', 'Edit User', modalBody, modalFooter); + + // Handle form submission + document.getElementById('editUserForm').addEventListener('submit', (e) => { + e.preventDefault(); + saveUserChanges(userId); + }); + + modal.show(); + + } catch (error) { + handleApiError(error, 'loading user for editing'); + } +} + +// Save user changes +async function saveUserChanges(userId) { + const name = document.getElementById('editUserName').value.trim(); + const email = document.getElementById('editUserEmail').value.trim(); + const teamId = document.getElementById('editUserTeam').value; + const isAdmin = document.getElementById('editUserIsAdmin').checked; + const password = document.getElementById('editUserPassword').value; + + if (!name || !email || !teamId) { + showAlert('Name, email, and team are required', 'danger'); + return; + } + + const saveButton = document.querySelector('#editUserModal .btn-primary'); + setLoadingState(saveButton); + + try { + const updateData = { + name, + email, + team_id: teamId, + is_admin: isAdmin + }; + + if (password) { + if (password.length < 8) { + showAlert('Password must be at least 8 characters long', 'danger'); + return; + } + updateData.password = password; + } + + await apiClient.updateUser(userId, updateData); + + showAlert('User updated successfully!', 'success'); + + // Close modal and refresh users + bootstrap.Modal.getInstance(document.getElementById('editUserModal')).hide(); + removeModal('editUserModal'); + loadUsers(); + + // Close view modal if open + const viewModal = document.getElementById('viewUserModal'); + if (viewModal) { + bootstrap.Modal.getInstance(viewModal)?.hide(); + removeModal('viewUserModal'); + } + + } catch (error) { + handleApiError(error, 'updating user'); + } finally { + setLoadingState(saveButton, false); + } +} + +// Delete user +function deleteUser(userId) { + confirmAction('Are you sure you want to delete this user? This action cannot be undone.', async () => { + try { + await apiClient.deleteUser(userId); + showAlert('User deleted successfully!', 'success'); + loadUsers(); + + // Close any open modals + const modals = ['viewUserModal', 'editUserModal']; + modals.forEach(modalId => { + const modalElement = document.getElementById(modalId); + if (modalElement) { + bootstrap.Modal.getInstance(modalElement)?.hide(); + removeModal(modalId); + } + }); + + } catch (error) { + handleApiError(error, 'deleting user'); + } + }); +} + +// Add CSS for avatar circles +document.addEventListener('DOMContentLoaded', () => { + const style = document.createElement('style'); + style.textContent = ` + .avatar-circle { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 14px; + } + + .avatar-circle-large { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 32px; + } + `; + document.head.appendChild(style); +}); \ No newline at end of file diff --git a/client/requirements.txt b/client/requirements.txt index bbe3354..2707298 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -1,3 +1,22 @@ -Flask==2.3.3 -requests==2.31.0 -Werkzeug==2.3.7 \ No newline at end of file +# SeReact Frontend Client +# +# This is a pure frontend application that runs in the browser. +# No Python dependencies are required. +# +# To serve the files locally for development, you can use: +# +# Python 3: +# python -m http.server 8080 +# +# Python 2: +# python -m SimpleHTTPServer 8080 +# +# Node.js: +# npx http-server -p 8080 +# +# Or deploy to any static hosting service like: +# - Netlify +# - Vercel +# - GitHub Pages +# - AWS S3 +# - Azure Static Web Apps \ No newline at end of file diff --git a/client/serve.py b/client/serve.py new file mode 100644 index 0000000..129da71 --- /dev/null +++ b/client/serve.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Simple HTTP server for serving the SeReact frontend during development. +""" + +import http.server +import socketserver +import webbrowser +import os +import sys +from pathlib import Path + +# Configuration +PORT = 8080 +HOST = 'localhost' + +class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + """Custom handler to serve index.html for SPA routing""" + + def end_headers(self): + # Add CORS headers for development + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key') + super().end_headers() + + def do_OPTIONS(self): + # Handle preflight requests + self.send_response(200) + self.end_headers() + +def main(): + # Change to the directory containing this script + script_dir = Path(__file__).parent + os.chdir(script_dir) + + # Check if index.html exists + if not Path('index.html').exists(): + print("Error: index.html not found in current directory") + print(f"Current directory: {os.getcwd()}") + sys.exit(1) + + # Create server + with socketserver.TCPServer((HOST, PORT), CustomHTTPRequestHandler) as httpd: + server_url = f"http://{HOST}:{PORT}" + + print(f"🚀 SeReact Frontend Development Server") + print(f"📁 Serving files from: {os.getcwd()}") + print(f"🌐 Server running at: {server_url}") + print(f"📱 Open in browser: {server_url}") + print(f"⏹️ Press Ctrl+C to stop the server") + print() + + # Try to open browser automatically + try: + webbrowser.open(server_url) + print("✅ Browser opened automatically") + except Exception as e: + print(f"⚠️ Could not open browser automatically: {e}") + + print() + print("🔧 Development Tips:") + print(" - Edit files and refresh browser to see changes") + print(" - Check browser console (F12) for errors") + print(" - Configure API settings in the app") + print() + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n🛑 Server stopped by user") + print("👋 Thanks for using SeReact!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/client/styles.css b/client/styles.css new file mode 100644 index 0000000..f05fc92 --- /dev/null +++ b/client/styles.css @@ -0,0 +1,300 @@ +/* Custom styles for SeReact Frontend */ + +:root { + --primary-color: #0d6efd; + --secondary-color: #6c757d; + --success-color: #198754; + --info-color: #0dcaf0; + --warning-color: #ffc107; + --danger-color: #dc3545; + --light-color: #f8f9fa; + --dark-color: #212529; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; +} + +.navbar-brand { + font-weight: bold; + font-size: 1.5rem; +} + +.page { + min-height: 70vh; +} + +.jumbotron { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 15px; +} + +.card { + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + transition: box-shadow 0.15s ease-in-out; +} + +.card:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +.btn { + border-radius: 8px; + font-weight: 500; + transition: all 0.2s ease-in-out; +} + +.btn:hover { + transform: translateY(-1px); +} + +.image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.image-card { + border-radius: 12px; + overflow: hidden; + transition: transform 0.2s ease-in-out; +} + +.image-card:hover { + transform: scale(1.02); +} + +.image-card img { + width: 100%; + height: 200px; + object-fit: cover; +} + +.search-result { + border-left: 4px solid var(--primary-color); + background-color: white; + margin-bottom: 1rem; + border-radius: 0 8px 8px 0; +} + +.similarity-score { + background: linear-gradient(90deg, var(--success-color), var(--warning-color), var(--danger-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: bold; +} + +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.alert { + border-radius: 8px; + border: none; +} + +.modal-content { + border-radius: 12px; + border: none; +} + +.form-control, .form-select { + border-radius: 8px; + border: 1px solid #dee2e6; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus, .form-select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.table { + border-radius: 8px; + overflow: hidden; +} + +.table thead th { + background-color: var(--light-color); + border-bottom: 2px solid var(--primary-color); + font-weight: 600; +} + +.badge { + font-size: 0.75em; + padding: 0.5em 0.75em; + border-radius: 6px; +} + +.nav-link { + border-radius: 6px; + margin: 0 2px; + transition: background-color 0.2s ease-in-out; +} + +.nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.nav-link.active { + background-color: rgba(255, 255, 255, 0.2); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .image-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + + .jumbotron { + padding: 2rem 1rem; + } + + .display-4 { + font-size: 2rem; + } +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--secondary-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--primary-color); +} + +/* Animation for page transitions */ +.page { + animation: fadeIn 0.3s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* File upload area */ +.upload-area { + border: 2px dashed var(--primary-color); + border-radius: 12px; + padding: 2rem; + text-align: center; + background-color: rgba(13, 110, 253, 0.05); + transition: all 0.2s ease-in-out; + cursor: pointer; +} + +.upload-area:hover { + background-color: rgba(13, 110, 253, 0.1); + border-color: var(--primary-color); +} + +.upload-area.dragover { + background-color: rgba(13, 110, 253, 0.15); + border-color: var(--primary-color); + transform: scale(1.02); +} + +/* Status indicators */ +.status-online { + color: var(--success-color); +} + +.status-offline { + color: var(--danger-color); +} + +.status-pending { + color: var(--warning-color); +} + +/* Tag styles */ +.tag { + display: inline-block; + background-color: var(--light-color); + color: var(--dark-color); + padding: 0.25rem 0.5rem; + margin: 0.125rem; + border-radius: 12px; + font-size: 0.875rem; + border: 1px solid var(--secondary-color); +} + +.tag-input { + border: none; + outline: none; + background: transparent; + min-width: 100px; +} + +/* Progress bars */ +.progress { + height: 8px; + border-radius: 4px; + background-color: var(--light-color); +} + +.progress-bar { + border-radius: 4px; +} + +/* Utility classes */ +.text-muted-light { + color: #6c757d !important; +} + +.bg-gradient-primary { + background: linear-gradient(135deg, var(--primary-color), #0056b3); +} + +.bg-gradient-success { + background: linear-gradient(135deg, var(--success-color), #146c43); +} + +.bg-gradient-info { + background: linear-gradient(135deg, var(--info-color), #0aa2c0); +} + +.shadow-sm-hover:hover { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.cursor-pointer { + cursor: pointer; +} + +.user-select-none { + user-select: none; +} \ No newline at end of file diff --git a/client/templates/api_key_form.html b/client/templates/api_key_form.html deleted file mode 100644 index 9824202..0000000 --- a/client/templates/api_key_form.html +++ /dev/null @@ -1,111 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Create API Key - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- Create API Key -
-
-
-
-
- - -
- A descriptive name for this API key -
-
- -
- - -
- Optional description of the key's purpose or usage -
-
- -
- - -
- Leave empty to use your current team. Admin users can select any team. -
-
- -
- - -
- Leave empty to create the key for yourself. Admin users can create keys for other users. -
-
- -
- - Back to API Keys - - -
-
-
-
- -
-
-
- Important Information -
-
-
-
-
-
Security
-
    -
  • API keys provide full access to your account
  • -
  • The key value will only be shown once
  • -
  • Store the key securely and never share it
  • -
  • Revoke keys that are no longer needed
  • -
-
-
-
Expiration
-
    -
  • API keys have automatic expiration dates
  • -
  • You'll need to create new keys when they expire
  • -
  • Monitor key usage and expiration dates
  • -
  • Inactive keys are automatically disabled
  • -
-
-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/client/templates/api_keys.html b/client/templates/api_keys.html deleted file mode 100644 index 3b99d70..0000000 --- a/client/templates/api_keys.html +++ /dev/null @@ -1,101 +0,0 @@ -{% extends "base.html" %} - -{% block title %}API Keys - SEREACT Web Client{% endblock %} - -{% block content %} -
-

API Keys

- - Create API Key - -
- -{% if api_keys %} -
- {% for key in api_keys %} -
-
-
-
- {{ key.name }} - {% if not key.is_active %} - Inactive - {% endif %} -
-

{{ key.description or 'No description provided' }}

-
-
Team ID: {{ key.team_id }}
-
User ID: {{ key.user_id }}
-
Created: {{ key.created_at.strftime('%Y-%m-%d %H:%M') if key.created_at else 'Unknown' }}
- {% if key.expiry_date %} -
Expires: {{ key.expiry_date.strftime('%Y-%m-%d %H:%M') }}
- {% endif %} - {% if key.last_used %} -
Last used: {{ key.last_used.strftime('%Y-%m-%d %H:%M') }}
- {% endif %} -
-
- -
-
- {% endfor %} -
-{% else %} -
- -

No API Keys Found

-

Create your first API key to get started.

- - Create First API Key - -
-{% endif %} - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/base.html b/client/templates/base.html deleted file mode 100644 index ec9f530..0000000 --- a/client/templates/base.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - {% block title %}SEREACT Web Client{% endblock %} - - - - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} - - {% block content %}{% endblock %} -
- -
-
-

SEREACT Web Client - Secure Image Management

-
-
- - - {% block scripts %}{% endblock %} - - \ No newline at end of file diff --git a/client/templates/bootstrap.html b/client/templates/bootstrap.html deleted file mode 100644 index 517bd66..0000000 --- a/client/templates/bootstrap.html +++ /dev/null @@ -1,140 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Bootstrap - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- Bootstrap Initial Setup -
-
-
- - -
-
- - -
- The name of your initial team/organization -
-
- -
- - -
- Full name of the administrator user -
-
- -
- - -
- Email address for the administrator user -
-
- -
- - -
- A descriptive name for the initial API key -
-
- -
- - Back - - -
-
-
-
- -
-
-
- Important Notes -
-
-
-
-
-
Security
-
    -
  • This endpoint should be disabled in production after initial setup
  • -
  • The generated API key will be displayed only once
  • -
  • Make sure to save the API key securely
  • -
-
-
-
What This Creates
-
    -
  • Initial team with the specified name
  • -
  • Admin user with full privileges
  • -
  • API key for accessing the system
  • -
  • Automatic configuration of this client
  • -
-
-
-
-
- -
-
-
- Prerequisites -
-
-
-

Before running bootstrap, ensure:

-
-
-
    -
  • SEREACT API server is running
  • -
  • Database (Firestore) is configured
  • -
  • No existing teams in the system
  • -
-
-
-
    -
  • API base URL is configured correctly
  • -
  • Network connectivity to API server
  • -
  • All required services are available
  • -
-
-
- -
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/client/templates/config.html b/client/templates/config.html deleted file mode 100644 index cbd25e3..0000000 --- a/client/templates/config.html +++ /dev/null @@ -1,150 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Configuration - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- API Configuration -
-
-
-
-
- - -
- The base URL of your SEREACT API server (without /api/v1) -
-
- -
- -
- - -
-
- Your SEREACT API key for authentication -
-
- -
- - Back - - -
-
-
-
- -
-
-
- Configuration Help -
-
-
-
-
-
API Base URL
-

- This should point to your running SEREACT API server. Common examples: -

-
    -
  • http://localhost:8000 - Local development
  • -
  • https://your-api.example.com - Production server
  • -
  • http://192.168.1.100:8000 - Network server
  • -
-
-
-
API Key
-

- You can obtain an API key by: -

-
    -
  • Using the Bootstrap process for initial setup
  • -
  • Creating one through the API Keys management page
  • -
  • Having an admin create one for you
  • -
-
-
-
-
- - {% if api_key %} -
-
-
- Connection Test -
-
-
-

- - Configuration appears to be set. You can test the connection by trying to access any of the management pages. -

- -
-
- {% else %} -
-
-
- No API Key Set -
-
-
-

- You need to set an API key to use this client. If this is your first time: -

- - Start with Bootstrap - -
-
- {% endif %} -
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/image_detail.html b/client/templates/image_detail.html deleted file mode 100644 index 7ac4790..0000000 --- a/client/templates/image_detail.html +++ /dev/null @@ -1,246 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ image.filename }} - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- {{ image.filename }} -
- -
-
- {{ image.filename }} -
-
-
- -
- -
-
-
- Image Information -
-
-
-
-
Filename:
-
{{ image.filename }}
- -
Original:
-
{{ image.original_filename }}
- -
Size:
-
{{ (image.file_size / 1024 / 1024) | round(2) }} MB
- -
Type:
-
{{ image.content_type }}
- -
Uploaded:
-
{{ image.upload_date.strftime('%Y-%m-%d %H:%M') if image.upload_date else 'Unknown' }}
- -
Uploader:
-
{{ image.uploader_id }}
- -
Team:
-
{{ image.team_id }}
- - {% if image.collection_id %} -
Collection:
-
{{ image.collection_id }}
- {% endif %} -
-
-
- - -
-
-
- AI Processing Status -
-
-
- {% if image.has_embedding %} - - {% else %} - - {% endif %} -
-
- - - {% if image.description %} -
-
-
- Description -
-
-
-

{{ image.description }}

-
-
- {% endif %} - - - {% if image.tags %} -
-
-
- Tags -
-
-
- {% for tag in image.tags %} - {{ tag }} - {% endfor %} -
-
- {% endif %} - - - {% if image.metadata %} -
-
-
- Technical Metadata -
-
-
-
{{ image.metadata | tojson(indent=2) }}
-
-
- {% endif %} - - -
-
-
- Actions -
-
-
-
- - Edit Image - - - Download Original - - {% if image.has_embedding %} - - {% endif %} - -
-
-
-
-
- - - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/image_edit.html b/client/templates/image_edit.html deleted file mode 100644 index 2789c77..0000000 --- a/client/templates/image_edit.html +++ /dev/null @@ -1,186 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Edit {{ image.filename }} - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- Edit Image -
-
-
-
-
- - -
- A description of what the image contains or represents -
-
- -
- - -
- Tags help organize and search for images. Example: nature, landscape, sunset -
-
- -
- - Back to Image - - -
-
-
-
-
- -
- -
-
-
- Image Preview -
-
-
- {{ image.filename }} -
- {{ image.filename }} -
-
-
- - -
-
-
- Current Information -
-
-
-
-
Size:
-
{{ (image.file_size / 1024 / 1024) | round(2) }} MB
- -
Type:
-
{{ image.content_type }}
- -
Uploaded:
-
{{ image.upload_date.strftime('%Y-%m-%d') if image.upload_date else 'Unknown' }}
- -
AI Status:
-
- {% if image.has_embedding %} - - Processed - - {% else %} - - Processing - - {% endif %} -
-
-
-
- - - {% if image.tags %} -
-
-
- Current Tags -
-
-
- {% for tag in image.tags %} - {{ tag }} - {% endfor %} -
-
- {% endif %} - - -
-
-
- Quick Actions -
-
-
-
- - View Full Details - - - Download Image - - {% if image.has_embedding %} - - Search Similar - - {% endif %} -
-
-
-
-
- -
-
-
-
-
- Tips for Better Metadata -
-
-
-
-
-
Descriptions
-
    -
  • Describe what you see in the image
  • -
  • Include context and important details
  • -
  • Mention people, objects, and activities
  • -
  • Use natural, descriptive language
  • -
-
-
-
Tags
-
    -
  • Use specific, descriptive tags
  • -
  • Include objects, colors, emotions, locations
  • -
  • Separate multiple tags with commas
  • -
  • Use consistent naming conventions
  • -
-
-
- -
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/client/templates/image_upload.html b/client/templates/image_upload.html deleted file mode 100644 index 0e7336a..0000000 --- a/client/templates/image_upload.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Upload Image - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- Upload Image -
-
-
-
-
- - -
- Supported formats: PNG, JPG, JPEG, GIF, BMP, WebP. Maximum size: 10MB. -
-
- -
- - -
- A description of what the image contains or represents -
-
- -
- - -
- Tags help organize and search for images. Example: nature, landscape, sunset -
-
- -
- - -
- Optional collection ID to group related images -
-
- -
- - Back to Images - - -
-
-
-
- -
-
-
- Upload Information -
-
-
-
-
-
AI Processing
-
    -
  • Images are automatically processed with AI
  • -
  • Embeddings are generated for semantic search
  • -
  • Processing happens asynchronously in the background
  • -
  • You can search for images once processing is complete
  • -
-
-
-
Storage
-
    -
  • Images are securely stored in Google Cloud Storage
  • -
  • Access is controlled by team membership
  • -
  • Metadata is stored in the database
  • -
  • Original filenames and formats are preserved
  • -
-
-
-
-
- -
-
-
- Tips for Better Results -
-
-
-
-
-
Tagging
-
    -
  • Use descriptive, specific tags
  • -
  • Include objects, colors, emotions, locations
  • -
  • Separate tags with commas
  • -
  • Use consistent naming conventions
  • -
-
-
-
Searchability
-
    -
  • Good descriptions improve search accuracy
  • -
  • Include context and important details
  • -
  • Mention people, objects, and activities
  • -
  • Use natural language descriptions
  • -
-
-
-
-
-
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/images.html b/client/templates/images.html deleted file mode 100644 index 733bd69..0000000 --- a/client/templates/images.html +++ /dev/null @@ -1,192 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Images - SEREACT Web Client{% endblock %} - -{% block content %} -
-

Images

- - Upload Image - -
- - -
-
-
-
- - -
-
- - - Clear - -
-
-
-
- -{% if images %} - -
- {% for image in images %} -
-
-
- {{ image.filename }} -
-
-
- {{ image.filename }} -
-

- {{ image.description or 'No description' }} -

-
-
{{ (image.file_size / 1024 / 1024) | round(2) }} MB
-
{{ image.upload_date.strftime('%Y-%m-%d') if image.upload_date else 'Unknown' }}
- {% if image.tags %} -
- {% for tag in image.tags %} - {{ tag }} - {% endfor %} -
- {% endif %} - {% if image.has_embedding %} -
- - AI Ready - -
- {% endif %} -
-
- -
-
- {% endfor %} -
- - -{% if total > limit %} - - -
- Showing {{ ((page - 1) * limit + 1) }} to {{ [page * limit, total] | min }} of {{ total }} images -
-{% endif %} - -{% else %} -
- -

No Images Found

-

Upload your first image to get started.

- - Upload First Image - -
-{% endif %} - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/index.html b/client/templates/index.html deleted file mode 100644 index a596487..0000000 --- a/client/templates/index.html +++ /dev/null @@ -1,141 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Home - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-

- SEREACT Web Client -

-

Secure Image Management with AI-Powered Search

-
-

Upload, organize, and search your images using advanced AI embeddings and vector similarity.

-
-
-
- -
-
-
-
- -
Upload Images
-

Upload and organize your images with metadata and tags.

- - Upload - -
-
-
- -
-
-
- -
Search Images
-

Find images using AI-powered semantic search capabilities.

- - Search - -
-
-
- -
-
-
- -
Browse Images
-

View and manage all your uploaded images in one place.

- - Browse - -
-
-
- -
-
-
- -
Management
-

Manage teams, users, and API keys for your organization.

- -
-
-
-
- -
-
-
-
-
- Getting Started -
-
-
-
-
-
First Time Setup
-
    -
  1. Configure your API endpoint in Settings
  2. -
  3. Use Bootstrap to create initial team and admin user
  4. -
  5. Start uploading and searching images!
  6. -
-
-
-
API Configuration
-

Make sure to:

-
    -
  • Set the correct API base URL
  • -
  • Have a valid API key
  • -
  • Ensure your SEREACT API is running
  • -
-
-
-
-
-
-
- -
-
-
-
-
- Features -
-
-
-
-
-
AI-Powered Search
-

Search images using natural language queries powered by Google Cloud Vision API embeddings.

-
-
-
Secure Storage
-

Images are securely stored in Google Cloud Storage with team-based access control.

-
-
-
Rich Metadata
-

Organize images with descriptions, tags, and automatic metadata extraction.

-
-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/client/templates/search.html b/client/templates/search.html deleted file mode 100644 index ff09102..0000000 --- a/client/templates/search.html +++ /dev/null @@ -1,264 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Search Images - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- AI-Powered Image Search -
-
-
-
-
-
- - -
- Use natural language to describe the image content you're looking for -
-
-
- -
-
- - -
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
-
- -{% if query %} -
-
-
-

- Search Results for "{{ query }}" - {% if total is defined %} - {{ total }} found - {% endif %} -

- - Clear Search - -
-
-
- -{% if results %} -
- {% for image in results %} -
-
-
- {{ image.filename }} -
- - - {% if image.similarity_score %} -
-
- Similarity - {{ (image.similarity_score * 100) | round(1) }}% -
-
-
-
-
-
- {% endif %} - -
-
- {{ image.filename }} -
-

- {{ image.description or 'No description' }} -

-
-
{{ (image.file_size / 1024 / 1024) | round(2) }} MB
-
{{ image.upload_date.strftime('%Y-%m-%d') if image.upload_date else 'Unknown' }}
- {% if image.tags %} -
- {% for tag in image.tags %} - {{ tag }} - {% endfor %} -
- {% endif %} -
-
- -
-
- {% endfor %} -
-{% else %} -
- -

No Results Found

-

Try adjusting your search query or reducing the similarity threshold.

-
- -
-
-{% endif %} - -{% else %} - -
-
-
-
-
- Search Examples -
-
-
-

Try these example searches to see how AI-powered semantic search works:

-
-
-
Visual Content
-
- - - - -
-
-
-
Colors & Moods
-
- - - - -
-
-
-
-
-
Places & Objects
-
- - - - -
-
-
-
Activities & Emotions
-
- - - - -
-
-
-
-
-
-
- -
-
-
-
-
- How AI Search Works -
-
-
-
-
-
Semantic Understanding
-

The AI understands the meaning behind your words, not just exact matches. It can find images based on concepts, emotions, and visual elements.

-
-
-
Vector Similarity
-

Images are converted to high-dimensional vectors that capture visual features. Search finds images with similar vector representations.

-
-
-
Adjustable Precision
-

Use the similarity threshold to control how strict the matching is. Lower values return more results, higher values are more precise.

-
-
-
-
-
-
-{% endif %} -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/team_form.html b/client/templates/team_form.html deleted file mode 100644 index afd1d6c..0000000 --- a/client/templates/team_form.html +++ /dev/null @@ -1,145 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ action }} Team - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- {{ action }} Team -
-
-
-
-
- - -
- A unique name for this team -
-
- -
- - -
- Optional description of the team's purpose or role -
-
- - {% if team %} -
- -
-
-
-
- - Created: - {{ team.created_at.strftime('%Y-%m-%d %H:%M') if team.created_at else 'Unknown' }} - -
-
- - Updated: - {{ team.updated_at.strftime('%Y-%m-%d %H:%M') if team.updated_at else 'Unknown' }} - -
-
-
- - ID: {{ team.id }} - -
-
-
-
- {% endif %} - -
- - Back to Teams - - -
-
-
-
- - {% if action == 'Edit' %} -
-
-
- Danger Zone -
-
-
-

- Deleting a team is permanent and cannot be undone. All associated data may be affected. -

- -
-
- - - - {% endif %} -
-
-{% endblock %} - -{% block scripts %} -{% if action == 'Edit' %} - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/client/templates/teams.html b/client/templates/teams.html deleted file mode 100644 index 964129d..0000000 --- a/client/templates/teams.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Teams - SEREACT Web Client{% endblock %} - -{% block content %} - - -{% if teams %} -
- {% for team in teams %} -
-
-
-
- {{ team.name }} -
-

{{ team.description or 'No description provided' }}

-
-
Created: {{ team.created_at.strftime('%Y-%m-%d %H:%M') if team.created_at else 'Unknown' }}
- {% if team.updated_at and team.updated_at != team.created_at %} -
Updated: {{ team.updated_at.strftime('%Y-%m-%d %H:%M') }}
- {% endif %} -
-
- -
-
- {% endfor %} -
-{% else %} -
- -

No Teams Found

-

Create your first team to get started.

- - Create First Team - -
-{% endif %} - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/client/templates/user_form.html b/client/templates/user_form.html deleted file mode 100644 index 97843ab..0000000 --- a/client/templates/user_form.html +++ /dev/null @@ -1,182 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ action }} User - SEREACT Web Client{% endblock %} - -{% block content %} -
-
-
-
-
- {{ action }} User -
-
-
-
-
- - -
- -
- - -
- - {% if action == 'Create' %} -
- - -
- Leave empty to assign to current user's team -
-
- {% endif %} - -
-
- - -
- Administrators have full access to all system features -
-
-
- - {% if user %} -
- -
-
-
-
- - ID: {{ user.id }} - -
-
- - Team ID: {{ user.team_id }} - -
-
-
-
- - Created: - {{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'Unknown' }} - -
-
- - - {{ 'Active' if user.is_active else 'Inactive' }} - -
-
-
-
-
- {% endif %} - -
- - Back to Users - - -
-
-
-
- - {% if action == 'Edit' %} -
-
-
- Danger Zone -
-
-
-

- Deleting a user is permanent and cannot be undone. All user data and associated content will be affected. -

- -
-
- - - - {% endif %} -
-
-{% endblock %} - -{% block scripts %} -{% if action == 'Edit' %} - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/client/templates/users.html b/client/templates/users.html deleted file mode 100644 index 15c79f6..0000000 --- a/client/templates/users.html +++ /dev/null @@ -1,112 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Users - SEREACT Web Client{% endblock %} - -{% block content %} - - -{% if users %} -
- {% for user in users %} -
-
-
-
- {{ user.name }} - {% if user.is_admin %} - - Admin - - {% endif %} -
-

- {{ user.email }} -

-

- Team ID: {{ user.team_id }} -

-
-
- - {{ 'Active' if user.is_active else 'Inactive' }} -
-
Created: {{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'Unknown' }}
-
-
- -
-
- {% endfor %} -
-{% else %} -
- -

No Users Found

-

Create your first user to get started.

- - Create First User - -
-{% endif %} - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index d6bc3a3..4a63906 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -82,6 +82,60 @@ PROJECT_ID=my-project-id REGION=us-west1 IMAGE_TAG=v1.0.0 ./scripts/deploy-to-cl - `BUILD`: Set to "true" to build the image before deployment (default: "false") - `PUSH`: Set to "true" to push the image before deployment (default: "false") +### Frontend Client Script (`client.sh`) + +Manages the SeReact frontend client development, building, and deployment. + +**Usage:** +```bash +# Start development server +./scripts/client.sh dev +# or +./scripts/client.sh serve + +# Build production files +./scripts/client.sh build + +# Deploy to static hosting +DEPLOY_TARGET=netlify ./scripts/client.sh deploy + +# Clean build artifacts +./scripts/client.sh clean + +# Install dependencies +./scripts/client.sh install + +# Check code quality +./scripts/client.sh lint + +# Run tests +./scripts/client.sh test + +# Show help +./scripts/client.sh help +``` + +**Environment Variables:** +- `PORT`: Development server port (default: 8080) +- `HOST`: Development server host (default: localhost) +- `DEPLOY_TARGET`: Deployment target - netlify, vercel, s3, github, or manual (default: manual) +- `S3_BUCKET`: S3 bucket name (required for S3 deployment) + +**Features:** +- **Development Server**: Starts a local HTTP server with CORS headers for frontend development +- **Production Build**: Creates optimized production files in `dist/` directory +- **Multiple Deployment Options**: Supports Netlify, Vercel, AWS S3, GitHub Pages, and manual deployment +- **Code Quality**: Integrates with ESLint, Stylelint, and HTML Tidy (if installed) +- **Dependency Management**: Handles Python virtual environment setup +- **Clean Build**: Removes build artifacts and cache files + +**Deployment Targets:** +- `netlify`: Deploy using Netlify CLI +- `vercel`: Deploy using Vercel CLI +- `s3`: Deploy to AWS S3 bucket (requires AWS CLI and S3_BUCKET env var) +- `github`: Shows instructions for GitHub Pages deployment +- `manual`: Shows manual deployment instructions + ## Example Workflows ### Basic workflow: @@ -99,6 +153,21 @@ DEPLOY_TO_CLOUD_RUN=true PROJECT_ID=my-project-id IMAGE_TAG=v1.0.0 ./scripts/dep PROJECT_ID=my-project-id BUILD=true PUSH=true ./scripts/deploy-to-cloud-run.sh ``` +### Frontend Development Workflow: +```bash +# Install dependencies +./scripts/client.sh install + +# Start development server +./scripts/client.sh dev + +# Build for production +./scripts/client.sh build + +# Deploy to Netlify +DEPLOY_TARGET=netlify ./scripts/client.sh deploy +``` + # Scripts Documentation This directory contains utility scripts for the SEREACT application. diff --git a/scripts/client.sh b/scripts/client.sh new file mode 100644 index 0000000..a24fb53 --- /dev/null +++ b/scripts/client.sh @@ -0,0 +1,322 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration +CLIENT_DIR="client" +DIST_DIR="dist" +PORT=${PORT:-8080} +HOST=${HOST:-localhost} + +# Print colored output +print_color() { + local color=$1 + local message=$2 + echo -e "${color}${message}${NC}" +} + +print_header() { + echo + print_color $CYAN "🚀 SeReact Frontend Client Manager" + echo +} + +print_usage() { + print_header + echo "Usage: $0 [COMMAND]" + echo + echo "Commands:" + echo " dev, serve Start development server" + echo " build Build production files" + echo " deploy Deploy to static hosting" + echo " clean Clean build artifacts" + echo " install Install dependencies" + echo " lint Check code quality" + echo " test Run tests (if available)" + echo " help Show this help message" + echo + echo "Environment Variables:" + echo " PORT Development server port (default: 8080)" + echo " HOST Development server host (default: localhost)" + echo " DEPLOY_TARGET Deployment target (netlify, vercel, s3, github)" + echo +} + +# Check if we're in the right directory +check_directory() { + if [ ! -d "$CLIENT_DIR" ]; then + print_color $RED "❌ Error: Client directory not found!" + print_color $YELLOW " Make sure you're running this from the project root." + exit 1 + fi +} + +# Start development server +start_dev_server() { + print_color $GREEN "🔧 Starting development server..." + + cd "$CLIENT_DIR" + + # Check if index.html exists + if [ ! -f "index.html" ]; then + print_color $RED "❌ Error: index.html not found in client directory" + exit 1 + fi + + # Check if Python is available + if command -v python3 &> /dev/null; then + print_color $BLUE "🐍 Using Python development server" + python3 serve.py + elif command -v python &> /dev/null; then + print_color $BLUE "🐍 Using Python development server" + python serve.py + else + print_color $YELLOW "⚠️ Python not found, using basic HTTP server" + # Fallback to basic HTTP server + if command -v npx &> /dev/null; then + npx http-server . -p $PORT --cors + else + print_color $RED "❌ No suitable HTTP server found" + print_color $YELLOW " Please install Python or Node.js" + exit 1 + fi + fi +} + +# Build production files +build_production() { + print_color $GREEN "🏗️ Building production files..." + + # Create dist directory + mkdir -p "$DIST_DIR" + + # Copy client files to dist + print_color $BLUE "📁 Copying files to $DIST_DIR..." + cp -r "$CLIENT_DIR"/* "$DIST_DIR/" + + # Remove development-only files + if [ -f "$DIST_DIR/serve.py" ]; then + rm "$DIST_DIR/serve.py" + print_color $YELLOW "🗑️ Removed serve.py (development only)" + fi + + if [ -d "$DIST_DIR/venv" ]; then + rm -rf "$DIST_DIR/venv" + print_color $YELLOW "🗑️ Removed venv directory (development only)" + fi + + if [ -f "$DIST_DIR/requirements.txt" ]; then + rm "$DIST_DIR/requirements.txt" + print_color $YELLOW "🗑️ Removed requirements.txt (development only)" + fi + + # Minify files if tools are available + if command -v terser &> /dev/null; then + print_color $BLUE "🗜️ Minifying JavaScript files..." + find "$DIST_DIR/js" -name "*.js" -exec terser {} -o {} \; + fi + + if command -v csso &> /dev/null; then + print_color $BLUE "🗜️ Minifying CSS files..." + find "$DIST_DIR" -name "*.css" -exec csso {} --output {} \; + fi + + print_color $GREEN "✅ Build completed successfully!" + print_color $CYAN "📦 Production files are in: $DIST_DIR/" +} + +# Deploy to static hosting +deploy_production() { + local target=${DEPLOY_TARGET:-"manual"} + + print_color $GREEN "🚀 Deploying to $target..." + + # Build first + build_production + + case $target in + "netlify") + if command -v netlify &> /dev/null; then + cd "$DIST_DIR" + netlify deploy --prod --dir . + else + print_color $RED "❌ Netlify CLI not found" + print_color $YELLOW " Install with: npm install -g netlify-cli" + fi + ;; + "vercel") + if command -v vercel &> /dev/null; then + cd "$DIST_DIR" + vercel --prod + else + print_color $RED "❌ Vercel CLI not found" + print_color $YELLOW " Install with: npm install -g vercel" + fi + ;; + "s3") + if command -v aws &> /dev/null; then + if [ -z "$S3_BUCKET" ]; then + print_color $RED "❌ S3_BUCKET environment variable not set" + exit 1 + fi + aws s3 sync "$DIST_DIR/" "s3://$S3_BUCKET/" --delete + print_color $GREEN "✅ Deployed to S3 bucket: $S3_BUCKET" + else + print_color $RED "❌ AWS CLI not found" + print_color $YELLOW " Install AWS CLI first" + fi + ;; + "github") + print_color $BLUE "📋 GitHub Pages deployment instructions:" + echo "1. Push the contents of $DIST_DIR/ to your gh-pages branch" + echo "2. Enable GitHub Pages in repository settings" + echo "3. Set source to gh-pages branch" + ;; + *) + print_color $BLUE "📋 Manual deployment instructions:" + echo "1. Upload the contents of $DIST_DIR/ to your web server" + echo "2. Configure your server to serve index.html for all routes" + echo "3. Ensure CORS is configured for API calls" + ;; + esac +} + +# Clean build artifacts +clean_build() { + print_color $YELLOW "🧹 Cleaning build artifacts..." + + if [ -d "$DIST_DIR" ]; then + rm -rf "$DIST_DIR" + print_color $GREEN "✅ Removed $DIST_DIR directory" + fi + + # Clean any cache directories + if [ -d "$CLIENT_DIR/__pycache__" ]; then + rm -rf "$CLIENT_DIR/__pycache__" + print_color $GREEN "✅ Removed Python cache" + fi + + print_color $GREEN "✅ Clean completed!" +} + +# Install dependencies +install_dependencies() { + print_color $GREEN "📦 Installing dependencies..." + + cd "$CLIENT_DIR" + + # Check if virtual environment exists + if [ ! -d "venv" ]; then + print_color $BLUE "🐍 Creating Python virtual environment..." + python3 -m venv venv + fi + + # Activate virtual environment + if [ -f "venv/Scripts/activate" ]; then + # Windows with Git Bash + source venv/Scripts/activate + else + # Linux/macOS + source venv/bin/activate + fi + + # Install Python dependencies + if [ -f "requirements.txt" ]; then + print_color $BLUE "📋 Installing Python packages..." + pip install -r requirements.txt + fi + + print_color $GREEN "✅ Dependencies installed!" +} + +# Lint code +lint_code() { + print_color $GREEN "🔍 Checking code quality..." + + cd "$CLIENT_DIR" + + # Check HTML + if command -v tidy &> /dev/null; then + print_color $BLUE "🔍 Checking HTML..." + tidy -q -e index.html || true + fi + + # Check JavaScript + if command -v eslint &> /dev/null; then + print_color $BLUE "🔍 Checking JavaScript..." + eslint js/*.js || true + fi + + # Check CSS + if command -v stylelint &> /dev/null; then + print_color $BLUE "🔍 Checking CSS..." + stylelint *.css || true + fi + + print_color $GREEN "✅ Code quality check completed!" +} + +# Run tests +run_tests() { + print_color $GREEN "🧪 Running tests..." + + cd "$CLIENT_DIR" + + # Check if test files exist + if [ -d "tests" ] || [ -f "test.html" ]; then + print_color $BLUE "🧪 Running frontend tests..." + # Add test runner here when tests are implemented + print_color $YELLOW "⚠️ No test runner configured yet" + else + print_color $YELLOW "⚠️ No tests found" + fi + + print_color $GREEN "✅ Tests completed!" +} + +# Main script logic +main() { + case "${1:-help}" in + "dev"|"serve") + check_directory + start_dev_server + ;; + "build") + check_directory + build_production + ;; + "deploy") + check_directory + deploy_production + ;; + "clean") + clean_build + ;; + "install") + check_directory + install_dependencies + ;; + "lint") + check_directory + lint_code + ;; + "test") + check_directory + run_tests + ;; + "help"|*) + print_usage + ;; + esac +} + +# Run main function with all arguments +main "$@" \ No newline at end of file