""" Win11 Fluent UI Chatbot ======================= Un chatbot avec design Windows 11 Fluent Design et mémoire de conversation. Intégration du modèle: darkc0de/XortronCriminalComputingConfig Auteur: MiniMax Agent """ import gradio as gr from gradio.themes import Base import time import uuid import os from typing import List, Tuple, Optional, Dict, Any from dataclasses import dataclass, field from datetime import datetime import json # ============================================================ # CONFIGURATION DU MODÈLE # ============================================================ MODEL_NAME = "darkc0de/XortronCriminalComputingConfig" # Configuration de la mémoire MAX_MEMORY_TOKENS = 4096 # Limite de tokens pour l'historique MEMORY_WINDOW_SIZE = 10 # Nombre de messages à conserver SYSTEM_PROMPT = """Tu es un assistant IA intelligent et serviable. Tu réponses de manière claire et concise. Tu te souviens du contexte de la conversation pour fournir des réponses cohérentes. Tutoie l'utilisateur et utilise un ton amical mais professionnel.""" # ============================================================ # SYSTÈME DE MÉMOIRE DE CONVERSATION # ============================================================ @dataclass class ConversationMessage: """Représente un message dans la conversation.""" role: str # 'user', 'assistant', 'system' content: str timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M")) token_count: int = 0 @dataclass class ConversationMemory: """ Système de mémoire de conversation avec gestion du contexte. Préserve l'historique complet pour des réponses cohérentes. """ messages: List[ConversationMessage] = field(default_factory=list) max_tokens: int = MAX_MEMORY_TOKENS max_messages: int = MEMORY_WINDOW_SIZE session_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) def add_message(self, role: str, content: str) -> None: """Ajoute un message à la conversation.""" # Estimation approximative des tokens (1 token ≈ 4 caractères) token_count = len(content) // 4 message = ConversationMessage( role=role, content=content, token_count=token_count ) self.messages.append(message) self._prune_if_needed() def _prune_if_needed(self) -> None: """Supprime les anciens messages si nécessaire pour respecter les limites.""" # Calculer le nombre total de tokens total_tokens = sum(m.token_count for m in self.messages) # Supprimer les anciens messages si on dépasse la limite de tokens while total_tokens > self.max_tokens and len(self.messages) > 2: removed = self.messages.pop(0) total_tokens -= removed.token_count # Limiter le nombre de messages si on dépasse la fenêtre while len(self.messages) > self.max_messages: self.messages.pop(0) def get_prompt_messages(self) -> List[Dict[str, str]]: """Retourne les messages formatés pour le modèle.""" result = [] for msg in self.messages: result.append({ "role": msg.role, "content": msg.content }) return result def get_conversation_text(self) -> str: """Retourne le texte complet de la conversation pour le contexte.""" lines = [] for msg in self.messages: role_label = "Utilisateur" if msg.role == "user" else "Assistant" lines.append(f"[{msg.timestamp}] {role_label}: {msg.content}") return "\n".join(lines) def clear(self) -> None: """Efface la conversation.""" self.messages.clear() self.session_id = str(uuid.uuid4())[:8] def get_message_count(self) -> int: """Retourne le nombre de messages dans la conversation.""" return len(self.messages) # ============================================================ # GESTIONNAIRE DE MODÈLE # ============================================================ class ModelManager: """ Gestionnaire pour charger et interroger le modèle. Supporte le modèle Hugging Face avec mémoire de conversation. """ def __init__(self, model_name: str = MODEL_NAME): self.model_name = model_name self.model = None self.tokenizer = None self.device = None self.is_loaded = False self.load_error = None def load_model(self) -> bool: """ Charge le modèle depuis Hugging Face. Retourne True si le chargement est réussi. """ try: import torch from transformers import AutoModelForCausalLM, AutoTokenizer # Déterminer le périphérique (GPU si disponible) self.device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Chargement du modèle {self.model_name} sur {self.device}...") # Charger le tokenizer self.tokenizer = AutoTokenizer.from_pretrained( self.model_name, trust_remote_code=True ) # Charger le modèle avec optimisation mémoire model_kwargs = { "torch_dtype": torch.float16 if self.device == "cuda" else torch.float32, "low_cpu_mem_usage": True, } if self.device == "cuda": model_kwargs["device_map"] = "auto" self.model = AutoModelForCausalLM.from_pretrained( self.model_name, **model_kwargs ) self.model.eval() self.is_loaded = True print(f"Modèle {self.model_name} chargé avec succès!") return True except Exception as e: self.load_error = str(e) print(f"Erreur lors du chargement du modèle: {e}") return False def generate_response( self, user_message: str, memory: ConversationMemory, max_new_tokens: int = 512, temperature: float = 0.7, top_p: float = 0.95, do_sample: bool = True ) -> str: """ Génère une réponse en utilisant le modèle avec le contexte de conversation. Args: user_message: Le message de l'utilisateur memory: L'objet ConversationMemory contenant l'historique max_new_tokens: Nombre maximum de tokens à générer temperature: Température pour la génération (créativité) top_p: Paramètre top-p pour le sampling do_sample: Si True, utilise le sampling au lieu du beam search Returns: La réponse générée par le modèle """ try: if not self.is_loaded: return self._get_fallback_response(user_message, memory) # Ajouter le message utilisateur à la mémoire memory.add_message("user", user_message) # Préparer les messages pour le format conversation messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.extend(memory.get_prompt_messages()) # Appliquer le template de chat try: # Essayer d'utiliser le chat template du tokenizer prompt = self.tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) except Exception: # Fallback: format simple si pas de chat template prompt = self._format_prompt_fallback(messages) # Tokeniser l'entrée inputs = self.tokenizer( prompt, return_tensors="pt", truncation=True, max_length=self.max_input_length() ).to(self.device) # Générer la réponse with torch.no_grad(): outputs = self.model.generate( **inputs, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, do_sample=do_sample, pad_token_id=self.tokenizer.eos_token_id, no_repeat_ngram_size=3, ) # Décoder la réponse response_ids = outputs[0][inputs["input_ids"].shape[1]:] response = self.tokenizer.decode(response_ids, skip_special_tokens=True) # Nettoyer la réponse response = self._clean_response(response) # Ajouter la réponse à la mémoire memory.add_message("assistant", response) return response except Exception as e: print(f"Erreur lors de la génération: {e}") return self._get_fallback_response(user_message, memory) def _format_prompt_fallback(self, messages: List[Dict[str, str]]) -> str: """Format de prompt simple si le chat template n'est pas disponible.""" prompt_parts = [] for msg in messages: if msg["role"] == "system": continue role_prefix = "Human:" if msg["role"] == "user" else "Assistant:" prompt_parts.append(f"{role_prefix} {msg['content']}") prompt_parts.append("Assistant:") return "\n\n".join(prompt_parts) def _clean_response(self, response: str) -> str: """Nettoie la réponse générée.""" # Supprimer les caractères de contrôle response = response.strip() # Si la réponse commence par un préfixe unwanted, le supprimer unwanted_prefixes = ["Assistant:", "assistant:", "AI:", "ai:"] for prefix in unwanted_prefixes: if response.startswith(prefix): response = response[len(prefix):].strip() return response def _get_fallback_response(self, user_message: str, memory: ConversationMemory) -> str: """Réponse de fallback si le modèle n'est pas disponible.""" # Réponses contextuelles basées sur l'historique msg_lower = user_message.lower() # Salutations if any(word in msg_lower for word in ['bonjour', 'hello', 'hi', 'salut']): response = "Bonjour ! Ravi de vous revoir. Comment puis-je vous aider aujourd'hui ?" # Remerciements elif any(word in msg_lower for word in ['merci', 'thanks', 'remercie']): response = "De rien ! C'est toujours un plaisir de vous aider. N'hésitez pas si vous avez d'autres questions." # Questions sur l'identité elif any(word in msg_lower for word in ['qui es-tu', 'who are you', 'identity', 'appel']): response = "Je suis un assistant IA conçu avec le modèle darkc0de/XortronCriminalComputingConfig. Je suis là pour répondre à vos questions en maintenant le contexte de notre conversation." # Questions sur la mémoire elif any(word in msg_lower for word in ['mémoire', 'memory', 'souviens', 'rappelle']): msg_count = memory.get_message_count() if msg_count > 2: response = f"Bien sûr que je me souviens ! Nous avons déjà échangé {msg_count // 2} messages. Je conserve l'historique de notre conversation pour vous fournir des réponses cohérentes." else: response = "Je commence tout juste notre conversation, mais je suis prêt à mémoriser tout ce que nous allons discuter !" # Aide elif any(word in msg_lower for word in ['aide', 'help', 'comment', 'peux-tu']): response = "Je suis là pour vous aider ! Vous pouvez me poser des questions, me demander des explications, ou simplement discuter. Je conserve le contexte de notre conversation pour des échanges plus riches." # Au revoir elif any(word in msg_lower for word in ['au revoir', 'bye', 'salut', 'ciao', 'à bientôt']): response = "Au revoir ! N'hésitez pas à revenir si vous avez d'autres questions. Bonne journée !" # Réponse générique avec contexte else: context_info = "" if memory.get_message_count() > 2: context_info = " En gardant à l'esprit notre conversation précédente, " responses = [ f"Merci pour votre message.{context_info} Pourriez-vous m'en dire plus pour que je puisse vous aider au mieux ?", f"Intéressant !{context_info} Pouvez-vous développer votre pensée ?", f"Je comprends.{context_info} Voici ce que je peux vous dire à ce sujet...", f"Message reçu !{context_info} Laissez-moi réfléchir à votre question.", ] import random response = random.choice(responses) # Ajouter le message à la mémoire memory.add_message("assistant", response) return response def max_input_length(self) -> int: """Retourne la longueur maximale d'entrée du modèle.""" return 2048 # Valeur par défaut, peut être ajustée selon le modèle def get_model_info(self) -> Dict[str, Any]: """Retourne les informations sur le modèle.""" return { "model_name": self.model_name, "is_loaded": self.is_loaded, "device": self.device, "load_error": self.load_error, "max_tokens": self.max_input_length() } # Instance globale du gestionnaire de modèle model_manager = ModelManager(MODEL_NAME) # ============================================================ # DÉFINITIONS DES STYLES CSS WINDOWS 11 FLUENT DESIGN # ============================================================ WINDOWS11_CSS = """ /* ============================================ WINDOWS 11 FLUENT DESIGN SYSTEM ============================================ */ /* Variables CSS Windows 11 */ :root { /* Couleurs Light Mode */ --win11-bg: #f3f3f3; --win11-surface: rgba(255, 255, 255, 0.85); --win11-surface-hover: rgba(255, 255, 255, 0.95); --win11-surface-active: rgba(243, 243, 243, 1); --win11-accent: #0078d4; --win11-accent-hover: #1a86d9; --win11-accent-active: #006cbf; --win11-text-primary: #1f1f1f; --win11-text-secondary: #5e5e5e; --win11-text-tertiary: #8e8e8e; --win11-border: rgba(0, 0, 0, 0.08); --win11-shadow: rgba(0, 0, 0, 0.1); --win11-shadow-strong: rgba(0, 0, 0, 0.15); /* Mica Material Effect */ --mica-noise: url('data:image/svg+xml,'); /* Border Radius */ --win11-radius-window: 12px; --win11-radius-button: 8px; --win11-radius-input: 8px; --win11-radius-bubble: 12px; /* Transitions */ --win11-transition-fast: 100ms cubic-bezier(0.25, 0.46, 0.45, 0.94); --win11-transition-normal: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); --win11-transition-slow: 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94); /* Depth & Effects */ --win11-depth-1: 0 1px 3px rgba(0, 0, 0, 0.08); --win11-depth-2: 0 4px 12px rgba(0, 0, 0, 0.1); --win11-depth-3: 0 8px 24px rgba(0, 0, 0, 0.12); --win11-glow-accent: 0 0 0 2px rgba(0, 120, 212, 0.3); --win11-glow-hover: 0 0 20px rgba(0, 120, 212, 0.2); } /* Dark Mode Variables */ .dark { --win11-bg: #202020; --win11-surface: rgba(32, 32, 32, 0.85); --win11-surface-hover: rgba(50, 50, 50, 0.95); --win11-surface-active: rgba(40, 40, 40, 1); --win11-accent: #60cdff; --win11-accent-hover: #8bd8ff; --win11-accent-active: #4bb6e6; --win11-text-primary: #ffffff; --win11-text-secondary: #a0a0a0; --win11-text-tertiary: #6e6e6e; --win11-border: rgba(255, 255, 255, 0.08); --win11-shadow: rgba(0, 0, 0, 0.3); --win11-shadow-strong: rgba(0, 0, 0, 0.5); --win11-glow-accent: 0 0 0 2px rgba(96, 205, 255, 0.4); --win11-glow-hover: 0 0 20px rgba(96, 205, 255, 0.25); } /* ============================================ BASE STYLES & RESET ============================================ */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI Variable', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background: transparent; } /* ============================================ MAIN CONTAINER - WINDOW SIMULATION ============================================ */ .win11-window { width: 100%; max-width: 1000px; margin: 0 auto; background: var(--win11-surface); backdrop-filter: blur(30px) saturate(125%); -webkit-backdrop-filter: blur(30px) saturate(125%); border-radius: var(--win11-radius-window); box-shadow: var(--win11-depth-3); overflow: hidden; position: relative; border: 1px solid var(--win11-border); } /* Mica Material Background Effect */ .win11-window::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: var(--mica-noise); opacity: 1; pointer-events: none; z-index: -1; border-radius: var(--win11-radius-window); } /* ============================================ TITLE BAR ============================================ */ .win11-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 100%); border-bottom: 1px solid var(--win11-border); user-select: none; -webkit-user-select: none; } .dark .win11-titlebar { background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(0,0,0,0) 100%); } .win11-titlebar-left { display: flex; align-items: center; gap: 12px; } .win11-icon { width: 20px; height: 20px; border-radius: 4px; background: linear-gradient(135deg, #00a4ef 0%, #0078d4 50%, #004275 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold; box-shadow: var(--win11-depth-1); } .win11-title { font-size: 14px; font-weight: 600; color: var(--win11-text-primary); letter-spacing: -0.2px; } .win11-titlebar-right { display: flex; align-items: center; gap: 8px; } .win11-control-btn { width: 32px; height: 24px; border: none; background: transparent; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--win11-transition-fast); color: var(--win11-text-secondary); font-size: 10px; } .win11-control-btn:hover { background: rgba(0, 0, 0, 0.06); color: var(--win11-text-primary); } .win11-control-btn.close:hover { background: #e81123; color: white; } .dark .win11-control-btn:hover { background: rgba(255, 255, 255, 0.06); } .dark .win11-control-btn.close:hover { background: #e81123; } /* ============================================ CHAT AREA ============================================ */ .win11-chat-container { height: 500px; overflow-y: auto; padding: 16px 20px; scroll-behavior: smooth; } /* Custom Scrollbar */ .win11-chat-container::-webkit-scrollbar { width: 6px; } .win11-chat-container::-webkit-scrollbar-track { background: transparent; } .win11-chat-container::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; } .win11-chat-container::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } .dark .win11-chat-container::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); } .dark .win11-chat-container::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } /* ============================================ MESSAGE BUBBLES ============================================ */ .win11-message { display: flex; flex-direction: column; margin-bottom: 16px; animation: messageFlyIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; opacity: 0; } @keyframes messageFlyIn { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } .win11-message.user { align-items: flex-end; } .win11-message.assistant { align-items: flex-start; } .win11-bubble { max-width: 75%; padding: 12px 16px; position: relative; word-wrap: break-word; font-size: 14px; line-height: 1.5; box-shadow: var(--win11-depth-1); } /* User Message Bubble */ .win11-bubble.user { background: linear-gradient(135deg, var(--win11-accent) 0%, var(--win11-accent-hover) 100%); color: white; border-radius: var(--win11-radius-bubble) var(--win11-radius-bubble) 2px var(--win11-radius-bubble); } .win11-bubble.user::after { content: ''; position: absolute; bottom: 0; right: -8px; width: 0; height: 0; border-style: solid; border-width: 0 0 12px 12px; border-color: transparent transparent var(--win11-accent) transparent; } /* Assistant Message Bubble */ .win11-bubble.assistant { background: var(--win11-surface); color: var(--win11-text-primary); border: 1px solid var(--win11-border); border-radius: var(--win11-radius-bubble) var(--win11-radius-bubble) var(--win11-radius-bubble) 2px; } .win11-bubble.assistant::after { content: ''; position: absolute; bottom: 0; left: -8px; width: 0; height: 0; border-style: solid; border-width: 0 12px 12px 0; border-color: transparent var(--win11-surface) transparent transparent; } /* Message Avatar */ .win11-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; margin-bottom: 4px; box-shadow: var(--win11-depth-1); } .win11-avatar.user { background: linear-gradient(135deg, var(--win11-accent) 0%, #005a9e 100%); color: white; } .win11-avatar.assistant { background: linear-gradient(135deg, #107c10 0%, #0b5a0b 100%); color: white; } /* Message Timestamp */ .win11-timestamp { font-size: 11px; color: var(--win11-text-tertiary); margin-top: 4px; padding: 0 4px; } /* ============================================ LOADING INDICATOR (Windows 11 Style) ============================================ */ .win11-loading { display: flex; align-items: center; gap: 4px; padding: 8px 0; } .win11-loading-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--win11-accent); animation: loadingBounce 1.4s infinite ease-in-out both; } .win11-loading-dot:nth-child(1) { animation-delay: -0.32s; } .win11-loading-dot:nth-child(2) { animation-delay: -0.16s; } @keyframes loadingBounce { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } } /* ============================================ INPUT AREA ============================================ */ .win11-input-container { padding: 16px 20px; background: linear-gradient(180deg, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0.05) 100%); border-top: 1px solid var(--win11-border); } .dark .win11-input-container { background: linear-gradient(180deg, rgba(255,255,255,0.02) 0%, rgba(0,0,0,0.1) 100%); } .win11-input-wrapper { display: flex; align-items: flex-end; gap: 12px; background: var(--win11-surface); border: 1px solid var(--win11-border); border-radius: var(--win11-radius-input); padding: 8px 12px; transition: all var(--win11-transition-fast); box-shadow: var(--win11-depth-1); } .win11-input-wrapper:focus-within { border-color: var(--win11-accent); box-shadow: var(--win11-glow-accent); } .win11-input { flex: 1; border: none; outline: none; background: transparent; font-family: inherit; font-size: 14px; color: var(--win11-text-primary); resize: none; max-height: 120px; min-height: 24px; line-height: 1.5; } .win11-input::placeholder { color: var(--win11-text-tertiary); } .win11-send-btn { width: 36px; height: 36px; border: none; background: var(--win11-accent); color: white; border-radius: var(--win11-radius-button); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--win11-transition-fast); box-shadow: var(--win11-depth-1); } .win11-send-btn:hover { background: var(--win11-accent-hover); transform: scale(1.05); box-shadow: var(--win11-glow-hover); } .win11-send-btn:active { background: var(--win11-accent-active); transform: scale(0.95); } .win11-send-btn:disabled { background: var(--win11-text-tertiary); cursor: not-allowed; transform: none; box-shadow: none; } /* ============================================ THEME TOGGLE ============================================ */ .win11-theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; background: rgba(0, 0, 0, 0.2); cursor: pointer; transition: all var(--win11-transition-normal); border: none; } .win11-theme-toggle:hover { background: rgba(0, 0, 0, 0.3); } .win11-theme-toggle .toggle-thumb { position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: white; box-shadow: var(--win11-depth-1); transition: all var(--win11-transition-normal); display: flex; align-items: center; justify-content: center; font-size: 10px; } .dark .win11-theme-toggle { background: rgba(255, 255, 255, 0.2); } .dark .win11-theme-toggle .toggle-thumb { left: 22px; transform: rotate(180deg); } /* ============================================ PROMPT EXAMPLES (Windows 11 Style) ============================================ */ .win11-examples { padding: 12px 20px; display: flex; flex-wrap: wrap; gap: 8px; border-top: 1px solid var(--win11-border); background: rgba(0, 0, 0, 0.02); } .dark .win11-examples { background: rgba(255, 255, 255, 0.02); } .win11-example-chip { padding: 6px 12px; background: var(--win11-surface); border: 1px solid var(--win11-border); border-radius: 20px; font-size: 12px; color: var(--win11-text-secondary); cursor: pointer; transition: all var(--win11-transition-fast); } .win11-example-chip:hover { background: var(--win11-surface-hover); border-color: var(--win11-accent); color: var(--win11-accent); box-shadow: var(--win11-glow-accent); } /* ============================================ MEMORY INDICATOR ============================================ */ .win11-memory-indicator { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: linear-gradient(90deg, rgba(0, 120, 212, 0.1) 0%, transparent 100%); border-radius: 8px; font-size: 12px; color: var(--win11-text-secondary); } .win11-memory-icon { width: 16px; height: 16px; background: linear-gradient(135deg, var(--win11-accent) 0%, #005a9e 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 10px; } .win11-memory-bar { flex: 1; height: 4px; background: rgba(0, 0, 0, 0.1); border-radius: 2px; overflow: hidden; } .win11-memory-fill { height: 100%; background: linear-gradient(90deg, var(--win11-accent) 0%, #60cdff 100%); border-radius: 2px; transition: width 0.3s ease; } /* ============================================ GLOW EFFECTS & PARTICLES ============================================ */ .win11-glow-effect { position: relative; } .win11-glow-effect::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200%; height: 200%; background: radial-gradient(circle, rgba(0, 120, 212, 0.1) 0%, transparent 70%); pointer-events: none; opacity: 0; transition: opacity var(--win11-transition-slow); } .win11-glow-effect:hover::after { opacity: 1; } /* ============================================ ANIMATIONS ============================================ */ /* Pulse Effect for Focus */ @keyframes pulse-glow { 0% { box-shadow: 0 0 0 0 rgba(0, 120, 212, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(0, 120, 212, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 120, 212, 0); } } .win11-pulse { animation: pulse-glow 2s infinite; } /* Fade In */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .win11-fade-in { animation: fadeIn var(--win11-transition-normal) forwards; } /* Scale In */ @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } .win11-scale-in { animation: scaleIn var(--win11-transition-slow) forwards; } /* Typewriter Cursor */ .typewriter-cursor { display: inline-block; width: 2px; height: 1em; background: var(--win11-text-primary); margin-left: 2px; animation: cursorBlink 1s step-end infinite; } @keyframes cursorBlink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } /* ============================================ GRADIO OVERRIDES ============================================ */ .gradio-container { background: transparent !important; min-height: 100vh; padding: 20px; } .gradio-row { gap: 0 !important; } .gradio-button { background: transparent !important; border: none !important; } /* Hide default Gradio elements */ .prose { display: none !important; } /* ============================================ RESPONSIVE DESIGN ============================================ */ @media (max-width: 768px) { .win11-window { max-width: 100%; border-radius: 0; height: 100vh; } .win11-chat-container { height: calc(100vh - 220px); } .win11-bubble { max-width: 85%; } .win11-titlebar { padding: 10px 12px; } .win11-title { font-size: 12px; } } @media (max-width: 480px) { .win11-bubble { max-width: 90%; padding: 10px 14px; font-size: 13px; } .win11-input-wrapper { padding: 6px 10px; } } /* ============================================ TOOLTIP ============================================ */ .win11-tooltip { position: relative; } .win11-tooltip::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%) translateY(-8px); padding: 6px 10px; background: var(--win11-text-primary); color: var(--win11-bg); font-size: 12px; border-radius: 4px; white-space: nowrap; opacity: 0; visibility: hidden; transition: all var(--win11-transition-fast); z-index: 1000; } .win11-tooltip:hover::after { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(-4px); } /* ============================================ STATUS INDICATOR ============================================ */ .win11-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--win11-text-tertiary); } .win11-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #107c10; box-shadow: 0 0 8px rgba(16, 124, 16, 0.5); } .win11-status-dot.typing { background: var(--win11-accent); box-shadow: 0 0 8px rgba(0, 120, 212, 0.5); animation: statusPulse 1.5s ease-in-out infinite; } @keyframes statusPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(0.9); } } /* ============================================ CLEAR MEMORY BUTTON ============================================ */ .win11-clear-btn { padding: 6px 12px; background: rgba(232, 17, 35, 0.1); border: 1px solid rgba(232, 17, 35, 0.3); border-radius: 6px; color: #e81123; font-size: 11px; cursor: pointer; transition: all var(--win11-transition-fast); } .win11-clear-btn:hover { background: rgba(232, 17, 35, 0.2); border-color: #e81123; } """ # ============================================ # JAVASCRIPT POUR LES INTERACTIONS # ============================================ WINDOWS11_JS = """ // ============================================ // WINDOWS 11 CHATBOT JAVASCRIPT WITH MEMORY // ============================================ // Global state let chatHistory = []; let isProcessing = false; let sessionId = null; // Initialize on DOM ready document.addEventListener('DOMContentLoaded', function() { initThemeToggle(); initAutoResize(); initEnterKeySubmit(); initSmoothScroll(); initSession(); }); // Session initialization function initSession() { sessionId = localStorage.getItem('chat_session_id'); if (!sessionId) { sessionId = generateSessionId(); localStorage.setItem('chat_session_id', sessionId); } updateMemoryIndicator(); logToConsole('Session initialized: ' + sessionId); } function generateSessionId() { return Math.random().toString(36).substring(2, 10); } // Theme Toggle function initThemeToggle() { const themeToggle = document.querySelector('.win11-theme-toggle'); if (!themeToggle) return; // Check system preference or saved preference const savedTheme = localStorage.getItem('theme'); if (savedTheme) { document.body.classList.toggle('dark', savedTheme === 'dark'); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.body.classList.add('dark'); } // Listen for system changes if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { if (!localStorage.getItem('theme')) { document.body.classList.toggle('dark', e.matches); } }); } themeToggle.addEventListener('click', function() { const isDark = document.body.classList.toggle('dark'); localStorage.setItem('theme', isDark ? 'dark' : 'light'); }); } // Auto-resize textarea function initAutoResize() { const input = document.querySelector('.win11-input'); if (!input) return; input.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 120) + 'px'; }); } // Enter key to submit function initEnterKeySubmit() { const input = document.querySelector('.win11-input'); const sendBtn = document.querySelector('.win11-send-btn'); if (!input || !sendBtn) return; input.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); } // Smooth scroll to bottom function initSmoothScroll() { const chatContainer = document.querySelector('.win11-chat-container'); if (!chatContainer) return; const observer = new MutationObserver(() => { chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: 'smooth' }); }); observer.observe(chatContainer, { childList: true }); } // Typewriter effect for assistant messages function typewriterEffect(element, text, speed = 30) { return new Promise(resolve => { let i = 0; element.textContent = ''; function type() { if (i < text.length) { element.textContent += text.charAt(i); i++; setTimeout(type, speed); } else { resolve(); } } type(); }); } // Add message with animation function addMessage(role, content, timestamp) { const chatContainer = document.querySelector('.win11-chat-container'); if (!chatContainer) return null; const messageDiv = document.createElement('div'); messageDiv.className = `win11-message ${role}`; messageDiv.innerHTML = `