gustavehub / app.py
datacipen's picture
Update app.py
07dd43a verified
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
@staticmethod
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
@staticmethod
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
@login_manager.user_loader
def load_user(user_id):
user = User.get(user_id)
return user
@server.route('/login', methods=['GET', 'POST'])
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
@server.route('/logout')
def logout():
logout_user()
return redirect(url_for('login')) # Redirect to login page after logout
@app.server.before_request
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"}
)
]
)
]
)
@server.route("/start_rac_agent", methods=['POST'])
def start_rac_agent():
rac_graph_agent.invoke({})
return {"status": "started"}
@socketio.on('connect')
def handle_connect():
print('Client connected')
@socketio.on('disconnect')
def handle_disconnect():
print('Client disconnected')
@app.callback(
Output("history-drawer", "opened"), # Cible directement la propriété 'opened' du tiroir
Input("history-button", "n_clicks"),
State("history-drawer", "opened"), # Lit l'état actuel du tiroir
prevent_initial_call=True,
)
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
@app.callback(
Output('breadcrumb-container', 'children'),
[Input('url', 'pathname')]
)
def update_breadcrumb(pathname):
return create_breadcrumb(pathname)
@app.callback(
Output("mantine-provider", "theme"),
Output("app-shell", "navbar", allow_duplicate=True),
Input("theme-switch", "checked"),
Input("sidebar-burger", "opened"), # We'll keep this input for now for theme/header updates
prevent_initial_call=True,
)
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
@app.callback(
Output("main-header", "children"), # New callback for updating header children
Input("theme-switch", "checked"),
prevent_initial_call=True,
)
def update_header_theme(theme_checked):
return create_header(theme_checked).children
@app.callback(
Output("chatbot-drawer", "opened"), # Cible directement la propriété 'opened' du tiroir
Input("chatbot-button", "n_clicks"),
State("chatbot-drawer", "opened"), # Lit l'état actuel du tiroir
prevent_initial_call=True,
)
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
@app.callback(
Output("app-shell-main", "style", allow_duplicate=True),
Input("app-shell", "navbar"), # Changed input to app-shell's navbar property
State("theme-switch", "checked"),
prevent_initial_call='initial_duplicate',
)
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
@app.callback(
Output('url', 'href'),
Input('logout-button', 'n_clicks'),
prevent_initial_call=True
)
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)