{% extends 'base.html.twig' %} {% block title %}Serenia - Assistant Pédagogique{% endblock %} {% block stylesheets %} {{ parent() }} {% if tool_color_details.serenia is defined %} {% endif %} {% endblock %} {# Main occupe toute la hauteur de l'écran en flex colonne — fonctionne à tout niveau de zoom #} {% block main_class %}p-0 bg-white flex flex-col h-screen overflow-hidden{% endblock %} {% block body %} {# ============================================================================ SERENIA - Chat Interface ============================================================================ Redesigned with Tailwind CSS and Atomic Design. Uses atoms: icon, avatar, badge, logo, spinner Uses molecules: mini-stat ============================================================================ #}
{# Sidebar conversations #} {% include 'seren_ia/_conversation-sidebar.html.twig' with { sessions: sessions, currentSessionUuid: session is not null ? session.uuid : null, tokenStats: tokenStats } only %} {# Backdrop mobile pour la sidebar (cliquable pour fermer) #} {# Zone chat principale #}
{# Header mobile uniquement (hamburger + titre) #}
Serenia
{# ==================================================================== BANDEAU « DISCUSSION PUBLIQUE » ==================================================================== Contrepartie du mode « vivant » : la discussion partagée reste à jour, donc tout message écrit APRÈS le partage devient public. Ce rappel doit rester visible PENDANT la conversation, pas seulement au chargement — c'est sa seule raison d'être. D'où sa place ici : FRÈRE du conteneur de messages et non descendant, il ne défile pas avec le fil et ne peut pas être poussé hors de l'écran par les messages que chat_controller y ajoute. TOUJOURS RENDU, y compris sur /serenia/new où `session` vaut null, et c'est la correction d'un défaut réel : une garde `{% if session is not null %}` laissait tout le parcours le plus naturel de la fonctionnalité sans bandeau. L'utilisateur ouvre une discussion vierge, écrit — chat_controller crée la session et se contente d'un history.replaceState() suivi d'un rechargement de la SEULE turbo-frame de la sidebar, jamais de la page — puis partage depuis l'entrée « Partager » de cette sidebar, désormais pleinement fonctionnelle. Le bandeau, lui, n'était pas dans le DOM et n'y entrait qu'au prochain rechargement complet : discussion vivante et publique, sans aucun rappel. Le rechargement de la frame ne sauve rien, il aggrave — c'est lui qui rend « Partager » atteignable pendant que le bandeau est absent. Rendu vide et masqué quand il n'y a pas (encore) de discussion : #syncShareBanner adopte l'uuid au moment où le serveur confirme un partage pour la discussion COURANTE, et le révèle sans rechargement. DÉPENDANCE NON ÉCRITE AILLEURS — le style ci-dessous (state initial au chargement) et #syncShareBanner (état après un partage en direct) sont les DEUX SEULES écritures de ce bandeau. Aucune des deux ne se déclenche quand on passe d'une discussion à une autre : ce cas n'est couvert QUE parce que sidebar_controller.js#loadSession fait un window.location.href — une navigation PLEINE PAGE qui refait ce rendu Twig depuis zéro à chaque changement de discussion. Si la sidebar passait un jour en Turbo ou en XHR (fetch + remplacement du fragment de conversation sans recharger la page), ce bandeau resterait figé sur l'état de la discussion PRÉCÉDENTE : il continuerait d'afficher « publique » en ouvrant une discussion privée, ou l'inverse — et aucun test ne le détecterait, puisque rien ne rougirait côté serveur. ==================================================================== #} {# RESPONSIVE — pourquoi ces classes, et pas `flex-wrap` : `flex-wrap` ne suffisait pas. Le bloc de texte portait `min-w-0` (donc autorisé à tomber sous la largeur de son contenu) et le bloc d'actions `shrink-0` (donc refusant de rendre ses ~269 px). Le texte s'écrasait jusqu'à 0 px de large AVANT que le retour à la ligne ne soit seulement envisagé, la paire dépassait quand même, et le `overflow-hidden` des ancêtres (`main`, puis `.serenia`) cisaillait la droite — emportant la fin de l'URL et le bouton « Gérer ». Mesuré à 320 px : bandeau de 212 px, bloc d'actions de 268,5 px débordant de 90,5 px de son parent et de 57,5 px du viewport, « Gérer » hors écran. D'où : EMPILEMENT sous `lg` (`flex-col lg:flex-row`) plutôt que rétrécissement, le texte prend la place restante et RETOURNE À LA LIGNE (jamais de `truncate` : un bandeau tronqué n'annonce plus ce qu'il est censé annoncer), et le bloc d'actions perd son `shrink-0` pour occuper une ligne entière. Le point de bascule est `lg` et non `sm` PARCE QUE le panneau des conversations reprend sa place dans le flux dès `md` : à 768 px, la conversation ne fait plus que ~476 px et le bandeau ~428 px. Sur une seule ligne, le champ d'URL y tombait à 95 px, soit une dizaine de caractères — mesuré. Empiler jusqu'à 1024 px coûte une ligne de hauteur et rend le lien lisible sur toute la plage tablette. `flex` doit rester un jeton LITTÉRAL de la classe : #syncShareBanner révèle le bandeau en écrivant `style.display = ''`, il retire le style inline et laisse la classe reprendre la main. Un `lg:flex` seul laisserait le bandeau en `display:block` sous 1024 px. #}
{% include '_atoms/icon/icon.html.twig' with { name: 'share', size: 'sm', color: 'current', class: 'mt-0.5 lg:mt-0' } only %} Cette discussion est publique : tout nouveau message y sera visible.
{# Plus de `shrink-0` : c'est lui qui gelait 269 px et poussait « Gérer » hors du cadre. `min-w-0` autorise le champ à se réduire, `shrink-0` reste sur le SEUL bouton — l'accès à la gestion du partage ne doit jamais être le premier sacrifié. #}
{# Les QUATRE attributs que share_link_controller.js#open() lit sur son déclencheur : n'en poser qu'un laisserait son dataset à moitié initialisé, et la modale s'ouvrirait sur un état inventé. Tous rendus, même vides, pour que #syncShareBanner n'ait qu'à les mettre à jour — jamais à deviner lesquels existent. #}
{# ==================================================================== MESSAGES CONTAINER ==================================================================== #}
{# Welcome Message #}
{# Icône #}
{% include '_atoms/icon/icon.html.twig' with { name: 'bubul-serenia', size: '2xl', color: 'cohorts' } only %}
{# Greeting #}

Bonjour, {{ app.user.firstName }}

Comment puis-je vous aider aujourd'hui ?

{# Panneau de préférences compact #}
{# Style #}
Style
{# Mode #}
Mode
{# Modèle IA #}
IA
{# Alerte modèle — affichée par JS quand Complexe (o4-mini) sélectionné #} {# Security notice #}

{% include '_atoms/icon/icon.html.twig' with { name: 'shield', size: 'xs', color: 'current' } only %} Données protégées · Conversations chiffrées

{# Messages de la session chargée — lus par chat_controller.js au connect() #} {% if messages is defined and messages|length > 0 %} {% for message in messages %} {% endfor %} {% endif %}
{# ==================================================================== INPUT AREA ==================================================================== #}
{# Uploaded files display #} {# Input bar #}
{# Attach button #} {# Textarea #} {# Voice button #} {# Send button #} {# Hidden file input #}
{# Footer disclaimer #}
L'IA peut faire des erreurs. Vérifiez les informations importantes.
{# ==================================================================== HIDDEN ELEMENTS (used by Stimulus controller) ==================================================================== #} {# Token notification template #} {# Site logo template for assistant messages #}
{# end serenia__chat #}
{# ============================================================================ MODALE DE PARTAGE — un seul exemplaire par page ============================================================================ HORS du {% if session is not null %} du bandeau, et c'est délibéré : c'est cette molécule qui rend le jeton CSRF 'share_link' dans le DOM, y compris sur /serenia/new où aucune discussion n'est encore ouverte. La restreindre aux pages de session priverait SereniaShareEndpointTest du jeton qu'il y lit. `withDownload: false` : une discussion ne se télécharge pas. Le contrôleur shared--share-link tolère l'absence de la case (cf. son docblock de tête). ============================================================================ #} {% include '_molecules/share-link/share-link-modal.html.twig' with { endpoint: path('app_seren_ia_share_update', {uuid: '__UUID__'})|replace({'__UUID__': '{uuid}'}), accentHex: tool_color_details['serenia'].primary, withDownload: false } only %} {% endblock %} {% block javascripts %} {{ parent() }} {% endblock %}