Spaces:
Sleeping
Sleeping
import dash | |
from dash import Dash, html, dcc, Input, Output, State | |
import dash_mantine_components as dmc | |
from flask import Flask, redirect, url_for, request, session | |
from components.header import create_header | |
from components.sidebar import create_sidebar | |
from components.breadcrumb import create_breadcrumb | |
from components.chatbot import create_chatbot_panel | |
from components.footer import create_footer | |
from data.caching import setup_caching | |
from agents.rac_generation_agent import rac_graph_agent | |
from sockets import socketio, init_sockets | |
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required | |
from dotenv import load_dotenv | |
import os | |
import json | |
import datetime | |
from datetime import timedelta | |
load_dotenv() | |
server = Flask(__name__) | |
server.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'supersecretkey') # Default for dev | |
server.config['SESSION_COOKIE_SECURE'] = True | |
server.config['SESSION_COOKIE_HTTPONLY'] = True | |
server.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(seconds=3600) | |
login_manager = LoginManager() | |
login_manager.init_app(server) | |
login_manager.login_view = 'login' | |
app = Dash(__name__, server=server, use_pages=True, | |
external_scripts=[ | |
"https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" | |
]) | |
init_sockets(server) | |
users_data = json.loads(os.getenv('USERS', '{}')) | |
class User(UserMixin): | |
def __init__(self, id, username, password): | |
self.id = id | |
self.username = username | |
self.password = password | |
def get(user_id): | |
for user_data in users_data.values(): | |
if user_data['id'] == int(user_id): | |
return User(user_data['id'], user_data['username'], user_data['password']) | |
return None | |
def get_by_username(username): | |
for user_id, user_data in users_data.items(): | |
if user_data['username'] == username: | |
return User(user_data['id'], user_data['username'], user_data['password']) | |
return None | |
def load_user(user_id): | |
user = User.get(user_id) | |
return user | |
def login(): | |
if current_user.is_authenticated: | |
return redirect('/') # Changed from dash_app | |
error_message = None | |
if request.method == 'POST': | |
username = request.form['username'] | |
password = request.form['password'] | |
user = User.get_by_username(username) | |
if user and user.password == password: | |
login_user(user) | |
return redirect('/') # Redirect to the protected Dash app root | |
else: | |
error_message = 'Invalid username or password' | |
# HTML content for the login page | |
html_content = f''' | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Gustave IA - Create Account</title> | |
<style> | |
body {{ margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; background-color: #060621; }} | |
.container {{ display: flex; height: 100vh; }} | |
.left-panel {{ flex: 1; display: flex; justify-content: center; align-items: center; background-color: #060621; }} | |
.login-form-container {{ width: 400px; padding: 20px; color: white; }} | |
.logo {{ text-align: center; margin-bottom: 20px; }} | |
.social-button {{ width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #555; background-color: #333; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1rem; border-radius: 4px; }} | |
.social-button img {{ height: 16px; margin-right: 8px; }} | |
.or-separator {{ text-align: center; margin: 20px 0; color: #888; }} | |
.form-group {{ margin-bottom: 15px; }} | |
.form-group label {{ display: block; margin-bottom: 5px; font-size: 0.9em; color: #ccc; }} | |
.form-group input {{ width: calc(100% - 20px); padding: 10px; background-color: #2c2c2c; border: 1px solid #444; color: white; box-sizing: border-box; border-radius: 4px; }} | |
.checkbox-group {{ display: flex; align-items: center; margin-bottom: 20px; font-size: 0.85em; }} | |
.checkbox-group input {{ margin-right: 8px; }} | |
.checkbox-group a {{ color: #64B5F6; text-decoration: none; }} | |
.submit-button {{ width: 100%; padding: 10px; background-color: #3B5BDB; border: none; color: white; cursor: pointer; font-size: 1.1em; margin-bottom: 15px; border-radius: 4px; }} | |
.footer-links {{ text-align: center; font-size: 0.9em; }} | |
.footer-links p {{ margin-bottom: 5px; }} | |
.footer-links a {{ color: #64B5F6; text-decoration: none; }} | |
.right-panel {{ flex: 1; background: linear-gradient(45deg, #007bff 0%, #00c6ff 100%); display: flex; justify-content: center; align-items: flex-end; padding: 20px; | |
position: relative; /* Ajouté pour le positionnement absolu des enfants */ | |
}} | |
.chat-bubble {{ background-color: rgba(255, 255, 255, 0.2); padding: 10px 20px; border-radius: 20px; width: 80%; max-width: 400px; display: flex; align-items: center; justify-content: space-between; | |
position: absolute; /* Positionnement absolu dans le parent */ | |
bottom: 50px; /* Positionnement initial */ | |
left: 50%; /* Centrage initial */ | |
transform: translateX(-50%); /* Centrage initial */ | |
transition: all 4s ease-in-out; /* Transition douce pour un mouvement plus grand */ | |
}} | |
.chat-bubble span {{ color: white; }} | |
.chat-bubble button {{ background-color: rgba(255, 255, 255, 0.3); border: none; border-radius: 50%; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; cursor: pointer; }} | |
.error-message {{ color: #FF6B6B; text-align: center; margin-bottom: 15px; font-size: 0.9em; }} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="left-panel"> | |
<div class="login-form-container"> | |
<div class="logo" style="margin-bottom: 5px;"> | |
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<circle cx="12" cy="12" r="10"></circle> | |
<line x1="2" y1="12" x2="22" y2="12"></line> | |
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> | |
</svg> | |
</div> | |
<h2 style="text-align: center; margin-bottom: 10px; font-size: 50px; font-weight: 900; background-image: linear-gradient(45deg, #3B5BDB, #bf5abf, #b86bff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; color: transparent;">Gustave HUB</h2> | |
{f'<p class="error-message">{error_message}</p>' if error_message else ''} | |
<form method="post"> | |
<div class="form-group"> | |
<label for="username">Username</label> | |
<input type="text" id="username" name="username" placeholder="Username"> | |
</div> | |
<div class="form-group"> | |
<label for="password">Password</label> | |
<input type="password" id="password" name="password"> | |
</div> | |
<button type="submit" class="submit-button">Login</button> | |
</form> | |
<div style="text-align: center; font-size: 0.8em; color: white; margin-top: 20px;"> | |
<p>Propulsé par le CIPEN de l'université Gustave Eiffel</p> | |
<p>datacipen@univ-eiffel.fr</p> | |
</div> | |
</div> | |
</div> | |
<div class="right-panel"> | |
<div class="chat-bubble" id="animated-chat-bubble"> | |
<span>Gustave IA au service des analyses automatiques et des datavisualisations dynamiques.</span> | |
<button> | |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M12 4L20 12L12 20M12 4L4 12L12 20" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
<script src="/static/login_animations.js"></script> | |
</body> | |
</html> | |
''' | |
return html_content | |
def logout(): | |
logout_user() | |
return redirect(url_for('login')) # Redirect to login page after logout | |
def before_request(): | |
# Si la requête concerne la page de connexion ou si l'utilisateur est authentifié, autoriser | |
if request.endpoint == 'login' or current_user.is_authenticated or request.endpoint == 'logout': | |
return None | |
# Si la requête concerne les ressources internes de Dash (assets, suites de composants, callbacks, rechargement), autoriser | |
# C'est crucial pour que Dash fonctionne même sans authentification, mais uniquement pour les requêtes internes | |
if request.path.startswith('/assets/') or \ | |
request.path.startswith('/_dash-component-suites/') or \ | |
request.path.startswith('/_favicon.ico') or \ | |
request.path.startswith('/_dash-update-component') or \ | |
request.path.startswith('/_dash-reload-app') or \ | |
request.path.startswith('/static/'): | |
return None | |
# Pour toutes les autres requêtes (y compris toutes les pages Dash et la racine '/'), rediriger vers la connexion | |
return redirect(url_for('login')) | |
get_data = setup_caching(app) | |
# You can now use get_data('path/to/your/excel.xlsx') in your page layouts | |
# and the data will be cached. | |
app.layout = dmc.MantineProvider( | |
id="mantine-provider", | |
theme={ | |
"primaryColor": "indigo", | |
"colorScheme": "dark", | |
"components": { | |
"NavLink": { | |
"styles": { | |
"root": { | |
"&:hover": {"backgroundColor": "#181F2F"} | |
} | |
} | |
} | |
} | |
}, | |
forceColorScheme="dark", | |
withGlobalClasses=True, | |
children=[ | |
dcc.Location(id='url', refresh=True), | |
dcc.Store(id='client-session-id', data=None), | |
create_chatbot_panel(), | |
dmc.Drawer( | |
id="history-drawer", | |
title="Historique de consultation", | |
padding="md", | |
position="right", | |
opened=False, # Explicitly set to closed for initial load | |
children=[ | |
dmc.Text("Contenu de l'historique ici...") | |
], | |
), | |
dmc.AppShell( | |
id="app-shell", | |
padding="md", | |
header={"height": 70}, | |
footer={"height": 60,"zIndex": 1000,"bgcolor": "#060621"}, | |
navbar={ | |
"width": 300, | |
"breakpoint": "sm", | |
"collapsed": {"mobile": False, "desktop": False}, # Initialize as NOT collapsed for both mobile and desktop | |
}, | |
children=[ | |
create_header(theme_checked=True), # Pass theme_checked value | |
create_sidebar(), | |
create_footer(), | |
dmc.AppShellMain( | |
id="app-shell-main", # Added ID here | |
children=[ | |
html.Div(id='breadcrumb-container'), | |
dash.page_container | |
], | |
style={"backgroundColor": "#060621"} | |
) | |
] | |
) | |
] | |
) | |
def start_rac_agent(): | |
rac_graph_agent.invoke({}) | |
return {"status": "started"} | |
def handle_connect(): | |
print('Client connected') | |
def handle_disconnect(): | |
print('Client disconnected') | |
def toggle_history_drawer(n_clicks, is_opened): | |
if n_clicks is None: | |
return is_opened # Ne rien faire si ce n'est pas un clic | |
return not is_opened | |
def update_breadcrumb(pathname): | |
return create_breadcrumb(pathname) | |
def update_theme_and_navbar(theme_checked, sidebar_opened): | |
scheme = "dark" if theme_checked else "light" | |
theme = {"primaryColor": "indigo", "colorScheme": scheme} | |
# The navbar config will now be controlled by this callback for responsive behavior | |
navbar_config = {"width": 300, "breakpoint": "sm", "collapsed": {"mobile": sidebar_opened, "desktop": sidebar_opened}} | |
# style = {"backgroundColor": "#020817"} if theme_checked else {"backgroundColor": "#f8f9fa"} | |
# if sidebar_opened: | |
# style["paddingLeft"] = 300 | |
# else: | |
# style["paddingLeft"] = 80 | |
# return theme, navbar_config, create_header(theme_checked).children # Removed create_header.children | |
return theme, navbar_config | |
def update_header_theme(theme_checked): | |
return create_header(theme_checked).children | |
def toggle_chatbot(n_clicks, is_opened): | |
if n_clicks is None: | |
return is_opened # Ne rien faire si ce n'est pas un clic | |
return not is_opened | |
def update_main_content_padding(navbar_config, theme_checked): | |
style = {"backgroundColor": "#060621"} if theme_checked else {"backgroundColor": "#f8f9fa"} | |
# Extract collapsed state from navbar_config (assuming mobile collapsed state dictates padding) | |
is_collapsed = navbar_config.get("collapsed", {}).get("mobile", True) | |
if is_collapsed: | |
style["paddingLeft"] = 80 | |
else: | |
style["paddingLeft"] = 300 | |
return style | |
def redirect_on_logout(n_clicks): | |
if n_clicks: | |
return '/logout' | |
return dash.no_update | |
if __name__ == "__main__": | |
socketio.run(server, debug=True, port=5001) |