diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..89f7c13 --- /dev/null +++ b/client/README.md @@ -0,0 +1,208 @@ +# SEREACT Web 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. + +## 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 +- **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 + +## Prerequisites + +- Python 3.8+ +- Running SEREACT API server +- Network access to the SEREACT API + +## Installation + +1. Navigate to the client directory: + ```bash + cd client + ``` + +2. Create and activate a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # Linux/macOS + venv\Scripts\activate # Windows + ``` + +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +## 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 + +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 + +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 + - 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 + +### Environment Variables + +You can set these environment variables to configure default values: + +```bash +export SEREACT_API_URL=http://localhost:8000 +export SEREACT_API_KEY=your-api-key +``` + +### Session Storage + +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 + +## API Integration + +The client integrates with the following SEREACT API endpoints: + +- `/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 + +## Security Notes + +- **API Keys**: Store API keys securely and never share them +- **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 + +## Troubleshooting + +### Common Issues + +1. **Connection Errors**: + - Verify the API base URL is correct + - Ensure the SEREACT API server is running + - Check network connectivity + +2. **Authentication Errors**: + - Verify your API key is valid and active + - Check if the API key has expired + - Ensure you have the necessary permissions + +3. **Image Upload Issues**: + - Check file size (max 10MB) + - Verify file format is supported + - Ensure sufficient storage space + +4. **Search Not Working**: + - Verify images have been processed (have embeddings) + - Check if the vector database is configured + - Try adjusting similarity thresholds + +### 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 +- **"File size exceeds limit"**: Choose a smaller image file + +## Development + +### Project Structure + +``` +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 + +## License + +This web client is part of the SEREACT project and follows the same license terms. \ No newline at end of file diff --git a/client/app.py b/client/app.py new file mode 100644 index 0000000..c2b688c --- /dev/null +++ b/client/app.py @@ -0,0 +1,423 @@ +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/requirements.txt b/client/requirements.txt new file mode 100644 index 0000000..bbe3354 --- /dev/null +++ b/client/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.3 +requests==2.31.0 +Werkzeug==2.3.7 \ No newline at end of file diff --git a/client/templates/api_key_form.html b/client/templates/api_key_form.html new file mode 100644 index 0000000..9824202 --- /dev/null +++ b/client/templates/api_key_form.html @@ -0,0 +1,111 @@ +{% 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 new file mode 100644 index 0000000..3b99d70 --- /dev/null +++ b/client/templates/api_keys.html @@ -0,0 +1,101 @@ +{% 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 new file mode 100644 index 0000000..ec9f530 --- /dev/null +++ b/client/templates/base.html @@ -0,0 +1,116 @@ + + + + + + {% 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 new file mode 100644 index 0000000..517bd66 --- /dev/null +++ b/client/templates/bootstrap.html @@ -0,0 +1,140 @@ +{% 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 new file mode 100644 index 0000000..cbd25e3 --- /dev/null +++ b/client/templates/config.html @@ -0,0 +1,150 @@ +{% 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 new file mode 100644 index 0000000..7ac4790 --- /dev/null +++ b/client/templates/image_detail.html @@ -0,0 +1,246 @@ +{% 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 new file mode 100644 index 0000000..2789c77 --- /dev/null +++ b/client/templates/image_edit.html @@ -0,0 +1,186 @@ +{% 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 new file mode 100644 index 0000000..0e7336a --- /dev/null +++ b/client/templates/image_upload.html @@ -0,0 +1,168 @@ +{% 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 new file mode 100644 index 0000000..733bd69 --- /dev/null +++ b/client/templates/images.html @@ -0,0 +1,192 @@ +{% 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 new file mode 100644 index 0000000..a596487 --- /dev/null +++ b/client/templates/index.html @@ -0,0 +1,141 @@ +{% 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 new file mode 100644 index 0000000..ff09102 --- /dev/null +++ b/client/templates/search.html @@ -0,0 +1,264 @@ +{% 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 new file mode 100644 index 0000000..afd1d6c --- /dev/null +++ b/client/templates/team_form.html @@ -0,0 +1,145 @@ +{% 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 new file mode 100644 index 0000000..964129d --- /dev/null +++ b/client/templates/teams.html @@ -0,0 +1,96 @@ +{% 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 new file mode 100644 index 0000000..97843ab --- /dev/null +++ b/client/templates/user_form.html @@ -0,0 +1,182 @@ +{% 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 new file mode 100644 index 0000000..15c79f6 --- /dev/null +++ b/client/templates/users.html @@ -0,0 +1,112 @@ +{% 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