1/* BUILD-V166-WEBMCP */ 2 3/* âââ [26/08/2026] Google Ads â AW-17973607590, avec Consent Mode v2 âââââââ 4 Pose ici parce que site-nav-v6.js est charge par CHAQUE page du site : 5 une seule installation couvre les 10 000+ fiches, sans toucher un 6 generateur. Meme raisonnement que la charte CSS plus bas. 7 8 CONSENTEMENT : le tag se charge toujours, mais Google demarre en 9 « denied » â aucun cookie publicitaire n'est ecrit tant que le visiteur 10 n'a pas accepte. C'est le Consent Mode v2, la methode officielle de 11 Google et la seule conforme au RGPD pour un site francais. Le bandeau 12 existant ecrit son choix dans localStorage.osai_consent ('accepted', 13 'declined' ou 'custom') : on le relit au chargement, et setCookieChoice() 14 previent Google en direct quand le visiteur tranche. 15 16 Le texte du bandeau disait « No advertising » : c'etait vrai avant ce 17 jour, ca ne l'est plus. Il a ete corrige dans index.html â un 18 consentement obtenu sur une information fausse ne vaut rien. */ 19(function () { 20 var ID = 'AW-17973607590'; 21 window.dataLayer = window.dataLayer || []; 22 function gtag() { dataLayer.push(arguments); } 23 window.gtag = window.gtag || gtag; 24 25 var choix = null; 26 try { choix = localStorage.getItem('osai_consent'); } catch (e) {} 27 var ok = (choix === 'accepted'); 28 29 /* Par defaut TOUT est refuse : c'est ce qui rend l'installation legale 30 avant le clic. On n'accorde que ce que le visiteur a accepte. */ 31 gtag('consent', 'default', { 32 ad_storage: ok ? 'granted' : 'denied', 33 ad_user_data: ok ? 'granted' : 'denied', 34 ad_personalization: ok ? 'granted' : 'denied', 35 analytics_storage: ok ? 'granted' : 'denied', 36 wait_for_update: 500 37 }); 38 39 var sc = document.createElement('script'); 40 sc.async = true; 41 sc.src = 'https://www.googletagmanager.com/gtag/js?id=' + ID; 42 (document.head || document.documentElement).appendChild(sc); 43 44 gtag('js', new Date()); 45 gtag('config', ID); 46 47 /* Le bandeau appelle setCookieChoice() : on s'y greffe pour prevenir 48 Google a la seconde ou le visiteur tranche, sans attendre un 49 rechargement de page. */ 50 var poser = function () { 51 var origine = window.setCookieChoice; 52 if (typeof origine !== 'function' || origine.__osaiConsent) return; 53 window.setCookieChoice = function (c) { 54 try { 55 var g = (c === 'accepted') ? 'granted' : 'denied'; 56 gtag('consent', 'update', { 57 ad_storage: g, ad_user_data: g, ad_personalization: g, analytics_storage: g 58 }); 59 } catch (e) {} 60 return origine.apply(this, arguments); 61 }; 62 window.setCookieChoice.__osaiConsent = true; 63 }; 64 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', poser); 65 else poser(); 66 setTimeout(poser, 1200); /* le bandeau peut etre defini apres nous */ 67})(); 68/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 69 olud.ai â Navigation & Footer partagés 70 Un seul fichier pour le menu + pied de page de TOUTES les pages. 71 Pour modifier le menu : éditer CE fichier uniquement. 72 73 Usage dans chaque page : 74 1. Mettre <div id="site-nav"></div> en haut du body (à la place du <nav>) 75 2. Mettre <div id="site-footer"></div> en bas (à la place du <footer>) 76 3. Ajouter avant </body> : <script src="/site-nav.js"></script> 77 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 78(function(){ 79 /* âââ V132 â PERSISTANCE DE LANGUE âââ 80 Un visiteur qui a choisi le français retombait en anglais dès qu'il 81 revenait sur une URL sans préfixe. Désormais : le choix est mémorisé 82 (localStorage), et à l'arrivée sur une page anglaise on redirige vers la 83 version localisée â MAIS uniquement si elle existe vraiment (signal fiable : 84 la balise <link rel="alternate" hreflang="xx"> que seules les pages 85 traduites portent â jamais vers une fiche projet inexistante en /fr/). */ 86 (function(){ 87 try { 88 var onPrefix = /^\/(fr|it|es|de|pt)(\/|$)/.test(location.pathname); 89 if (onPrefix) return; // déjà localisé : rien à faire 90 var pref = localStorage.getItem('osai_lang'); 91 if (!pref || pref === 'en') return; // pas de préférence, ou anglais choisi 92 if (sessionStorage.getItem('osai_lang_stay') === '1') return; // « rester en anglais » pour cette session 93 var alt = document.querySelector('link[rel="alternate"][hreflang="' + pref + '"]'); 94 if (!alt) return; // pas de version traduite de CETTE page 95 var href = alt.getAttribute('href') || ''; 96 if (!href) return; 97 var target = href.replace(/^https?:\/\/[^\/]+/, ''); 98 if (target && target !== location.pathname) { 99 location.replace(target + location.search + location.hash); 100 } 101 } catch(e){} 102 })(); 103 104 // âââ Google Analytics 4 âââ 105 // N'installe GA que si AUCUN analytics n'est déjà présent sur la page 106 // (évite tout double comptage avec un snippet gtag existant, GTM ou Cloudflare Zaraz). 107 // Garantit que TOUTES les pages (y compris les nouvelles) sont suivies. 108 // Expose window.osaiTrack(nom, params) pour les événements custom. 109 (function(){ 110 var GA_ID = 'G-WBRQRVBJ07'; 111 /* ââ [31/08/2026] Le garde-fou se retournait contre lui-meme âââââââââââââ
112 La condition etait « n'installer GA que si aucun analytics n'est deja 113 la », testee sur window.gtag et window.dataLayer. Elle protegeait d'un 114 double comptage. Mais depuis l'ajout de Google Ads plus haut dans ce 115 fichier, gtag et dataLayer EXISTENT toujours quand on arrive ici : la 116 condition etait donc systematiquement fausse, et GA4 n'etait plus 117 configure nulle part â sauf sur l'accueil, seule page a porter encore 118 son propre extrait en dur. 119 120 Constat de Romain le 31/08 : 283 sessions sur Clarity, 41 sur GA. Les 121 10 000 fiches, les outils, les alternatives et les classements 122 n'etaient plus mesures du tout. 123 124 On teste desormais ce qu'il fallait tester depuis le debut : est-ce que 125 CET identifiant-la est deja configure ? Un meme gtag.js sert plusieurs 126 identifiants â c'est prevu par Google â donc quand la balise Ads a deja 127 charge le script, il suffit d'ajouter la configuration GA4. */ 128 var dejaConfigure = false; 129 try { 130 var dl = window.dataLayer || []; 131 for (var i = 0; i < dl.length; i++) { 132 if (dl[i] && dl[i][0] === 'config' && dl[i][1] === GA_ID) { dejaConfigure = true; break; } 133 } 134 } catch(e){} 135 136 if (!dejaConfigure && !window.zaraz) { 137 window.dataLayer = window.dataLayer || []; 138 if (typeof window.gtag !== 'function') { 139 window.gtag = function(){ window.dataLayer.push(arguments); }; 140 } 141 /* Charger gtag.js seulement s'il ne l'est pas deja pour un autre ID. */ 142 var dejaCharge = false; 143 var sc = document.querySelectorAll('script[src*="googletagmanager.com/gtag/js"]'); 144 if (sc.length) dejaCharge = true; 145 if (!dejaCharge) { 146 var gs = document.createElement('script'); 147 gs.async = true; 148 gs.src = 'https://www.googletagmanager.com/gtag/js?id=' + GA_ID; 149 document.head.appendChild(gs); 150 window.gtag('js', new Date()); 151 } 152 window.gtag('config', GA_ID); 153 } 154 window.osaiTrack = function(name, params){ 155 params = params || {}; 156 try { 157 if (typeof window.gtag === 'function') { window.gtag('event', name, params); } 158 else if (window.dataLayer && window.dataLayer.push) { 159 var o = { event: name }; 160 for (var k in params) { if (Object.prototype.hasOwnProperty.call(params, k)) o[k] = params[k]; } 161 window.dataLayer.push(o); 162 } else if (window.zaraz && window.zaraz.track) { window.zaraz.track(name, params); } 163 } catch(e){} 164 }; 165 })(); 166 167 // âââ Microsoft Clarity (cartes de chaleur + rejeu de sessions) âââ 168 // Chargé une seule fois, pour TOUTES les pages, à côté de GA. Idempotent : 169 // ne réinstalle pas Clarity s'il est déjà présent sur la page. 170 (function(){ 171 if (window.clarity) return; 172 (function(c,l,a,r,i,t,y){ 173 c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; 174 t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; 175 y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); 176 })(window, document, "clarity", "script", "xr0c6wbj7q"); 177 })(); 178 179 // âââ i18n du nav/footer (FR/IT/ES/DE/PT) âââ 180 // Détecte la langue depuis le préfixe d'URL (/fr/, /it/, /es/, /de/, /pt/). 181 // localizeNav() traduit les libellés APRÃS injection. Gardé : sur les pages 182 // anglaises, on ne touche à RIEN (zéro risque pour le trafic actuel). 183 var NAV_LANG = (function(){ 184 var m = location.pathname.match(/^\/(fr|it|es|de|pt)(\/|$)/); 185 return m ? m[1] : 'en'; 186 })(); 187 var LP = (NAV_LANG === 'en') ? '' : '/' + NAV_LANG; // préfixe langue ('' ou '/fr') â défini tôt car utilisé par le localisateur de liens 188 try { 189 if (!document.querySelector('link[rel="icon"]')) { 190 var fav = document.createElement('link'); fav.rel = 'icon'; fav.type = 'image/svg+xml'; 191 fav.href = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImZnIiB4MT0iMCIgeTE9IjEiIHgyPSIxIiB5Mj0iMCI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I1N0ZGIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjQTI1N0ZGIi8+PC9saW5lYXJHcmFkaWVudD48bWFzayBpZD0iZm4iIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjAiIHk9IjAiIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjZmZmIi8+PGNpcmNsZSBjeD0iNTEuMDkiIGN5PSIxNi4zNSIgcj0iOC4yNyIgZmlsbD0iIzAw
191MCIvPjwvbWFzaz48L2RlZnM+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTQiIGZpbGw9IiMwQjBFMUEiLz48ZyBtYXNrPSJ1cmwoI2ZuKSI+PGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjAuNjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNmZykiIHN0cm9rZS13aWR0aD0iMTAuODEiLz48L2c+PGNpcmNsZSBjeD0iNTEuMDkiIGN5PSIxNi4zNSIgcj0iNS41NyIgZmlsbD0idXJsKCNmZykiLz48L3N2Zz4='; 192 document.head.appendChild(fav); 193 } 194 if (!document.querySelector('link[href*="family=Poppins"][href*="display=swap"]')) { 195 var fl = document.createElement('link'); fl.rel = 'stylesheet'; 196 fl.href = 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap'; 197 document.head.appendChild(fl); 198 } 199 } catch(e){} 200 try { /* âââ V130 â la couche sociale des fiches MODÃLES : cÅur (favoris) + partage. 201 Les fiches /model/ sont générées sans ces boutons ; le nav les leur offre. âââ */ 202(function(){ 203 var mpath = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 204 if (!/^\/model\/[^\/]+\.html$/.test(mpath)) return; 205 /* le cÅur a besoin de favorites.js â certaines fiches modèles ne l'embarquent pas */ 206 if (typeof window.favBtnHtml !== 'function' && !document.querySelector('script[src*="favorites.js"]')) { 207 var fs2 = document.createElement('script'); fs2.src = '/favorites.js'; document.head.appendChild(fs2); 208 } 209 var bootTries = 0; 210 function boot(){ 211 var h1 = document.querySelector('main h1') || document.querySelector('h1') 212 || document.querySelector('.hero h1, .hero-main h1, header h1'); 213 if (document.getElementById('osaiModelActions')) return; 214 if (!h1) { if (++bootTries < 4) setTimeout(boot, 700); return; } 215 var name = h1.textContent.trim(); 216 var url = 'https://olud.ai' + mpath; 217 var row = document.createElement('div'); 218 row.id = 'osaiModelActions'; 219 row.style.cssText = 'display:flex;align-items:center;gap:10px;margin:12px 0 4px'; 220 var share = document.createElement('button'); 221 share.type = 'button'; 222 share.style.cssText = 'display:inline-flex;align-items:center;gap:7px;height:40px;padding:0 15px;border-radius:11px;' 223 + 'border:1px solid var(--border2,rgba(0,0,0,.15));background:var(--surface2,transparent);color:var(--txt2,#444);' 224 + 'font:inherit;font-size:.82rem;font-weight:600;cursor:pointer'; 225 share.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></svg><span>' + (typeof window.osaiT === 'function' ? window.osaiT('Share') : 'Share') + '</span>'; 226 share.addEventListener('click', function(e){ 227 if (typeof window.shareModel === 'function') window.shareModel(e, name, url); 228 }); 229 row.appendChild(share); 230 function addHeart(tries){ 231 if (typeof window.favBtnHtml === 'function') { 232 var wrap = document.createElement('span'); 233 wrap.innerHTML = window.favBtnHtml(name, 'AI model', 'model', url); 234 row.appendChild(wrap); 235 if (typeof window.favPaintAll === 'function') try { window.favPaintAll(); } catch(e){} 236 } else if (tries > 0) setTimeout(function(){ addHeart(tries - 1); }, 600); 237 } 238 addHeart(4); 239 h1.insertAdjacentElement('afterend', row); 240 } 241 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ setTimeout(boot, 300); }); 242 else setTimeout(boot, 300); 243 setTimeout(boot, 1600); /* filet : contenus tardifs */ 244})(); 245 246 247 /* âââ V135 â ASSISTANT DE SITE (sans IA, sans coût) âââ 248 Une bulle d'aide qui répond à partir d'une base Q/R + recherche par 249 mots-clés. Aucune requête réseau, aucune clé, aucun dérapage possible. 250 Bilingue EN/FR (EN par défaut pour les autres langues). âââ */ 251 try { 252 (function(){ 253 if (document.getElementById('osaiHelpBtn')) return; 254 var LG = (typeof NAV_LANG !== 'undefined' ? NAV_LANG : 'en'); 255 var docL = (document.documentElement.lang || '').slice(0,2); 256 var L = (LG === 'fr' || docL === 'fr') ? 'fr' : 'en'; 257 /* [01/08] tt() ne connaissait que deux langues : sur les pages allemande, 258 espagnole, italienne et portugaise, TOUT l'assistant restait en anglais 259 - titre, salutation, suggestions, note - alors que le modele, lui, 260 repond desormais dans la langue de la question. Une meme conversation 261 melangeait donc deux langues. On passe par le dictionnaire du site pour 262 les cinq langues ; le francais garde sa formulation ecrite a la main. */ 263 function tt(en, fr){ 264 if (L === 'fr') return fr;
265 if (typeof window.osaiT === 'function') { var t = window.osaiT(en); if (t && t !== en) return t; } 266 return en; 267 } 268 269 /* ââ base de connaissances : mots-clés déclencheurs â réponse ââ */ 270 var KB = [ 271 { k:['best model','which model','recommend model','model for images','model for pictures','picture','pictures','image generation','generate image','generate pictures','create image','create picture','best image','video model','generate video','best video','voice model','speech model','text to speech','best for images','meilleur modele','quel modele','modele pour image','generer image','generer video','creer image','meilleure image'], 272 a:{en:'For the best model at a given task, check our human-preference leaderboards. For images, video and speech, the Media Leaderboard ranks models by blind human votes (open-source and proprietary side by side). For text/coding, see the benchmarks leaderboard.', 273 fr:"Pour le meilleur modèle sur une tâche donnée, consultez nos classements par préférence humaine. Pour l'image, la vidéo et la voix, le Media Leaderboard classe les modèles par votes humains à l'aveugle (open-source et propriétaire côte à côte). Pour le texte/code, voyez le classement des benchmarks."}, 274 links:[{l:tt('Media leaderboard','Classement média'),u:'/media-leaderboard.html'},{l:tt('Benchmarks','Benchmarks'),u:'/leaderboard.html'}] }, 275 { id:'about', k:['opensourceai','about the site','what is this site','presentation','sert le site','a quoi sert'], 276 a:{en:'olud.ai is the AI hub: thousands of open-source projects tracked for health and momentum, model benchmarks, live pricing for open and commercial models, and human-preference leaderboards â all in six languages.', 277 fr:"olud.ai est le hub de l'IA : des milliers de projets open source suivis pour leur santé et leur dynamique, des benchmarks de modèles, les prix en direct des modèles ouverts et commerciaux, et des classements par préférence humaine â le tout en six langues."}, 278 links:[{l:'Explore projects',u:'/projects.html'},{l:'Live terminal',u:'/dashboard.html'}] }, 279 { id:'plans', k:['plan','plans','price','pricing','cost','subscribe','what are the plans','quels sont les plans','les plans','the plans','tarif','tarifs','prix','abonnement','combien'], 280 a:{en:'olud.ai is currently free to use â paid plans are on pause while we focus on the product. If the platform is useful to you, you can support its development with a voluntary contribution.', 281 fr:"olud.ai est actuellement gratuit â les offres payantes sont en pause, cap sur le produit. Si la plateforme vous est utile, vous pouvez soutenir son développement par une contribution volontaire."}, 282 links:[{l:tt('Support olud.ai','Soutenir olud.ai'),u:'/support.html'}] }, 283 { k:['dashboard','widget','my dashboard','tableau','bord','personnalis'], 284 a:{en:'My Dashboard lets you compose your own view of the ecosystem: pick from 14 widgets (watchlist health, emerging, price drops, benchmarks, industry projectsâ¦), arrange them, and your layout follows your account everywhere.', 285 fr:"My Dashboard vous laisse composer votre propre vue de l'écosystème : choisissez parmi 14 widgets (santé de la watchlist, émergents, baisses de prix, benchmarks, projets par industrieâ¦), organisez-les, et votre disposition vous suit partout."}, 286 links:[{l:'My Dashboard',u:'/my-dashboard.html'}] }, 287 { id:'follow', k:['follow','watch','watchlist','alert','alerts','follow a project','suivre un projet','suivre','suivi','alerte','alertes','notification'], 288 a:{en:'Open any project page and click Follow to add it to your watchlist. Then the bell icon turns on email alerts for health changes, new releases and star spikes. You can follow up to 10 projects.', 289 fr:"Ouvrez une fiche projet et cliquez sur Suivre pour l'ajouter à votre watchlist. La cloche active ensuite les alertes email (changements de santé, nouvelles versions, pics d'étoiles). Vous pouvez suivre jusqu'à 10 projets."}, 290 links:[{l:tt('Projects','Projets'),u:'/projects.html'}] }, 291 { k:['health','score','sante','santé','thriving','risk'], 292 a:{en:'Every tracked project gets a daily health score (0â100) from four signals: activity, momentum, community and maintenance. Green means thriving, amber means slowing, red means at risk.', 293 fr:"Chaque projet suivi reçoit un score de santé quotidien (0â100) basé sur quatre signaux : activité, dynamique, communauté et maintenance. Vert = prospère, ambre = ralentit, rouge = à risque."}, 294 links:[{l:tt('Health monitor','Moniteur de santé'),u:'/health.html'}] }, 295 { k:['emerging','new project','trending','discover','emergent','émergent','nouveau','decouverte','découverte'], 296 a:{en:'The Emerging page surfaces projects our engine just detected gaining traction â before they hit the mainstream. It refreshes with the morning scan.', 297 fr:"La page Ãmergents fait remonter les projets que notre moteur vient de détecter en train de décoller â avant qu'ils ne deviennent connus. Elle se rafraîchit avec le scan du matin."}, 298 links:[{l:tt('Emerging','Ãmergents'),u:'/emerging.html'}] }, 299 { k:['stack','builder','tools','stack builder','outil','construire'], 300 a:{en:'The Stack Builder asks what you want to do (chat, RAG, image generationâ¦) and assembles a compatible open-source stack, with a docker-compose file and compatibility scoring.', 301 fr:"Le Stack Builder vous demande ce que vous voulez faire (chat, RAG, génération d'imagesâ¦) et assemble une stack open-source compatible, avec un f
301ichier docker-compose et un score de compatibilité."}, 302 links:[{l:'Stack Builder',u:'/stack-builder.html'}] }, 303 { k:['benchmark','benchmarks','intelligence index','coding benchmark','best coding model','smartest model','meilleur modele intelligence'], 304 a:{en:'We track model benchmarks (intelligence, coding, math) from Artificial Analysis, plus a media leaderboard with human-preference ELO rankings for image, video and speech models.', 305 fr:"Nous suivons les benchmarks de modèles (intelligence, code, maths) via Artificial Analysis, plus un classement média avec des scores ELO par préférence humaine pour l'image, la vidéo et la voix."}, 306 links:[{l:tt('Leaderboard','Classement'),u:'/leaderboard.html'},{l:'Media ELO',u:'/media-leaderboard.html'}] }, 307 { k:['price','pricing','model price','cost model','token','prix modele','prix modèle','openrouter'], 308 a:{en:'The pricing page shows live model prices per 1M tokens (input and output), and the terminal highlights the biggest price drops and rises each day.', 309 fr:"La page pricing montre les prix des modèles en direct par 1M de tokens (entrée et sortie), et le terminal met en avant les plus grosses baisses et hausses de prix chaque jour."}, 310 links:[{l:'Pricing',u:'/pricing.html'},{l:tt('Terminal','Terminal'),u:'/dashboard.html'}] }, 311 { k:['api','developer','key','endpoint','integrate','integration','développeur','clé','cle'], 312 a:{en:'The public Developer API is paused for now: olud.ai focuses on the free platform. Keys already issued keep working as before.', 313 fr:"L'API développeur publique est en pause pour le moment : olud.ai se concentre sur la plateforme gratuite. Les clés déjà émises continuent de fonctionner."}, 314 links:[{l:tt('Contact us','Nous contacter'),u:'/contact.html'}] }, 315 { k:['claim','maintainer','my project','owner','revendiquer','mainteneur','proprietaire','propriétaire'], 316 a:{en:'If you maintain a project we list, open its page and click "Claim this project". You prove ownership via GitHub, then you appear as the verified maintainer.', 317 fr:'Si vous maintenez un projet que nous listons, ouvrez sa fiche et cliquez sur « Réclamer ce projet ». Vous prouvez la propriété via GitHub, puis vous apparaissez comme mainteneur vérifié.'}, 318 links:[] }, 319 { id:'signup', k:['create account','create an account','make an account','open an account','get started','join the site','sign up','signup','register','how do i sign','creer un compte','créer un compte','inscription','inscrire','nouveau compte'], 320 a:{en:'Creating an account is free, in three steps: 1) open the Account page (avatar, top right); 2) enter your email â or continue with Google/GitHub; 3) click the confirmation link we email you. You are in: dashboard, watchlist, collections and follows are unlocked.', 321 fr:"Créer un compte est gratuit, en trois étapes : 1) ouvrez la page Compte (avatar, en haut à droite) ; 2) entrez votre email â ou continuez avec Google/GitHub ; 3) cliquez le lien de confirmation reçu par email. C'est fait : dashboard, watchlist, collections et suivi sont débloqués."}, 322 links:[{l:tt('Create your account','Créer votre compte'),u:'/account.html'}] }, 323 { k:['sign in','log in','login','connexion','connecter','me connecter'], 324 a:{en:'Click the avatar top-right â enter your email (or Google/GitHub). No password to remember: we email you a sign-in link.', 325 fr:"Cliquez sur l'avatar en haut à droite â entrez votre email (ou Google/GitHub). Pas de mot de passe à retenir : nous vous envoyons un lien de connexion par email."}, 326 links:[{l:tt('Sign in','Se connecter'),u:'/account.html'}] }, 327 { k:['account settings','configure account','account page','settings','your account section','parametrer','paramétrer','parametres','paramètres','regler mon compte','régler','section compte'], 328 a:{en:'The Account page groups everything: your public profile (handle, bio, links, public/private toggle), the alerts you receive, models you follow (with prices), your watchlist, favorites, stacks and collections, and Your data (export or delete). Each section saves in place.', 329 fr:"La page Compte regroupe tout : votre profil public (handle, bio, liens, visible/privé), les alertes que vous recevez, les modèles suivis (avec leurs prix), votre watchlist, favoris, stacks et collections, et Vos données (export ou suppression). Chaque section s'enregistre sur place."}, 330 links:[{l:tt('Open your account','Ouvrir votre compte'),u:'/account.html'}] }, 331 { k:['public profile','profile','handle','bio','my page','profil public','profil','ma page','pseudo'], 332 a:{en:'In Account â "Your public profile": pick a handle, add a bio and links, and choose Public (listed in the members directory, your stacks and collections visible) or Private (only you). Your public page lives at /u/your-handle.', 333 fr:"Dans Compte â « Votre profil public » : choisissez un handle, ajoutez une bi
333o et des liens, puis Public (visible dans l'annuaire des membres, vos stacks et collections affichées) ou Privé (vous seul). Votre page publique vit sur /u/votre-handle."}, 334 links:[{l:tt('Set up your profile','Configurer votre profil'),u:'/account.html'}] }, 335 { k:['alert settings','configure alerts','email alerts settings','mute','threshold','regler alertes','régler les alertes','parametrer alertes','seuil','couper les alertes'], 336 a:{en:'In Account â "Alerts you receive" you control the global switches (new members activity, price moves, new projects). Per-project alerts are set with the bell on each project page or in My Dashboard: health changes, releases, and a star-spike threshold you choose.', 337 fr:"Dans Compte â « Vos alertes », vous contrôlez les interrupteurs globaux (activité des membres suivis, mouvements de prix, nouveaux projets). Les alertes par projet se règlent avec la cloche sur chaque fiche ou dans My Dashboard : santé, releases, et un seuil de pic d'étoiles à votre choix."}, 338 links:[{l:tt('Alert settings','Régler les alertes'),u:'/account.html'}] }, 339 { k:['api key','my key','webhook','webhooks','developer section','cle api','clé api','ma cle'], 340 a:{en:'API keys and webhooks already issued keep working, but this area is closed to new sign-ups for now. If your key misbehaves, write to us.', 341 fr:"Les clés API et webhooks déjà émis continuent de fonctionner, mais cet espace est fermé aux nouvelles inscriptions pour le moment. Un souci avec votre clé ? Ãcrivez-nous."}, 342 links:[{l:tt('Contact us','Nous contacter'),u:'/contact.html'}] }, 343 { k:['cancel','unsubscribe','billing','invoice','manage subscription','upgrade','downgrade','annuler','resilier','résilier','facture','abonnement gerer','gérer abonnement','changer de plan'], 344 a:{en:'There is nothing to upgrade right now: olud.ai is free and paid plans are on pause. Existing subscribers keep their access and can manage billing from Account â the secure Stripe portal.', 345 fr:"Rien à débloquer pour l'instant : olud.ai est gratuit, les offres payantes sont en pause. Les abonnés existants gardent leur accès et gèrent leur facturation depuis Compte â portail Stripe sécurisé."}, 346 links:[{l:tt('Your account','Votre compte'),u:'/account.html'},{l:tt('Support olud.ai','Soutenir olud.ai'),u:'/support.html'}] }, 347 { k:['delete account','export data','my data','privacy','gdpr','supprimer compte','supprimer mon compte','exporter','mes donnees','mes données','rgpd'], 348 a:{en:'In Account â "Your data": export everything you own (watchlist, collections, profile) or delete your account permanently. Deletion is immediate and irreversible.', 349 fr:"Dans Compte â « Vos données » : exportez tout ce qui vous appartient (watchlist, collections, profil) ou supprimez définitivement votre compte. La suppression est immédiate et irréversible."}, 350 links:[{l:tt('Your data','Vos données'),u:'/account.html'}] }, 351 { k:['language','translate','french','english','langue','traduction','francais','français'], 352 a:{en:'The site is available in English, French, German, Spanish, Italian and Portuguese. Use the language switcher in the top bar â your choice is remembered for next time.', 353 fr:"Le site existe en anglais, français, allemand, espagnol, italien et portugais. Utilisez le sélecteur de langue dans la barre du haut â votre choix est mémorisé pour la prochaine fois."}, 354 links:[] }, 355 { k:['report','weekly','portfolio','rapport','hebdo','hebdomadaire'], 356 a:{en:'The weekly portfolio report was part of the paid plans, which are currently on pause. You can still follow projects from your account and get alerts.', 357 fr:"Le rapport de portefeuille hebdomadaire faisait partie des offres payantes, actuellement en pause. Vous pouvez toujours suivre des projets depuis votre compte et recevoir des alertes."}, 358 links:[{l:tt('Support olud.ai','Soutenir olud.ai'),u:'/support.html'}] }, 359 { k:['contact','help','support','email','human','aide','contacter'], 360 a:{en:'For anything I cannot answer, email [email protected] and a human will get back to you.', 361 fr:"Pour tout ce à quoi je ne peux pas répondre, écrivez à [email protected] et un humain vous répondra."}, 362 links:[] } 363 ]; 364 365 var SUGGEST = [ 366 { t: tt('What is olud.ai?','C\u2019est quoi olud.ai ?'), id:'about' }, 367 { t: tt('How do I create an account?','Comment créer un compte ?'), id:'signup' }, 368 { t: tt('What are the plans?','Quels sont les plans ?'), id:'plans' }, 369 { t: tt('How do I follow a project?','Comment suivre un projet ?'), id:'follow' } 370 ]; 371 function kbById(id){ for (var i=0;i<KB.length;i++){ if (KB[i].id === id) return KB[i]; } return null; } 372 373 function norm(s){ return (' ' + String(s).toLowerCase() + ' ').replace(/[^a-z0-9\u00e0-\u00ff]+/g,' '); } 374 function answer(q){ 375 var nq = norm(q), best = null, bestScore = 0; 376 for (var i=0;i<KB.length;i++){ 377 var sc = 0; 378 for (var j=0;j<KB[i].k.length;j++){ 379 var kw = norm(KB[i].k[j]).trim(); 380 if (!kw) continue; 381 if (nq.indexOf(' ' + kw + ' ') >= 0){ 382 /* un mot-clé spécifique (multi-mots ou long) pèse plus qu'un mot court */ 383 sc += (kw.indexOf(' ') >= 0 ? 3 : (kw.length >= 6 ? 2 : 1)); 384 } 385 } 386 if (sc > bestScore){ bestScore = sc; best = KB[i]; } 387 } 388 /* seuil : au moins un signal réel, sinon on ne devine pas */ 389 if (!best || bestScore < 2) return null; 390 return best; 391 } 392 393 /* âââ Empilement en bas à droite âââ 394 Trois éléments visent le même coin : la bulle d'aide, le panneau #osaiCta, 395 et la barre de cookies â qui vit dans le HTML de chaque page, hors de ce 396 script. Au premier passage d'un visiteur, la bulle recouvrait le bouton 397 « Accept all », dans la même couleur qui plus est. 398 On mesure donc ce qui occupe le bas de l'écran et on remonte la bulle 399 au-dessus. Aucune connaissance du markup de la bannière n'est requise : 400 on repère tout élément fixé en bas, visible et assez large. */ 401 function osaiStackBottom(){
402 try{ 403 var vh = window.innerHeight || 0, vw = window.innerWidth || 0; 404 var fab = document.getElementById('osaiHelpBtn'); 405 var fabW = fab ? fab.offsetWidth || 52 : 52; 406 /* [02/08/2026, Romain] Les deux pastilles vivent desormais dans les deux 407 coins du bas â assistant a GAUCHE, « remonter » a DROITE â et a la 408 MEME hauteur. La zone surveillee couvre donc les deux coins : ce qui 409 gene l'une gene l'autre, puisqu'elles partagent leur ligne. */ 410 var zoneL = 12, zoneR = vw - 12; 411 var need = 20; 412 413 /* On balaie deux niveaux : la plupart des calques fixes sont enfants 414 directs de <body>, mais certains sont enveloppés une fois. Aller 415 plus profond coûterait un getComputedStyle sur toute la page. */ 416 var nodes = []; 417 var kids = document.body ? document.body.children : []; 418 for (var a = 0; a < kids.length; a++) { 419 nodes.push(kids[a]); 420 var sub = kids[a].children; 421 for (var b = 0; b < sub.length && b < 12; b++) nodes.push(sub[b]); 422 } 423 424 for (var i = 0; i < nodes.length; i++) { 425 var el = nodes[i]; 426 if (!el.tagName) continue; 427 if (el.id === 'osaiHelpBtn' || el.id === 'osaiHelpPanel') continue; 428 if (fab && (el === fab || el.contains(fab))) continue; 429 /* [02/08/2026, Romain : « ils se baladent sur l'ecran quand on scroll »] 430 LE BOUTON « REMONTER » N'EST PAS UN OBSTACLE, C'EST UN PAIR. 431 Il partage la meme ligne et la meme variable --osai-fab-bottom que 432 l'assistant. En le laissant dans le balayage, chacun mesurait 433 l'autre et le repoussait : l'assistant monte, donc le bouton monte, 434 donc l'assistant monte encore. La boucle ne s'arretait qu'a la 435 butee « jamais plus haut que la moitie de l'ecran » â d'ou les deux 436 pastilles plantees au milieu du telephone. 437 Sur mobile la barre d'URL qui se replie declenche un resize a 438 chaque defilement, ce qui relançait la boucle : d'ou l'impression 439 qu'elles se baladent. */ 440 if (el.matches && el.matches('.back-to-top, #backToTop')) continue; 441 if (el.querySelector && el.querySelector('.back-to-top, #backToTop')) continue; 442 var cs = window.getComputedStyle(el); 443 if (cs.position !== 'fixed' || cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') continue; 444 var r = el.getBoundingClientRect(); 445 if (!r.height || !r.width) continue; 446 447 /* Deux cas seulement nous concernent : 448 · une BANNIÃRE large occupant le bas de l'écran ; 449 · un élément qui chevauche la COLONNE de la bulle. 450 â L'erreur précédente : j'exigeais que l'élément touche le bas de 451 l'écran (r.bottom >= vh - 4). Un bouton posé à bottom:20px a son 452 bord 20 px plus haut â il était donc toujours ignoré. On accepte 453 maintenant tout ce qui se trouve dans le tiers inférieur. */ 454 var lower = r.bottom > vh * 0.6; 455 if (!lower) continue; 456 var wideBanner = r.width >= vw * 0.5; 457 var overlapsFab = (r.right > zoneL) && (r.left < zoneR); 458 if (!wideBanner && !overlapsFab) continue; 459 460 need = Math.max(need, (vh - r.top) + 14); 461 } 462 /* Garde-fou : jamais plus haut que la moitié de l'écran. Une mesure 463 aberrante ne doit pas envoyer la bulle au milieu de la page. */ 464 if (need > vh * 0.5) need = 20; 465 document.documentElement.style.setProperty('--osai-fab-bottom', Math.round(need) + 'px'); 466 }catch(e){} 467 } 468 469 /* Les bannières de cookies apparaissent souvent avec un délai : on repasse. */ 470 [0, 400, 1200, 3000].forEach(function(d){ setTimeout(osaiStackBottom, d); }); 471 /* Le bandeau cookies deplace la ligne commune : « remonter » doit suivre. */ 472 [500, 1300, 3100].forEach(function(d){ setTimeout(function(){
473 try { if (typeof placerAssistant === 'function') placerAssistant(); } catch(e){} 474 }, d); }); 475 window.addEventListener('resize', osaiStackBottom); 476 if (window.MutationObserver && document.body) { 477 new MutationObserver(osaiStackBottom).observe(document.body, { childList: true }); 478 } 479 480 /* ââ CSS ââ */ 481 var css = document.createElement('style'); 482 css.textContent = 483 /* [01/08, Romain] L'assistant partageait le coin bas-droit avec le 484 bouton « remonter en haut » : deux pastilles empilees dans le meme 485 angle, l'une masquant l'autre au defilement. Il passe a GAUCHE â 486 le coin est libre, et la separation rend les deux gestes lisibles. */ 487 '#osaiHelpBtn{position:fixed;bottom:var(--osai-fab-bottom,20px);left:20px;width:52px;height:52px;border-radius:50%;border:none;cursor:pointer;transition:bottom .25s ease;' 488 + 'background:var(--surface,#141a29);color:var(--accent,#6366f1);border:1px solid var(--border2,rgba(255,255,255,.18));box-shadow:0 4px 18px rgba(0,0,0,.22);z-index:99998;display:flex;align-items:center;justify-content:center;transition:transform .15s}' 489 + '#osaiHelpBtn:hover{transform:scale(1.06);border-color:rgba(99,102,241,.55)}' 490 + '#osaiHelpBtn svg{width:30px;height:30px}' 491 /* Le signal IA, sans ecrire le mot : la diode du logo respire quand on 492 approche. Rien ne bouge tant qu'on ne s'en approche pas â c'est une 493 icone presente sur toutes les pages du site. */ 494 + '#osaiHelpBtn .oludbot-led{transform-origin:26.2px 5.8px;transition:opacity .2s}' 495 + '#osaiHelpBtn:hover .oludbot-led{animation:oludbotPulse 1.9s ease-in-out infinite}' 496 + '@keyframes oludbotPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.72)}}' 497 + '@media (prefers-reduced-motion: reduce){#osaiHelpBtn:hover .oludbot-led{animation:none}}' 498 + '#osaiHelpPanel{position:fixed;bottom:88px;left:20px;width:min(370px,calc(100vw - 40px));height:min(520px,calc(100vh - 130px));' 499 + 'height:min(520px,calc(100dvh - 130px));' 500 + 'background:var(--surface,#fff);border:1px solid var(--border2,rgba(0,0,0,.12));border-radius:18px;box-shadow:0 18px 60px rgba(0,0,0,.28);' 501 + 'z-index:99999;display:none;flex-direction:column;overflow:hidden}' 502 + '#osaiHelpPanel.open{display:flex}' 503 504 /* [01/08, Romain : « assez ringard »] Le bandeau sombre en degrade avec 505 un logo en PNG, c'est le chatbot de 2019. On passe sur la surface du 506 site, une hairline en degrade de marque pour separer, et la mascotte 507 posee dans une pastille teintee. Tout le reste du panneau suit les 508 variables du site : il change avec le theme au lieu d'imposer son 509 noir. */ 510 + '.ohp-head{background:var(--surface,#fff);color:var(--txt,#111);padding:13px 14px;flex:none;' 511 + 'display:flex;align-items:center;gap:11px;position:relative}' 512 + '.ohp-head::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;' 513 + 'background:linear-gradient(90deg,#3B57FF,#A257FF,#0ea5e9)}' 514 /* [01/08, Romain : « on est oblige de le mettre dans une carte ? »] Non. 515 La pastille teintee etait une habitude, pas une necessite : elle 516 enfermait la mascotte et lui volait 8px de chaque cote. Sans elle, 517 la tete respire et peut grandir a taille egale. */ 518 + '.ohp-logo{flex:none;display:flex;align-items:center;justify-content:center}' 519 + '.ohp-logo svg{width:36px;height:36px}' 520 + '.ohp-ttl{flex:1;min-width:0}' 521 + '.ohp-x{flex:none;width:30px;height:30px;border:0;border-radius:9px;background:var(--surface2,#f1f3f8);' 522 + 'color:var(--muted,#667);font-size:19px;line-height:1;cursor:pointer;padding:0;transition:all .15s}' 523 + '.ohp-x:hover{background:var(--border2,#e3e7ef);color:var(--txt,#111)}' 524 + '.ohp-head b{font-size:.95rem;font-weight:700;letter-spacing:-.01em}' 525 + '.ohp-head span{display:block;font-size:.7rem;line-height:1.35;color:var(--muted,#667);margin-top:1px;' 526 + 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' 527 /* Le corps est le seul element elastique : il absorbait tout le deficit 528 et tombait a 36px. Un plancher, qui se rabaisse tout seul sur un tres 529 petit ecran pour ne jamais chasser le champ de saisie. */ 530 + '.ohp-body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;' 531 + 'background:var(--bg,#fff);min-height:min(96px,22vh)}' 532 + '.ohp-body{min-height:min(96px,22dvh)}' 533 + '.ohp-msg{max-width:85%;padding:10px 13px;border-radius:13px;font-size:.84rem;line-height:1.5}' 534 + '.ohp-bot{align-self:flex-start;background:var(--surface2,#f4f5f8);color:var(--txt,#111);' 535 + 'border:1px solid var(--border,rgba(0,0,0,.06));border-bottom-left-radius:5px}' 536 /* la bulle du visiteur porte le degrade DU LOGO, pas un indigo plat */ 537 + '.ohp-user{align-self:flex-end;background:linear-gradient(135deg,#3B57FF,#A257FF);color:#fff;' 538 + 'border-bottom-right-radius:5px}' 539 + '.ohp-msg a{color:var(--accent,#6366f1);font-weight:600;text-decoration:none}.ohp-bot a{display:inline-block;margin-top:4px}' 540 + '.ohp-sugg{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}' 541 + '.ohp-sugg button{background:none;border:1px solid var(--accent,#6366f1);color:var(--accent,#6366f1);border-radius:999px;' 542 + 'padding:6px 11px;font-family:inherit;font-size:.74rem;cursor:pointer;transition:background .15s}' 543 + '.ohp-sugg button:hover{background:var(--accent,#6366f1);color:#fff}' 544 + '.ohp-foot{flex:none;display:flex;gap:8px;padding:11px;border-top:1px solid var(--border,rgba(0,0,0,.08));background:var(--surface,#fff)}' 545 + '.ohp-foot input{flex:1;border:1px solid var(--border2,rgba(0,0,0,.15));border-radius:999px;padding:9px 14px;font-family:inherit;font-size:.83rem;background:var(--bg,#fff);color:var(--txt,#111);outline:none}'
546 + '.ohp-foot input:focus{border-color:var(--accent,#6366f1)}' 547 + '.ohp-foot button{background:var(--accent,#6366f1);border:none;color:#fff;border-radius:50%;width:38px;height:38px;flex:none;cursor:pointer;font-size:1.05rem}' 548 + '.ohp-note{flex:none;padding:0 13px 9px;font-size:.62rem;line-height:1.35;color:var(--muted,#666);background:var(--surface,#fff)}' 549 /* Sous 520px de haut â bureau reduit, telephone en paysage â le panneau 550 prend la fenetre au lieu d'en reserver 130px. Sur la fenetre mesuree 551 a 374px : 192px pour les messages au lieu de 36. */ 552 + '@media (max-height:520px){#osaiHelpPanel{bottom:12px;left:12px;right:auto;' 553 + 'width:min(370px,calc(100vw - 24px));height:calc(100vh - 24px);height:calc(100dvh - 24px)}' 554 + '.ohp-head{padding:9px 14px}.ohp-note{padding:0 13px 7px;font-size:.58rem}}' 555 /* Le champ garde son rayon en pastille : la regle globale de index.html 556 (input{border-radius:8px !important}) le rabotait a 8px sur les pages 557 qui la portent â l'assistant n'avait pas la meme tete selon la page. 558 Regle separee, pour ne pas deplacer la specificite du :focus. */
559 + '.ohp-foot input:not([type="range"]){border-radius:999px !important}' 560 /* Un focus qui se voit sur les DEUX fonds : l'indigo translucide seul 561 tombait a 1,3:1 sur le pied sombre. */ 562 + '.ohp-foot input:focus{box-shadow:0 0 0 3px rgba(99,102,241,.30)}' 563 + 'body.dark .ohp-foot input:focus{box-shadow:0 0 0 3px rgba(165,180,252,.34)}'; 564 document.head.appendChild(css); 565 566 /* ââ DOM ââ */ 567 var btn = document.createElement('button'); 568 btn.id = 'osaiHelpBtn'; btn.type = 'button'; 569 btn.setAttribute('aria-label', tt('Help','Aide')); 570 /* âââ LA MASCOTTE ââââââââââââââââââââââââââââââââââââââââââââââââââââ 571 [01/08, Romain] « une tete de robot qui ressemble a notre logo, et une 572 icone qui fait comprendre subtilement que c'est de l'IA ». 573 Elle N'EST PAS inspiree du logo : elle EST le logo. L'anneau du "O" 574 devient le visage, en trait (2,6 sur un viewBox de 32, soit environ 2px 575 a la taille du bouton â la meme graisse que les icones du menu). Deux 576 yeux rectangulaires arrondis, un regard de machine. PAS DE BOUCHE : 577 Romain la voulait sans sourire, et un outil professionnel n'a pas a 578 faire risette. Le point detache du logo, en haut a droite, devient la 579 diode : c'est LUI qui porte le signal IA â il respire lentement, et 580 seulement au survol, jamais en permanence sur toutes les pages. 581 L'encoche ronde qui le separe de l'anneau est celle du logo, au meme 582 endroit, a l'echelle. */ 583 btn.innerHTML = '<svg class="oludbot" viewBox="0 0 32 32" width="30" height="30" aria-hidden="true" focusable="false">' 584 + '<defs><linearGradient id="oludbot-bg" x1="0" y1="1" x2="1" y2="0">' 585 + '<stop offset="0" stop-color="#3B57FF"/><stop offset="1" stop-color="#A257FF"/></linearGradient>' 586 + '<mask id="oludbot-bm"><rect width="32" height="32" fill="#fff"/>' 587 + '<circle cx="26.2" cy="5.8" r="4.4" fill="#000"/></mask></defs>' 588 + '<g mask="url(#oludbot-bm)"><circle cx="14.4" cy="17.6" r="11.2" fill="none" stroke="url(#oludbot-bg)" stroke-width="2.5"/></g>' 589 + '<rect x="9.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-bg)"/>' 590 + '<rect x="17.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-bg)"/>' 591 + '<circle class="oludbot-led" cx="26.2" cy="5.8" r="2.6" fill="url(#oludbot-bg)"/></svg>'; 592 593 var panel = document.createElement('div'); 594 panel.id = 'osaiHelpPanel'; 595 panel.innerHTML = 596 /* [30/07] L'en-tete n'avait ni logo ni bouton de fermeture : une fois 597 l'assistant ouvert, on ne pouvait plus le refermer (releve par 598 Romain). Trois colonnes : la marque, le titre, la croix. */ 599 '<div class="ohp-head">' 600 + '<span class="ohp-logo">' + '<svg class="oludbot" viewBox="0 0 32 32" width="26" height="26" aria-hidden="true" focusable="false">' 601 + '<defs><linearGradient id="oludbot-hg" x1="0" y1="1" x2="1" y2="0">' 602 + '<stop offset="0" stop-color="#3B57FF"/><stop offset="1" stop-color="#A257FF"/></linearGradient>' 603 + '<mask id="oludbot-hm"><rect width="32" height="32" fill="#fff"/>' 604 + '<circle cx="26.2" cy="5.8" r="4.4" fill="#000"/></mask></defs>' 605 + '<g mask="url(#oludbot-hm)"><circle cx="14.4" cy="17.6" r="11.2" fill="none" stroke="url(#oludbot-hg)" stroke-width="2.5"/></g>' 606 + '<rect x="9.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-hg)"/>' 607 + '<rect x="17.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-hg)"/>' 608 + '<circle class="oludbot-led" cx="26.2" cy="5.8" r="2.6" fill="url(#oludbot-hg)"/></svg>
608' + '</span>' 609 + '<div class="ohp-ttl"><b>' + tt('Olud assistant','Assistant Olud') + '</b>' 610 + '<span>' + tt('\u2728 AI \u2014 ask me anything about olud.ai', 611 '\u2728 IA \u2014 posez-moi vos questions sur olud.ai') + '</span></div>' 612 + '<button type="button" class="ohp-x" id="ohpClose" aria-label="' + tt('Close','Fermer') + '">×</button>' 613 + '</div>' 614 + '<div class="ohp-body" id="ohpBody"></div>' 615 + '<div class="ohp-foot"><input type="text" id="ohpInput" maxlength="500" placeholder="' + tt('Type your questionâ¦','Ãcrivez votre questionâ¦') + '" autocomplete="off">' 616 + '<button type="button" id="ohpSend" aria-label="Send">➤</button></div>' 617 + '<div class="ohp-note">' + tt('Questions are logged anonymously to improve the answers.','Vos questions sont enregistrées de façon anonyme pour améliorer les réponses.') + '</div>'; 618 619 document.body.appendChild(btn); 620 document.body.appendChild(panel); 621 622 var body = panel.querySelector('#ohpBody'); 623 var input = panel.querySelector('#ohpInput'); 624 625 function esc2(s){ return String(s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } 626 627 /* âââ RENDU D'UNE REPONSE DU MODELE âââââââââââââââââââââââââââââââââââ 628 [01/08, Romain] « l'IA pourrait proposer les liens des pages plutot que 629 des markdown ». Le modele ecrit naturellement [ComfyUI](/tool/comfyui.html) 630 et **en gras** ; on n'en rendait que les chemins NUS, donc les crochets 631 et les etoiles s'affichaient tels quels. C'etait illisible. 632 633 La regle de securite ne bouge pas d'un pouce : le texte est ECHAPPE 634 d'abord, donc rien de ce que le modele ecrit ne peut devenir du HTML. 635 On ne re-fabrique ensuite que trois choses, et seulement a partir de 636 motifs verifies : un lien INTERNE (le chemin doit commencer par une 637 barre suivie d'une lettre ou d'un chiffre - ni « javascript: », ni 638 « // » vers un autre domaine), du gras, et les retours a la ligne. */ 639 function rendreReponse(txt){ 640 var s = echapper(String(txt == null ? '' : txt)); 641 /* [libelle](/chemin) -> vrai lien */ 642 s = s.replace(/\[([^\]\n]{1,90})\]\((\/[a-z0-9][a-z0-9._\-\/]*)\)/gi, 643 function(m, lib, ch){ return '<a href="' + ch + '">' + lib + '</a>'; }); 644 /* chemin nu, hors d'un lien deja fabrique */ 645 s = s.replace(/(^|[\s(])(\/[a-z][a-z0-9\-\/]*(?:\.html)?)(?![^<]*<\/a>)/g, 646 function(m, av, ch){ return av + '<a href="' + ch + '">' + ch + '</a>'; }); 647 s = s.replace(/\*\*([^*\n]{1,120})\*\*/g, '<strong>$1</strong>'); 648 /* une puce en debut de ligne devient une vraie puce */ 649 s = s.replace(/(^|\n)[-\u2022]\s+/g, '$1\u2022\u00a0'); 650 return s.replace(/\n/g, '<br>'); 651 } 652 function addBot(html){ var d = document.createElement('div'); d.className = 'ohp-msg ohp-bot'; d.innerHTML = html; body.appendChild(d); body.scrollTop = body.scrollHeight; } 653 function addUser(txt){ var d = document.createElement('div'); d.className = 'ohp-msg ohp-user'; d.textContent = txt; body.appendChild(d); body.scrollTop = body.scrollHeight; } 654 655 function botAnswer(entry){ 656 var html = esc2(entry.a[L] || entry.a.en); 657 if (entry.links && entry.links.length){ 658 html += '<br>' + entry.links.map(function(x){ return '<a href="' + esc2(x.u) + '">' + esc2(x.l) + ' →</a>'; }).join(' '); 659 } 660 addBot(html); 661 } 662 function fallback(){ 663 addBot(tt('I did not catch that. Try one of these, or email <a href="mailto:[email protected]">[email protected]</a>.', 664 'Je n\u2019ai pas bien compris. Essayez un de ces sujets, ou écrivez à <a href="mailto:[email protected]">[email protected]</a>.')); 665 showSuggest(); 666 } 667 function showSuggest(){ 668 var wrap = document.createElement('div'); wrap.className = 'ohp-sugg'; 669 SUGGEST.forEach(function(s){ 670 var b = document.createElement('button'); b.type = 'button'; b.textContent = s.t; 671 b.onclick = function(){ 672 addUser(s.t); 673 var e = kbById(s.id); 674 report(s.t, e); 675 if (e) botAnswer(e); else fallback(); /* référence directe : ne peut pas échouer */ 676 }; 677 wrap.appendChild(b); 678 }); 679 body.appendChild(wrap); body.scrollTop = body.scrollHeight; 680 } 681 function report(q, entry){ 682 var topic = entry ? (entry.id || (entry.k && entry.k[0]) || 'topic') : 'none'; 683 /* Google Analytics (événement personnalisé) */
684 try { if (window.osaiTrack) window.osaiTrack('assistant_question', { question: String(q).slice(0,90), matched: topic, lang: L }); } catch (e0) {} 685 /* Journal serveur : c'est lui qui donne la liste des questions ratées */ 686 try { 687 fetch('/api/assistant-log.php', { 688 method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, 689 body: JSON.stringify({ q: String(q).slice(0,200), matched: topic, lang: L, path: location.pathname }) 690 }).catch(function(){}); 691 } catch (e1) {} 692 } 693 /* [01/08/2026] L'assistant repondait par regles figees : il ne 694 comprenait que les formulations prevues, et « fallback » pour tout le 695 reste. Les reponses connues restent PRIORITAIRES (instantanees, 696 gratuites, exactes) ; l'IA ne prend le relais que sur ce qu'elles ne 697 couvrent pas. La cle vit sur le serveur, jamais ici. */ 698 function echapper(t){ return String(t).replace(/[&<>]/g, function(c){ 699 return {'&':'&','<':'<','>':'>'}[c]; }); } 700 701 function ask(q){ 702 addUser(q); 703 /* [01/08] Les reponses toutes faites passaient AVANT l'IA, et elles se 704 declenchent sur un simple mot-cle : « which model runs on 16GB » 705 contient « model », l'assistant repondait donc par le paragraphe 706 generique sur le classement. Desormais, des qu'on lui parle en 707 phrase â quatre mots ou plus, ou un point d'interrogation â la 708 question part directement au modele. Les regles gardent ce pour quoi 709 elles sont bonnes et instantanees : « submit », « badge », « claim », 710 « rss », tapes en un ou deux mots. */ 711 var mots = String(q).trim().split(/\s+/).filter(Boolean).length; 712 var phrase = (mots >= 4) || /[?\uFF1F]/.test(q); 713 var e = answer(q); 714 report(q, e); 715 /* Un ou deux mots : la reponse ecrite d'avance est bonne et instantanee. 716 Une phrase : le modele repond mieux â et la reponse ecrite d'avance 717 devient le FILET si l'appel echoue, au lieu d'un « je n'ai pas 718 compris ». On ne perd rien, on gagne les vraies questions. */ 719 if (e && !phrase) { botAnswer(e); return; } 720 var attente = document.createElement('div'); 721 attente.className = 'ohp-msg ohp-bot'; 722 attente.textContent = tt('Thinking\u2026', 'Je cherche\u2026'); 723 body.appendChild(attente); body.scrollTop = body.scrollHeight; 724 fetch('/api/ask.php', { method:'POST', headers:{'Content-Type':'application/json'}, 725 body: JSON.stringify({ q: q }) }) 726 .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, j:j }; }); }) 727 .then(function(res){ 728 attente.remove(); 729 if (res.ok && res.j && res.j.answer) { 730 /* le texte est echappe : une reponse de modele n'injecte pas de 731 HTML dans la page. Seuls nos propres liens internes sont 732 rendus cliquables, et uniquement des chemins du site. */ 733 addBot(rendreReponse(res.j.answer)); 734 } else if (res.j && res.j.error) { 735 addBot(echapper(res.j.error)); 736 } else if (e) { botAnswer(e); } 737 else { fallback(); } 738 }) 739 .catch(function(){ attente.remove(); if (e) { botAnswer(e); } else { fallback(); } }); 740 } 741 742 var greeted = false; 743 function greet(){ 744 if (greeted) return; greeted = true; 745 addBot(tt('Hi! I\u2019m the olud.ai assistant. How can I help?','Bonjour ! Je suis l\u2019assistant olud.ai. Comment puis-je vous aider ?')); 746 showSuggest(); 747 } 748 749 /* ââ LE COIN OPPOSE A « REMONTER EN HAUT » ââââââââââââââââââââââââââââ 750 [01/08, Romain] « son bouton est a droite sur ordinateur comme le 751 bouton remonter en haut, pourquoi ne pas le mettre a gauche ? » 752 Une regle figee ne suffit pas : « remonter en haut » n'est pas au meme 753 endroit partout â a droite sur la plupart des pages, mais a GAUCHE sous 754 900px sur l'accueil et sur compare-tools (pour liberer le bouton de 755 menu). Une largeur ecrite en dur aurait donc recree l'empilement 756 ailleurs. On regarde ou il est REELLEMENT, et l'assistant prend l'autre 757 coin. Recalcule au redimensionnement, puisque la position change avec 758 la largeur. */ 759 function placerAssistant(){ 760 /* [02/08/2026, Romain] « sur mobile les deux boutons sont a gauche ; mets 761 remonter a droite, laisse l'assistant a gauche, a la meme hauteur. » 762 763 La regle « coin oppose » qui vivait ici lisait la position de 764 « remonter » et prenait l'autre coin. Elle etait juste sur le principe 765 et fausse en pratique : sous 900px, l'accueil et compare-tools posent 766 « remonter » A GAUCHE, l'assistant filait donc a droite â l'inverse de 767 ce qu'on veut, et differemment d'une page a l'autre. 768 769 On fixe donc les deux places au lieu de les deduire. L'assistant est le 770 geste qu'on propose, il occupe le coin naturel du pouce ; « remonter » 771 est un utilitaire, il va en face. Et les deux partagent la meme ligne : 772 c'est --osai-fab-bottom, qui sait deja s'ecarter du bandeau cookies. */ 773 [btn, panel].forEach(function(el){ 774 el.style.setProperty('left', '20px', 'important'); 775 el.style.setProperty('right', 'auto', 'important'); 776 }); 777 var haut = document.querySelector('.back-to-top, #backToTop'); 778 if (haut) { 779 haut.style.setProperty('right', '20px', 'important'); 780 haut.style.setProperty('left', 'auto', 'important'); 781 /* Meme ligne que l'assistant, au centrage pres : les deux pastilles 782 n'ont pas la meme taille (44 contre 52), on aligne leurs CENTRES. */ 783 var hb = haut.offsetHeight || 44, fb = btn.offsetHeight || 52; 784 var base = parseFloat(getComputedStyle(document.documentElement) 785 .getPropertyValue('--osai-fab-bottom')) || 20; 786 haut.style.setProperty('bottom', Math.round(base + (fb - hb) / 2) + 'px', 'important'); 787 } 788 } 789 placerAssistant(); 790 var minuteur = null; 791 window.addEventListener('resize', function(){ 792 clearTimeout(minuteur); minuteur = setTimeout(placerAssistant, 150); 793 }); 794 795 btn.addEventListener('click', function(){ 796 panel.classList.toggle('open'); 797 if (panel.classList.contains('open')){ greet(); setTimeout(function(){ input.focus(); }, 80); } 798 }); 799 /* [01/08] Ouvrir l'assistant depuis ailleurs â la palette de recherche 800 propose « demander a l'assistant » quand la recherche texte ne suffit 801 pas. C'est le pont honnete entre les deux : la recherche ne devine 802 rien, l'assistant si. */
803 /* âââ REPONDRE SUR PLACE, SANS OUVRIR LE PANNEAU ââââââââââââââââââââââ 804 [01/08, Romain] « quand on tape dans la barre de recherche, pourquoi ca 805 nous renvoie vers Olud assistant ? y'aurait pas moyen d'avoir une bulle 806 de reponse ? ». Si. Chercher et demander sont deux gestes differents : 807 le premier ne doit pas faire changer de meuble. La reponse s'affiche 808 donc DANS la liste de resultats, et le panneau ne s'ouvre que si le 809 visiteur veut poursuivre la conversation. 810 Une seule implementation pour les deux barres du site : le meme appel, 811 le meme rendu, les memes garde-fous. */ 812 window.osaiAskInto = function(question, hote){ 813 if (!hote) return; 814 var q = String(question || '').trim(); 815 if (!q) return; 816 var t2 = function(x){ return (typeof window.osaiT === 'function') ? window.osaiT(x) : x; }; 817 hote.innerHTML = '<div class="osai-inline osai-inline-wait">' + esc2(t2('Thinking\u2026')) + '</div>'; 818 819 var repli = answer(q); /* filet : nos reponses ecrites d'avance */ 820 report(q, repli); 821 822 var poser = function(html, suite){ 823 hote.innerHTML = '<div class="osai-inline">' + html 824 + (suite === false ? '' : 825 '<button type="button" class="osai-inline-more">' + esc2(t2('Continue in the assistant')) + '</button>') 826 + '</div>'; 827 var b = hote.querySelector('.osai-inline-more'); 828 if (b) b.addEventListener('click', function(){ window.osaiAsk(q); }); 829 }; 830 831 fetch('/api/ask.php', { method:'POST', headers:{'Content-Type':'application/json'}, 832 body: JSON.stringify({ q: q }) }) 833 .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, j:j }; }); }) 834 .then(function(res){ 835 if (res.ok && res.j && res.j.answer) poser(rendreReponse(res.j.answer)); 836 else if (repli) poser(rendreReponse(repli.a[L] || repli.a.en)); 837 else if (res.j && res.j.error) poser(esc2(res.j.error), false); 838 else poser(esc2(t2('I could not answer that one.')), false); 839 }) 840 .catch(function(){ 841 if (repli) poser(rendreReponse(repli.a[L] || repli.a.en)); 842 else poser(esc2(t2('I could not answer that one.')), false); 843 }); 844 }; 845 846 window.osaiAsk = function(question){ 847 panel.classList.add('open'); 848 greet(); 849 if (question) { setTimeout(function(){ ask(question); }, 120); } 850 else { setTimeout(function(){ input.focus(); }, 80); } 851 }; 852 /* [30/07] Trois facons de refermer, parce qu'une seule ne suffit jamais : 853 la croix, la touche Echap, et le bouton flottant qui bascule deja. 854 Sans elles, l'assistant ouvert ne se refermait plus â le panneau 855 recouvre en partie le bouton flottant (releve par Romain). */ 856 var xBtn = panel.querySelector('#ohpClose'); 857 if (xBtn) xBtn.addEventListener('click', function(e){ 858 e.stopPropagation(); panel.classList.remove('open'); btn.focus(); 859 }); 860 document.addEventListener('keydown', function(e){ 861 if (e.key === 'Escape' && panel.classList.contains('open')){ 862 panel.classList.remove('open'); btn.focus(); 863 } 864 }); 865 function send(){ var v = input.value.trim(); if (!v) return; input.value = ''; ask(v); } 866 panel.querySelector('#ohpSend').addEventListener('click', send); 867 input.addEventListener('keydown', function(e){ if (e.key === 'Enter') send(); }); 868 })(); 869 } catch(e){} 870 871window.__OSAI_NAV = 'V166-WEBMCP'; console.log('%c[OSAI-NAV] version qui tourne : V166-WEBMCP â lang=' + NAV_LANG, 'color:#6366f1;font-weight:bold'); } catch(e){} 872 var NAV_I18N = { 873 fr: { 874 "Online":"En ligne","I can help you find AI tools, compare models and explore projects â or just search.":"Je peux vous aider à trouver un outil, comparer des modèles, explorer des projets â ou simplement chercher.","Ask anything.":"Demandez ce que vous voulez.",
875 "Updates":"Mise à jour", 876 "Hourly":"Chaque heure", 877 "Continue in the assistant":"Continuer dans lâassistant","I could not answer that one.":"Je nâai pas su répondre à celle-là .", 878 "Search, or ask anythingâ¦":"Cherchez, ou posez votre questionâ¦","Search tools, projects, models":"Rechercher outils, projets, modèles", 879 "Ask the assistant":"Demander à lâassistant","Answered with this site's live data":"Réponse à partir des données du site","No text match":"Aucune correspondance de texte", 880 "Verified \u2014 you are now listed as the maintainer of this project.":"Vérifié â vous êtes désormais listé comme mainteneur de ce projet.","Already verified \u2014 you are listed as the maintainer.":"Déjà vérifié â vous êtes listé comme mainteneur.","To claim a project, your account must be signed in with GitHub.":"Pour réclamer un projet, votre compte doit être connecté via GitHub.","This repository belongs to another GitHub account.":"Ce dépôt appartient à un autre compte GitHub.","You must be a public member of the organization that owns this repository.":"Vous devez être membre public de lâorganisation propriétaire du dépôt.","You must appear among the contributors of this repository.":"Vous devez figurer parmi les contributeurs du dépôt.","Claim failed \u2014 contact [email protected] if you believe this is wrong.":"Ãchec du claim â écrivez à [email protected] si vous pensez quâil y a erreur.", 881 "Badge copied \u2014 click to copy again":"Badge copié â cliquer pour recopier", 882 "Health badge":"Badge santé","Copy the Markdown badge for your README":"Copiez le badge Markdown pour votre README","Markdown copied!":"Markdown copié !", 883 "My Dashboard":"Mon Dashboard","Compose your own \u2014 members":"Composez le vôtre â membres", 884 "Save this dashboard":"Enregistrez ce dashboard","Get notified when a new project appears":"Soyez alerté à chaque nouveau projet","Follow these projects":"Suivez ces projets","Save discoveries to your portfolio":"Ajoutez vos découvertes à votre portfolio","Track health evolution over time":"Suivez l'évolution de la santé dans le temps","Follow your favorite models":"Suivez vos modèles favoris","Watch these trends from your account":"Surveillez ces tendances depuis votre compte","Build your own AI toolbox":"Construisez votre boîte à outils IA","Get price-drop alerts on the models you use":"Recevez les baisses de prix de vos modèles","Follow this industryâs projects":"Suivez les projets de ce secteur","Save projects":"Sauvegardez des projets","Build AI portfolios":"Créez des portfolios IA","Follow emerging tools":"Suivez les outils émergents","Get daily alerts":"Recevez des alertes quotidiennes","Create your free account":"Créez votre compte gratuit", 885 "Plans":"Tarifs","Free to Business plans":"Du plan Free au Business", 886 "Industries":"Industries","AI by sector: health, finance\u2026":"IA par secteur : santé, financeâ¦", 887 "Maintained by":"Maintenu par","Are you the maintainer?":"Vous êtes le mainteneur ?","Claim this project":"Réclamez ce projet", 888 "see all":"tout voir", 889 "accelerating":"en accélération","detected":"détecté","health":"santé","/ 7 days":"/ 7 jours","projects":"projets","more emerging projects":"autres projets émergents","// no project matches these filters":"// aucun projet ne correspond à ces filtres","The discovery scan has not run yet â come back after the next morning build.":"Le scan de découverte n'a pas encore tourné â revenez après le prochain build matinal.","Could not load the discovery data â please refresh.":"Impossible de charger les données de découverte â rechargez la page.", 890 "Emerging":"Ãmergents","Detected by our signals":"Détectés par nos signaux","Developer API":"API développeur","Keys, quotas, fused records":"Clés, quotas, fiches fusionnées", 891 "Click to rename":"Cliquez pour renommer", 892 "What is it for?":"Ãa sert à quoi ?","Not qualified yet":"Pas encore qualifiée","Saved":"Enregistré","Chat assistant":"Assistant de chat","Ask my documents (RAG)":"Interroger mes documents (RAG)","Coding assistant":"Assistant de code","Agents & automation":"Agents et automatisation","Generate images":"Générer des images","Generate video":"Générer des vidéos","Speech & voice":"Parole et voix","Music & audio":"Musique et audio","AI search engine":"Moteur de recherche IA","OCR & documents":"OCR et documents","Fine-tune models":"Affiner des modèles","Browse & automate the web":"Naviguer et automatiser le web","Connect my tools":"Connecter mes outils","Structured JSON output":"Sortie JSON structurée","One API for all models":"Une seule API pour tous les modèles","Scrape sites into data":"Extraire des sites en données","100% local & private":"100 % local et privé","Easiest setup":"Installation la plus simple","Modest hardware":"Matériel modeste","Serve an API":"Exposer une API", 893 "members use it":"membres l'utilisent","member uses it":"membre l'utilise",
894 "use it":"l'utilisent","Use this stack":"Utiliser cette stack","Copied to your account":"Copiée dans votre compte","Already in your account":"Déjà dans votre compte","used by":"utilisée par","Builder":"Builder", 895 "Members who use it":"Des membres l'utilisent","Community stacks":"Stacks de la communauté", 896 "Models you follow":"Les modèles que vous suivez","Models":"Modèles","open":"ouvert","closed":"fermé","per 1M out":"par 1M sortie","Price change over 30 days":"Variation sur 30 jours","No model followed yet â search one above.":"Aucun modèle suivi â cherchez-en un ci-dessus.","Models I follow (with prices)":"Les modèles que je suis (avec leurs prix)", 897 "members saved this":"membres l'ont enregistré","member saved this":"membre l'a enregistré","public stacks":"stacks publiques","public stack":"stack publique","collections":"collections","collection":"collection","Featured in":"Présent dans","by":"par","projects":"projets","project":"projet", 898 "Public â anyone with the link":"Public â accessible avec le lien", 899 "Add":"Ajouter","Add selected":"Ajouter la sélection","Select all":"Tout sélectionner","from watchlist":"depuis la watchlist","Remove":"Retirer","Delete collection":"Supprimer la collection","Delete this collection?":"Supprimer cette collection ?","Give it a title first.":"Donnez-lui d'abord un titre.","Everything from your watchlist is already here.":"Tout ce qui est dans votre watchlist y est déjà .","No projects yet â add one below or pick from your watchlist.":"Aucun projet â ajoutez-en un ci-dessous ou piochez dans votre watchlist.","View public page":"Voir la page publique","Links":"Liens","Projects I follow (with health)":"Les projets que je suis (avec leur santé)","Save this stack (free account)":"Enregistrer cette stack (compte gratuit)","Save this stack":"Enregistrer cette stack","Save to my account":"Enregistrer dans mon compte","Cancel":"Annuler","Bio":"Bio", 900 "Public profile":"Profil public","Alerts":"Alertes","Homepage":"Page d'accueil","Following":"Abonnements","Favorites":"Favoris","Stacks":"Stacks","Collections":"Collections","Watchlist":"Watchlist","Your data":"Vos données","Delete account":"Supprimer le compte","Public â listed in the members directory":"Public â visible dans l'annuaire des membres","Private â only you can see it":"Privé â vous seul le voyez", 901 "Check your inbox â one click to confirm.":"Vérifiez votre boîte â un clic pour confirmer.","You're already on the list.":"Vous êtes déjà inscrit.", 902 "Members":"Membres","People building with open-source AI. Open a profile to see their stacks, the projects they follow and their links â and follow them to keep up.":"Des gens qui construisent avec l'IA open source. Ouvrez un profil pour voir leurs stacks, les projets qu'ils suivent et leurs liens â et suivez-les pour rester au courant.", 903 "Follow":"Suivre","Following":"Suivi","People you follow":"Membres que vous suivez","I want to":"Je veux","Constraints":"Contraintes","Open â":"Ouvrir â","Stack":"Stack", 904 "Your stacks":"Vos stacks","Open in Stack Builder":"Rouvrir dans le Stack Builder","No stack saved yet.":"Aucune stack enregistrée.","Build one in the Stack Builder â":"Construisez-en une dans le Stack Builder â", 905 "Your collections":"Vos collections","Your public profile":"Votre profil public","Your data":"Vos données","Alerts you receive":"Vos alertes","Why create an account?":"Pourquoi créer un compte ?","Show on my page":"Afficher sur ma page","Save profile":"Enregistrer le profil","Public profile":"Profil public", 906 "Search":"Rechercher", 907 "Member":"Membre","Featured project":"Projet mis en avant","My stack â projects I follow":"Ma stack â les projets que je suis","Favorites":"Favoris","Public collections":"Collections publiques","No public collection yet.":"Aucune collection publique pour l'instant.","An olud.ai member":"Un membre olud.ai","Create your own profile":"Créez votre profil","Profile not found":"Profil introuvable","This profile doesn't exist, or its owner keeps it private.":"Ce profil n'existe pas, ou son propriétaire le garde privé.","Collection":"Collection","Member collection":"Collection d'un membre","This collection is empty for now.":"Cette c
907ollection est vide pour l'instant.","Curated by":"Sélection de","Create your own collection":"Créez votre collection", 908 "My public page":"Ma page publique", 909 "Your homepage":"Votre page d'accueil","Live terminal":"Terminal live","Trending on GitHub":"Tendances GitHub","What's new":"Nouveautés","Latest releases":"Dernières versions","News feed":"Fil d'actualité","AI tools":"Outils IA","More tools":"Plus d'outils","Open-source alternatives":"Alternatives open source","Model comparisons":"Comparatifs de modèles","Model comparator":"Comparateur de modèles","Leaderboard":"Classement","Categories":"Catégories","Prompts":"Prompts","Why olud.ai":"Pourquoi olud.ai", 910 "Private â preview":"Privé â aperçu","Save to move your page to":"Enregistrez pour déplacer votre page vers","Copied!":"Copié !","Public":"Public", 911 "Keep track of what matters":"Gardez un Åil sur ce qui compte","Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.":"Suivez des projets et recevez un email quand leur santé décline, qu'une version sort ou que la licence change. Gratuit, sans mot de passe.","30 seconds":"30 secondes", 912 "Add to collection":"Ajouter à une collection","Collection":"Collection","In collection":"Dans une collection","New collectionâ¦":"Nouvelle collectionâ¦","Create":"Créer","No collection yet â create your first one below.":"Aucune collection â créez la première ci-dessous.","Savingâ¦":"Enregistrementâ¦","Profile saved.":"Profil enregistré.", 913 "Your account":"Votre compte","Account":"Compte", 914 "5 favorites max without an account":"5 favoris maximum sans compte","Create a free account to save unlimited favorites, sync them across devices and get alerts.":"Créez un compte gratuit pour enregistrer autant de favoris que vous voulez, les retrouver sur tous vos appareils et recevoir des alertes.","Create a free account":"Créer un compte gratuit","Your favorites":"Vos favoris","No favorites yet â tap the â¡ on any project or tool.":"Aucun favori â cliquez sur le â¡ d'un projet ou d'un outil.", 915 "Enter your email first.":"Entrez d'abord votre email.","Sendingâ¦":"Envoiâ¦","Network error â try again.":"Erreur réseau â réessaie.","You are signed in.":"Tu es connecté.","Delete your account and all data? This cannot be undone.":"Supprimer votre compte et toutes vos données ? Irréversible.","Health alerts":"Alertes santé","Release alerts":"Alertes releases","Star spikes":"Pics d'étoiles","Off":"Off","Signed in as":"Connecté en tant que","Sign out":"Se déconnecter","Your watchlist":"Votre watchlist","Delete account":"Supprimer le compte","Delete my account":"Supprimer mon compte","Sign in / create account":"Connexion / créer un compte","Email me a link":"Envoyer un lien","If the address is valid, a sign-in link is on its way. Check your inbox (and spam).":"Si l'adresse est valide, un lien de connexion arrive. Vérifiez votre boîte (et les spams).", 916 "Follow":"Suivre","Following":"Suivi","Sign in to follow projects":"Connecte-toi pour suivre des projets", 917 "new AI projects today":"nouveaux projets IA aujourd'hui", 918 "Language":"Langue", 919 "Model profiles":"Fiches modèles","Stay in the loop":"Restez informé","Trends":"Tendances","Languages":"Langages","Countries":"Pays","Contact":"Contact","DNA":"ADN","DNA families":"Familles ADN","Advertise":"Annoncer","Open data & API":"Données ouvertes & API","Documentation":"Documentation","Step-by-step setup":"Mise en place pas à pas","Connect":"Connecter","MCP, API, webhooks, n8n":"MCP, API, webhooks, n8n","Site & Data":"Site & données","My favorites":"Mes favoris","Weekly report":"Rapport hebdo","Tool profiles":"Fiches outils","Security advisories":"Avis de sécurité","Pulse ranking":"Classement Pulse","Privacy":"Confidentialité","Terms":"Conditions", 920 "Search":"Rechercher", 921 "All":"Tous", 922 "Updated":"Mis à jour", 923 "New Space":"Nouvel espace", 924 "New model":"Nouveau modèle", 925 "New project":"Nouveau projet", 926 "No favorites yet.":"Aucun favori pour l'instant.", 927 "No tools found.":"Aucun outil trouvé.", 928 "No tools match your filters.":"Aucun outil ne correspond à vos filtres.", 929 "Trending tools are temporarily unavailable.":"Les outils tendance sont temporairement indisponibles.", 930 "Click the heart on any tool to save it here.":"Cliquez sur le cÅur d'un outil pour l'enregistrer ici.",
931 "Close":"Fermer", 932 "Copied!":"Copié !", 933 "AI Tools":"Outils IA","Directory":"Annuaire","HF Directory":"Annuaire HF","Spaces":"Espaces","Top Projects":"Meilleurs projets","Trending":"Tendances", 934 "Model profiles":"Fiches modèles","Advertise":"Annoncer","Open data & API":"Données ouvertes & API","Documentation":"Documentation","Step-by-step setup":"Mise en place pas à pas","Connect":"Connecter","MCP, API, webhooks, n8n":"MCP, API, webhooks, n8n","Site & Data":"Site & données","Privacy":"Confidentialité","Terms":"Conditions","Tools":"Outils","Models":"Modèles","Discover":"Découvrir","Explore":"Explorer", 935 "Tools & Agents":"Outils & Agents","Alternatives":"Alternatives", 936 "AI Agents":"Agents IA","Open-source frameworks":"Frameworks open source","New":"Nouveau", 937 "Tool Guides":"Guides d'outils","76 in-depth guides":"76 guides détaillés", 938 "Tools Directory":"Annuaire d'outils","225+ tools":"225+ outils","Compare Tools":"Comparer les outils","Categories":"Catégories", 939 "AI Pricing":"Prix des IA","Live costs + calculator":"Coûts en direct + calculateur", 940 "Free vs Paid":"Gratuit vs Payant","Open vs frontier":"Open vs propriétaire","Leaderboard":"Classement", 941 "Compare Models":"Comparer les modèles","Paid Models":"Modèles payants", 942 "All Alternatives":"Toutes les alternatives","75+ tools mapped":"75+ outils répertoriés", 943 "Devin alternatives":"Alternatives à Devin","Cursor alternatives":"Alternatives à Cursor","Notion alternatives":"Alternatives à Notion","Photoshop alternatives":"Alternatives à Photoshop", 944 "Projects":"Projets","News":"Actualités","Latest":"Nouveautés","About":"à propos","Methodology":"Méthodologie","My favorites":"Mes favoris","Dashboards":"Tableaux de bord", 945 "Tools catalog":"Catalogue d'outils","Catalog":"Catalogue","Thinkingâ¦":"Je chercheâ¦","Olud assistant":"Assistant Olud","The AI hub.":"Le hub de l'IA.","Open and commercial AI, side by side.":"IA ouverte et commerciale, côte à côte.","Submit a tool":"Proposer un outil","Submit a tool or project":"Proposer un outil ou un projet","Add to the directory":"Ajouter au hub","Add it to the hub":"L'ajouter au hub","Get a badge":"Obtenir un badge","For your README":"Pour votre README", 946 "Featured:":"à la une :","Explore agents":"Explorer les agents", 947 "Stay in the loop":"Restez informé","Subscribe":"S'abonner", 948 "The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.":"Le meilleur de l'IA open source dans votre boîte mail â nouveaux modèles, projets tendance et sorties. Pas de spam, désabonnement à tout moment.", 949 "The world's most complete":"L'annuaire le plus complet","open-source AI directory.":"d'IA open source au monde.","Built for the global community.":"C
949onçu pour la communauté mondiale.", 950 "More":"Plus","Home":"Accueil","Models · Free vs Paid":"Modèles · Gratuit vs Payant","Alternatives to top tools":"Alternatives aux outils populaires","AI Spaces":"Espaces IA","News Feed":"Fil d'actualités", 951 "Independent · Open data":"Indépendant · Données ouvertes" 952 }, 953 it: { 954 "Online":"Online","I can help you find AI tools, compare models and explore projects â or just search.":"Posso aiutarti a trovare strumenti, confrontare modelli, esplorare progetti â o semplicemente cercare.","Ask anything.":"Chiedi quello che vuoi.", 955 "Updates":"Aggiornamento", 956 "Hourly":"Ogni ora", 957 "Continue in the assistant":"Continua nellâassistente","I could not answer that one.":"A questa non ho saputo rispondere.", 958 "Olud assistant":"Assistente Olud","⨠AI â ask me anything about olud.ai":"⨠IA â chiedimi tutto su olud.ai","Type your questionâ¦":"Scrivi la tua domandaâ¦","Questions are logged anonymously to improve the answers.":"Le domande sono registrate in forma anonima per migliorare le risposte.","Hi! Iâm the olud.ai assistant. How can I help?":"Ciao! Sono lâassistente di olud.ai. Come posso aiutarti?","Thinkingâ¦":"Sto cercandoâ¦","Help":"Aiuto","Close":"Chiudi","What is olud.ai?":"Che cosâè olud.ai?","How do I create an account?":"Come creo un account?","What are the plans?":"Quali sono i piani?","How do I follow a project?":"Come seguo un progetto?", 959 "Search, or ask anythingâ¦":"Cerca, o fai una domandaâ¦","Search tools, projects, models":"Cerca strumenti, progetti, modelli", 960 "Ask the assistant":"Chiedi allâassistente","Answered with this site's live data":"Risposta dai dati aggiornati del sito","No text match":"Nessuna corrispondenza testuale", 961 "Verified \u2014 you are now listed as the maintainer of this project.":"Verificato â ora risulti maintainer di questo progetto.","Already verified \u2014 you are listed as the maintainer.":"Già verificato â risulti maintainer.","To claim a project, your account must be signed in with GitHub.":"Per rivendicare un progetto, il tuo account deve accedere con GitHub.","This repository belongs to another GitHub account.":"Questo repository appartiene a un altro account GitHub.","You must be a public member of the organization that owns this repository.":"Devi essere membro pubblico dellâorganizzazione proprietaria.","You must appear among the contributors of this repository.":"Devi comparire tra i contributor del repository.","Claim failed \u2014 contact [email protected] if you believe this is wrong.":"Rivendicazione non riuscita â scrivi a [email protected].", 962 "Badge copied \u2014 click to copy again":"Badge copiato â clic per copiarlo di nuovo", 963 "Health badge":"Badge salute","Copy the Markdown badge for your README":"Copia il badge Markdown per il tuo README","Markdown copied!":"Markdown copiato!", 964 "My Dashboard":"La mia Dashboard","Compose your own \u2014 members":"Componi la tua â membri", 965 "Save this dashboard":"Salva questa dashboard","Get notified when a new project appears":"Ricevi avvisi sui nuovi progetti","Follow these projects":"Segui questi progetti","Save discoveries to your portfolio":"Salva le scoperte nel tuo portfolio","Track health evolution over time":"Segui l'evoluzione dell'health score","Follow your favorite models":"Segui i tuoi modelli preferiti","Watch these trends from your account":"Osserva queste tendenze dal tuo account","Build your own AI toolbox":"Costruisci il tuo toolbox IA","Get price-drop alerts on the models you use":"Avvisi sui ribassi dei tuoi modelli","Follow this industryâs projects":"Segui i progetti di questo settore","Save projects":"Salva progetti","Build AI portfolios":"Crea portfolio IA","Follow emerging tools":"Segui gli strumenti emergenti","Get daily alerts":"Ricevi avvisi quotidiani","Create your free account":"Crea il tuo account gratuito", 966 "Plans":"Prezzi","Free to Business plans":"Dal piano Free al Business", 967 "Industries":"Settori","AI by sector: health, finance\u2026":"IA per settore: salute, finanzaâ¦", 968 "Maintained by":"Mantenuto da","Are you the maintainer?":"Sei il maintainer?","Claim this project":"Rivendica questo progetto", 969 "see all":"vedi tutti", 970 "accelerating":"in accelerazione","detected":"rilevato","health":"salute","/ 7 days":"/ 7 giorni","projects":"progetti","more emerging projects":"altri progetti emergenti","// no project matches these filters":"// nessun progetto corrisponde a questi filtri","The discovery scan has not run yet â come back after the next morning build.":"La scansione di scoperta non è ancora partita â torna dopo il prossimo build mattutino.","Could not load the discovery data â please refresh.":"Impossibile caricare i dati di scoperta â ricarica la pagina.", 971 "Emerging":"Emergenti","Detected by our signals":"Rilevati dai nostri segnali","Developer API":"API per sviluppatori","Keys, quotas, fused records":"Chiavi, quote, schede unificate", 972 "Click to rename":"Clicca per rinominare", 973 "What is it for?":"A cosa serve?","Not qualified yet":"Non ancora qualificato","Saved":"Salvato","Chat assistant":"Assistente chat","Coding assistant":"Assistente di codice","Generate images":"Generare immagini","100% local & private":"100% locale e privato","Easiest setup":"Installazione più semplice","Modest hardware":"Hardware modesto","Serve an API":"Esporre una API", 974 "members use it":"membri lo usano","member uses it":"membro lo usa", 975 "use it":"lo usano","Use this stack":"Usa questo stack","Copied to your account":"Copiato nel tuo account","Already in your account":"Già nel tuo account","used by":"usato da", 976 "Members who use it":"Membri che lo usano","Community stacks":"Stack della community", 977 "Models you follow":"I modelli che segui","Models":"Modelli","open":"aperto","closed":"chiuso","per 1M out":"per 1M output","Price change over 30 days":"Variazione su 30 giorni","No model followed yet â search one above.":"Nessun modello seguito â cercane uno sopra.","Models I follow (with prices)":"I modelli che seguo (con i prezzi)", 978 "members saved this":"membri l'hanno salvato","member saved this":"membro l'ha salvato","public stacks":"stack pubblici","public stack":"stack pubblico","collections":"raccolte","collection":"raccolta","Featured in":"Presente in","by":"di","projects":"progetti","project":"progetto", 979 "Public â anyone with the link":"Pubblico â accessibile con il link",
980 "Add":"Aggiungi","Add selected":"Aggiungi selezionati","Select all":"Seleziona tutto","from watchlist":"dalla watchlist","Remove":"Rimuovi","Delete collection":"Elimina raccolta","Delete this collection?":"Eliminare questa raccolta?","Give it a title first.":"Dai prima un titolo.","Everything from your watchlist is already here.":"Tutto ciò che è nella tua watchlist è già qui.","No projects yet â add one below or pick from your watchlist.":"Nessun progetto â aggiungine uno o scegli dalla watchlist.","View public page":"Vedi la pagina pubblica","Links":"Link","Projects I follow (with health)":"I progetti che seguo (con la salute)","Save this stack (free account)":"Salva questo stack (account gratuito)","Save this stack":"Salva questo stack","Save to my account":"Salva nel mio account","Cancel":"Annulla","Bio":"Bio", 981 "Public profile":"Profilo pubblico","Alerts":"Avvisi","Homepage":"Home","Following":"Seguiti","Stacks":"Stack","Watchlist":"Watchlist","Your data":"I tuoi dati","Delete account":"Elimina account", 982 "Check your inbox â one click to confirm.":"Controlla la posta â un clic per confermare.", 983 "Members":"Membri", 984 "Follow":"Segui","Following":"Seguito","People you follow":"Membri che segui","I want to":"Voglio","Constraints":"Vincoli","Open â":"Apri â", 985 "Your stacks":"I tuoi stack","Open in Stack Builder":"Apri nello Stack Builder","No stack saved yet.":"Nessuno stack salvato.","Build one in the Stack Builder â":"Creane uno nello Stack Builder â", 986 "Your collections":"Le tue raccolte","Your public profile":"Il tuo profilo pubblico","Your data":"I tuoi dati","Alerts you receive":"I tuoi avvisi","Why create an account?":"Perché creare un account?","Show on my page":"Mostra sulla mia pagina","Save profile":"Salva il profilo","Public profile":"Profilo pubblico", 987 "Search":"Cerca", 988 "Member":"Membro","Featured project":"Progetto in evidenza","My stack â projects I follow":"Il mio stack â i progetti che seguo","Favorites":"Preferiti","Public collections":"Raccolte pubbliche","No public collection yet.":"Nessuna raccolta pubblica.","An olud.ai member":"Un membro olud.ai","Create your own profile":"Crea il tuo profilo","Profile not found":"Profilo non trovato","This profile doesn't exist, or its owner keeps it private.":"Questo profilo non esiste, o il proprietario lo tiene privato.","Member collection":"Raccolta di un membro","This collection is empty for now.":"Questa raccolta è vuota.","Curated by":"A cura di","Create your own collection":"Crea la tua raccolta", 989 "My public page":"La mia pagina pubblica", 990 "Your homepage":"La tua home","Live terminal":"Terminale live","Trending on GitHub":"Tendenze GitHub","What's new":"Novità ","Latest releases":"Ultime versioni","News feed":"Notizie","AI tools":"Strumenti IA","More tools":"Altri strumenti","Open-source alternatives":"Alternative open source","Model comparisons":"Confronti tra modelli","Model comparator":"Comparatore di modelli","Leaderboard":"Classifica","Categories":"Categorie","Prompts":"Prompt","Why olud.ai":"Perché olud.ai", 991 "Private â preview":"Privato â anteprima","Save to move your page to":"Salva per spostare la tua pagina su","Copied!":"Copiato!","Public":"Pubblico", 992 "Keep track of what matters":"Tieni d'occhio ciò che conta","Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.":"Segui i progetti e ricevi un'email quando la loro salute cala, esce una nuova versione o cambia la licenza. Gratis, senza password.","30 seconds":"30 secondi", 993 "Add to collection":"Aggiungi a una raccolta","Collection":"Raccolta","In collection":"Nella raccolta","New collectionâ¦":"Nuova raccoltaâ¦","Create":"Crea","No collection yet â create your first one below.":"Nessuna raccolta â creane una qui sotto.","Savingâ¦":"Salvataggioâ¦","Profile saved.":"Profilo salvato.", 994 "Your account":"Il tuo account","Account":"Account", 995 "5 favorites max without an account":"Massimo 5 preferiti senza account","Create a free account to save unlimited favorites, sync them across devices and get alerts.":"Crea un account gratuito per salvare preferiti illimitati, sincronizzarli su tutti i dispositivi e ricevere avvisi.","Create a free account":"Crea un account gratuito","Your favorites":"I tuoi preferiti","No favorites yet â tap the â¡ on any project or tool.":"Nessun preferito â tocca il â¡ su un progetto o strumento.", 996 "Enter your email first.":"Inserisci prima la tua email.","Sendingâ¦":"Invioâ¦","Network error â try again.":"Errore di rete â riprova.","You are signed in.":"Sei connesso.","Delete your account and all data? This cannot be undone.":"Eliminare il tuo account e tutti i dati? Irreversibile.","Health alerts":"Avvisi salute","Release alerts":"Avvisi release","Star spikes":"Picchi di stelle","Off":"Off","Signed in as":"Connesso come","Sign out":"Esci","Your watchlist":"La tua watchlist","Delete account":"Elimina account","Delete my account":"Elimina il mio account","Sign in / create account":"Accedi / crea account","Email me a link":"Inviami un link","If the address is valid, a sign-in link is on its way. Check your inbox (and spam).":"Se l'indirizzo è valido, un link di accesso è in arrivo. Controlla la posta (e lo spam).", 997 "Follow":"Segui","Following":"Seguito","Sign in to follow projects":"Accedi per seguire i progetti", 998 "new AI projects today":"nuovi progetti IA oggi", 999 "Language":"Lingua", 1000 "My favorites":"I miei preferiti","Weekly report":"Report settimanale","Tool profiles":"Schede strumenti","Security advisories":"Avvisi di sicurezza","Pulse ranking":"Classifica Pulse", 1001 "Search":"Cerca", 1002 "All":"Tutti",
1003 "Updated":"Aggiornato", 1004 "New Space":"Nuovo spazio", 1005 "New model":"Nuovo modello", 1006 "New project":"Nuovo progetto", 1007 "No favorites yet.":"Nessun preferito.", 1008 "No tools found.":"Nessuno strumento trovato.", 1009 "No tools match your filters.":"Nessuno strumento corrisponde ai filtri.", 1010 "Trending tools are temporarily unavailable.":"Gli strumenti di tendenza non sono al momento disponibili.", 1011 "Click the heart on any tool to save it here.":"Clicca sul cuore di uno strumento per salvarlo qui.", 1012 "Close":"Chiudi", 1013 "Copied!":"Copiato!", 1014 "AI Tools":"Strumenti IA","Directory":"Directory","HF Directory":"Directory HF","Spaces":"Spazi","Top Projects":"Progetti migliori","Trending":"Di tendenza", 1015 "Model profiles":"Schede modelli","Stay in the loop":"Resta aggiornato","Trends":"Tendenze","Languages":"Linguaggi","Countries":"Paesi","Contact":"Contatto","DNA":"DNA","DNA families":"Famiglie DNA","Advertise":"Pubblicità ","Open data & API":"Dati aperti e API","Documentation":"Documentazione","Step-by-step setup":"Configurazione passo a passo","Connect":"Collegare","MCP, API, webhooks, n8n":"MCP, API, webhook, n8n","Site & Data":"Sito e dati","Privacy":"Privacy","Terms":"Termini","Tools":"Strumenti","Models":"Modelli","Discover":"Scopri","Explore":"Esplora", 1016 "Tools & Agents":"Strumenti & Agenti","Alternatives":"Alternative", 1017 "AI Agents":"Agenti IA","Open-source frameworks":"Framework open source","New":"Novità ", 1018 "Tool Guides":"Guide agli strumenti","76 in-depth guides":"76 guide approfondite", 1019 "Tools Directory":"Directory strumenti","225+ tools":"225+ strumenti","Compare Tools":"Confronta strumenti","Categories":"Categorie", 1020 "AI Pricing":"Prezzi IA","Live costs + calculator":"Costi live + calcolatore", 1021 "Free vs Paid":"Gratis vs A pagamento","Open vs frontier":"Open vs frontier","Leaderboard":"Classifica", 1022 "Compare Models":"Confronta modelli","Paid Models":"Modelli a pagamento", 1023 "All Alternatives":"Tutte le alternative","75+ tools mapped":"75+ strumenti mappati", 1024 "Devin alternatives":"Alternative a Devin","Cursor alternatives":"Alternative a Cursor","Notion alternatives":"Alternative a Notion","Photoshop alternatives":"Alternative a Photoshop", 1025 "Projects":"Progetti","News":"Notizie","Latest":"Ultimi","About":"Chi siamo","Methodology":"Metodologia", 1026 "Tools catalog":"Catalogo strumenti","Catalog":"Catalogo","The AI hub.":"L'hub dell'IA.","Open and commercial AI, side by side.":"IA aperta e commerciale, fianco a fianco.","Submit a tool":"Proponi uno strumento","Submit a tool or project":"Proponi uno strumento o un progetto","Add to the directory":"Aggiungi all'hub","Add it to the hub":"Aggiungilo all'hub","Get a badge":"Ottieni un badge","For your README":"Per il tuo README", 1027 "Featured:":"In evidenza:","Explore agents":"Esplora gli agenti", 1028 "Stay in the loop":"Resta aggiornato","Subscribe":"Iscriviti", 1029 "The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.":"Il meglio dell'IA open source nella tua casella â nuovi modelli, progetti di tendenza e release. Niente spam, disiscrizione in qualsiasi momento.", 1030 "The world's most complete":"La directory più completa","open-source AI directory.":"di IA open source al mondo.","Built for the global community.":"C
1030ostruita per la comunità globale.", 1031 "More":"Altro","Home":"Home","Models · Free vs Paid":"Modelli · Gratis vs A pagamento","Alternatives to top tools":"Alternative agli strumenti popolari","AI Spaces":"Spaces IA","News Feed":"Feed notizie", 1032 "Independent · Open data":"Indipendente · Dati aperti" 1033 }, 1034 es: { 1035 "Online":"En lÃnea","I can help you find AI tools, compare models and explore projects â or just search.":"Puedo ayudarte a encontrar herramientas, comparar modelos y explorar proyectos â o simplemente buscar.","Ask anything.":"Pregunta lo que quieras.", 1036 "Updates":"Actualización", 1037 "Hourly":"Cada hora", 1038 "Continue in the assistant":"Continuar en el asistente","I could not answer that one.":"A esa no he sabido responder.", 1039 "Olud assistant":"Asistente Olud","⨠AI â ask me anything about olud.ai":"⨠IA â pregúntame lo que quieras sobre olud.ai","Type your questionâ¦":"Escribe tu preguntaâ¦","Questions are logged anonymously to improve the answers.":"Las preguntas se registran de forma anónima para mejorar las respuestas.","Hi! Iâm the olud.ai assistant. How can I help?":"¡Hola! Soy el asistente de olud.ai. ¿En qué puedo ayudarte?","Thinkingâ¦":"Buscandoâ¦","Help":"Ayuda","Close":"Cerrar","What is olud.ai?":"¿Qué es olud.ai?","How do I create an account?":"¿Cómo creo una cuenta?","What are the plans?":"¿Cuáles son los planes?","How do I follow a project?":"¿Cómo sigo un proyecto?", 1040 "Search, or ask anythingâ¦":"Busca, o haz una preguntaâ¦","Search tools, projects, models":"Buscar herramientas, proyectos, modelos", 1041 "Ask the assistant":"Pregunta al asistente","Answered with this site's live data":"Respuesta con los datos en vivo del sitio","No text match":"Sin coincidencias de texto", 1042 "Verified \u2014 you are now listed as the maintainer of this project.":"Verificado â ahora figuras como maintainer de este proyecto.","Already verified \u2014 you are listed as the maintainer.":"Ya verificado â figuras como maintainer.","To claim a project, your account must be signed in with GitHub.":"Para reclamar un proyecto, tu cuenta debe iniciar sesión con GitHub.","This repository belongs to another GitHub account.":"Este repositorio pertenece a otra cuenta de GitHub.","You must be a public member of the organization that owns this repository.":"Debes ser miembro público de la organización propietaria.","You must appear among the contributors of this repository.":"Debes figurar entre los contribuidores del repositorio.","Claim failed \u2014 contact [email protected] if you believe this is wrong.":"Reclamo fallido â escribe a [email protected].", 1043 "Badge copied \u2014 click to copy again":"Insignia copiada â clic para copiar de nuevo", 1044 "Health badge":"Insignia de salud","Copy the Markdown badge for your README":"Copia la insignia Markdown para tu README","Markdown copied!":"¡Markdown copiado!", 1045 "My Dashboard":"Mi Dashboard","Compose your own \u2014 members":"Compón el tuyo â miembros", 1046 "Save this dashboard":"Guarda este dashboard","Get notified when a new project appears":"Recibe avisos de nuevos proyectos","Follow these projects":"Sigue estos proyectos","Save discoveries to your portfolio":"Guarda descubrimientos en tu portfolio","Track health evolution over time":"Sigue la evolución del health score","Follow your favorite models":"Sigue tus modelos favoritos","Watch these trends from your account":"Vigila estas tendencias desde tu cuenta","Build your own AI toolbox":"Crea tu caja de herramientas IA","Get price-drop alerts on the models you use":"Alertas de bajadas de precio de tus modelos","Follow this industryâs projects":"Sigue los proyectos de este sector","Save projects":"Guarda proyectos","Build AI portfolios":"Crea portfolios de IA","Follow emerging tools":"Sigue herramientas emergentes","Get daily alerts":"Recibe alertas diarias","Create your free account":"Crea tu cuenta gratis", 1047 "Plans":"Precios","Free to Business plans":"Del plan Free al Business", 1048 "Industries":"Industrias","AI by sector: health, finance\u2026":"IA por sector: salud, finanzasâ¦", 1049 "Maintained by":"Mantenido por","Are you the maintainer?":"¿Eres el maintainer?","Claim this project":"Reclama este proyecto", 1050 "see all":"ver todos", 1051 "accelerating":"acelerando","detected":"detectado","health":"salud","/ 7 days":"/ 7 dÃas","projects":"proyectos","more emerging projects":"proyectos emergentes más","// no project matches these filters":"// ningún proyecto coincide con estos filtros","The discovery scan has not run yet â come back after the next morning build.":"El escaneo de descubrimiento aún no se ha ejecutado â vuelve tras el próximo build matinal.","Could not load the discovery data â please refresh.":"No se pudieron cargar los datos de descubrimiento â recarga la página.", 1052 "Emerging":"Emergentes","Detected by our signals":"Detectados por nuestras señales","Developer API":"API para desarrolladores","Keys, quotas, fused records":"Claves, cuotas, fichas fusionadas", 1053 "Click to rename":"Clic para renombrar", 1054 "What is it for?":"¿Para qué sirve?","Not qualified yet":"Sin calificar","Saved":"Guardado","Chat assistant":"Asistente de chat","Coding assistant":"Asistente de código","Generate images":"Generar imágenes","100% local & private":"100 % local y privado","Easiest setup":"Instalación más simple","Modest hardware":"Hardware modesto","Serve an API":"Servir una API", 1055 "members use it":"miembros lo usan","member uses it":"miembro lo usa", 1056 "use it":"lo usan","Use this stack":"Usar este stack","Copied to your account":"Copiado en tu cuenta","Already in your account":"Ya está en tu cuenta","used by":"usado por", 1057 "Members who use it":"Miembros que lo usan","Community stacks":"Stacks de la comunidad", 1058 "Models you follow":"Los modelos que sigues","Models":"Modelos","open":"abierto","closed":"cerrado","per 1M out":"por 1M s
1058alida","Price change over 30 days":"Variación en 30 dÃas","No model followed yet â search one above.":"Ningún modelo seguido â busca uno arriba.","Models I follow (with prices)":"Los modelos que sigo (con precios)", 1059 "members saved this":"miembros lo guardaron","member saved this":"miembro lo guardó","public stacks":"stacks públicos","public stack":"stack público","collections":"colecciones","collection":"colección","Featured in":"Presente en","by":"por","projects":"proyectos","project":"proyecto", 1060 "Public â anyone with the link":"Público â accesible con el enlace", 1061 "Add":"Añadir","Add selected":"Añadir selección","Select all":"Seleccionar todo","from watchlist":"desde la watchlist","Remove":"Quitar","Delete collection":"Eliminar colección","Delete this collection?":"¿Eliminar esta colección?","Give it a title first.":"Ponle un tÃtulo primero.","Everything from your watchlist is already here.":"Todo lo de tu watchlist ya está aquÃ.","No projects yet â add one below or pick from your watchlist.":"Ningún proyecto â añade uno abajo o elige de tu watchlist.","View public page":"Ver la página pública","Links":"Enlaces","Projects I follow (with health)":"Los proyectos que sigo (con su salud)","Save this stack (free account)":"Guardar este stack (cuenta gratuita)","Save this stack":"Guardar este stack","Save to my account":"Guardar en mi cuenta","Cancel":"Cancelar","Bio":"Bio", 1062 "Public profile":"Perfil público","Alerts":"Alertas","Homepage":"Inicio","Following":"Siguiendo","Stacks":"Stacks","Your data":"Tus datos","Delete account":"Eliminar cuenta", 1063 "Check your inbox â one click to confirm.":"Revisa tu correo â un clic para confirmar.", 1064 "Members":"Miembros", 1065 "Follow":"Seguir","Following":"Siguiendo","People you follow":"Miembros que sigues","I want to":"Quiero","Constraints":"Restricciones","Open â":"Abrir â", 1066 "Your stacks":"Tus stacks","Open in Stack Builder":"Abrir en el Stack Builder","No stack saved yet.":"Ningún stack guardado.","Build one in the Stack Builder â":"Crea uno en el Stack Builder â", 1067 "Your collections":"Tus colecciones","Your public profile":"Tu perfil público","Your data":"Tus datos","Alerts you receive":"Tus alertas","Why create an account?":"¿Por qué crear una cuenta?","Show on my page":"Mostrar en mi página","Save profile":"Guardar perfil","Public profile":"Perfil público", 1068 "Search":"Buscar", 1069 "Member":"Miembro","Featured project":"Proyecto destacado","My stack â projects I follow":"Mi stack â los proyectos que sigo","Favorites":"Favoritos","Public collections":"Colecciones públicas","No public collection yet.":"Aún no hay colecciones públicas.","An olud.ai member":"Un miembro de olud.ai","Create your own profile":"Crea tu perfil","Profile not found":"Perfil no encontrado","This profile doesn't exist, or its owner keeps it private.":"Este perfil no existe, o su propietario lo mantiene privado.","Member collection":"Colección de un miembro","This collection is empty for now.":"Esta colección está vacÃa.","Curated by":"Seleccionado por","Create your own collection":"Crea tu colección", 1070 "My public page":"Mi página pública", 1071 "Your homepage":"Tu página de inicio","Live terminal":"Terminal en vivo","Trending on GitHub":"Tendencias en GitHub","What's new":"Novedades","Latest releases":"Ãltimas versiones","News feed":"Noticias","AI tools":"Herramientas IA","More tools":"Más herramientas","Open-source alternatives":"Alternativas de código abierto","Model comparisons":"Comparativas de modelos","Model comparator":"Comparador de modelos","Leaderboard":"Clasificación","Categories":"CategorÃas","Prompts":"Prompts","Why olud.ai":"Por qué olud.ai", 1072 "Private â preview":"Privado â vista previa","Save to move your page to":"Guarda para mover tu página a","Copied!":"¡Copiado!","Public":"Público", 1073 "Keep track of what matters":"Sigue lo que importa","Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.":"Sigue proyectos y recibe un email cuando su salud baje, salga una nueva ver
1073sión o cambie la licencia. Gratis, sin contraseña.","30 seconds":"30 segundos", 1074 "Add to collection":"Añadir a una colección","Collection":"Colección","In collection":"En la colección","New collectionâ¦":"Nueva colecciónâ¦","Create":"Crear","No collection yet â create your first one below.":"Ninguna colección â crea la primera abajo.","Savingâ¦":"Guardandoâ¦","Profile saved.":"Perfil guardado.", 1075 "Your account":"Tu cuenta","Account":"Cuenta", 1076 "5 favorites max without an account":"Máximo 5 favoritos sin cuenta","Create a free account to save unlimited favorites, sync them across devices and get alerts.":"Crea una cuenta gratuita para guardar favoritos ilimitados, sincronizarlos entre dispositivos y recibir alertas.","Create a free account":"Crear una cuenta gratuita","Your favorites":"Tus favoritos","No favorites yet â tap the â¡ on any project or tool.":"Aún no hay favoritos â pulsa el â¡ en cualquier proyecto o herramienta.", 1077 "Enter your email first.":"Introduce primero tu email.","Sendingâ¦":"Enviandoâ¦","Network error â try again.":"Error de red â inténtalo de nuevo.","You are signed in.":"Has iniciado sesión.","Delete your account and all data? This cannot be undone.":"¿Eliminar tu cuenta y todos tus datos? Irreversible.","Health alerts":"Alertas de salud","Release alerts":"Alertas de releases","Star spikes":"Picos de estrellas","Off":"Off","Signed in as":"Conectado como","Sign out":"Cerrar sesión","Your watchlist":"Tu watchlist","Delete account":"Eliminar cuenta","Delete my account":"Eliminar mi cuenta","Sign in / create account":"Iniciar sesión / crear cuenta","Email me a link":"EnvÃame un enlace","If the address is valid, a sign-in link is on its way. Check your inbox (and spam).":"Si la dirección es válida, un enlace de acceso está en camino. Revisa tu bandeja (y el spam).", 1078 "Follow":"Seguir","Following":"Siguiendo","Sign in to follow projects":"Inicia sesión para seguir proyectos", 1079 "new AI projects today":"nuevos proyectos de IA hoy", 1080 "Language":"Idioma", 1081 "My favorites":"Mis favoritos","Weekly report":"Informe semanal","Tool profiles":"Fichas de herramientas","Security advisories":"Avisos de seguridad","Pulse ranking":"Ranking Pulse", 1082 "Search":"Buscar", 1083 "All":"Todos", 1084 "Updated":"Actualizado", 1085 "New Space":"Nuevo espacio", 1086 "New model":"Nuevo modelo", 1087 "New project":"Nuevo proyecto", 1088 "No favorites yet.":"Aún no hay favoritos.", 1089 "No tools found.":"No se encontraron herramientas.", 1090 "No tools match your filters.":"Ninguna herramienta coincide con tus filtros.", 1091 "Trending tools are temporarily unavailable.":"Las herramientas en tendencia no están disponibles temporalmente.", 1092 "Click the heart on any tool to save it here.":"Haz clic en el corazón de una herramienta para guardarla aquÃ.", 1093 "Close":"Cerrar", 1094 "Copied!":"¡Copiado!", 1095 "AI Tools":"Herramientas IA","Directory":"Directorio","HF Directory":"Directorio HF","Spaces":"Espacios","Top Projects":"Mejores proyectos","Trending":"Tendencias", 1096 "Model profiles":"Fichas de modelos","Stay in the loop":"Mantente al dÃa","Trends":"Tendencias","Contact":"Contacto","DNA":"ADN","Languages":"Lenguajes","Countries":"Paises","DNA families":"Familias de ADN","Advertise":"Anunciarse","Open data & API":"Datos abiertos y API","Documentation":"Documentación","Step-by-step setup":"Configuración paso a paso","Connect":"Conectar","MCP, API, webhooks, n8n":"MCP, API, webhooks, n8n","Site & Data":"Sitio y datos","Privacy":"Privacidad","Terms":"Términos","Tools":"Herramientas","Models":"Modelos","Discover":"Descubrir","Explore":"Explorar", 1097 "Tools & Agents":"Herramientas y Agentes","Alternatives":"Alternativas", 1098 "AI Agents":"Agentes de IA","Open-source frameworks":"Frameworks open source","New":"Nuevo", 1099 "Tool Guides":"GuÃas de herramientas","76 in-depth guides":"76 guÃas detalladas", 1100 "Tools Directory":"Directorio de herramientas","225+ tools":"225+ herramientas","Compare Tools":"Comparar herramientas","Categories":"CategorÃas", 1101 "AI Pricing":"Precios de IA","Live costs + calculator":"Costes en vivo + calculadora", 1102 "Free vs Paid":"Gratis vs De pago","Open vs frontier":"Open vs frontera","Leaderboard":"Clasificación", 1103 "Compare Models":"Comparar modelos","Paid Models":"Modelos de pago", 1104 "All Alternatives":"Todas las alternativas","75+ tools mapped":"75+ herramientas mapeadas", 1105 "Devin alternatives":"Alternativas a Devin","Cursor alternatives":"Alternativas a Cursor","Notion alternatives":"Alternativas a Notion","Photoshop alternatives":"Alternativas a Photoshop", 1106 "Projects":"Proyectos","News":"Noticias","Latest":"Recientes","About":"Acerca de","Methodology":"MetodologÃa","My favorites":"Mis favoritos","Dashboards":"Paneles", 1107 "Tools catalog":"Catálogo de herramientas","Catalog":"Catálogo","The AI hub.":"El hub de la IA.","Open and commercial AI, side by side.":"IA abierta y comercial, lado a lado.","Submit a tool":"Enviar una herramienta","Submit a tool or project":"Enviar una herramienta o un proyecto","Add to the directory":"Añadir al hub","Add it to the hub":"Añádelo al hub","Get a badge":"Consigue una insignia","For your README":"Para tu README", 1108 "Featured:":"Destacado:","Explore agents":"Explorar agentes", 1109 "Stay in the loop":"Mantente al dÃa","Subscribe":"Suscribirse", 1110 "The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.":"Lo mejor de la IA open source en tu bandeja â nuevos modelos, proyectos en tendencia y lanzamientos. Sin spam, cancela cuando quieras.", 1111 "The world's most complete":"El directorio más completo","open-source AI directory.":"de IA open source del mundo.","Built for the global community.":"Hecho para la comunidad global.", 1112 "More":"Más","Home":"Inicio","Models · Free vs Paid":"Modelos · Gratis vs De pago","Alternatives to top tools":"Alternativas a herramientas populares","AI Spaces":"Espacios IA","News Feed":"Noticias", 1113 "Independent · Open data":"Independiente · Datos abiertos" 1114 }, 1115 de: { 1116 "Online":"Online","I can help you find AI tools, compare models and explore projects â or just search.":"Ich helfe Ihnen, Tools zu finden, Modelle zu vergleichen und Projekte zu entdecken â oder einfach zu suchen.","Ask anything.":"Fragen Sie, was Sie wollen.",
1117 "Updates":"Aktualisierung", 1118 "Hourly":"Stündlich", 1119 "Continue in the assistant":"Im Assistenten fortfahren","I could not answer that one.":"Diese Frage konnte ich nicht beantworten.", 1120 "Olud assistant":"Olud-Assistent","⨠AI â ask me anything about olud.ai":"⨠KI â fragen Sie mich alles zu olud.ai","Type your questionâ¦":"Ihre Frage eingebenâ¦","Questions are logged anonymously to improve the answers.":"Fragen werden anonym gespeichert, um die Antworten zu verbessern.","Hi! Iâm the olud.ai assistant. How can I help?":"Hallo! Ich bin der olud.ai-Assistent. Wie kann ich helfen?","Thinkingâ¦":"Einen Momentâ¦","Help":"Hilfe","Close":"SchlieÃen","What is olud.ai?":"Was ist olud.ai?","How do I create an account?":"Wie erstelle ich ein Konto?","What are the plans?":"Welche Tarife gibt es?","How do I follow a project?":"Wie folge ich einem Projekt?", 1121 "Search, or ask anythingâ¦":"Suchen oder eine Frage stellenâ¦","Search tools, projects, models":"Tools, Projekte, Modelle suchen", 1122 "Ask the assistant":"Den Assistenten fragen","Answered with this site's live data":"Antwort aus den Live-Daten dieser Seite","No text match":"Keine Textübereinstimmung", 1123 "Verified \u2014 you are now listed as the maintainer of this project.":"Verifiziert â Sie sind jetzt als Maintainer gelistet.","Already verified \u2014 you are listed as the maintainer.":"Bereits verifiziert â Sie sind als Maintainer gelistet.","To claim a project, your account must be signed in with GitHub.":"Zum Beanspruchen muss Ihr Konto mit GitHub angemeldet sein.","This repository belongs to another GitHub account.":"Dieses Repository gehört einem anderen GitHub-Konto.","You must be a public member of the organization that owns this repository.":"Sie müssen öffentliches Mitglied der Organisation sein.","You must appear among the contributors of this repository.":"Sie müssen unter den Contributors des Repositories erscheinen.","Claim failed \u2014 contact [email protected] if you believe this is wrong.":"Claim fehlgeschlagen â schreiben Sie an [email protected].", 1124 "Badge copied \u2014 click to copy again":"Badge kopiert â zum erneuten Kopieren klicken", 1125 "Health badge":"Health-Badge","Copy the Markdown badge for your README":"Markdown-Badge für Ihr README kopieren","Markdown copied!":"Markdown kopiert!", 1126 "My Dashboard":"Mein Dashboard","Compose your own \u2014 members":"Stellen Sie Ihres zusammen â Mitglieder", 1127 "Save this dashboard":"Dashboard speichern","Get notified when a new project appears":"Bei neuen Projekten benachrichtigt werden","Follow these projects":"Diesen Projekten folgen","Save discoveries to your portfolio":"Entdeckungen ins Portfolio speichern","Track health evolution over time":"Health-Verlauf im Zeitverlauf verfolgen","Follow your favorite models":"Ihren Lieblingsmodellen folgen","Watch these trends from your account":"Trends im Konto beobachten","Build your own AI toolbox":"Eigene KI-Toolbox aufbauen","Get price-drop alerts on the models you use":"Preisalarme für Ihre Modelle erhalten","Follow this industryâs projects":"Projekten dieser Branche folgen","Save projects":"Projekte speichern","Build AI portfolios":"KI-Portfolios erstellen","Follow emerging tools":"Neuen Tools folgen","Get daily alerts":"Tägliche Alerts erhalten","Create your free account":"Kostenloses Konto erstellen", 1128 "Plans":"Preise","Free to Business plans":"Vom Free- zum Business-Plan", 1129 "Industries":"Branchen","AI by sector: health, finance\u2026":"KI nach Branche: Gesundheit, Finanzenâ¦", 1130 "Maintained by":"Gepflegt von","Are you the maintainer?":"Sind Sie der Maintainer?","Claim this project":"Projekt beanspruchen", 1131 "see all":"alle ansehen", 1132 "accelerating":"beschleunigt","detected":"erkannt","health":"Gesundheit","/ 7 days":"/ 7 Tage","projects":"Projekte","more emerging projects":"weitere aufstrebende Projekte","// no project matches these filters":"// kein Projekt entspricht diesen Filtern","The discovery scan has not run yet â come back after the next morning build.":"Der Discovery-Scan lief noch nicht â schauen Sie nach dem nächsten Morgen-Build vorbei.","Could not load the discovery data â please refresh.":"Discovery-Daten konnten nicht geladen werden â bitte aktualisieren.", 1133 "Emerging":"Aufstrebend","Detected by our signals":"Von unseren Signalen erkannt","Developer API":"Entwickler-API","Keys, quotas, fused records":"Schlüssel, Kontingente, fusionierte Daten", 1134 "Click to rename":"Zum Umbenennen klicken", 1135 "What is it for?":"Wofür ist das?","Not qualified yet":"Noch nicht qualifiziert","Saved":"Gespeichert","Chat assistant":"Chat-Assistent","Coding assistant":"Coding-Assistent","Generate images":"Bilder erzeugen","100% local & private":"100 % lokal & privat","Easiest setup":"Einfachste Einrichtung","Modest hardware":"Bescheidene Hardware","Serve an API":"Eine API bereitstellen", 1136 "members use it":"Mitglieder nutzen ihn","member uses it":"Mitglied nutzt ihn", 1137 "use it":"nutzen ihn","Use this stack":"Diesen Stack nutzen","Copied to your account":"In dein Konto kopiert","Already in your account":"Bereits in deinem Konto","used by":"genutzt von", 1138 "Members who use it":"Mitglieder, die es nutzen","Community stacks":"Community-Stacks", 1139 "Models you follow":"Modelle, denen du folgst","Models":"Modelle","open":"offen","closed":"geschlossen","per 1M out":"pro 1M Output","Price change over 30 days":"Preisänderung über 30 Tage","No model followed yet â search one above.":"Noch kein Modell â suche oben eines.","Models I follow (with prices)":"Modelle, denen ich folge (mit Preisen)", 1140 "members saved this":"Mitglieder haben es gespeichert","member saved this":"Mitglied hat es gespeichert","public stacks":"öffentliche Stacks","public stack":"öffentlicher Stack","collections":"Sammlungen","collection":"Sammlung","Featured in":"Enthalten in","by":"von","projects":"Projekte","project":"Projekt", 1141 "Public â anyone with the link":"Ãffentlich â mit dem Link erreichbar", 1142 "Add":"Hinzufügen","Add selected":"Auswahl hinzufügen","Select all":"Alle au
1142swählen","from watchlist":"aus der Watchlist","Remove":"Entfernen","Delete collection":"Sammlung löschen","Delete this collection?":"Diese Sammlung löschen?","Give it a title first.":"Gib ihr zuerst einen Titel.","Everything from your watchlist is already here.":"Alles aus deiner Watchlist ist bereits hier.","No projects yet â add one below or pick from your watchlist.":"Noch keine Projekte â füge unten eines hinzu oder wähle aus deiner Watchlist.","View public page":"Ãffentliche Seite ansehen","Links":"Links","Projects I follow (with health)":"Projekte, denen ich folge (mit Gesundheit)","Save this stack (free account)":"Diesen Stack speichern (kostenloses Konto)","Save this stack":"Diesen Stack speichern","Save to my account":"In meinem Konto speichern","Cancel":"Abbrechen","Bio":"Bio", 1143 "Public profile":"Ãffentliches Profil","Alerts":"Alerts","Homepage":"Startseite","Following":"Folge ich","Stacks":"Stacks","Your data":"Deine Daten","Delete account":"Konto löschen", 1144 "Check your inbox â one click to confirm.":"Prüfe dein Postfach â ein Klick zur Bestätigung.", 1145 "Members":"Mitglieder", 1146 "Follow":"Folgen","Following":"Folgt","People you follow":"Mitglieder, denen du folgst","I want to":"Ich möchte","Constraints":"Einschränkungen","Open â":"Ãffnen â", 1147 "Your stacks":"Deine Stacks","Open in Stack Builder":"Im Stack Builder öffnen","No stack saved yet.":"Noch kein Stack gespeichert.","Build one in the Stack Builder â":"Erstelle einen im Stack Builder â", 1148 "Your collections":"Deine Sammlungen","Your public profile":"Dein öffentliches Profil","Your data":"Deine Daten","Alerts you receive":"Deine Alerts","Why create an account?":"Warum ein Konto erstellen?","Show on my page":"Auf meiner Seite zeigen","Save profile":"Profil speichern","Public profile":"Ãffentliches Profil", 1149 "Search":"Suchen", 1150 "Member":"Mitglied","Featured project":"Hervorgehobenes Projekt","My stack â projects I follow":"Mein Stack â Projekte, denen ich folge","Favorites":"Favoriten","Public collections":"Ãffentliche Sammlungen","No public collection yet.":"Noch keine öffentliche Sammlung.","An olud.ai member":"Ein olud.ai-Mitglied","Create your own profile":"Erstelle dein Profil","Profile not found":"Profil nicht gefunden","This profile doesn't exist, or its owner keeps it private.":"Dieses Profil existiert nicht oder ist privat.","Member collection":"Sammlung eines Mitglieds","This collection is empty for now.":"Diese Sammlung ist noch leer.","Curated by":"Kuratiert von","Create your own collection":"Erstelle deine Sammlung", 1151 "My public page":"Meine öffentliche Seite", 1152 "Your homepage":"Deine Startseite","Live terminal":"Live-Terminal","Trending on GitHub":"GitHub-Trends","What's new":"Neuheiten","Latest releases":"Neueste Releases","News feed":"News-Feed","AI tools":"KI-Tools","More tools":"Weitere Tools","Open-source alternatives":"Open-Source-Alternativen","Model comparisons":"Modellvergleiche","Model comparator":"Modell-Vergleicher","Leaderboard":"Rangliste","Categories":"Kategorien","Prompts":"Prompts","Why olud.ai":"Warum olud.ai", 1153 "Private â preview":"Privat â Vorschau","Save to move your page to":"Speichern, um deine Seite zu verschieben nach","Copied!":"Kopiert!","Public":"Ãffentlich", 1154 "Keep track of what matters":"Behalte im Blick, was zählt","Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.":"Folge Projekten und erhalte eine E-Mail, wenn ihre Gesundheit sinkt, ein Release erscheint oder die Lizenz wechselt. Kostenlos, ohne Passwort.","30 seconds":"30 Sekunden", 1155 "Add to collection":"Zu Sammlung hinzufügen","Collection":"Sammlung","In collection":"In Sammlung","New collectionâ¦":"Neue Sammlungâ¦","Create":"Erstellen","No collection yet â create your first one below.":"Noch keine Sammlung â erstelle unten deine erste.","Savingâ¦":"Speichernâ¦","Profile saved.":"Profil gespeichert.", 1156 "Your account":"Dein Konto","Account":"Konto", 1157 "5 favorites max without an account":"Maximal 5 Favoriten ohne Konto","Create a free account to save unlimited favorites, sync them across devices and get alerts.":"Erstelle ein kostenloses Konto, um unbegrenzt Favoriten zu speichern, geräteübergreifend zu synchronisieren und Alerts zu erhalten.","Create a free account":"Kostenloses Konto erstellen","Your favorites":"Deine Favoriten","No favorites yet â tap the â¡ on any project or tool.":"Noch keine Favoriten â tippe auf das â¡ bei einem Projekt oder Tool.", 1158 "Enter your email first.":"Gib zuerst deine E-Mail ein.","Sendingâ¦":"Sendenâ¦","Network error â try again.":"Netzwerkfehler â versuch es erneut.","You are signed in.":"Du bist angemeldet.","Delete your account and all data? This cannot be undone.":"Konto und alle Daten löschen? Unwiderruflich.","Health alerts":"Gesundheits-Alerts","Release alerts":"Release-Alerts","Star spikes":"Stern-Spitzen","Off":"Aus","Signed in as":"Angemeldet als","Sign out":"Abmelden","Your watchlist":"Deine Watchlist","Delete account":"Konto löschen","Delete my account":"Mein Konto löschen","Sign in / create account":"Anmelden / Konto erstellen","Email me a link":"Link per E-Mail senden","If the address is valid, a sign-in link is on its way. Check your inbox (and spam).":"Wenn die Adresse gültig ist, ist ein Anmeldelink unterwegs. Prüfe dein Postfach (und Spam).", 1159 "Follow":"Folgen","Following":"Gefolgt","Sign in to follow projects":"Melde dich an, um Projekten zu folgen", 1160 "new AI projects today":"neue KI-Projekte heute", 1161 "Language":"Sprache", 1162 "My favorites":"Meine Favoriten","Weekly report":"Wochenbericht","Tool profiles":"Tool-Profile","Security advisories":"Sicherheitshinweise","Pulse ranking":"Pulse-
1162Ranking", 1163 "Search":"Suche", 1164 "All":"Alle", 1165 "Updated":"Aktualisiert", 1166 "New Space":"Neuer Space", 1167 "New model":"Neues Modell", 1168 "New project":"Neues Projekt", 1169 "No favorites yet.":"Noch keine Favoriten.", 1170 "No tools found.":"Keine Tools gefunden.", 1171 "No tools match your filters.":"Keine Tools entsprechen deinen Filtern.", 1172 "Trending tools are temporarily unavailable.":"Trend-Tools sind vorübergehend nicht verfügbar.", 1173 "Click the heart on any tool to save it here.":"Klicke auf das Herz eines Tools, um es hier zu speichern.", 1174 "Close":"SchlieÃen", 1175 "Copied!":"Kopiert!", 1176 "AI Tools":"KI-Tools","Directory":"Verzeichnis","HF Directory":"HF-Verzeichnis","Spaces":"Spaces","Top Projects":"Top-Projekte","Trending":"Im Trend", 1177 "Model profiles":"Modell-Profile","Stay in the loop":"Bleib auf dem Laufenden","Trends":"Trends","Languages":"Sprachen","Countries":"Länder","Contact":"Kontakt","DNA":"DNA","DNA families":"DNA-Familien","Advertise":"Werben","Open data & API":"Offene Daten & API","Documentation":"Dokumentation","Step-by-step setup":"Einrichtung Schritt für Schritt","Connect":"Verbinden","MCP, API, webhooks, n8n":"MCP, API, Webhooks, n8n","Site & Data":"Website & Daten","Privacy":"Datenschutz","Terms":"AGB","Tools":"Tools","Models":"Modelle","Discover":"Entdecken","Explore":"Mehr", 1178 "Tools & Agents":"Tools & Agents","Alternatives":"Alternativen", 1179 "AI Agents":"KI-Agenten","Open-source frameworks":"Open-Source-Frameworks","New":"Neu", 1180 "Tool Guides":"Tool-Guides","76 in-depth guides":"76 ausführliche Guides", 1181 "Tools Directory":"Tool-Verzeichnis","225+ tools":"225+ Tools","Compare Tools":"Tools vergleichen","Categories":"Kategorien", 1182 "AI Pricing":"KI-Preise","Live costs + calculator":"Live-Kosten + Rechner", 1183 "Free vs Paid":"Kostenlos vs Bezahlt","Open vs frontier":"Open vs Frontier","Leaderboard":"Rangliste", 1184 "Compare Models":"Modelle vergleichen","Paid Models":"Bezahlte Modelle", 1185 "All Alternatives":"Alle Alternativen","75+ tools mapped":"75+ Tools erfasst", 1186 "Devin alternatives":"Devin-Alternativen","Cursor alternatives":"Cursor-Alternativen","Notion alternatives":"Notion-Alternativen","Photoshop alternatives":"Photoshop-Alternativen", 1187 "Projects":"Projekte","News":"News","Latest":"Neueste","About":"Ãber uns","Methodology":"Methodik","My favorites":"Meine Favoriten","Dashboards":"Dashboards", 1188 "Tools catalog":"Tool-Katalog","Catalog":"Katalog","The AI hub.":"Der KI-Hub.","Open and commercial AI, side by side.":"Offene und kommerzielle KI, Seite an Seite.","Submit a tool":"Tool einreichen","Submit a tool or project":"Tool oder Projekt einreichen","Add to the directory":"Zum Hub hinzufügen","Add it to the hub":"Zum Hub hinzufügen","Get a badge":"Badge holen","For your README":"Für deine README", 1189 "Featured:":"Empfohlen:","Explore agents":"Agenten entdecken", 1190 "Stay in the loop":"Bleib auf dem Laufenden","Subscribe":"Abonnieren", 1191 "The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.":"Das Beste aus Open-Source-KI in deinem Postfach â neue Modelle, angesagte Projekte und Releases. Kein Spam, jederzeit abbestellbar.", 1192 "The world's most complete":"Das vollständigste Verzeichnis","open-source AI directory.":"für Open-Source-KI weltweit.","Built for the global community.":"Für die globale Community.", 1193 "More":"Mehr","Home":"Startseite","Models · Free vs Paid":"Modelle · Kostenlos vs Bezahlt","Alternatives to top tools":"Alternativen zu Top-Tools","AI Spaces":"KI-Spaces","News Feed":"News-Feed", 1194 "Independent · Open data":"Unabhängig · Offene Daten" 1195 }, 1196 pt: { 1197 "Online":"Online","I can help you find AI tools, compare models and explore projects â or just search.":"Posso ajudá-lo a encontrar ferramentas, comparar modelos e explorar projetos â ou simplesmente pesquisar.","Ask anything.":"Pergunte o que quiser.", 1198 "Updates":"Atualização", 1199 "Hourly":"A cada hora", 1200 "Continue in the assistant":"Continuar no assistente","I could not answer that one.":"A essa não consegui responder.", 1201 "Olud assistant":"Assistente Olud","⨠AI â ask me anything about olud.ai":"⨠IA â pergunte-me tudo sobre olud.ai","Type your questionâ¦":"Escreva a sua perguntaâ¦","Questions are logged anonymously to improve the answers.":"As perguntas são registadas de forma anónima para melhorar as respostas.","Hi! Iâm the olud.ai assistant. How can I help?":"Olá! Sou o assistente do olud.ai. Como posso ajudar?","Thinkingâ¦":"A procurarâ¦","Help":"Ajuda","Close":"Fechar","What is olud.ai?":"O que é o olud.ai?","How do I create an account?":"Como crio uma conta?","What are the plans?":"Quais são os planos?","How do I follow a project?":"C
1201omo sigo um projeto?", 1202 "Search, or ask anythingâ¦":"Pesquise, ou faça uma perguntaâ¦","Search tools, projects, models":"Pesquisar ferramentas, projetos, modelos", 1203 "Ask the assistant":"Pergunte ao assistente","Answered with this site's live data":"Resposta com os dados ao vivo do site","No text match":"Sem correspondência de texto", 1204 "Verified \u2014 you are now listed as the maintainer of this project.":"Verificado â agora consta como maintainer deste projeto.","Already verified \u2014 you are listed as the maintainer.":"Já verificado â consta como maintainer.","To claim a project, your account must be signed in with GitHub.":"Para reivindicar um projeto, a sua conta deve iniciar sessão com GitHub.","This repository belongs to another GitHub account.":"Este repositório pertence a outra conta GitHub.","You must be a public member of the organization that owns this repository.":"Deve ser membro público da organização proprietária.","You must appear among the contributors of this repository.":"Deve constar entre os contribuidores do repositório.","Claim failed \u2014 contact [email protected] if you believe this is wrong.":"Reivindicação falhou â escreva para [email protected].", 1205 "Badge copied \u2014 click to copy again":"Selo copiado â clique para copiar novamente", 1206 "Health badge":"Selo de saúde","Copy the Markdown badge for your README":"Copie o selo Markdown para o seu README","Markdown copied!":"Markdown copiado!", 1207 "My Dashboard":"Meu Dashboard","Compose your own \u2014 members":"Componha o seu â membros", 1208 "Save this dashboard":"Guarde este dashboard","Get notified when a new project appears":"Seja avisado de novos projetos","Follow these projects":"Siga estes projetos","Save discoveries to your portfolio":"Guarde descobertas no seu portfólio","Track health evolution over time":"Acompanhe a evolução do health score","Follow your favorite models":"Siga os seus modelos favoritos","Watch these trends from your account":"Acompanhe estas tendências na sua conta","Build your own AI toolbox":"Monte a sua toolbox de IA","Get price-drop alerts on the models you use":"Alertas de descidas de preço dos seus modelos","Follow this industryâs projects":"Siga os projetos deste setor","Save projects":"Guarde projetos","Build AI portfolios":"Crie portfólios de IA","Follow emerging tools":"Siga ferramentas emergentes","Get daily alerts":"Receba alertas diários","Create your free account":"Crie a sua conta gratuita", 1209 "Plans":"Preços","Free to Business plans":"Do plano Free ao Business", 1210 "Industries":"Indústrias","AI by sector: health, finance\u2026":"IA por setor: saúde, finançasâ¦", 1211 "Maintained by":"Mantido por","Are you the maintainer?":"à o maintainer?","Claim this project":"Reivindique este projeto", 1212 "see all":"ver todos", 1213 "accelerating":"em aceleração","detected":"detetado","health":"saúde","/ 7 days":"/ 7 dias","projects":"projetos","more emerging projects":"mais projetos emergentes","// no project matches these filters":"// nenhum projeto corresponde a estes filtros","The discovery scan has not run yet â come back after the next morning build.":"O scan de descoberta ainda não correu â volte após o próximo build matinal.","Could not load the discovery data â please refresh.":"Não foi possÃvel carregar os dados de descoberta â recarregue a página.", 1214 "Emerging":"Emergentes","Detected by our signals":"Detetados pelos nossos sinais","Developer API":"API para programadores","Keys, quotas, fused records":"Chaves, quotas, fichas fundidas", 1215 "Click to rename":"Clique para renomear", 1216 "What is it for?":"Para que serve?","Not qualified yet":"Ainda não qualificada","Saved":"Salvo","Chat assistant":"Assistente de chat","Coding assistant":"Assistente de código","Generate images":"Gerar imagens","100% local & private":"100% local e privado","Easiest setup":"Instalação mais simples","Modest hardware":"Hardware modesto","Serve an API":"Servir uma API", 1217 "members use it":"membros a usam","member uses it":"membro a usa", 1218 "use it":"a usam","Use this stack":"Usar esta stack","Copied to your account":"Copiad
1218a na sua conta","Already in your account":"Já está na sua conta","used by":"usada por", 1219 "Members who use it":"Membros que usam","Community stacks":"Stacks da comunidade", 1220 "Models you follow":"Modelos que você segue","Models":"Modelos","open":"aberto","closed":"fechado","per 1M out":"por 1M saÃda","Price change over 30 days":"Variação em 30 dias","No model followed yet â search one above.":"Nenhum modelo seguido â busque um acima.","Models I follow (with prices)":"Modelos que sigo (com preços)", 1221 "members saved this":"membros salvaram","member saved this":"membro salvou","public stacks":"stacks públicas","public stack":"stack pública","collections":"coleções","collection":"coleção","Featured in":"Presente em","by":"por","projects":"projetos","project":"projeto", 1222 "Public â anyone with the link":"Público â acessÃvel com o link", 1223 "Add":"Adicionar","Add selected":"Adicionar selecionados","Select all":"Selecionar tudo","from watchlist":"da watchlist","Remove":"Remover","Delete collection":"Excluir coleção","Delete this collection?":"Excluir esta coleção?","Give it a title first.":"Dê um tÃtulo primeiro.","Everything from your watchlist is already here.":"Tudo da sua watchlist já está aqui.","No projects yet â add one below or pick from your watchlist.":"Nenhum projeto â adicione um abaixo ou escolha da sua watchlist.","View public page":"Ver a página pública","Links":"Links","Projects I follow (with health)":"Projetos que sigo (com saúde)","Save this stack (free account)":"Salvar esta stack (conta gratuita)","Save this stack":"Salvar esta stack","Save to my account":"Salvar na minha conta","Cancel":"Cancelar","Bio":"Bio", 1224 "Public profile":"Perfil público","Alerts":"Alertas","Homepage":"InÃcio","Following":"Seguindo","Stacks":"Stacks","Your data":"Seus dados","Delete account":"Excluir conta", 1225 "Check your inbox â one click to confirm.":"Verifique sua caixa â um clique para confirmar.", 1226 "Members":"Membros", 1227 "Follow":"Seguir","Following":"Seguindo","People you follow":"Membros que você segue","I want to":"Eu quero","Constraints":"Restrições","Open â":"Abrir â", 1228 "Your stacks":"Suas stacks","Open in Stack Builder":"Abrir no Stack Builder","No stack saved yet.":"Nenhuma stack salva.","Build one in the Stack Builder â":"Crie uma no Stack Builder â", 1229 "Your collections":"Suas coleções","Your public profile":"Seu perfil público","Your data":"Seus dados","Alerts you receive":"Seus alertas","Why create an account?":"Por que criar uma conta?","Show on my page":"Mostrar na minha página","Save profile":"Salvar perfil","Public profile":"Perfil público", 1230 "Search":"Buscar", 1231 "Member":"Membro","Featured project":"Projeto em destaque","My stack â projects I follow":"Minha stack â projetos que sigo","Favorites":"Favoritos","Public collections":"Coleções públicas","No public collection yet.":"Nenhuma coleção pública ainda.","An olud.ai member":"Um membro olud.ai","Create your own profile":"Crie seu perfil","Profile not found":"Perfil não encontrado","This profile doesn't exist, or its owner keeps it private.":"Este perfil não existe, ou seu dono o mantém privado.","Member collection":"Coleção de um membro","This collection is empty for now.":"Esta coleção está vazia.","Curated by":"Curadoria de","Create your own collection":"Crie sua coleção", 1232 "My public page":"Minha página pública", 1233 "Your homepage":"Sua página inicial","Live terminal":"Terminal ao vivo","Trending on GitHub":"Tendências no GitHub","What's new":"Novidades","Latest releases":"Ãltimos lançamentos","News feed":"NotÃcias","AI tools":"Ferramentas de IA","More tools":"Mais ferramentas","Open-source alternatives":"Alternativas open source","Model comparisons":"Comparações de modelos","Model comparator":"Comparador de modelos","Leaderboard":"Ranking","Categories":"Categorias","Prompts":"Prompts","Why olud.ai":"Por que olud.ai", 1234 "Private â preview":"Privado â prévia","Save to move your page to":"Salve para mover sua página para","Copied!":"Copiado!","Public":"Público", 1235 "Keep track of what matters":"Fique de olho no que importa","Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.":"Siga projetos e receba um email quando a saúde cair, sair uma nova versão ou a licença mudar. Grátis, sem senha.","30 seconds":"30 segundos", 1236 "Add to collection":"Adicionar a uma coleção","Collection":"Coleção","In collection":"Na coleção","New collectionâ¦":"Nova coleçãoâ¦","Create":"Criar","No collection yet â create your first one below.":"Nenhuma coleção â crie a primeira abaixo.","Savingâ¦":"Salvandoâ¦","Profile saved.":"Perfil salvo.", 1237 "Your account":"Sua conta","Account":"Conta", 1238 "5 favorites max without an account":"Máximo de 5 favoritos sem conta","Create a free account to save unlimited favorites, sync them across devices and get alerts.":"Crie uma conta gratuita para salvar favoritos ilimitados, sincronizá-los entre dispositivos e receber alertas.","Create a free account":"Criar conta gratuita","Your favorites":"Seus favoritos","No favorites yet â tap the â¡ on any project or tool.":"Nenhum favorito ainda â toque no â¡ de qualquer projeto ou ferramenta.", 1239 "Enter your email first.":"Digite primeiro seu email.","Sendingâ¦":"Enviandoâ¦","Network error â try again.":"Erro de rede â tente novamente.","You are signed in.":"Você está conectado.","Delete your account and all data? This cannot be undone.":"Excluir sua conta e todos os dados? IrreversÃvel.","Health alerts":"Alertas de saúde","Release alerts":"Alertas de releases","Star spikes":"Picos de estrelas","Off":"Off","Signed in as":"Conectado como","Sign out":"Sair","Your watchlist":"Sua watchlist","Delete account":"Excluir conta","Delete my account":"Excluir minha conta","Sign in / create account":"Entrar / criar conta","Email me a link":"Env
1239iar um link","If the address is valid, a sign-in link is on its way. Check your inbox (and spam).":"Se o endereço for válido, um link de acesso está a caminho. Verifique sua caixa (e o spam).", 1240 "Follow":"Seguir","Following":"Seguindo","Sign in to follow projects":"Entre para seguir projetos", 1241 "new AI projects today":"novos projetos de IA hoje", 1242 "Language":"Idioma", 1243 "My favorites":"Meus favoritos","Pulse ranking":"Ranking Pulse","Security advisories":"Avisos de segurança", 1244 "Search":"Buscar", 1245 "All":"Todos", 1246 "Updated":"Atualizado", 1247 "New Space":"Novo espaço", 1248 "New model":"Novo modelo", 1249 "New project":"Novo projeto", 1250 "No favorites yet.":"Nenhum favorito ainda.", 1251 "No tools found.":"Nenhuma ferramenta encontrada.", 1252 "No tools match your filters.":"Nenhuma ferramenta corresponde aos filtros.", 1253 "Trending tools are temporarily unavailable.":"As ferramentas em alta estão temporariamente indisponÃveis.", 1254 "Click the heart on any tool to save it here.":"Clique no coração de uma ferramenta para salvá-la aqui.", 1255 "Close":"Fechar", 1256 "Copied!":"Copiado!", 1257 "AI Tools":"Ferramentas IA","Directory":"Diretório","HF Directory":"Diretório HF","Spaces":"Espaços","Top Projects":"Melhores projetos","Trending":"Em alta", 1258 "Model profiles":"Fichas de modelos","Stay in the loop":"Fique por dentro","Trends":"Tendências","Contact":"Contacto","DNA":"ADN","Languages":"Linguagens","Countries":"Paises","DNA families":"Familias de ADN","Advertise":"Anunciar","Open data & API":"Dados abertos e API","Documentation":"Documentação","Step-by-step setup":"Configuração passo a passo","Connect":"Ligar","MCP, API, webhooks, n8n":"MCP, API, webhooks, n8n","Site & Data":"Site e dados","Privacy":"Privacidade","Terms":"Termos","Tools":"Ferramentas","Models":"Modelos","Discover":"Descobrir","Explore":"Explorar", 1259 "Tools & Agents":"Ferramentas & Agentes","Alternatives":"Alternativas", 1260 "AI Agents":"Agentes de IA","Open-source frameworks":"Frameworks open source","New":"Novo", 1261 "Tool Guides":"Guias de ferramentas","76 in-depth guides":"76 guias detalhados", 1262 "Tools Directory":"Diretório de ferramentas","225+ tools":"225+ ferramentas","Compare Tools":"Comparar ferramentas","Categories":"Categorias", 1263 "AI Pricing":"Preços de IA","Live costs + calculator":"Custos ao vivo + calculadora", 1264 "Free vs Paid":"Grátis vs Pago","Open vs frontier":"Open vs frontier","Leaderboard":"Classificação", 1265 "Compare Models":"Comparar modelos","Paid Models":"Modelos pagos", 1266 "All Alternatives":"Todas as alternativas","75+ tools mapped":"75+ ferramentas mapeadas", 1267 "Devin alternatives":"Alternativas ao Devin","Cursor alternatives":"Alternativas ao Cursor","Notion alternatives":"Alternativas ao Notion","Photoshop alternatives":"Alternativas ao Photoshop", 1268 "Projects":"Projetos","News":"NotÃcias","Latest":"Recentes","About":"Sobre","Methodology":"Metodologia","Dashboards":"Painéis", 1269 "Tools catalog":"Catálogo de ferramentas","Catalog":"Catálogo","The AI hub.":"O hub da IA.","Open and commercial AI, side by side.":"IA aberta e comercial, lado a lado.","Submit a tool":"Enviar uma ferramenta","Submit a tool or project":"Enviar uma ferramenta ou um projeto","Add to the directory":"Adicionar ao hub","Add it to the hub":"Adicionar ao hub","Get a badge":"Obter um selo","For your README":"Para o seu README", 1270 "Featured:":"Destaque:","Explore agents":"Explorar agentes", 1271 "Stay in the loop":"Fique por dentro","Subscribe":"Inscrever-se", 1272 "The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.":"O melhor da IA open source na sua caixa de entrada â novos modelos, projetos em alta e lançamentos. Sem spam, cancele quando quiser.", 1273 "The world's most complete":"O diretório mais completo","open-source AI directory.":"de IA open source do mundo.","Built for the global community.":"Feito para a comunidade global.", 1274 "More":"Mais","Home":"InÃcio","Models · Free vs Paid":"Modelos · Grátis vs Pago","Alternatives to top tools":"Alternativas a ferramentas populares","AI Spaces":"Espaços IA","News Feed":"Feed de notÃcias", 1275 "Independent · Open data":"Independente · Dados abertos" 1276 } 1277 }; 1278 /* âââ V22 â i18n du badge santé + page /health.html (fusionné dans NAV_I18N) âââ */ 1279 (function(){ 1280 var H = { 1281 fr: { "Alive or abandoned?":"Vivant ou abandonné ?","Project health":"Santé du projet","Project Health":"Santé des projets", 1282 "Thriving":"Florissant","Healthy":"Sain","Maintained":"Maintenu","Slowing down":"En ralentissement","At risk":"à risque","Archived":"Archivé","Disabled":"Désactivé", 1283 "Activity":"Activité","Momentum":"Ãlan","Community":"Communauté","Maintenance":"Maintenance", 1284 "Checked":"Vérifié le","See all project health":"Voir la santé de tous les projets", 1285 "Actively developed, with a healthy community behind it.":"Développement actif, porté par une communauté solide.", 1286 "Regularly maintained. Safe to build on.":"Maintenu régulièrement. Fiable pour construire dessus.", 1287 "Still maintained, but development has slowed.":"Toujours maintenu, mais le développement a ralenti.", 1288 "Activity is dropping. Check before you build on it.":"L'activité baisse. Vérifiez avant de construire dessus.", 1289 "Little to no recent activity. It may be abandoned.":"Peu ou pas d'activité récente. Il est peut-être abandonné.", 1290 "The maintainers have archived this repository. It is read-only and will not be updated.":"Les mainteneurs ont archivé ce dépôt. Il est en lecture seule et ne sera plus mis à jour.", 1291 "This repository has been disabled on GitHub.":"Ce dépôt a été désactivé sur GitHub.", 1292 "No commits in the last 4 weeks.":"Aucun commit depuis 4 semaines." }, 1293 it: { "Alive or abandoned?":"Vivo o abbandonato?","Project health":"Salute del progetto","Project Health":"Salute dei progetti", 1294 "Thriving":"Fiorente","Healthy":"Sano","Maintained":"Mantenuto","Slowing down":"In rallentamento","At risk":"A rischio","Archived":"Archiviato","Disabled":"Disabilitato", 1295 "Activity":"Attività ","Momentum":"Slancio","Community":"Comunità ","Maintenance":"Manutenzione", 1296 "Checked":"Verificato il","See all project health":"Vedi la salute di tutti i progetti", 1297 "Actively developed, with a healthy community behind it.":"Sviluppo attivo, sostenuto da una comunità solida.", 1298 "Regularly maintained. Safe to build on.":"Mantenuto regolarmente. Affidabile come base.", 1299 "Still maintained, but development has slowed.":"Ancora mantenuto, ma lo sviluppo è rallentato.", 1300 "Activity is dropping. Check before you build on it.":"L'attività sta calando. Verifica prima di usarlo come base.", 1301 "Little to no recent activity. It may be abandoned.":"Attività recente scarsa o nulla. Potrebbe essere abbandonato.", 1302 "The maintainers have archived this repository. It is read-only and will not be updated.":"I manutentori hanno archiviato questo repository. à in sola lettura e non sarà più aggiornato.", 1303 "This repository has been disabled on GitHub.":"Questo repository è stato disabilitato su GitHub.", 1304 "No commits in the last 4 weeks.":"Nessun commit nelle ultime 4 settimane." }, 1305 es: { "Alive or abandoned?":"¿Vivo o abandonado?","Project health":"Salud del proyecto","Project Health":"Salud de los proyectos", 1306 "Thriving":"Próspero","Healthy":"Saludable","Maintained":"Mantenido","Slowing down":"En desaceleración","At risk":"En riesgo","Archived":"Archivado","Disabled":"Deshabilitado", 1307 "Activity":"Actividad","Momentum":"Impulso","Community":"Comunidad","Maintenance":"Mantenimiento", 1308 "Checked":"Verificado el","See all project health":"Ver la salud de todos los proyectos", 1309 "Actively developed, with a healthy community behind it.":"Desarrollo activo, respaldado por una comunidad sólida.", 1310 "Regularly maintained. Safe to build on.":"Mantenido con regularid
1310ad. Fiable para construir sobre él.", 1311 "Still maintained, but development has slowed.":"TodavÃa mantenido, pero el desarrollo se ha ralentizado.", 1312 "Activity is dropping. Check before you build on it.":"La actividad está bajando. Compruébalo antes de construir sobre él.", 1313 "Little to no recent activity. It may be abandoned.":"Poca o ninguna actividad reciente. Puede estar abandonado.", 1314 "The maintainers have archived this repository. It is read-only and will not be updated.":"Los mantenedores han archivado este repositorio. Es de solo lectura y no se actualizará.", 1315 "This repository has been disabled on GitHub.":"Este repositorio ha sido deshabilitado en GitHub.", 1316 "No commits in the last 4 weeks.":"Ningún commit en las últimas 4 semanas." }, 1317 de: { "Alive or abandoned?":"Lebendig oder aufgegeben?","Project health":"Projektgesundheit","Project Health":"Projektgesundheit", 1318 "Thriving":"Blühend","Healthy":"Gesund","Maintained":"Gepflegt","Slowing down":"Verlangsamt sich","At risk":"Gefährdet","Archived":"Archiviert","Disabled":"Deaktiviert", 1319 "Activity":"Aktivität","Momentum":"Dynamik","Community":"Community","Maintenance":"Wartung", 1320 "Checked":"Geprüft am","See all project health":"Gesundheit aller Projekte ansehen", 1321 "Actively developed, with a healthy community behind it.":"Aktiv entwickelt, getragen von einer starken Community.", 1322 "Regularly maintained. Safe to build on.":"RegelmäÃig gepflegt. Eine sichere Basis.", 1323 "Still maintained, but development has slowed.":"Wird noch gepflegt, aber die Entwicklung hat sich verlangsamt.", 1324 "Activity is dropping. Check before you build on it.":"Die Aktivität nimmt ab. Vor dem Einsatz prüfen.", 1325 "Little to no recent activity. It may be abandoned.":"Kaum oder keine aktuelle Aktivität. Möglicherweise aufgegeben.", 1326 "The maintainers have archived this repository. It is read-only and will not be updated.":"Die Maintainer haben dieses Repository archiviert. Es ist schreibgeschützt und wird nicht mehr aktualisiert.", 1327 "This repository has been disabled on GitHub.":"Dieses Repository wurde auf GitHub deaktiviert.", 1328 "No commits in the last 4 weeks.":"Keine Commits in den letzten 4 Wochen." }, 1329 pt: { "Alive or abandoned?":"Vivo ou abandonado?","Project health":"Saúde do projeto","Project Health":"Saúde dos projetos", 1330 "Thriving":"Próspero","Healthy":"Saudável","Maintained":"Mantido","Slowing down":"Em desaceleração","At risk":"Em risco","Archived":"Arquivado","Disabled":"Desativado", 1331 "Activity":"Atividade","Momentum":"Impulso","Community":"Comunidade","Maintenance":"Manutenção", 1332 "Checked":"Verificado em","See all project health":"Ver a saúde de todos os projetos", 1333 "Actively developed, with a healthy community behind it.":"Desenvolvimento ativo, apoiado por uma comunidade sólida.", 1334 "Regularly maintained. Safe to build on.":"Mantido regularmente. Confiável para construir em cima.", 1335 "Still maintained, but development has slowed.":"Ainda mantido, mas o desenvolvimento desacelerou.", 1336 "Activity is dropping. Check before you build on it.":"A atividade está caindo. Verifique antes de construir em cima.", 1337 "Little to no recent activity. It may be abandoned.":"Pouca ou nenhuma atividade recente. Pode estar abandonado.", 1338 "The maintainers have archived this repository. It is read-only and will not be updated.":"Os mantenedores arquivaram este repositório. Está em modo somente leitura e não será mais atualizado.", 1339 "This repository has been disabled on GitHub.":"Este repositório foi desativado no GitHub.", 1340 "No commits in the last 4 weeks.":"Nenhum commit nas últimas 4 semanas." } 1341 }; 1342 for (var hl in H){ if (!NAV_I18N[hl]) NAV_I18N[hl] = {}; for (var hk in H[hl]) NAV_I18N[hl][hk] = H[hl][hk]; } 1343 })(); 1344 /* âââ V23 â i18n du Terminal (dashboard.html, contenu rendu en JS) âââ */ 1345 (function(){ 1346 var T = { 1347 fr: { "projects today":"projets aujourd'hui","today":"aujourd'hui","this week":"cette semaine","new Spaces":"nouveaux Spaces", 1348 "cheaper output than the top paid flagship":"moins cher en sortie que le meilleur flagship payant", 1349 "healthy":"sains","slowing":"en ralentissement","at risk":"à risque","archived":"archivés","ARCHIVED":"ARCHIVÃ", 1350 "Paper of the day":"Paper du jour", 1351 "No velocity data yet.":"Pas encore de données de vélocité.", 1352 "No repricing detected recently.":"Aucun changement de prix récent.", 1353 "Model data unavailable.":"Données des modèles indisponibles.", 1354 "Health data unavailable.":"Données de santé indisponibles.", 1355 "Rankings unavailable.":"Classements indisponibles.", 1356 "Nothing yet today.":"Rien pour l'instant aujourd'hui.", 1357 "Releases are being collected.":"Les releases sont en cours de collecte." }, 1358 it: { "projects today":"progetti oggi","today":"oggi","this week":"questa settimana","new Spaces":"nuovi Spaces", 1359 "cheaper output than the top paid flagship":"più economico in output del miglior flagship a pagamento", 1360 "healthy":"sani","slowing":"in rallentamento","at risk":"a rischio","archived":"archiviati","ARCHIVED":"ARCHIVIATO", 1361 "Paper of the day":"Paper del giorno", 1362 "No velocity data yet.":"Ancora nessun dato di velocità .", 1363 "No repricing detected recently.":"Nessuna variazione di prezzo recente.", 1364 "Model data unavailable.":"Dati dei modelli non disponibili.", 1365 "Health data unavailable.":"Dati sulla salute non disponibili.", 1366 "Rankings unavailable.":"Classifiche non disponibili.", 1367 "Nothing yet today.":"Ancora niente oggi.", 1368 "Releases are being collected.":"Le release sono in fase di raccolta." }, 1369 es: { "projects today":"proyectos hoy","today":"hoy","this week":"esta semana","new Spaces":"nuevos Spaces", 1370 "cheaper output than the top paid flagship":"más barato en salida que el mejor flagship de pago", 1371 "healthy":"saludables","slowing":"en desaceleración","at risk":"en riesgo","archived":"archivados","ARCHIVED":"ARCHIVADO", 1372 "Paper of the day":"Paper del dÃa", 1373 "No velocity data yet.":"Aún no hay datos de velocidad.", 1374 "No repricing detected recently.":"Sin cambios de precio recientes.",
1375 "Model data unavailable.":"Datos de modelos no disponibles.", 1376 "Health data unavailable.":"Datos de salud no disponibles.", 1377 "Rankings unavailable.":"Rankings no disponibles.", 1378 "Nothing yet today.":"Nada todavÃa hoy.", 1379 "Releases are being collected.":"Las releases se están recopilando." }, 1380 de: { "projects today":"Projekte heute","today":"heute","this week":"diese Woche","new Spaces":"neue Spaces", 1381 "cheaper output than the top paid flagship":"günstigerer Output als das beste Bezahlmodell", 1382 "healthy":"gesund","slowing":"verlangsamt","at risk":"gefährdet","archived":"archiviert","ARCHIVED":"ARCHIVIERT", 1383 "Paper of the day":"Paper des Tages", 1384 "No velocity data yet.":"Noch keine Velocity-Daten.", 1385 "No repricing detected recently.":"Keine aktuellen Preisänderungen.", 1386 "Model data unavailable.":"Modelldaten nicht verfügbar.", 1387 "Health data unavailable.":"Gesundheitsdaten nicht verfügbar.", 1388 "Rankings unavailable.":"Rankings nicht verfügbar.", 1389 "Nothing yet today.":"Heute noch nichts.", 1390 "Releases are being collected.":"Releases werden gesammelt." }, 1391 pt: { "projects today":"projetos hoje","today":"hoje","this week":"esta semana","new Spaces":"novos Spaces", 1392 "cheaper output than the top paid flagship":"mais barato na saÃda que o melhor flagship pago", 1393 "healthy":"saudáveis","slowing":"em desaceleração","at risk":"em risco","archived":"arquivados","ARCHIVED":"ARQUIVADO", 1394 "Paper of the day":"Paper do dia", 1395 "No velocity data yet.":"Ainda sem dados de velocidade.", 1396 "No repricing detected recently.":"Nenhuma mudança de preço recente.", 1397 "Model data unavailable.":"Dados dos modelos indisponÃveis.", 1398 "Health data unavailable.":"Dados de saúde indisponÃveis.", 1399 "Rankings unavailable.":"Rankings indisponÃveis.", 1400 "Nothing yet today.":"Ainda nada hoje.", 1401 "Releases are being collected.":"As releases estão sendo coletadas." } 1402 }; 1403 for (var tl in T){ if (!NAV_I18N[tl]) NAV_I18N[tl] = {}; for (var tk in T[tl]) NAV_I18N[tl][tk] = T[tl][tk]; } 1404 })(); 1405 /* âââ V25 â i18n des vues agrandies du Terminal (titres + explications) âââ */ 1406 (function(){ 1407 var E = { 1408 fr: { "more":"de plus","Recent changes":"Changements récents","All tracked prices":"Tous les prix suivis","New HF Spaces":"Nouveaux Spaces HF", 1409 "Market movers":"Les plus fortes hausses","Price action":"Mouvements de prix","Open vs paid":"Open vs payant","Health monitor":"Moniteur de santé","Media ELO leaders":"Leaders ELO médias","Fresh today":"Nouveautés du jour","Releases":"Releases", 1410 "Stars gained in the last 24 hours. A sudden spike usually means a launch, a viral post or a major release worth checking out.":"Ãtoiles gagnées ces dernières 24 heures. Un pic soudain signale généralement un lancement, un post viral ou une release majeure qui mérite le coup d'Åil.", 1411 "Every time a provider changes a price on OpenRouter, it lands here. Green means it got cheaper. Open-weight models are in a constant price war; frontier prices rarely move. Open-model prices are the cheapest provider rate via OpenRouter; official maker rates may be higher.":"Chaque changement de prix d'un fournisseur sur OpenRouter atterrit ici. Vert = moins cher. Les modèles open se livrent une guerre des prix permanente ; les prix frontier bougent rarement. Les prix des modèles open sont ceux du fournisseur le moins cher via OpenRouter ; les tarifs officiels des éditeurs peuvent être plus élevés.", 1412 "The top-ranked open-weight models side by side with the top paid ones. Prices are per 1M tokens, input / output.":"Les meilleurs modèles open face aux meilleurs payants. Prix pour 1M de tokens, entrée / sortie.", 1413 "Our daily GitHub health check: recent activity, commit momentum, community size and issue upkeep. Archived or at-risk projects are flagged here first.":"Notre bilan de santé GitHub quotidien : activité récente, élan des commits, taille de la communauté et gestion des issues. Les projets archivés ou à risque apparaissent ici en premier.",
1414 "Blind human preference: people compare two outputs without knowing which model produced them, and vote. The only honest way to rank media models.":"Préférence humaine en aveugle : des personnes comparent deux résultats sans savoir quel modèle les a produits, et votent. La seule façon honnête de classer les modèles médias.", 1415 "Brand-new repositories created on GitHub, and the research paper the community is upvoting right now on Hugging Face.":"Les tout nouveaux dépôts créés sur GitHub, et le papier de recherche que la communauté plébiscite en ce moment sur Hugging Face.", 1416 "The latest versions shipped by the open-source tools we track, with their release level.":"Les dernières versions publiées par les outils open source que nous suivons, avec leur niveau de release." }, 1417 it: { "more":"altri","Recent changes":"Variazioni recenti","All tracked prices":"Tutti i prezzi monitorati","New HF Spaces":"Nuovi Spaces HF", 1418 "Market movers":"I maggiori rialzi","Price action":"Movimenti di prezzo","Open vs paid":"Open vs a pagamento","Health monitor":"Monitor di salute","Media ELO leaders":"Leader ELO media","Fresh today":"Novità di oggi","Releases":"Release", 1419 "Stars gained in the last 24 hours. A sudden spike usually means a launch, a viral post or a major release worth checking out.":"Stelle guadagnate nelle ultime 24 ore. Un picco improvviso indica di solito un lancio, un post virale o una release importante da tenere d'occhio.", 1420 "Every time a provider changes a price on OpenRouter, it lands here. Green means it got cheaper. Open-weight models are in a constant price war; frontier prices rarely move. Open-model prices are the cheapest provider rate via OpenRouter; official maker rates may be higher.":"Ogni variazione di prezzo su OpenRouter finisce qui. Verde = più economico. I modelli open sono in una guerra dei prezzi continua; i prezzi frontier si muovono raramente. I prezzi dei modelli open sono la tariffa del provider più economico via OpenRouter; le tariffe ufficiali dei produttori possono essere più alte.", 1421 "The top-ranked open-weight models side by side with the top paid ones. Prices are per 1M tokens, input / output.":"I migliori modelli open affiancati ai migliori a pagamento. Prezzi per 1M di token, input / output.", 1422 "Our daily GitHub health check: recent activity, commit momentum, community size and issue upkeep. Archived or at-risk projects are flagged here first.":"Il nostro check di salute GitHub quotidiano: attività recente, slancio dei commit, dimensione della comunità e gestione delle issue. I progetti archiviati o a rischio compaiono qui per primi.", 1423 "Blind human preference: people compare two outputs without knowing which model produced them, and vote. The only honest way to rank media models.":"Preferenza umana alla cieca: le persone confrontano due risultati senza sapere quale modello li ha prodotti, e votano. L'unico modo onesto di classificare i modelli media.", 1424 "Brand-new repositories created on GitHub, and the research paper the community is upvoting right now on Hugging Face.":"I repository appena creati su GitHub e il paper di ricerca che la comunità sta votando in questo momento su Hugging Face.", 1425 "The latest versions shipped by the open-source tools we track, with their release level.":"Le ultime versioni pubblicate dagli strumenti open source che seguiamo, con il loro livello di release." }, 1426 es: { "more":"más","Recent changes":"Cambios recientes","All tracked prices":"Todos los precios monitorizados","New HF Spaces":"Nuevos Spaces HF", 1427 "Market movers":"Mayores subidas","Price action":"Movimientos de precios","Open vs paid":"Open vs de pago","Health monitor":"Monitor de salud","Media ELO leaders":"LÃderes ELO de medios","Fresh today":"Novedades de hoy","Releases":"Releases", 1428 "Stars gained in the last 24 hours. A sudden spike usually means a launch, a viral post or a major release worth checking out.":"Estrellas ganadas en las últimas 24 horas. Un pico repentino suele indicar un lanzamiento, un post viral o una release importante que conviene mirar.", 1429 "Every time a provider changes a price on OpenRouter, it lands here. Green means it got cheaper. Open-weight models are in a constant price war; frontier prices rarely move. Open-model prices are the cheapest provider rate via OpenRouter;
1429 official maker rates may be higher.":"Cada cambio de precio de un proveedor en OpenRouter aparece aquÃ. Verde = más barato. Los modelos open viven una guerra de precios constante; los precios frontier apenas se mueven. Los precios de los modelos open son la tarifa del proveedor más barato vÃa OpenRouter; las tarifas oficiales de los creadores pueden ser más altas.", 1430 "The top-ranked open-weight models side by side with the top paid ones. Prices are per 1M tokens, input / output.":"Los mejores modelos open junto a los mejores de pago. Precios por 1M de tokens, entrada / salida.", 1431 "Our daily GitHub health check: recent activity, commit momentum, community size and issue upkeep. Archived or at-risk projects are flagged here first.":"Nuestro chequeo diario de salud en GitHub: actividad reciente, ritmo de commits, tamaño de la comunidad y gestión de issues. Los proyectos archivados o en riesgo aparecen aquà primero.", 1432 "Blind human preference: people compare two outputs without knowing which model produced them, and vote. The only honest way to rank media models.":"Preferencia humana a ciegas: las personas comparan dos resultados sin saber qué modelo los produjo, y votan. La única forma honesta de clasificar modelos de medios.", 1433 "Brand-new repositories created on GitHub, and the research paper the community is upvoting right now on Hugging Face.":"Los repositorios recién creados en GitHub y el paper de investigación que la comunidad está votando ahora mismo en Hugging Face.", 1434 "The latest versions shipped by the open-source tools we track, with their release level.":"Las últimas versiones publicadas por las herramientas open source que seguimos, con su nivel de release." }, 1435 de: { "more":"mehr","Recent changes":"Aktuelle Ãnderungen","All tracked prices":"Alle beobachteten Preise","New HF Spaces":"Neue HF Spaces", 1436 "Market movers":"GröÃte Anstiege","Price action":"Preisbewegungen","Open vs paid":"Open vs. Bezahlt","Health monitor":"Gesundheitsmonitor","Media ELO leaders":"Media-ELO-Spitzenreiter","Fresh today":"Neu heute","Releases":"Releases", 1437 "Stars gained in the last 24 hours. A sudden spike usually means a launch, a viral post or a major release worth checking out.":"In den letzten 24 Stunden gewonnene Sterne. Ein plötzlicher Anstieg bedeutet meist einen Launch, einen viralen Post oder ein wichtiges Release, das einen Blick wert ist.", 1438 "Every time a provider changes a price on OpenRouter, it lands here. Green means it got cheaper. Open-weight models are in a constant price war; frontier prices rarely move. Open-model prices are the cheapest provider rate via OpenRouter; official maker rates may be higher.":"Jede Preisänderung eines Anbieters auf OpenRouter landet hier. Grün = günstiger. Open-Modelle liefern sich einen ständigen Preiskampf; Frontier-Preise bewegen sich selten. Die Preise der Open-Modelle entsprechen dem günstigsten Anbieter über OpenRouter; die offiziellen Herstellerpreise können höher liegen.", 1439 "The top-ranked open-weight models side by side with the top paid ones. Prices are per 1M tokens, input / output.":"Die besten Open-Weight-Modelle neben den besten Bezahlmodellen. Preise pro 1M Tokens, Input / Output.", 1440 "Our daily GitHub health check: recent activity, commit momentum, community size and issue upkeep. Archived or at-risk projects are flagged here first.":"Unser täglicher GitHub-Gesundheitscheck: aktuelle Aktivität, Commit-Dynamik, Community-GröÃe und Issue-Pflege. Archivierte oder gefährdete Projekte erscheinen hier zuerst.", 1441 "Blind human preference: people compare two outputs without knowing which model produced them, and vote. The only honest way to rank media models.":"Blinde menschliche Präferenz: Menschen vergleichen zwei Ergebnisse, ohne zu wissen, welches Modell sie erzeugt hat, und stimmen ab. Die einzig ehrliche Art, Media-Modelle zu bewerten.", 1442 "Brand-new repositories created on GitHub, and the research paper the community is upvoting right now on Hugging Face.":"Brandneue Repositories auf GitHub und das Forschungspaper, das die Community gerade auf Hugging Face hochvotet.", 1443 "The latest versions shipped by the open-source tools we track, with their release level.":"Die neuesten Versionen der Open-Source-Tools, die wir verfolgen, mit ihrem Release-Level." }, 1444 pt: { "more":"mais","Recent changes":"Mudanças recentes","All tracked prices":"Todos os preços monitorados","New HF Spaces":"Novos Spaces HF", 1445 "Market movers":"Maiores altas","Price action":"Movimentos de preços","Open vs paid":"Open vs pago","Health monitor":"Monitor de saúde","Media ELO leaders":"LÃderes ELO de mÃdia","Fresh today":"Novidades de hoje","Releases":"Releases", 1446 "Stars gained in the last 24 hours. A sudden spike usually means a launch, a viral post or a major release worth checking out.":"Estrelas ganhas nas últimas 24 horas. Um pico repentino geralmente indica um lançamento, um post viral ou uma release importante que vale conferir.", 1447 "Every time a provider changes a price on OpenRouter, it lands here. Green means it got cheaper. Open-weight models are in a constant price war; frontier prices rarely move. Open-model prices are the cheapest provider rate via OpenRouter; official maker rates may be higher.":"Cada mudança de preço de um provedor no OpenRouter ap
1447arece aqui. Verde = mais barato. Os modelos open vivem uma guerra de preços constante; os preços frontier raramente mudam. Os preços dos modelos open são a tarifa do provedor mais barato via OpenRouter; as tarifas oficiais dos criadores podem ser mais altas.", 1448 "The top-ranked open-weight models side by side with the top paid ones. Prices are per 1M tokens, input / output.":"Os melhores modelos open lado a lado com os melhores pagos. Preços por 1M de tokens, entrada / saÃda.", 1449 "Our daily GitHub health check: recent activity, commit momentum, community size and issue upkeep. Archived or at-risk projects are flagged here first.":"Nosso check-up diário de saúde no GitHub: atividade recente, ritmo de commits, tamanho da comunidade e gestão de issues. Projetos arquivados ou em risco aparecem aqui primeiro.", 1450 "Blind human preference: people compare two outputs without knowing which model produced them, and vote. The only honest way to rank media models.":"Preferência humana à s cegas: as pessoas comparam dois resultados sem saber qual modelo os produziu, e votam. A única forma honesta de classificar modelos de mÃdia.", 1451 "Brand-new repositories created on GitHub, and the research paper the community is upvoting right now on Hugging Face.":"Os repositórios recém-criados no GitHub e o paper de pesquisa que a comunidade está votando agora no Hugging Face.", 1452 "The latest versions shipped by the open-source tools we track, with their release level.":"As versões mais recentes lançadas pelas ferramentas open source que acompanhamos, com seu nÃvel de release." } 1453 }; 1454 for (var el in E){ if (!NAV_I18N[el]) NAV_I18N[el] = {}; for (var ek in E[el]) NAV_I18N[el][ek] = E[el][ek]; } 1455 })(); 1456 /* âââ V26 â i18n de la barre recherche/tri des vues agrandies âââ */ 1457 (function(){ 1458 var Q = { 1459 fr: { "No match.":"Aucun résultat.","Stars":"Ãtoiles","Price":"Prix","Score":"Score","ELO":"ELO","Date":"Date" }, 1460 it: { "No match.":"Nessun risultato.","Stars":"Stelle","Price":"Prezzo","Score":"Punteggio","ELO":"ELO","Date":"Data" }, 1461 es: { "No match.":"Sin resultados.","Stars":"Estrellas","Price":"Precio","Score":"Puntuación","ELO":"ELO","Date":"Fecha" }, 1462 de: { "No match.":"Kein Treffer.","Stars":"Sterne","Price":"Preis","Score":"Score","ELO":"ELO","Date":"Datum" }, 1463 pt: { "No match.":"Nenhum resultado.","Stars":"Estrelas","Price":"Preço","Score":"Pontuação","ELO":"ELO","Date":"Data" } 1464 }; 1465 for (var ql in Q){ if (!NAV_I18N[ql]) NAV_I18N[ql] = {}; for (var qk in Q[ql]) NAV_I18N[ql][qk] = Q[ql][qk]; } 1466 })(); 1467 // Maillage : pages réellement traduites (mêmes que le générateur). Les autres restent EN â pas de 404. 1468 var NAV_LMAP = { 1469 '/':'/', '/index.html':'/', 1470 '/tools.html':'/tools/', '/tools/':'/tools/', 1471 '/blog.html':'/blog/', '/blog/':'/blog/', 1472 '/prompts.html':'/prompts.html', 1473 '/spaces.html':'/spaces/', '/spaces/':'/spaces/', 1474 '/compare.html':'/compare/', '/compare/':'/compare/', 1475 '/category.html':'/category.html', 1476 '/compare-tools.html':'/compare-tools.html', 1477 '/news.html':'/news.html', 1478 '/prompts.html':'/prompts.html', 1479 '/latest.html':'/latest.html', 1480 '/releases.html':'/releases.html', 1481 '/emerging.html':'/emerging.html', 1482 '/plans.html':'/plans.html', 1483 '/my-dashboard.html':'/my-dashboard.html', 1484 '/vertical/':'/vertical/', 1485 '/vertical/healthcare.html':'/vertical/healthcare.html', 1486 '/vertical/finance.html':'/vertical/finance.html', 1487 '/vertical/legal.html':'/vertical/legal.html', 1488 '/vertical/security.html':'/vertical/security.html', 1489 '/vertical/robotics.html':'/vertical/robotics.html', 1490 '/vertical/education.html':'/vertical/education.html', 1491 '/vertical/science.html':'/vertical/science.html', 1492 '/dashboard.html':'/dashboard.html', 1493 '/pricing.html':'/pricing.html', 1494 '/paid-models.html':'/paid-models.html', 1495 '/submit.html':'/submit.html', 1496 '/badge.html':'/badge.html', 1497 '/about.html':'/about.html', 1498 '/methodology.html':'/methodology.html', 1499 '/favorites.html':'/favorites.html', 1500 '/stack-builder.html':'/stack-builder.html', 1501 '/api.html':'/api.html', 1502 '/connect.html':'/connect.html', 1503 '/docs.html':'/docs.html', 1504 '/trends.html':'/trends.html', 1505 '/advertise.html':'/advertise.html', 1506 '/privacy-policy/':'/privacy-policy/', 1507 '/terms/':'/terms/', 1508 '/agents/':'/agents/', 1509 '/leaderboard.html':'/leaderboard.html', 1510 '/health.html':'/health.html', 1511 '/dna.html':'/dna.html', 1512 '/languages.html':'/languages.html', 1513 '/countries.html':'/countries.html', 1514 '/contact.html':'/contact.html', 1515 '/projects.html':'/projects/', '/projects/':'/projects/', 1516 '/models-vs/':'/models-vs/' 1517 }; 1518 var NAV_LPREF = ['/alternatives/', '/tool/', '/vs/', '/project/', '/reports/', '/models-vs/']; 1519 1520 // Localise UN href interne vers /xx/ (sinon le renvoie inchangé). 1521 function localizeHref(href){ 1522 if (NAV_LANG === 'en' || !href || href.charAt(0) !== '/') return href; // externes / # / javascript: ignorés 1523 var hash = ''; 1524 var hi = href.indexOf('#'); if (hi >= 0){ hash = href.slice(hi); href = href.slice(0, hi); } 1525 if (/^\/(fr|it|es|de|pt)(\/|$)/.test(href)) return href + hash; // déjà préfixé 1526 if (NAV_LMAP.hasOwnProperty(href)) return LP + NAV_LMAP[href] + hash; 1527 if (href.indexOf('/blog/') === 0) return LP + href + hash;
1527 // articles de blog traduits (y compris /blog/alternatives/) 1528 for (var i = 0; i < NAV_LPREF.length; i++){ if (href.indexOf(NAV_LPREF[i]) === 0) return LP + href + hash; } 1529 return href + hash; // non mappé -> reste EN (pas de 404) 1530 } 1531 1532 // Localise TOUS les liens internes d'un sous-arbre (menu + footer + contenu + cartes construites en JS). 1533 function localizeAllLinks(root){ 1534 if (NAV_LANG === 'en') return; 1535 var links = (root || document).querySelectorAll('a[href]'); 1536 for (var k = 0; k < links.length; k++){ 1537 var a = links[k], raw = a.getAttribute('href'), out = localizeHref(raw); 1538 if (out !== raw) a.setAttribute('href', out); 1539 } 1540 } 1541 1542 // âââ Localisation des liens AUTONOME (ne dépend PAS de l'injection du menu) âââ 1543 // Corrige tous les liens internes vers /xx/ sur les pages traduites : contenu statique 1544 // ET cartes construites en JS (en vedette, more tools, rechercheâ¦). Trois filets : 1545 // 1) au clic (avant navigation) â increvable, capte même les rendus tardifs 1546 // 2) passe initiale au chargement du DOM 1547 // 3) MutationObserver â capte les liens ajoutés après coup 1548 if (NAV_LANG !== 'en'){ 1549 document.addEventListener('click', function(e){ 1550 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 1551 /* V106 â Un bouton d'action posé DANS une carte-lien (cÅur, partage) n'est pas 1552 une navigation. Sans ce garde-fou, cet intercepteur (en phase CAPTURE, donc 1553 prioritaire sur le handler du bouton) forçait location.assign() et le clic 1554 sur le cÅur envoyait l'utilisateur sur la fiche. */ 1555 if (e.target && e.target.closest && e.target.closest('button, .fav-btn, .osai-share, [data-share]')) return; 1556 var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; 1557 if (!a) return; 1558 var tgt = a.getAttribute('target'); 1559 if (tgt && tgt !== '_self') return; // nouvel onglet / autre cible : laisser faire 1560 var raw = a.getAttribute('href'), loc = localizeHref(raw); 1561 if (loc && loc !== raw){ 1562 a.setAttribute('href', loc); // pour l'affichage au survol 1563 e.preventDefault(); 1564 if (e.stopImmediatePropagation) e.stopImmediatePropagation(); // couper tout handler qui irait en EN (ex. fiche-links.js) 1565 window.location.assign(loc); // on navigue nous-mêmes vers /xx/ 1566 } 1567 }, true); 1568 var _fixOne = function(n){ if (n && n.nodeType === 1 && n.tagName === 'A' && n.getAttribute('href')){ var o = localizeHref(n.getAttribute('href')); if (o !== n.getAttribute('href')) n.setAttribute('href', o); } }; 1569 var _runLinkLoc = function(){ 1570 localizeAllLinks(document); 1571 localizeTextTree(document.body); 1572 if (window.MutationObserver){ 1573 try { 1574 new MutationObserver(function(muts){ 1575 for (var i = 0; i < muts.length; i++){ 1576 var m = muts[i]; 1577 if (m.type === 'attributes'){ _fixOne(m.target); continue; } // href modifié après coup (ex. fiche-links.js) 1578 var ad = m.addedNodes; 1579 for (var j = 0; j < ad.length; j++){ 1580 var n = ad[j]; 1581 if (!n) continue; 1582 if (n.nodeType === 3){ localizeTextTree(n); continue; } 1583 if (n.nodeType !== 1) continue; 1584 _fixOne(n); 1585 if (n.querySelectorAll) localizeAllLinks(n); 1586 localizeTextTree(n); 1587 } 1588 } 1589 }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); 1590 } catch(e){} 1591 } 1592 }; 1593 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _runLinkLoc); else _runLinkLoc(); 1594 } 1595 1596 1597 // Remplit le sélecteur de langues (desktop #langMenu + mobile #osaiMMLang) 1598 // à partir des hreflang de la page (URL exactes). Repli : accueil de chaque langue. 1599 function buildLangSelector(){ 1600 var NAMES = { en:'English', fr:'Français', de:'Deutsch', es:'Español', it:'Italiano', pt:'Português' }; 1601 var SHORT = { en:'EN', fr:'FR', de:'DE', es:'ES', it:'IT', pt:'PT' }; 1602 var order = ['en','fr','de','es','it','pt']; 1603 var urls = {}; 1604 var links = document.querySelectorAll('link[rel="alternate"][hreflang]'); 1605 for (var i = 0; i < links.length; i++){ 1606 var lg = links[i].getAttribute('hreflang'); 1607 if (lg && lg !== 'x-default') urls[lg] = links[i].getAttribute('href'); 1608 } 1609 if (Object.keys(urls).length < 2){ 1610 /* Page sans hreflang (page compte pas encore régénérée, page PHPâ¦) :
1611 on reconstruit les URLs à partir du CHEMIN COURANT au lieu de renvoyer 1612 tout le monde à l'accueil â c'était la cause du « retour à l'accueil ». */ 1613 var bare = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/|$)/, '') || '/'; 1614 urls = {}; 1615 for (var k = 0; k < order.length; k++){ 1616 var l = order[k]; 1617 urls[l] = (l === 'en' ? '' : '/' + l) + bare + location.search; 1618 } 1619 } 1620 var cur = (NAV_LANG && urls[NAV_LANG]) ? NAV_LANG : 'en'; 1621 var lbl = document.getElementById('langCurrentLbl'); 1622 if (lbl) lbl.textContent = SHORT[cur] || cur.toUpperCase(); 1623 function items(){ 1624 var html = ''; 1625 for (var j = 0; j < order.length; j++){ 1626 var lg = order[j]; if (!urls[lg]) continue; 1627 var act = (lg === cur); 1628 var st = act ? ' style="color:var(--accent);font-weight:600;"' : ''; 1629 html += '<a href="' + urls[lg] + '" hreflang="' + lg + '"' + st + '>' + NAMES[lg] + (act ? ' ✓' : '') + '</a>'; 1630 } 1631 return html; 1632 } 1633 var dd = document.getElementById('langMenu'); if (dd) dd.innerHTML = items(); 1634 var mm = document.getElementById('osaiMMLang'); if (mm) mm.innerHTML = items(); 1635 /* mémoriser le choix explicite de langue (piloté par la persistance V132) */ 1636 [dd, mm].forEach(function(box){ 1637 if (!box) return; 1638 box.querySelectorAll('a[hreflang]').forEach(function(a){ 1639 a.addEventListener('click', function(){ 1640 try { 1641 var lg = a.getAttribute('hreflang'); 1642 localStorage.setItem('osai_lang', lg); 1643 if (lg === 'en') sessionStorage.setItem('osai_lang_stay', '1'); 1644 else sessionStorage.removeItem('osai_lang_stay'); 1645 } catch(e){} 1646 }); 1647 }); 1648 }); 1649 } 1650 1651 /* [01/08, releve en production] Sur la page d'accueil francaise, la plus 1652 grande barre du site affichait encore « Search tools, projects, models⦠». 1653 Le traducteur ne parcourt que les noeuds TEXTE : un placeholder est un 1654 ATTRIBUT, il lui echappe par construction. On les traduit donc a la main, 1655 une fois au demarrage, puis apres un delai pour les champs poses plus tard 1656 (la palette est construite a la volee). */ 1657 function localiserAttributs(){ 1658 if (NAV_LANG === 'en') return; 1659 var champs = document.querySelectorAll('input[placeholder], textarea[placeholder], [aria-label]'); 1660 for (var i = 0; i < champs.length; i++) { 1661 var e = champs[i]; 1662 ['placeholder', 'aria-label', 'title'].forEach(function(att){ 1663 var v = e.getAttribute(att); 1664 if (!v || e.getAttribute('data-i18n-' + att)) return; 1665 var tr = window.osaiT ? window.osaiT(v) : v; 1666 if (tr && tr !== v) { e.setAttribute(att, tr); e.setAttribute('data-i18n-' + att, '1'); } 1667 }); 1668 } 1669 } 1670 1671 function localizeNav(){ 1672 if (NAV_LANG !== 'en') { try { localStorage.setItem('osai_lang', NAV_LANG); } catch(e){} } 1673 if (NAV_LANG === 'en') return; // pages anglaises : aucune modif 1674 localizeAllLinks(document); // â localise les liens internes (menu + contenu) vers /xx/ 1675 var dict = NAV_I18N[NAV_LANG]; 1676 if (!dict) return; 1677 var ARROWS = /[\s\u25be\u25b8\u25b6\u25bc\u2192\u2190\u00bb\u203a\u2261\u2715\u00d7]+$/; 1678 localizeTextTree(document.body); 1679 localiserAttributs(); 1680 setTimeout(localiserAttributs, 1200); /* champs construits a la volee */ 1681 document.addEventListener('focusin', function(){ localiserAttributs(); }); 1682 document.documentElement.setAttribute('lang', NAV_LANG); 1683 } 1684 1685 // Traduction d'une chaîne unique â pour les ATTRIBUTS (placeholder, title, aria-label), 1686 // que le MutationObserver ne peut pas traiter. Repli : la chaîne d'origine. 1687 try { 1688 window.osaiT = function(s){ 1689 var d = NAV_I18N[NAV_LANG]; 1690 return (d && Object.prototype.hasOwnProperty.call(d, s)) ? d[s] : s; 1691 }; 1692 } catch(e){} 1693 1694 // Traduit les nÅuds texte d'un sous-arbre (correspondance EXACTE avec le dictionnaire â sûr pour le contenu). 1695 // Gère un préfixe emoji/symbole et un suffixe flèche. Accepte un nÅud texte ou un élément. 1696 function localizeTextTree(root){ 1697 if (NAV_LANG === 'en' || !root) return; 1698 var dict = NAV_I18N[NAV_LANG]; 1699 if (!dict || typeof document.createTreeWalker !== 'function') return; 1700 var ARROWS = /[\s\u25be\u25b8\u25b6\u25bc\u2192\u2190\u00bb\u203a\u2261\u2715\u00d7]+$/; 1701 (function walkAll(rt){ 1702 if (!rt) return; 1703 var walker = document.createTreeWalker(rt, NodeFilter.SHOW_TEXT, null, false); 1704 var nodes = [], n; 1705 while ((n = walker.nextNode())) nodes.push(n);
1706 nodes.forEach(function(node){ 1707 var raw = node.nodeValue; 1708 if (!raw) return; 1709 var trimmed = raw.trim(); 1710 if (!trimmed) return; 1711 var suffix = '', core = trimmed, m = trimmed.match(ARROWS); 1712 if (m) { suffix = m[0].trim(); core = trimmed.slice(0, trimmed.length - m[0].length).trim(); } 1713 var prefix = '', pm = core.match(/^([^\p{L}\p{N}]+?)\s*([\p{L}\p{N}][\s\S]*)$/u); 1714 if (pm) { prefix = pm[1]; core = pm[2]; } 1715 if (Object.prototype.hasOwnProperty.call(dict, core)) { 1716 var lead = (raw.match(/^\s*/) || [''])[0]; 1717 var tail = (raw.match(/\s*$/) || [''])[0]; 1718 node.nodeValue = lead + (prefix ? prefix + ' ' : '') + dict[core] + (suffix ? ' ' + suffix : '') + tail; 1719 } 1720 }); 1721 })(root.nodeType === 3 ? root.parentNode : root); 1722 } 1723 1724 // âââ CSS des boutons (cmdk + favoris) injecté pour toutes les pages âââ 1725 var navCSS = `:root{ --logo-green:var(--accent); } 1726body.dark{ --logo-green:var(--accent) !important; } 1727@media(min-width:901px){ #site-nav .nav-links{ gap:.8rem !important; } #site-nav .nav-links>a, #site-nav .nav-dropdown-btn{ font-size:.67rem !important; letter-spacing:.04em !important; } #site-nav .lang-dd .nav-dropdown-btn{ padding:0 !important; } } 1728:root{--logo-green:#6366f1;} body.dark{--logo-green:#818cf8;} #site-nav .theme-toggle{ display:inline-block !important; visibility:visible !important; opacity:1 !important; } 1729.theme-toggle{ background:var(--surface2); border:1px solid var(--border2); border-radius:100px; width:56px; height:30px; cursor:pointer; position:relative; transition:background .3s,border-color .3s; flex-shrink:0; overflow:hidden; } 1730.theme-toggle::before{ content:'ð'; position:absolute; right:5px; top:50%; transform:translateY(-50%); font-size:13px; line-height:1; transition:opacity .25s; opacity:1; } 1731.theme-toggle::after{ content:'âï¸'; position:absolute; left:5px; top:50%; transform:translateY(-50%); font-size:13px; line-height:1; transition:opacity .25s; opacity:0; } 1732body.dark .theme-toggle::before{ opacity:0; } 1733body.dark .theme-toggle::after{ opacity:1; } 1734.theme-toggle-knob{ position:absolute; left:3px; top:3px; width:24px; height:24px; background:var(--accent); border-radius:50%; transition:transform .3s cubic-bezier(.4,0,.2,1),background .3s; z-index:1; } 1735body.dark .theme-toggle-knob{ transform:translateX(26px); } 1736.theme-toggle-nav{ display:none; background:none; border:none; cursor:pointer; padding:8px; color:var(--txt); align-items:center; justify-content:center; } 1737.theme-toggle-nav svg{ width:22px; height:22px; } 1738.theme-toggle-nav .tt-moon{ display:none; } 1739.theme-toggle-nav .tt-sun{ display:block; color:var(--accent); } 1740body.dark .theme-toggle-nav .tt-sun{ display:none; } 1741body.dark .theme-toggle-nav .tt-moon{ display:block; color:var(--accent); } 1742@media(max-width:900px){ .theme-toggle-nav{ display:inline-flex; margin-left:auto; margin-right:4px; } } 1743.cmdk-trigger{ display:inline-flex; align-items:center; gap:7px; background:var(--surface2); border:2px solid var(--border2); border-radius:9px; padding:6px 10px; cursor:pointer; color:var(--muted); font-family:inherit; transition:border-color .2s, color .2s; } 1744.cmdk-trigger:hover{ border-color:var(--accent); color:var(--txt2); } 1745.cmdk-trigger svg{ width:15px; height:15px; } 1746.cmdk-trigger span{ font-size:.74rem; } 1747.cmdk-trigger kbd{ font-family:'DM Mono',monospace; font-size:.66rem; background:var(--surface); border:1px solid var(--border2); border-radius:5px; padding:1px 5px; color:var(--muted); } 1748body.dark .cmdk-trigger{ background:#1d2533 !important; border-color:rgba(255,255,255,.14) !important; } 1749body:not(.dark) .fav-nav-btn{ background:#f5f5f7 !important; border:2px solid rgba(0,0,0,.10) !important; color:#86868b !important; } 1750body:not(.dark) .cmdk-trigger{ background:#f5f5f7 !important; border:2px solid rgba(0,0,0,.10) !important; color:#86868b !important; } 1751body:not(.dark) .cmdk-trigger span{ display:inline !important; } 1752body:not(.dark) .cmdk-trigger kbd{ display:inline !important; } 1753 1754 1755.fav-nav-btn{ position:relative; background:var(--surface2); border:2px solid var(--border2); border-radius:9px; width:38px; height:32px; cursor:pointer; color:var(--muted); display:inline-flex; align-items:center; justify-content:center; transition:border-color .2s,color .2s; } 1756.fav-nav-btn:hover{ border-color:#e0245e;
1756 color:#e0245e; } 1757.fav-nav-btn svg{ width:16px; height:16px; } 1758.acct-nav-btn{ position:relative; background:var(--surface2); border:2px solid var(--border2); border-radius:9px; width:38px; height:32px; cursor:pointer; color:var(--muted); display:inline-flex; align-items:center; justify-content:center; transition:border-color .2s,color .2s; text-decoration:none; } 1759.acct-nav-btn:hover{ border-color:var(--accent); color:var(--accent); } 1760.acct-nav-btn svg{ width:16px; height:16px; } 1761.acct-nav-btn.is-in{ color:var(--accent); border-color:var(--accent); } 1762.acct-nav-btn::after{ content:''; position:absolute; top:-3px; right:-3px; width:8px; height:8px; border-radius:50%; background:#ef4444; border:2px solid var(--bg); } 1763.acct-nav-btn.is-in::after{ background:#10b981; } 1764.acct-menu{ position:absolute; top:calc(100% + 10px); right:0; min-width:212px; background:var(--surface); border:1px solid var(--border2); border-radius:12px; box-shadow:0 14px 40px rgba(0,0,0,.22); overflow:hidden; z-index:400; display:none; } 1765.acct-menu.open{ display:block; } 1766.acct-menu .who{ padding:11px 14px; border-bottom:1px solid var(--border2); font-size:.74rem; color:var(--muted); font-family:'DM Mono',monospace; word-break:break-all; } 1767.acct-menu a,.acct-menu button{ display:flex; align-items:center; gap:9px; width:100%; padding:10px 14px; background:none; border:0; cursor:pointer; font-family:'Poppins',sans-serif; color:var(--txt2); text-decoration:none; text-align:left; 1768 /* le menu vit dans .nav-links, qui impose uppercase + letter-spacing aux <a> : on annule */ 1769 text-transform:none !important; letter-spacing:0 !important; font-size:.85rem !important; font-weight:500 !important; } 1770.acct-menu a:hover,.acct-menu button:hover{ background:var(--card-hover); color:var(--accent); } 1771.acct-menu svg{ width:15px; height:15px; flex:none; } 1772.acct-menu .out{ border-top:1px solid var(--border2); color:#dc2626; } 1773.acct-menu .out:hover{ color:#dc2626; background:rgba(220,38,38,.08); } 1774.acct-wrap{ position:relative; display:inline-flex; } 1775body:not(.dark) .acct-nav-btn{ background:#f5f5f7 !important; border:2px solid rgba(0,0,0,.10) !important; } 1776body.dark .acct-nav-btn{ background:#1d2533; border-color:rgba(255,255,255,.14); } 1777body.dark .fav-nav-btn{ background:#1d2533; border-color:rgba(255,255,255,.14); } 1778.fav-count-badge{ position:absolute; top:-6px; right:-6px; background:#e0245e; color:#fff; font-size:.6rem; font-weight:600; min-width:16px; height:16px; border-radius:8px; display:none; align-items:center; justify-content:center; padding:0 4px; } 1779.fav-count-badge.show{ display:flex; } 1780 1781 1782/* Compenser la nav fixed : le contenu démarre sous la barre */ 1783body{ padding-top:64px; } 1784nav{ position:fixed; top:0; left:0; right:0; } 1785@media(max-width:768px){ body{ padding-top:64px; } } 1786.nav-dropdown-menu a{ white-space:nowrap; display:flex; align-items:center; gap:9px; } 1787.nav-dropdown-menu a svg{ width:15px; height:15px; flex-shrink:0; opacity:.6; } 1788.nav-dropdown-menu{ width:max-content; min-width:210px; max-width:min(360px,92vw); } 1789/* Ãléments masqués hors mobile */ 1790.nav-close{ display:none !important; } 1791.nav-backdrop{ display:none !important; } 1792/* Sur mobile, le menu inline est masqué : remplacé par l'overlay autonome #osaiMM ci-dessous. */ 1793/* âââ MOBILE : compte, favoris et recherche restent dans la barre du haut. 1794 .nav-links redevient visible, mais seuls ces 3 boutons y apparaissent â 1795 liens, méga-menus et sélecteur de langue restent dans le menu burger. */ 1796/* âââ MOBILE : dans la barre du haut, SEULE l'icône compte reste (à côté du 1797 burger). Favoris et recherche montent en tête du menu ouvert. âââ */ 1798@media(max-width:900px){ 1799 #site-nav nav{ padding:0 .9rem; gap:6px; } 1800 .nav-links{ display:flex !important; align-items:center; gap:6px; margin-left:auto; } 1801 .nav-links > *:not(.acct-wrap){ display:none !important; } 1802 #site-nav .nav-links .theme-toggle{ display:none !important; } /* doublon forcé par un !important */ 1803 .theme-toggle-nav{ margin-left:0 !important; margin-right:0 !important; } 1804 #site-nav nav > a{ flex:none; min-width:0; } 1805} 1806@media(max-width:400px){ 1807 #site-nav .logo small{ display:none; } 1808 #site-nav .logo{ font-size:1rem; } 1809 #site-nav nav{ padding:0 .7rem; } 1810} 1811/* âââ MENU MOBILE AUTONOME : overlay enfant direct de <body>, z-index maximal. 1812 Insensible au CSS des pages ET aux contextes d'empilement (nav fixed, etc.). âââ */ 1813#osaiMM{ position:fixed; inset:0; z-index:2147483600; display:none; } 1814#osaiMM.osaiMM-open{ display:block; } 1815#osaiMM .osaiMM-bd{ position:absolute; inset:0; background:rgba(0,0,0,.5); opacity:0; transition:opacity .25s ease; } 1816#osaiMM.osaiMM-open .osaiMM-bd{ opacity:1; } 1817#osaiMM .osaiMM-panel{ position:absolute; top:0; right:0; height:100%; width:86%; max-width:340px; background:var(--surface,#ffffff); box-shadow:-6px 0 34px rgba(0,0,0,.3); transform:translateX(100%); transition:transform .28s ease; padding:4.2rem 1.3rem 2rem; display:flex; flex-direction:column; gap:0; overflow-y:auto; -webkit-overflow-scrolling:touch; box-sizing:border-box; } 1818#osaiMM.osaiMM-open .osaiMM-panel{ transform:translateX(0); } 1819#osaiMM .osaiMM-close{ position:absolute; top:.5rem; right:1rem; width:44px; height:44px; font-size:2rem; line-height:1; background:none; border:none;
1819 color:var(--txt,#1d1d1f); cursor:pointer; } 1820#osaiMM a, #osaiMM .osaiMM-act{ display:flex; align-items:center; gap:.6rem; width:100%; text-align:left; padding:.92rem .3rem; color:var(--txt2,#424245); text-decoration:none; font-family:'Poppins',sans-serif; font-size:1rem; font-weight:400; background:none; border:none; border-bottom:1px solid var(--border,rgba(0,0,0,.08)); cursor:pointer; } 1821#osaiMM a:hover, #osaiMM a:active, #osaiMM .osaiMM-act:hover, #osaiMM .osaiMM-act:active{ color:var(--accent); } 1822#osaiMM a svg, #osaiMM .osaiMM-act svg{ width:18px; height:18px; flex-shrink:0; opacity:.6; } 1823#osaiMM .osaiMM-quick{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin:0 0 1rem; } 1824#osaiMM .osaiMM-quick .osaiMM-q{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; padding:.85rem .3rem; border:1px solid var(--border2,rgba(0,0,0,.12)); border-radius:12px; background:var(--surface2,rgba(0,0,0,.03)); color:var(--txt2,#424245); text-decoration:none; font-size:.74rem; font-weight:600; cursor:pointer; font-family:'Poppins',sans-serif; position:relative; } 1825#osaiMM .osaiMM-quick .osaiMM-q:hover,#osaiMM .osaiMM-quick .osaiMM-q:active{ border-color:var(--accent); color:var(--accent); } 1826#osaiMM .osaiMM-quick .osaiMM-q svg{ width:20px; height:20px; opacity:1; } 1827#osaiMM .osaiMM-sec{ font-size:.66rem; text-transform:uppercase; letter-spacing:.1em; color:var(--muted,#86868b); margin:1.1rem .3rem .25rem; font-family:'Poppins',sans-serif; } 1828/* âââ CSS du footer partagé âââ */ 1829#site-footer footer{ border-top:1px solid var(--border); padding:3rem 0; background:var(--surface2); margin-top:3rem; } 1830body:not(.dark) #site-footer footer{ background:#f7f9fd !important; border-top-color:rgba(0,0,0,0.08) !important; } 1831body.dark #site-footer footer{ background:#1a2540 !important; border-top-color:rgba(255,255,255,0.10) !important; } 1832/* [09/08] Le bloc etait un enfant de la grille du pied de page : il heritait 1833 donc d'UNE COLONNE de 167 px, et tout badge y etait ecrase par le 1834 max-width:100% de la regle image. Il occupe desormais toute la largeur, 1835 sur sa propre ligne, aligne a gauche comme le logo et la mention legale. */ 1836#site-footer .footer-badges{grid-column:1/-1;display:flex;align-items:center;gap:14px;flex-wrap:wrap;max-width:1200px;margin:8px 0 20px;padding:0} 1837#site-footer .footer-badges img{max-width:100%;height:auto;opacity:.9;transition:opacity .15s;border-radius:6px} 1838body.dark #site-footer .footer-badges img{background:#fff;padding:2px} 1839#site-footer .footer-badges a:hover img{opacity:1} 1840#site-footer .loop-strip{display:flex;align-items:center;gap:18px;flex-wrap:wrap;max-width:1200px;margin:0 auto 2rem;padding:14px 18px;background:var(--surface);border:1px solid var(--border);border-radius:14px} 1841#site-footer .loop-strip .loop-t{font-family:'DM Mono',monospace;font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)} 1842#site-footer .loop-strip a{font-size:.82rem;font-weight:600;color:var(--txt);text-decoration:none} 1843#site-footer .loop-strip a:hover{color:var(--accent)} 1844/* [24/07] Mobile : le label prend sa ligne, les 3 liens se repartissent sur toute 1845 la largeur (fini le vide a droite + disposition plus nette). */ 1846@media(max-width:600px){.loop-strip{justify-content:space-between;gap:10px 8px;padding:12px 14px}.loop-strip .loop-t{flex-basis:100%}} 1847/* [09/08/2026, seconde passe] LE VRAI CORRECTIF. 1848 Le pied de page est injecte dans un <div class="wrap"> et heritait donc 1849 du .wrap de la page HOTE. Or ce .wrap n'a pas la meme valeur partout : 1850 · articles de blog (ai-top-turnoverâ¦) : AUCUNE regle .wrap -> pleine 1851 largeur, le footer partait a gauche, logo coupe ; 1852 · pages alternatives (chatgpt.html) : .wrap = 920px, largeur de 1853 LECTURE d'article -> le footer s'y comprimait, colonnes empilees. 1854 Ma premiere passe ne centrait que .footer-inner : elle reparait le premier 1855 cas et aggravait le second. Un composant injecte partout ne doit dependre 1856 d'AUCUNE classe generique de la page. On fixe donc la largeur du wrap du 1857 footer sur celle de l'accueil (1280 / 32px), mesuree et non devinee. 1858 #site-footer .wrap l'emporte sur tout .wrap de page (id > classe). */ 1859#site-footer .wrap{ max-width:1280px; margin-inline:auto; padding-inline:32px; box-sizing:border-box; } 1860/* [09/08/2026] margin-inline:auto AJOUTE. La regle plafonnait la largeur 1861 a 1200px mais comptait sur le .wrap de la page hote pour centrer. Or les 1862 articles de blog ne definissent AUCUNE regle .wrap : le pied de page y 1863 partait donc a gauche, logo et texte coupes hors de l'ecran. Le footer est 1864 injecte partout, il ne doit dependre d'aucune classe de la page d'accueil. */ 1865#site-footer .footer-inner{ display:grid; grid-template-columns:1.5fr 1fr 1fr; gap:2.5rem; max-width:1200px; margin-inline:auto; } 1866#site-footer .footer-brand .logo{ font-size:1rem; font-weight:600; } 1867#site-footer .footer-brand .logo span{ color:var(--accent); } 1868#site-footer .footer-brand p{ font-size:.82rem; color:var(--muted); line-height:1.6; } 1869#site-footer .footer-col h4{ font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); margin-bottom:1rem; font-weight:600; } 1870#site-footer .footer-col a{ display:block; font-size:.85rem; color:var(--txt2); text-decoration:none; margin-bottom:.6rem; transition:color .2s; } 1871#site-footer .footer-col a:hover{ color:var(--accent); } 1872#site-footer .footer-more{ column-count:2; column-gap:22px; } 1873#site-footer .footer-more h4{ column-span:all; } 1874#site-footer .footer-more a{ break-inside:avoid; } 1875@media(max-width:560px){ #site-footer .footer-more{ column-count:1; } } 1876#site-footer .footer-social{ display:flex; gap:10px; margin-top:14px; } 1877#site-footer .footer-social a{ width:34px; height:34px; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--border); border-radius:9px; color:var(--muted); margin:0; transition:color .2s,border-color .2s; } 1878#site-footer .footer-social a:hover{ color:var(--accent); border-color:var(--accent); } 1879#site-footer .footer-social svg{ width:16px; height:16px; } 1880#site-footer .footer-copy{ grid-column:1/-1; margin-top:1.5rem; padding-top:1.5rem; border-top:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem; font-size:.78rem; color:var(--muted); } 1881#site-footer .footer-news{ grid-column:1/-1; display:flex; align-items:center; justify-content:space-between; gap:1.2rem 2rem; flex-wrap:wrap; padding-bottom:2rem; margin-bottom:.3rem; border-bottom:1px solid var(--border); } 1882#site-footer .footer-news-txt h4{ font-size:1rem; font-weight:600; color:var(--txt); margin-bottom:.3rem; } 1883#site-footer .footer-news-txt p{ font-size:.82rem; color:var(--muted); line-height:1.5; max-width:430px; } 1884#site-footer .footer-news-form{ display:flex; gap:.5rem; flex-wrap:wrap; } 1885#site-footer .footer-news-form input{ font-family:'Poppins',sans-serif; font-size:.85rem; padding:.7rem 1rem; border:1px solid var(--border2); border-radius:10px; background:var(--surface); color:var(--txt); min-width:240px; outline:none; transition:border-color .2s; } 1886#site-footer .footer-news-form input:focus{ border-color:var(--accent); } 1887#site-footer .footer-news-form button{ font-family:'Poppins',sans-serif; font-size:.85rem; font-weight:600; padding:.7rem 1.4rem; border:none; border-radius:10px; background:#6366f1; color:#fff; cursor:pointer; transition:opacity .2s; white-space:nowrap; } 1888#site-footer .footer-news-form button:hover{ opacity:.9; } 1889#site-footer .footer-news-form button:disabled{ opacity:.6; cursor:default; } 1890#site-footer .footer-news-msg{ flex-basis:100%; font-size:.8rem; min-height:1px; } 1891body.dark #site-footer .footer-news-form button{ color:#fff; } 1892@media(max-width:768px){ #site-footer .footer-inner{ grid-template-columns:1fr; gap:1.5rem; } #site-footer .footer-copy{ flex-direction:column; align-items:flex-start; } #site-footer .footer-news{ flex-direction:column; align-items:flex-start; } #site-footer .footer-news-form{ width:100%; } #site-footer .footer-news-form input{ min-width:0; flex:1; } } 1893 1894/* âââ MEGA MENU âââ */ 1895.nav-dropdown-menu.mega{ position:fixed !important; top:64px !important; left:0 !important; right:0 !important; transform:none !important; 1896 width:100% !important; max-width:none !important; min-width:0 !important; box-sizing:border-box !important; 1897 padding:28px max(32px,calc((100vw - 1320px)/2)) 32px !important; 1898 border-radius:0 0 18px 18px !important; border-top:1px solid var(--border) !important; 1899 max-height:calc(100vh - 88px) !important; overflow-y:auto !important; overscroll-behavior:contain; 1900 scrollbar-width:thin; scrollbar-color:var(--accent) transparent; } 1901.nav-dropdown-menu.mega::-webkit-scrollbar{ width:7px; } 1902.nav-dropdown-menu.mega::-webkit-scrollbar-thumb{ background:var(--accent); border-radius:4px; opacity:.5; } 1903.mega-grid{ display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:28px 26px; } 1904@media(max-width:1120px){ .mega-grid{ grid-template-columns:repeat(3,minmax(0,1fr)); } } 1905.mega-col{ display:flex; flex-direction:column; gap:3px; } 1906.mega-h{ font-family:'DM Mono',monospace; font-size:.82rem; font-weight:700; text-transform:uppercase; letter-spacing:.09em; color:var(--accent); padding:0 12px 9px; } 1907.nav-dropdown-menu.mega a{ display:flex !important; align-items:center; gap:12px; padding:11px 12px !important; border-radius:10px;
1907 white-space:normal !important; color:var(--txt2); } 1908.nav-dropdown-menu.mega a:hover{ background:var(--card-hover); } 1909.nav-dropdown-menu.mega a svg{ width:19px !important; height:19px !important; flex-shrink:0; opacity:1 !important; color:var(--accent); } 1910.nav-dropdown-menu.mega a > span{ display:flex; flex-direction:column; min-width:0; flex:1 1 auto; overflow:hidden; } 1911.nav-dropdown-menu.mega a b{ font-size:.86rem; color:var(--txt); font-weight:400; line-height:1.3; } 1912.nav-dropdown-menu.mega a em{ display:none !important; } 1913.osai-em-home{ padding:48px 0 4px; } 1914.osai-em-home .oeh-wrap{ max-width:1220px; margin:0 auto; padding:0 20px; } 1915.oeh-panel{ background:var(--surface,#fff); border:2px solid var(--border,rgba(0,0,0,.08)); border-radius:16px; padding:18px 20px 16px; } 1916.oeh-head{ display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-bottom:13px; } 1917/* [02/08/2026] Devenu un h2 : on annule les marges par defaut du navigateur 1918 et on aligne l'icone. .sec-ic n'est style que par index.html â le nav 1919 doit donc porter lui-meme la taille et la couleur, sinon l'icone serait 1920 nue sur toute page qui reprendrait ce bloc. */ 1921.oeh-title{ display:flex; align-items:center; gap:9px; margin:0; 1922 font-family:'Poppins',sans-serif; font-size:1.05rem; font-weight:700; color:var(--txt,#0f1724); } 1923.oeh-title .sec-ic{ display:inline-flex; flex:none; } 1924.oeh-title .sec-ic svg{ width:19px; height:19px; color:var(--accent,#6366f1); } 1925body.dark .oeh-title .sec-ic svg{ color:var(--accent,#818cf8); } 1926.oeh-title > span{ color:var(--accent,#6366f1); } 1927.oeh-sub{ font-family:'DM Mono',monospace; font-size:.66rem; color:var(--muted,#5a6d87); } 1928.oeh-all{ margin-left:auto; font-family:'DM Mono',monospace; font-size:.74rem; font-weight:500; color:var(--accent,#6366f1); text-decoration:none; white-space:nowrap; border:2px solid var(--border2,rgba(0,0,0,.15)); border-radius:8px; padding:6px 12px; transition:border-color .15s; } 1929.oeh-all:hover{ border-color:var(--accent,#6366f1); } 1930.oeh-cta{ text-align:center; margin-top:16px; } 1931.oeh-cta a{ display:inline-flex; align-items:center; gap:8px; padding:12px 30px; 1932 background:#10b981; color:#fff; border-radius:980px; font-size:.82rem; 1933 font-weight:500; text-decoration:none; transition:background .2s,transform .15s; } 1934.oeh-cta a:hover{ background:#059669; transform:translateY(-1px); color:#fff; } 1935@media(max-width:640px){ .oeh-cta a{ width:100%; max-width:340px; justify-content:center; padding:14px 22px; } } 1936.oeh-grid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; } 1937a.oeh-card{ min-width:0; display:flex; align-items:flex-start; gap:12px; background:var(--surface2,#f7f8fa); border:2px solid var(--border,rgba(0,0,0,.08)); border-radius:12px; padding:14px 15px; text-decoration:none; transition:border-color .18s, transform .18s; } 1938a.oeh-card:hover{ border-color:var(--accent,#6366f1); transform:translateY(-2px); } 1939.oeh-av{ width:40px; height:40px; flex:none; border-radius:10px; overflow:hidden; position:relative; display:flex; align-items:center; justify-content:center; font-weight:700; color:#fff; font-size:1rem; font-family:'Poppins',sans-serif; } 1940.oeh-av img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; } 1941.oeh-body{ min-width:0; display:flex; flex-direction:column; gap:3px; } 1942.oeh-body b{ color:var(--txt,#0f1724); font-weight:700; font-size:.98rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } 1943a.oeh-card:hover .oeh-body b{ color:var(--accent,#6366f1); } 1944.oeh-line{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; } 1945.oeh-mom{ font-family:'DM Mono',monospace; font-size:.84rem; font-weight:500; color:var(--accent,#6366f1); } 1946.oeh-mom i{ font-style:normal; font-size:.6rem; color:var(--muted,#5a6d87); margin-left:2px; } 1947.oeh-fire{ font-family:'DM Mono',monospace; font-size:.62rem; color:#f24f00; background:rgba(242,79,0,.08); border:1px solid rgba(242,79,0,.3); border-radius:4px; padding:1px 7px; } 1948.oeh-desc{ font-size:.74rem; color:var(--muted,#5a6d87); line-height:1.4; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; } 1949.osai-em-home{ overflow-x:clip; } 1950@media(max-width:760px){ .oeh-grid{ grid-template-columns:1fr; } .oeh-sub{ display:none; } .oeh-panel{ padding:15px 14px 13px; } } 1951.mega-badge{ font-size:.54rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--bg); background:var(--accent); border-radius:5px; padding:2px 6px; align-self:center; flex-shrink:0; } 1952/* V108 â le badge ne pousse plus le libellé : absolu haut-droit, et le texte 1953 garde sa place (V16 met overflow:visible sur les libellés â un mot seul, 1954 insécable, débordait SOUS le badge : « Emerging », « Developer API »). */ 1955/* V113 â petits écrans en hauteur (portables 13", écrans externes 768p) : 1956 le méga-menu dépassait la fenêtre et coupait le bas de la colonne Discover 1957 sans aucun scroll. Plafond à la hauteur visible + défilement interne. */ 1958.nav-dropdown-menu.mega{ max-height:calc(100vh - 84px); overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin; scrollbar-color:var(--border2,rgba(0,0,0,.2)) transparent; } 1959.nav-dropdown-menu.mega::-webkit-scrollbar{ width:8px; } 1960.nav-dropdown-menu.mega::-webkit-scrollbar-thumb{ background:var(--border2,rgba(0,0,0,.2)); border-radius:8px; } 1961.nav-dropdown-menu.mega a{ position:relative; } 1962.nav-dropdown-menu.mega .mega-badge{ position:static; align-self:center; margin-left:auto; font-size:.52rem !important; line-height:1.4 !important; padding:2px 5px !important; flex-shrink:0; } 1963.nav-dropdown-menu.mega a:has(.mega-badge) span:not(.mega-badge){ padding-right:40px; } 1964.mega-foot{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:14px; padding:12px 14px; background:var(--surface2); border:1px solid var(--border2); border-radius:11px; flex-wrap:wrap; } 1965/* âââ V16 â Méga-menu lisible & moderne âââ */ 1966.nav-dropdown-menu.mega{ box-shadow:0 24px 70px rgba(0,0,0,.45) !important; backdrop-filter:blur(14px); border:1px solid var(--border2) !important; border-top:1px solid var(--border) !important; } 1967.mega-grid{ gap:22px 30px; } 1968.nav-dropdown-menu.mega a{ padding:8px 10px !important; gap:12px; border-radius:11px; transition:background .15s ease, transform .15s ease; } 1969.nav-dropdown-menu.mega a, .nav-dropdown-menu.mega a span, .nav-dropdown-menu.mega a b{ text-transform:none !important; letter-spacing:0 !important; font-size:.86rem !important; font-weight:400 !important;
1969 white-space:normal !important; overflow:visible !important; text-overflow:clip !important; line-height:1.25 !important; } 1970.nav-dropdown-menu.mega a em{ text-transform:none !important; letter-spacing:0 !important; } 1971.nav-dropdown-menu.mega a svg{ width:32px !important; height:32px !important; padding:7px; background:var(--card-hover); border-radius:9px; box-sizing:border-box; } 1972.nav-dropdown-menu.mega a:hover{ transform:translateX(3px); } 1973.nav-dropdown-menu.mega a:hover b{ color:var(--accent); } 1974.mega-h{ font-size:.8rem; font-weight:700; letter-spacing:.09em; color:var(--accent); opacity:1; padding:0 10px 9px; border-bottom:1px solid var(--border); margin-bottom:5px; } 1975.nav-dropdown-menu.open{ animation:osaiMegaIn .16s ease; } 1976@keyframes osaiMegaIn{ from{ opacity:0; transform:translateY(-8px); } to{ opacity:1; transform:translateY(0); } } 1977/* âââ V16 â Finitions globales âââ */ 1978/* boutons menthe : texte sombre lisible en dark (au lieu de blanc sur menthe) */ 1979body.dark #showMoreBtn, body.dark #projShowMoreBtn, body.dark #loadMoreLink, body.dark #moretoolsShowMore, body.dark #projectsShowMore, 1980body.dark #showMoreBtn:hover, body.dark #projShowMoreBtn:hover, body.dark #loadMoreLink:hover, body.dark #moretoolsShowMore:hover, body.dark #projectsShowMore:hover{ color:#ffffff !important; font-weight:700 !important; } 1981/* champ de recherche de la page Tools : sombre en dark (fini le blanc) */ 1982body.dark .tools-search{ background:var(--surface2) !important; color:var(--txt) !important; border:1px solid var(--border2) !important; } 1983body.dark .tools-search::placeholder{ color:var(--muted) !important; } 1984/* âââ V18 â CTA vert dur (#6366f1 inline) â menthe harmonisée en dark âââ */ 1985body.dark a[style*="background:#6366f1"], body.dark button[style*="background:#6366f1"]{ background:var(--accent) !important; color:#ffffff !important; } 1986/* âââ V18 â recherche de la section 1000+ tools (home) : sombre en dark âââ */ 1987body.dark #searchInput{ background:var(--surface2) !important; color:var(--txt) !important; border:1px solid var(--border2) !important; } 1988body.dark #searchInput::placeholder{ color:var(--muted) !important; } 1989/* âââ V21 â Harmonisation : plus AUCUN vert foncé (#6366f1) visible en dark âââ */ 1990:root{ --osai-green:#6366f1; } 1991/* âââ FILET INDIGO GLOBAL â recolore TOUTES les pages du site (jour+nuit), même celles non refondues âââ */ 1992:root, body:not(.dark){ --accent:#6366f1 !important; --accentbright:#818cf8 !important; --osai-green:#6366f1 !important; --logo-green:#6366f1 !important; --accent-2:#6366f1 !important; --accent-3:#6366f1 !important; --accent-2-soft:#eef0fe !important; --accent-3-soft:#eef0fe !important; --card-hover:rgba(99,102,241,.06) !important; --bg:#ffffff !important; --bg2:#ffffff !important; --surface2:#f7f8fa !important; --nav-bg:rgba(255,255,255,.85) !important; } 1993body.dark{ --accent:#818cf8 !important; --accentbright:#a5b0ff !important; --osai-green:#818cf8 !important; --logo-green:#818cf8 !important; --accent-2:#818cf8 !important; --accent-3:#818cf8 !important; --accent-2-soft:#1c2136 !important; --accent-3-soft:#1c2136 !important; --card-hover:rgba(129,140,248,.08) !important; --bg:#0b0f1a !important; --bg2:#0b0f1a !important; --surface:#141a29 !important; --surface2:#1a2133 !important; --nav-bg:rgba(11,15,26,.85) !important; } 1994/* âââ [02/08/2026, mockup] FOND GRIS DES FICHES âââ 1995 Le filet indigo ci-dessus force --bg:#ffffff !important sur tout le site : 1996 il ecrasait le gris #f7f8fc pose dans fiche.css. Meme arme, meme priorite, 1997 declare APRES lui donc gagnant : les pages qui portent la classe 1998 osai-fond-gris (posee par le JS selon le chemin) retrouvent le gris du 1999 mockup. Le mode nuit n'est pas concerne. */ 2000body.osai-fond-gris:not(.dark){ --bg:#f7f8fc !important; --bg2:#f7f8fc !important; } 2001 2002/* âââ LOGO + TOGGLE : indigo forcé sur TOUTES les pages (même vieux nav local) âââ */ 2003.logo span, .logo b, .logo em, .brand span, .navbar .logo span{ color:var(--accent) !important; }
2004.theme-toggle svg, .theme-toggle path, .darkmode svg, .toggle-theme svg, 2005[class*="theme"] svg path, [class*="dark-toggle"] svg{ fill:var(--accent) !important; stroke:var(--accent) !important; } 2006/* âââ BARRE DE NAV : couleur forcée (bat les nav sombres codées en dur des vieilles pages) âââ */ 2007#site-nav nav, nav{ background:var(--nav-bg) !important; border-bottom-color:var(--border) !important; } 2008/* La couleur reste globale : elle ne perturbe aucune mise en page. */ 2009#site-nav .nav-links a, nav .nav-links > a{ color:var(--txt2) !important; } 2010/* â La MISE EN PAGE, elle, est confinée au bureau. 2011 Sous 900 px, .nav-links > *:not(.acct-wrap){display:none !important} doit 2012 masquer les liens et laisser la place au hamburger. Or « #site-nav .nav-links a » 2013 pèse (1,2,1) contre (0,2,0) : mon display:inline-flex gagnait, les six liens 2014 débordaient de l'écran et poussaient le hamburger dehors. 2015 En n'existant pas sous 901 px, la règle ne peut plus interférer. 2016 Et « > a » plutôt que « a » : le sélecteur de descendance atteignait aussi 2017 les liens du sélecteur de langue, qu'il passait à l'horizontale. */ 2018@media(min-width:901px){ 2019 #site-nav .nav-links > a{ display:inline-flex !important; align-items:center; gap:6px; line-height:1; } 2020 #site-nav .nav-links > a svg{ width:14px; height:14px; flex-shrink:0; color:var(--accent); opacity:.85; } 2021 #site-nav .nav-links > a:hover svg{ opacity:1; } 2022} 2023/* Les icônes ajoutent ~20 px par lien : sous 1200 px la barre déborde. */ 2024@media(min-width:901px) and (max-width:1200px){ #site-nav .nav-links > a svg{ display:none !important; } } 2025/* Explore n'etait pas aligne : un <button> ne partage pas la ligne de base d'un <a>. 2026 On lui donne la meme boite flex et la meme hauteur de ligne. */ 2027@media(min-width:901px){ #site-nav .nav-links > .nav-dropdown{ display:inline-flex; align-items:center; } } 2028.nav-dropdown-btn{ display:inline-flex !important; align-items:center; gap:5px; line-height:1 !important; height:auto; } 2029#site-nav .nav-links a:hover, nav .nav-links a:hover{ color:var(--accent) !important; } 2030/* âââ BOUTONS DE CARTE (cÅur, partage) : zéro mouvement âââ */ 2031.fav-btn, .fav-btn:hover, .sf-btn, .sf-btn:hover, .home-share-wrap *:hover{ transform:none !important; transition:color .15s, opacity .15s !important; } 2032/* âââ POLICE UNIFIÃE : Poppins partout, DM Mono réservée au code (sélecteurs simples = compatibles tous navigateurs) âââ */ 2033body, body *{ font-family:'Poppins','Poppins-fb',sans-serif !important; } 2034pre, code, kbd, samp, pre *, code *{ font-family:'DM Mono',ui-monospace,SFMono-Regular,monospace !important; } 2035/* âââ #projects + proj-card : mouvement totalement figé âââ */ 2036#projectsGrid .project-card, #projectsGrid .project-card:hover, #projectsGrid .project-card *, #projectsGrid .project-card:hover *{ transform:none !important; transition:none !important; animation:none !important; } 2037#projectsGrid .project-card:hover{ background:var(--surface) !important; border-color:var(--border) !important; box-shadow:var(--shadow) !important; filter:none !important; } 2038#whatsnewGrid .wn-card, #whatsnewGrid .wn-card:hover{ transform:none !important; } 2039#whatsnewGrid > *, #projectsGrid > *{ height:100% !important; } 2040/* âââ CARTES TRENDING + TOP PROJECTS : ZÃRO effet au survol, sur toutes les pages âââ */ 2041.trend-card-wrap, .trend-card-wrap:hover, .trend-card, .trend-card:hover, 2042.project-card, .project-card:hover, .proj-card, .proj-card:hover, 2043body.dark .trend-card-wrap, body.dark .trend-card-wrap:hover, 2044body.dark .trend-card, body.dark .trend-card:hover, 2045body.dark .project-card, body.dark .project-card:hover, 2046body.dark .proj-card, body.dark .proj-card:hover{ 2047 transform:none !important; 2048 transition:none !important; 2049 box-shadow:none !important; 2050 border-color:var(--border,rgba(0,0,0,.08)) !important; 2051} 2052.trend-card-wrap:hover, .trend-card:hover, .project-card:hover, .proj-card:hover, 2053body.dark .trend-card-wrap:hover, body.dark .trend-card:hover, 2054body.dark .project-card:hover, body.dark .proj-card:hover{ 2055 background:inherit !important; 2056} 2057.trend-card-wrap::before, .trend-card::before, .project-card::before, .proj-card::before{ 2058 display:none !important; content:none !important; 2059} 2060.trend-card-wrap:hover .proj-arrow, .project-card:hover .proj-arrow, 2061.trend-card-wrap:hover .card-arrow, .project-card:hover .card-arrow, 2062.trend-card:hover .proj-arrow, .trend-card:hover .card-arrow{ transform:none !important; } 2063body.dark .logo span, body.dark .nav-links a:hover, body.dark .nav-dropdown-btn:hover, 2064body.dark .btn-ghost:hover, body.dark .section-link, body.dark .embed-section-link, 2065body.dark .footer-col a:hover, body.dark footer .footer-col a:hover, 2066body.dark .theme-toggle-nav .tt-moon{ color:#818cf8 !important; } 2067body.dark .btn-ghost:hover{ border-color:#818cf8 !important; } 2068body.dark .filter-btn.active, body.dark .tag-btn.active{ background:#818cf8 !important; color:#ffffff !important; border-color:#818cf8 !important; } 2069body:not(.dark) .back-to-top{ color:#fff !important; } 2070body:not(.dark) .back-to-top svg{ stroke:#fff; } 2071body.dark .back-to-top{ background:#818cf8 !important; } 2072body.dark .back-to-top:hover{ background:#a5b0ff !important; } 2073body.dark .back-to-top svg{ stroke:#ffffff !important; } 2074/* [24/07] Le bouton « remonter » percutait le widget de chat (tous deux en bas a 2075 droite). On le remonte au-dessus du chat, sur toutes les pages. */ 2076.back-to-top{ bottom:5.5rem !important; } 2077@media(max-width:600px){ .back-to-top{ bottom:5rem !important; right:1rem !important; } } 2078body.dark .osai-mint, body.dark .osai-mint:hover{ background:#818cf8 !important; color:#ffffff !important; border-color:transparent !important; } 2079body.dark .osai-mint *{ color:#ffffff !important; } 2080body.dark .osai-mint::before, body.dark .osai-mint::after{ color:#ffffff !important; } 2081body.dark .osai-mint:hover{ filter:brightness(1.06); } 2082body.dark .osai-mint svg{ color:#ffffff; } 2083body.dark .osai-mint-txt{ color:#818cf8 !important; } 2084body.dark .theme-toggle-knob{ background:#818cf8; }
2085/* âââ V17 â Cartes équilibrées (pied collé en bas, plus de gros vides) âââ */ 2086#projectsGrid > *, #trendingGrid > .trend-card, #trendingGrid > .trend-card-wrap{ display:flex; flex-direction:column; } 2087#projectsGrid > * > :last-child, #trendingGrid .trend-card > :last-child{ margin-top:auto; } 2088/* âââ V17 â Cartes OG GitHub adoucies en dark âââ */ 2089body.dark img[src*="opengraph.githubassets"]{ border-radius:12px; filter:brightness(.93) saturate(.95); } 2090/* âââ V17 â Micro-interactions unifiées âââ */ 2091.featured-card, .moretool-card, .trend-card{ transition:transform .18s ease, box-shadow .18s ease; } 2092.featured-card:hover, .moretool-card:hover, .trend-card:hover{ transform:translateY(-3px); box-shadow:0 12px 30px rgba(0,0,0,.28); } 2093/* âââ V17 â Skeletons (shimmer pendant le chargement des sections JS) âââ */ 2094#featuredGrid:empty, #projectsGrid:empty, #trendingGrid:empty, #moretoolsGrid:empty, #newsGrid:empty, #ttGrid:empty, #whatsnewGrid:empty{ min-height:190px; border-radius:16px; background:linear-gradient(100deg, var(--surface) 40%, var(--surface2) 50%, var(--surface) 60%); background-size:200% 100%; animation:osaiShimmer 1.15s linear infinite; } 2095@keyframes osaiShimmer{ to{ background-position:-200% 0; } } 2096/* âââ V17 â Badge live du hero âââ */ 2097.osai-live-pill{cursor:pointer;text-decoration:none;transition:border-color .2s;} .osai-live-pill:hover{border-color:var(--accent);} .osai-live-pill .olp-go{opacity:.7;} .osai-live-pill{ display:inline-flex; align-items:center; gap:8px; font-family:'DM Mono',monospace; font-size:.72rem; text-transform:uppercase; letter-spacing:.1em; color:var(--accent); background:var(--card-hover); border:2px solid var(--accent); border-radius:22px; padding:6px 14px; margin-bottom:18px; } 2098.osai-live-pill b{ font-weight:700; } 2099.osai-live-pill .olp-dot{ width:8px; height:8px; border-radius:50%; background:var(--accent); animation:osaiPulse 1.8s infinite; } 2100@keyframes osaiPulse{ 0%{ box-shadow:0 0 0 0 rgba(129,140,248,.55); } 70%{ box-shadow:0 0 0 8px rgba(129,140,248,0); } 100%{ box-shadow:0 0 0 0 rgba(129,140,248,0); } } 2101.mega-foot-t{ display:flex; align-items:center; gap:8px; font-size:.77rem; color:var(--txt2); line-height:1.4; } 2102.mega-foot-t svg{ width:15px; height:15px; color:var(--accent); flex-shrink:0; } 2103.mega-foot-t b{ color:var(--txt); } 2104.mega-foot-cta{ font-size:.78rem; font-weight:600; color:var(--accent); text-decoration:none; white-space:nowrap; } 2105.mega-foot-cta:hover{ text-decoration:underline; } 2106@media(max-width:980px){ .nav-dropdown-menu.mega{ width:min(740px,95vw) !important; } .mega-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); gap:20px 22px; } } 2107 2108/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2109 TOP OPEN-SOURCE AI PROJECTS (#projectsGrid) : AUCUN EFFET. 2110 Cible le wrapper (>*) ET la carte (.project-card) ET leurs enfants. 2111 Ãtat :hover rendu strictement identique à l'état normal. 2112 Règle finale du nav script = a le dernier mot sur tout. 2113 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 2114#projectsGrid, #projectsGrid *, 2115#projectsGrid > *, #projectsGrid > *:hover, #projectsGrid > *:focus, #projectsGrid > *:active, 2116#projectsGrid .project-card, #projectsGrid .project-card:hover, #projectsGrid .project-card:focus, #projectsGrid .project-card:active, 2117#projectsGrid .project-card *, #projectsGrid .project-card:hover *{ 2118 transform:none !important; 2119 transition:none !important; 2120 animation:none !important; 2121 box-shadow:none !important; 2122 filter:none !important; 2123 opacity:1 !important; 2124} 2125#projectsGrid > *:hover, #projectsGrid .project-card:hover, 2126#projectsGrid > *:focus, #projectsGrid .project-card:focus, 2127#projectsGrid > *:active, #projectsGrid .project-card:active{ 2128 background:var(--surface) !important; 2129 border-color:var(--border,rgba(0,0,0,.08)) !important; 2130} 2131#projectsGrid .project-card::before, #projectsGrid .project-card::after, 2132#projectsGrid > *::before, #projectsGrid > *::after{ display:none !important; content:none !important; } 2133/* Contours plus francs sur les fiches (injecté ici â aucune régénération de page, aucun impact SEO) */ 2134.chip{ border-width:2px !important; }
2135.fiche-ogcard,.fact,.rel-card{ border-width:2px !important; } 2136.fiche-act,.fiche-act-wrap .fav-btn{ border-width:2px !important; } 2137 2138/* âââ Rangée d'actions de la fiche : hiérarchiser au lieu d'aligner âââ 2139 Sept contrôles de trois hauteurs différentes cohabitaient au même niveau 2140 visuel (voir GitHub, â¡, partage, ð/ð, suivre, collection, badge) : le 2141 visiteur ne savait pas où cliquer. On ne touche NI au HTML des 60 000 fiches 2142 NI au JS d'injection â seulement à l'ordre et au poids visuel, en CSS. Trois 2143 rangs : l'action principale, les actions de membre, puis les utilitaires. 2144 Les marges en ligne des boutons injectés sont neutralisées, sinon elles 2145 s'ajoutent au gap et l'espacement devient irrégulier. */ 2146.cta-row{ align-items:center !important; gap:10px !important; row-gap:12px !important; } 2147.cta-row #osaiWatchBtn,.cta-row #osaiColBtn,.cta-row #osaiBadgeBtn{ margin-left:0 !important; } 2148 2149/* rang 1 â l'action principale reste seule en tête */ 2150.cta-row .btn-primary{ order:1; } 2151/* rang 2 â ce qu'un membre fait du projet : même style, côte à côte */ 2152.cta-row #osaiWatchBtn{ order:2; } 2153.cta-row #osaiColBtn{ order:3; } 2154/* rang 3 â utilitaires allégés : présents, pas concurrents. 2155 PAS de rejet à droite : un margin-left:auto creusait un grand vide entre 2156 « Collection » et « ⡠», et ces boutons paraissaient orphelins plutôt que 2157 groupés. La hiérarchie se lit à la taille et à la couleur, l'ordre de lecture 2158 reste continu. Un simple écart marque la frontière de groupe, et il se 2159 dégrade proprement quand la ligne passe à la suivante (contrairement à un 2160 séparateur, qui se retrouverait en début de ligne). */ 2161.cta-row #ficheFav{ order:6; margin-left:12px !important; } 2162.cta-row .fiche-share-wrap{ order:7; } 2163.cta-row #osaiBadgeBtn{ order:8; height:38px !important; padding:0 12px !important; 2164 font-size:.78rem !important; color:var(--muted,#5a6d87) !important; } 2165.cta-row #ficheFav .fav-btn,.cta-row .fiche-share{ width:38px !important; height:38px !important; } 2166.cta-row #ficheFav .fav-btn svg,.cta-row .fiche-share svg{ width:17px !important; height:17px !important; } 2167/* l'avis du public n'est pas une action sur le projet : dernier, et discret */ 2168.cta-row #osai-vote{ order:9; } 2169 2170/* âââ [02/08/2026, mockup de Romain] L'empilement « un seul bord de lecture » 2171 est ABANDONNà : le mockup validé met l'avatar en plaque à GAUCHE du bloc 2172 puce + titre + accroche, le ruban de rang à droite. Les règles !important 2173 qui forçaient .hero-main en pleine largeur (et donc l'avatar seul sur sa 2174 ligne) sont retirées ; fiche.css fait foi. Depuis, les tags, les boutons et 2175 la bande de chiffres ne vivent plus DANS .hero-main mais après </header>, 2176 au bord gauche : le zigzag que la règle corrigeait n'existe plus. */ 2177 2178@media(max-width:760px){ 2179 /* l'action principale sur toute la largeur : c'est elle qu'on vient chercher */ 2180 .cta-row #ficheFav{ margin-left:0 !important; } 2181 .cta-row .btn-primary{ flex:1 1 100%; justify-content:center; } 2182 2183 /* avatar encore un peu plus compact sur petit écran (l'alignement, lui, est 2184 déjà réglé plus haut pour toutes les tailles) */ 2185 .hero{ gap:14px !important; } 2186 .hero-ic{ width:72px !important; height:72px !important; border-radius:18px !important; } 2187} 2188 2189/* âââ Menu principal : lisibilite et alignement âââ 2190 Les intitules etaient a .67rem, soit environ 10,7 px : trop petits, et avec 2191 line-height:1 contre une icone de 14 px le texte paraissait mal cale. On 2192 remonte la taille, on aligne la hauteur de ligne et on grossit l'icone d'autant. 2193 Le crenage et l'ecart entre liens sont reduits en compensation, sinon les six 2194 liens plus la recherche, la langue et l'avatar debordent sur un ecran de 901 px 2195 â un incident deja survenu sur ce menu. Deux paliers pour cette raison. */ 2196@media(min-width:901px){ 2197 #site-nav .nav-links{ gap:.7rem !important; } 2198 #site-nav .nav-links>a, #site-nav .nav-dropdown-btn{ 2199 font-size:.78rem !important; letter-spacing:.01em !important; line-height:1.15 !important; } 2200 #site-nav .nav-links > a, #site-nav .nav-dropdown-btn{ align-items:center !important; } 2201 #site-nav .nav-links > a svg, #site-nav .nav-dropdown-btn svg{ width:15px !important; height:15px !important; } 2202} 2203@media(min-width:1200px){ 2204 #site-nav .nav-links{ gap:.85rem !important; } 2205 #site-nav .nav-links>a, #site-nav .nav-dropdown-btn{ font-size:.86rem !important; } 2206 #site-nav .nav-links > a svg, #site-nav .nav-dropdown-btn svg{ width:16px !important; height:16px !important; } 2207} 2208 2209/* âââ La casse des libelles de la barre âââ 2210 « Pricing », « Projects », « AI tools » s'affichaient en CAPITALES alors que le 2211 markup les ecrit normalement. Ce n'est pas notre fait : chaque page porte dans 2212 son CSS inline « .nav-links a{ text-transform:uppercase } » (index.html:198) et 2213 « .nav-dropdown-btn{...uppercase } » (index.html:707). Comme la barre est 2214 injectee DANS .nav-links, elle en herite. Les liens du mega-menu avaient deja 2215 ete deverrouilles un par un ; la barre elle-meme ne l'avait jamais ete. 2216 On annule ici, une fois, pour les quelque 300 pages du site. */ 2217#site-nav .nav-links a, #site-nav .nav-links a span, #site-nav .nav-links a b, 2218#site-nav .nav-dropdown-btn, nav .nav-links > a{ 2219 text-transform:none !important; 2220} 2221 2222/* âââ Filet pour les pages nees SANS le CSS de base de la nav âââ 2223 Les regles structurelles du menu vivent dans le CSS de chaque page. Une page 2224 nouvelle qui les oublie affiche les six colonnes du mega-menu ouvertes en 2225 permanence, par-dessus le contenu â bug constate sur report.html le 27/07. 2226 Ces deux regles sont identiques a celles des pages : en double elles s
2226ont 2227 inoffensives, seules elles sauvent la page. */ 2228.nav-dropdown-menu:not(.open){ display:none; } 2229.osaiMM:not(.open){ display:none; } 2230 2231/* âââ CHARTE : liseres 2 px au survol, PARTOUT âââ 2232 Romain : « a chaque fois que tu verras ca n'importe ou tu devras le corriger, 2233 c'est notre charte graphique ». L'audit du site a trouve 100 regles dans 30 2234 fichiers encore a 1 px. Les corriger fichier par fichier, sans pouvoir rendre 2235 les pages en local, aurait multiplie les risques pour un gain nul : navCSS est 2236 injecte sur CHAQUE page, une seule passe suffit. 2237 La bordure de REPOS passe aussi a 2 px â ne changer que le survol decalerait la 2238 mise en page d'un pixel au passage de la souris. 2239 19 des 85 selecteurs portent leur bordure en !important dans le CSS de leur 2240 page : sans !important ici, la regle perdait le duel â constate sur 2241 .project-card, signale par Romain. Et 12 selecteurs avaient echappe a l'audit 2242 parce que leur bordure vit dans une AUTRE regle que celle du survol. 2243 border-width n'a aucun effet sur un element sans bordure : rien ne peut 2244 apparaitre par surprise. */ 2245#histChips button, 2246#histOut li a, 2247.ac-links a, 2248.ag-callout .links a, 2249.ag-card, 2250.ag-cmp a, 2251.agc-links a.gh,
2252.agr-chip, 2253.also-card, 2254.alt-card, 2255.arc-card, 2256.bench-btn, 2257.btn-second, 2258.calc-presets button, 2259.card, 2260.cat-item, 2261.cattile, 2262.champion-card, 2263.chip, 2264.chip-toggle, 2265.cmdk-trigger, 2266.col-add button, 2267.compare-clear, 2268.cr-chip, 2269.dl-btn, 2270.dnah-card, 2271.export-btn, 2272.fav-nav-btn, 2273.feat-card, 2274.featured-card, 2275.follow-btn, 2276.fp-card, 2277.hero-quick-tags button, 2278.homesec label, 2279.hrel-card, 2280.hres-btn, 2281.hub-card, 2282.hub-chip, 2283.hub-item, 2284.img-action-btn, 2285.lang-btn, 2286.lb-share-btn, 2287.like-btn, 2288.mchip, 2289.mtab, 2290.mvs-card, 2291.nc, 2292.pc-btn, 2293.pf-link button, 2294.pg-btn, 2295.ph-card, 2296.pl-cat, 2297.pnl-x, 2298.pod, 2299.preset-pill, 2300.proj-cat, 2301.prompt-card, 2302.psug, 2303.qc, 2304.qchip, 2305.qual-toggle, 2306.rel-card, 2307.rel-cat, 2308.rel-more button, 2309.run-card, 2310.save-row button.ghost, 2311.share-btn, 2312.sp-card, 2313.st-card, 2314.t-mh button, 2315.t-paper, 2316.t-sb, 2317.tag, 2318.thub-card, 2319.today-item, 2320.today-stat-link, 2321.tools-cat, 2322.top, 2323.trend-card, 2324.trend-card-wrap, 2325.tt-item, 2326.vs-pill, 2327.wn-card, 2328a.tcard, 2329a.vcard, 2330#histChips button:hover, 2331#histOut li a:hover, 2332.ac-links a:hover, 2333.ag-callout .links a:hover, 2334.ag-card:hover, 2335.ag-cmp a:hover, 2336.agc-links a.gh:hover, 2337.agr-chip:hover, 2338.also-card:hover, 2339.alt-card:hover, 2340.arc-card:hover, 2341.bench-btn:hover, 2342.btn-second:hover, 2343.calc-presets button:hover, 2344.card:hover, 2345.cat-item:hover, 2346.cattile:hover, 2347.champion-card:hover, 2348.chip:hover, 2349.chip-toggle:hover, 2350.cmdk-trigger:hover, 2351.col-add button:hover, 2352.compare-clear:hover, 2353.cr-chip:hover, 2354.dl-btn:hover, 2355.dnah-card:hover, 2356.export-btn:hover, 2357.fav-nav-btn:hover, 2358.feat-card:hover, 2359.featured-card:hover, 2360.follow-btn:hover, 2361.fp-card:hover, 2362.hero-quick-tags button:hover, 2363.homesec label:hover, 2364.hrel-card:hover, 2365.hres-btn:hover, 2366.hub-card:hover, 2367.hub-chip:hover, 2368.hub-item:hover, 2369.img-action-btn:hover, 2370.lang-btn:hover, 2371.lb-share-btn:hover, 2372.like-btn:hover, 2373.mchip:hover, 2374.mtab:hover, 2375.mvs-card:hover, 2376.nc:hover, 2377.pc-btn:hover, 2378.pf-link button:hover, 2379.pg-btn:hover, 2380.ph-card:hover, 2381.pl-cat:hover, 2382.pnl-x:hover, 2383.pod:hover, 2384.preset-pill:hover, 2385.proj-cat:hover, 2386.prompt-card:hover, 2387.psug:hover, 2388.qc:hover, 2389.qchip:hover, 2390.qual-toggle:hover, 2391.rel-card:hover, 2392.rel-cat:hover, 2393.rel-more button:hover, 2394.run-card:hover, 2395.save-row button.ghost:hover, 2396.share-btn:hover, 2397.sp-card:hover, 2398.st-card:hover, 2399.t-mh button:hover, 2400.t-paper:hover, 2401.t-sb:hover, 2402.tag:hover, 2403.thub-card:hover, 2404.today-item:hover, 2405.today-stat-link:hover, 2406.tools-cat:hover, 2407.top:hover, 2408.trend-card:hover, 2409.trend-card-wrap:hover, 2410.tt-item:hover, 2411.vs-pill:hover, 2412.wn-card:hover, 2413a.tcard:hover, 2414a.vcard:hover, 2415#downloadChat, 2416.article-card, 2417.btn-ghost, 2418.ghost, 2419.model-pill, 2420.moretool-card, 2421.project-card, 2422.rel-toggle, 2423.summ-style, 2424.tbar, 2425.tcard, 2426.tpl, 2427#downloadChat:hover, 2428.article-card:hover, 2429.btn-ghost:hover, 2430.ghost:hover, 2431.model-pill:hover, 2432.moretool-card:hover, 2433.project-card:hover, 2434.rel-toggle:hover, 2435.summ-style:hover, 2436.tbar:hover, 2437.tcard:hover, 2438.tpl:hover{ border-width:2px !important; } 2439 2440/* ââ Médias des posts (u.php, members.php, account.html) âââââââââââââââââââ 2441 Une seule feuille pour les trois pages : une galerie qui ne saute pas au 2442 chargement, et une vignette vidéo qui ne contacte YouTube/Vimeo qu'au clic. 2443 Liserés 2 px au repos comme au survol : charte maison. ââ */ 2444.pmed{ display:grid; gap:7px; margin-top:11px; grid-template-columns:1fr; } 2445.pmed-n2,.pmed-n3,.pmed-n4{ grid-template-columns:1fr 1fr; } 2446.pmed-i{ display:block; margin:0; border:2px solid var(--border2,#e5e7eb); border-radius:13px; overflow:hidden; background:var(--surface2,#f4f4f5); line-height:0; transition:border-color .15s; } 2447.pmed-i:hover{ border-color:var(--accent,#6366f1); } 2448.pmed-i img{ display:block; width:100%; height:auto; max-height:520px; object-fit:cover; } 2449.pmed-n2 .pmed-i img,.pmed-n3 .pmed-i img,.pmed-n4 .pmed-i img{ aspect-ratio:4/3; max-height:none; } 2450.pmed-n3 .pmed-i:first-child{ grid-column:1 / -1; } 2451.pmed-v{ position:relative; display:block; width:100%; aspect-ratio:16/9; margin-top:11px; padding:0; border:2px solid var(--border2,#e5e7eb); border-radius:13px; overflow:hidden; cursor:pointer; background:#0f1115 center/cover no-repeat; transition:border-color .15s; } 2452.pmed-v:hover{ border-color:var(--accent,#6366f1); } 2453.pmed-play{ position:absolute; inset:0; margin:auto; width:56px; height:56px; border-radius:50%; background:rgba(15,17,21,.72); border:2px solid rgba(255,255,255,.9); } 2454.pmed-play::after{ content:''; position:absolute; top:50%; left:52%; transform:translate(-50%,-50%); border-style:solid; border-width:9px 0 9px 15px; border-color:transparent transparent transparent #fff; } 2455.pmed-v:hover .pmed-play{ background:var(--accent,#6366f1); } 2456.pmed-src{ position:absolute; left:9px; bottom:9px; padding:3px 9px; border-radius:999px; background:rgba(15,17,21,.72); color:#fff; font-size:.68rem; font-weight:600; letter-spacing:.03em; } 2457.pmed-f{ width:100%; aspect-ratio:16/9; margin-top:11px; border:2px solid var(--border2,#e5e7eb); border-radius:13px; display:block; } 2458`; 2459 2460 /* ââ Vidéo des posts : façade â lecteur ââââââââââââââââââââââââââââââââââââ 2461 La vignette est un <button data-src>. Au clic seulement, on la remplace par 2462 l'iframe : aucune requête vers YouTube/Vimeo tant que le visiteur n'a rien 2463 demandé (page légère, et pas de traceur tiers imposé). L'écouteur est 2464 délégué au document : il marche donc aussi sur les posts ajoutés après 2465 coup, comme dans le compte. ââ */ 2466 document.addEventListener('click', function (ev) { 2467 var t = ev.target; 2468 if (!t || !t.closest) return; 2469 var btn = t.closest('.pmed-v'); 2470 if (!btn) return; 2471 var src = btn.getAttribute('data-src') || ''; 2472 /* Ceinture : on n'insère une iframe que vers les deux lecteurs attendus. 2473 Si un jour un data-src arrivait d'ailleurs, il ne serait pas chargé. */ 2474 if (!/^https:\/\/(www\.youtube-nocookie\.com\/embed\/|player\.vimeo\.com\/video\/)/.test(src)) return; 2475 var f = document.createElement('iframe'); 2476 f.className = 'pmed-f'; 2477 f.src = src; 2478 f.setAttribute('title', btn.getAttribute('aria-label') || 'Video'); 2479 f.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); 2480 f.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 2481 f.setAttribute('allowfullscreen', ''); 2482 f.setAttribute('loading', 'lazy'); 2483 if (btn.parentNode) btn.parentNode.replaceChild(f, btn); 2484 }); 2485 2486 var styleEl = document.createElement('style'); 2487 /* .nav-cta : les regles generiques de la barre portent des !important 2488 (couleur de repos --txt2, couleur de survol --accent) : sans selecteur 2489 PLUS SPECIFIQUE (a.nav-cta) + !important, le survol devenait indigo sur 2490 indigo â libelle invisible (releve en revue le 29/07). Entre 901 et 2491 1199px la barre est deja pleine a craquer (voir l'incident du crenage 2492 plus haut) : le bouton s'efface, le mega-menu et le menu mobile gardent 2493 leur entree « Submit a tool or project ». */ 2494 styleEl.textContent = navCSS 2495 + '#site-nav .nav-links a.nav-cta{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:0;border-radius:50%;flex:0 0 auto;background:linear-gradient(135deg,#3B57FF 0%,#A257FF 100%);color:#fff !important;text-decoration:none;box-shadow:0 2px 10px rgba(90,80,255,.32);transition:transform .16s cubic-bezier(.2,.8,.3,1),box-shadow .16s}' 2496 + '#site-nav .nav-links a.nav-cta:hover, body.dark #site-nav .nav-links a.nav-cta:hover{background:linear-gradient(135deg,#3B57FF 0%,#A257FF 100%);color:#fff !important;transform:translateY(-2px) scale(1.07);box-shadow:0 6px 20px rgba(90,80,255,.48)}' 2497 + '#site-nav .nav-links a.nav-cta:active{transform:translateY(0) scale(.94);box-shadow:0 1px 6px rgba(90,80,255,.34)}' 2498 + '#site-nav .nav-links a.nav-cta:focus-visible{outline:2px solid var(--accent,#6366f1);outline-offset:3px}' 2499 + '@media(prefers-reduced-motion:reduce){#site-nav .nav-links a.nav-cta,#site-nav .nav-links a.nav-cta:hover,#site-nav .nav-links a.nav-cta:active{transition:none;transform:none}}' 2500 /* le svg de la barre a sa PROPRE couleur (regle 1513, color:var(--accent)) : 2501 sans ceci le + restait indigo sur fond indigo au survol (Romain, 29/07) */ 2502 + '#site-nav .nav-links a.nav-cta svg{color:inherit !important}' 2503 /* Deux regles generiques de la barre visaient ce + et l'auraient ruine : 2504 « > a svg{width:14px} » l'aurait laisse minuscule dans un rond de 36, 2505 et surtout « @media 901-1200px { > a svg{display:none} } » l'aurait 2506 carrement efface â un bouton rond parfaitement VIDE sur les ecrans 2507 entre 901 et 1200 px. On surcharge donc taille ET affichage. */ 2508 + '#site-nav .nav-links > a.nav-cta svg{width:19px !important;height:19px !important;opacity:1 !important;display:block !important}' 2509 /* [02/08] L'ancienne pilule et son libelle saturaient la barre entre 901 2510 et 1199 px, on la masquait donc. Un rond de 36 px tient a toutes les 2511 largeurs : la regle qui l'effacait est retiree, le bouton est 2512 desormais accessible partout. */
2513 /* âââ CODE COULEUR PAR TYPE DE CONTENU (30/07/2026, demande de Romain) âââ 2514 Une couleur par famille, disponible sur TOUTES les pages puisque ce 2515 fichier est charge partout. Regles : 2516 - l'indigo (--accent) reste la MARQUE : navigation, liens, titres. 2517 Il ne code aucune categorie, sinon tout se confond. 2518 - la couleur ne porte JAMAIS le sens toute seule : les pastilles 2519 .cat-* embarquent un libelle â 8 % des hommes sont daltoniens. 2520 - deux themes : les fonds passent en version sombre via body.dark. 2521 Les pages consomment var(--cat-project) etc., ou directement les 2522 classes .cat.cat-project | cat-tool | cat-model-open | cat-model-paid. */ 2523 + ':root{--cat-project:#10b981;--cat-project-bg:#ecfdf5;--cat-project-bd:#a7f3d0;--cat-project-tx:#047857;' 2524 + '--cat-tool:#f59e0b;--cat-tool-bg:#fffbeb;--cat-tool-bd:#fde68a;--cat-tool-tx:#b45309;' 2525 + '--cat-model-open:#0ea5e9;--cat-model-open-bg:#f0f9ff;--cat-model-open-bd:#bae6fd;--cat-model-open-tx:#0369a1;' 2526 + '--cat-model-paid:#a855f7;--cat-model-paid-bg:#faf5ff;--cat-model-paid-bd:#e9d5ff;--cat-model-paid-tx:#7e22ce}' 2527 + 'body.dark{--cat-project-bg:rgba(16,185,129,.13);--cat-project-bd:rgba(16,185,129,.36);--cat-project-tx:#34d399;' 2528 + '--cat-tool-bg:rgba(245,158,11,.13);--cat-tool-bd:rgba(245,158,11,.36);--cat-tool-tx:#fbbf24;' 2529 + '--cat-model-open-bg:rgba(14,165,233,.13);--cat-model-open-bd:rgba(14,165,233,.36);--cat-model-open-tx:#38bdf8;' 2530 + '--cat-model-paid-bg:rgba(168,85,247,.14);--cat-model-paid-bd:rgba(168,85,247,.38);--cat-model-paid-tx:#c084fc}' 2531 + '.cat{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:999px;border:1px solid;' 2532 + 'font-size:.68rem;font-weight:600;letter-spacing:.02em;white-space:nowrap;vertical-align:middle}' 2533 + '.cat-project{background:var(--cat-project-bg);border-color:var(--cat-project-bd);color:var(--cat-project-tx)}' 2534 + '.cat-tool{background:var(--cat-tool-bg);border-color:var(--cat-tool-bd);color:var(--cat-tool-tx)}' 2535 + '.cat-model-open{background:var(--cat-model-open-bg);border-color:var(--cat-model-open-bd);color:var(--cat-model-open-tx)}' 2536 + '.cat-model-paid{background:var(--cat-model-paid-bg);border-color:var(--cat-model-paid-bd);color:var(--cat-model-paid-tx)}' 2537 /* [30/07 soir, v2] Les puces rondes deplaisaient a Romain (et se 2538 deformaient selon le contexte). A la place : l'ICONE existante de 2539 chaque entree prend la couleur de sa famille. Les SVG des menus sont 2540 en stroke="currentColor" : une regle color suffit, le !important bat 2541 les styles inline que portent plusieurs entrees mobiles. */ 2542 + '#moreMenu a.cat-ic-project svg,#osaiMM a.cat-ic-project svg,#site-nav a.cat-ic-project svg{color:var(--cat-project,#10b981) !important}' 2543 + '#moreMenu a.cat-ic-tool svg,#osaiMM a.cat-ic-tool svg,#site-nav a.cat-ic-tool svg{color:var(--cat-tool,#f59e0b) !important}' 2544 + '#moreMenu a.cat-ic-model-open svg,#osaiMM a.cat-ic-model-open svg,#site-nav a.cat-ic-model-open svg{color:var(--cat-model-open,#0ea5e9) !important}' 2545 + '#moreMenu a.cat-ic-model-paid svg,#osaiMM a.cat-ic-model-paid svg,#site-nav a.cat-ic-model-paid svg{color:var(--cat-model-paid,#a855f7) !important}' 2546 /* [31/07, Romain] Les fils d'Ariane « Home / Tools Directory » au-dessus 2547 des titres disparaissent PARTOUT â 29 pages + 17 generateurs, donc ici 2548 plutot qu'un par un. On garde la HAUTEUR (leur padding haut compense la 2549 barre fixe : sans lui, les titres passeraient sous le menu) et on clippe 2550 le texte. Le BreadcrumbList JSON-LD, invisible, reste : Google continue 2551 d'afficher l'arborescence dans ses resultats. */ 2552 /* Un seul gabarit de titre de page pour tout le site (mesure du 31/07 : 2553 2,1rem/800 ici, clamp(2.4-4rem)/600 la). La couleur du dernier mot, elle, 2554 reste celle de la famille. */ 2555 /* âââ [01/08] LE SIGNE « IA » SUR LES CHAMPS DE RECHERCHE âââââââââââ 2556 Romain : « comment faire savoir que nos barres sont alimentees par 2557 l'IA ? des liseres arc-en-ciel qui bougent ? » L'usage qui s'est 2558 impose ailleurs (Notion, Linear, Arc, GitHub Copilot) : un degrade 2559 anime en liseré et une etincelle, jamais un arc-en-ciel complet â trop 2560 criard, et illisible pour qui distingue mal les couleurs. On reste 2561 donc dans NOS couleurs de familles (indigo, cyan, violet, ambre), le 2562 degrade ne s'anime QU'AU FOCUS (une bordure qui tourne en permanence 2563 fatigue et attire l'oeil loin du contenu), et il se fige pour qui a 2564 demande a reduire les animations. */ 2565 /* âââ [01/08, v2] DEUX DEFAUTS CORRIGES, releves par Romain âââââââââââ 2566 « le contour ne se fait pas bien, pas assez large ». 2567 2568 1. LA GEOMETRIE. L'anneau avait un rayon ECRIT EN DUR (20px) et le 2569 champ un autre. Or index.html pose, ligne 550, une regle globale 2570 input:not([type="range"]){border-radius:8px !important} qui bat les 2571 18px de la barre du hero : l'anneau arrondi a 20px entourait donc un 2572 champ arrondi a 8px. Aux quatre coins la bande calculee tombe a 2573 -0,7px : l'anneau y passe SOUS le champ et disparait, et le coin du 2574 champ ressort nu. On ne devine donc plus aucun rayon â marquerIA() 2575 MESURE le rayon reel du champ et le repose ici, augmente de 2576 l'epaisseur du lisere. Anneau et champ restent concentriques quoi 2577 qu'il arrive. 2578 2579 2. LA FORME DU DEGRADE. Un degrade CONIQUE repartit ses couleurs par 2580 ANGLE autour du centre. Sur la barre du hero, qui fait 648x76 (un 2581 rapport de 8,5 pour 1), chaque petit cote ne couvre que 13 degres 2582 sur 360 : les deux extremites n'affichaient qu'UNE couleur plate 2583 pendant que toute la palette s'ecrasait le long des bords haut et 2584 bas. C'est le rose d'un cote, le bleu de l'autre. Un degrade 2585 LINEAIRE qui glisse rend la meme idee et reste egal sur toute la 2586 longueur, quelle que soit la forme de la boite. 2587 2588 Le lisere ne s'anime toujours QU'AU FOCUS : une bordure qui tourne en 2589 permanence fatigue et attire l'oeil loin du contenu. */ 2590 + '.osai-ai{position:relative;background:var(--surface,#fff);border-radius:12px;' 2591 + '--osai-ai-w:2px;--osai-ai-tl:14px;--osai-ai-tr:14px;--osai-ai-br:14px;--osai-ai-bl:14px}' 2592 + '.osai-ai::before{content:"";position:absolute;pointer-events:none;z-index:-1;' 2593 + 'inset:calc(0px - var(--osai-ai-w,2px));' 2594 + 'border-top-left-radius:var(--osai-ai-tl);border-top-right-radius:var(--osai-ai-tr);' 2595 + 'border-bottom-right-radius:var(--osai-ai-br);border-bottom-left-radius:var(--osai-ai-bl);' 2596 /* Palette SYMETRIQUE : l'indigo de la marque aux deux extremites, l'ambre 2597 en simple accent au centre. Avec un degrade decale (220% de large), la 2598 barre du hero n'en montrait que la moitie et virait a l'orange sur tout 2599 son cote droit â la couleur du site avait disparu. Au repos, le degrade 2600 tient donc exactement dans la boite. Il ne s'etire et ne glisse qu'au 2601 focus, et l'animation s'arrete pour qui a demande moins de mouvement. */ 2602 + 'background:linear-gradient(100deg,#6366f1,#0ea5e9,#a855f7,#f59e0b,#a855f7,#0ea5e9,#6366f1);' 2603 + 'background-size:100% 100%;
2603opacity:.7;transition:opacity .25s}' 2604 + '.osai-ai:focus-within::before{opacity:1;background-size:200% 100%;' 2605 + 'animation:osaiAiSlide 4.5s linear infinite}' 2606 + '@keyframes osaiAiSlide{from{background-position:0 0}to{background-position:100% 0}}' 2607 + '@media (prefers-reduced-motion: reduce){.osai-ai:focus-within::before{animation:none}}' 2608 /* la pastille qui NOMME la chose : un degrade seul n'explique rien */ 2609 + '.osai-ai-tag{display:inline-flex;align-items:center;gap:4px;font-family:\'DM Mono\',ui-monospace,monospace;' 2610 + 'font-size:.6rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;' 2611 + 'padding:2px 8px;border-radius:999px;border:1px solid var(--border2);color:var(--muted);' 2612 + 'white-space:nowrap;cursor:help}' 2613 + '.osai-ai-tag b{color:var(--accent,#6366f1);font-weight:700}' 2614 + '.cmdk-ia .cmdk-ia-ic{display:flex;align-items:center;justify-content:center;width:34px;height:34px;' 2615 + 'border-radius:9px;background:linear-gradient(135deg,#6366f1,#a855f7);font-size:1rem;flex:none}' 2616 + '.cmdk-ia .cmdk-item-name{font-weight:600}' 2617 /* la barre du hero : meme signature, mais son rayon a elle (18px) et sa 2618 pastille posee DANS le champ, a gauche du bouton d'effacement */ 2619 + '.hero-search-box.osai-ai{border-radius:18px;background:transparent;--osai-ai-w:3px}' 2620 /* Le champ reprend le rayon de SA boite. Cette regle bat la regle globale 2621 de index.html par la SPECIFICITE (0,3,1 contre 0,1,1), pas par l'ordre 2622 de chargement : c'est ce qui rend le rayon previsible. On ne touche NI 2623 a la bordure NI a l'ombre du champ : elles sont indigo translucide, 2624 donc de la meme famille que le lisere, et l'oeil ne lit qu'un seul 2625 bord. Les neutraliser creait une couture grise entre l'arc-en-ciel et 2626 le champ, et effacait l'indicateur de focus au clavier. */ 2627 + '.hero-search-box.osai-ai>input:not([type="range"]){border-radius:inherit !important}' 2628 /* la pastille se pose SUR le bord haut, comme une legende : dans le champ 2629 elle passerait sous le texte tape (la zone de saisie va jusqu'a 56px du 2630 bord droit, la pastille en fait 50) */ 2631 + '.hero-search-box.osai-ai>.osai-ai-tag{position:absolute;top:-9px;right:20px;z-index:3;' 2632 + 'background:var(--bg,#f0f4fa);border-color:rgba(99,102,241,.45);padding:1px 8px}' 2633 + '@media (max-width:640px){.hero-search-box.osai-ai>.osai-ai-tag{right:12px}}' 2634 + '.hsr-ia .hsr-ia-ic{display:flex;align-items:center;justify-content:center;width:34px;height:34px;' 2635 + 'border-radius:9px;background:linear-gradient(135deg,#6366f1,#a855f7);font-size:1rem;flex:none}' 2636 + '.hsr-ia .hsr-name{font-weight:600}' 2637 /* âââ la carte assistant de l'accueil âââ */ 2638 /* Le fond opaque ne peut PAS vivre sur la carte elle-meme : index.html pose 2639 « #hero-search > * { position:relative; z-index:1 } », la carte est donc 2640 un contexte d'empilement, et le lisere en z-index:-1 se peint AU-DESSUS 2641 de son propre fond â il remplissait toute la carte. Le fond descend donc 2642 sur une couche intermediaire (::after, z-index 0) et le contenu remonte 2643 au-dessus (z-index 1) : le degrade ne se voit plus que sur les 2px qui 2644 depassent. Meme piege que le 01/08 sur la barre de recherche. */ 2645 /* [01/08, Romain : « sur mobile la barre est trop collee au lisere »] 2646 EFFONDREMENT DE MARGES. La carte n'a ni bordure ni remplissage propres 2647 â ils vivent sur la couche ::after â donc la marge basse de son dernier 2648 enfant SORT de la carte au lieu de l'espacer : le champ se retrouvait 2649 colle au bord. « flow-root » ouvre un contexte de formatage : les 2650 marges des enfants restent dedans. On ne peut PAS utiliser 2651 overflow:hidden pour ca, il couperait le lisere qui deborde de 2px. */ 2652 + '.hero-ai{position:relative;display:flow-root;max-width:640px;margin:0 auto;border-radius:18px;' 2653 + 'background:transparent;box-shadow:0 14px 44px rgba(15,23,42,.09);overflow:visible;text-align:left}' 2654 + '.hero-ai::after{content:"";position:absolute;inset:0;border-radius:inherit;z-index:0;' 2655 + 'background:var(--surface,#fff);border:1px solid var(--border2,rgba(0,0,0,.1))}' 2656 + '.hero-ai > *{position:relative;z-index:1}' 2657 + 'body.dark .hero-ai{box-shadow:0 14px 44px rgba(0,0,0,.4)}' 2658 + '.hero-ai-head{display:flex;align-items:center;gap:10px;padding:13px 16px 0}' 2659 + '.hero-ai-av{flex:none;display:flex}' 2660 + '.hero-ai-id{min-width:0}' 2661 + '.hero-ai-id b{display:block;font-size:.92rem;font-weight:700;letter-spacing:-.01em;color:var(--txt,#111)}' 2662 + '.hero-ai-on{display:inline-flex;align-items:center;gap:5px;font-size:.68rem;color:var(--muted,#5a6d87)}' 2663 + '.hero-ai-on::before{content:"";width:6px;height:6px;border-radius:50%;background:#10b981;flex:none}' 2664 + '.hero-ai-greet{display:flex;gap:10px;margin:11px 16px 0;padding:12px 14px;border-radius:12px;' 2665 + 'background:var(--surface2,#f5f7fb);font-size:.84rem;line-height:1.55;color:var(--txt,#111)}' 2666 + '.hero-ai-greet b{display:block;margin-top:3px;color:var(--accent,#6366f1);font-weight:600}' 2667 + '.hero-ai-spark{flex:none;display:flex;align-items:center;justify-content:center;width:30px;height:30px;' 2668 + 'border-radius:9px;background:linear-gradient(135deg,#6366f1,#a855f7);font-size:.9rem}' 2669 /* la barre garde son code, elle perd juste son cadre : c'est la carte qui le porte */ 2670 + '.hero-ai .hero-search-box{margin:12px 12px 12px;max-width:none}' 2671 + '.hero-ai .hero-search-box input{border-color:transparent !important;box-shadow:none !important;' 2672 + 'background:var(--bg,#f0f4fa) !important;padding-right:96px !important;font-size:1rem !important}' 2673 /* Champ vide = pas de croix d'effacement : reserver ses 38px tronquait 2674 l'invite sur telephone (« Search, or ask anythin⦠»). */ 2675 + '.hero-ai .hero-search-box input:placeholder-shown{padding-right:58px !important}' 2676 + 'body.dark .hero-ai .hero-search-box input{background:rgba(255,255,255,.05) !important}' 2677 + '.hero-ai .hero-search-box input:focus{border-color:var(--accent,#6366f1) !important}' 2678 + '.hero-ai-send{position:absolute;right:14px;top:50%;transform:translateY(-50%);width:36px;height:36px;' 2679 + 'border:none;border-radius:10px;background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;' 2680 + 'display:flex;align-items:center;justify-content:center;cursor:pointer;transition:filter .15s;z-index:3}' 2681 + '.hero-ai-send:hover{filter:brightness(1.12)}' 2682 + '.hero-ai #heroSearchClear{right:58px}' 2683 /* « relative » et non « static » : la liste doit rester POSITIONNEE pour 2684 que son z-index compte, sinon elle passe sous la couche de fond de la 2685 carte et la reponse s'affiche dans le vide (vu a l'essai : la bulle 2686 existait, mesuree 598x139, et ne se peignait pas). */ 2687 + '.hero-ai .hero-search-results{position:relative;top:auto;left:auto;right:auto;' 2688 + 'margin:0 12px 12px;box-shadow:none;' 2689 + 'border:1px solid var(--border,rgba(0,0,0,.07))}' 2690 /* le lisere anime cercle la CARTE, plus la barre */ 2691 + '.hero-ai.osai-ai{--osai-ai-w:2px}' 2692 + '.hero-search-box.osai-ai::before{display:none}' 2693 + '@media(max-width:640px){.hero-ai-greet{font-size:.8rem}.hero-ai .hero-search-box input{font-size:.95rem !important}}' 2694 /* la bulle de reponse, dans la liste de resultats */ 2695 + '.osai-inline{padding:14px 16px;font-size:.86rem;line-height:1.62;color:var(--txt,#111);' 2696 + 'border-left:2px solid var(--accent,#6366f1);background:var(--surface2,#f7f8fc);' 2697 + 'border-radius:0 10px 10px 0;margin:8px}' 2698 + '.osai-inline a{color:var(--accent,#6366f1);font-weight:600;text-decoration:none}' 2699 + '.osai-inline a:hover{text-decoration:underline}' 2700 + '.osai-inline-wait{color:var(--muted,#5a6d87);font-style:italic}' 2701 + '.osai-inline-more{display:block;width:-moz-fit-content;width:fit-content;' 2702 + 'margin-top:13px;padding:6px 12px;border-radius:999px;' 2703 + 'border:1px solid var(--border2,rgba(0,0,0,.15));background:none;color:var(--muted,#5a6d87);' 2704 + 'font-family:inherit;font-size:.74rem;cursor:pointer;transition:all .15s}' 2705 + '.osai-inline-more:hover{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1)}' 2706 /* [01/08] Le pied de page passe de deux colonnes fourre-tout a quatre 2707 colonnes thematiques. La grille s'adapte d'elle-meme : auto-fit avec un 2708 minimum de 150px, donc quatre colonnes sur un ecran large, deux sur une 2709 tablette, une sur un telephone â sans point de rupture ecrit a la main. */ 2710 + '.footer-inner{display:grid !important;grid-template-columns:minmax(210px,1.4fr) repeat(auto-fit,minmax(150px,1fr)) !important;' 2711 + 'gap:30px 26px !important;align-items:start !important}' 2712 + '.footer-col h4{font-size:.72rem !important;text-transform:uppercase !important;letter-spacing:.09em !important;' 2713 + 'font-family:\'DM Mono\',ui-monospace,monospace !important;color:var(--muted) !important;margin-bottom:11px !important}' 2714 + '.footer-col a{display:block !important;padding:3px 0 !important;font-size:.82rem !important;' 2715 + 'color:var(--txt) !important;opacity:.82;text-decoration:none !important;transition:opacity .12s,color .12s}' 2716 + '.footer-col a:hover{opacity:1;color:var(--accent,#6366f1) !important}' 2717 + '.loop-strip a .fic{vertical-align:-2px;margin-right:5px}' 2718 /* .footer-more etait sur deux colonnes internes : c'etait necessaire quand 2719 elle portait 36 liens, ca coupe les libelles en deux maintenant qu'elle 2720 en a 14. La grille exterieure fait deja le travail. */ 2721 + '.footer-more{column-count:1 !important}' 2722 + '@media(max-width:760px){.footer-inner{grid-template-columns:1fr 1fr !important}}' 2723 + '@media(max-width:460px){.footer-inner{grid-template-columns:1fr !important}}' 2724 /* âââ [01/08, Romain] LES BARRES DE RECHERCHE ETAIENT DISPARATES âââââââ 2725 Releve page par page : leaderboard un rectangle de 280px arrondi a 10, 2726 AI Tools un rectangle plein de 8 avec un lisere ORANGE de 2px, DNA un 2727 rectangle a 1px, Projects une PASTILLE completement arrondie. Quatre 2728 pages, quatre formes. On aligne tout sur celle d'AI Tools, que Romain 2729 cite en exemple : rectangle a 10px, lisere 2px, dans la couleur de la 2730 famille de la page (--osai-fam, posee par le JS a partir du chemin, 2731 la meme table que le titre et les points du menu). 2732 Le :not([type=range]) n'est pas la pour filtrer mais pour la 2733 SPECIFICITE : (0,3,1) bat le input{border-radius:8px !important} 2734 global de cinq pages sans dependre de l'ordre des feuilles. */ 2735 + 'input.osai-sb:not([type="range"]){border:2px solid var(--osai-fam,#6366f1) !important;' 2736 + 'border-radius:10px !important;background:var(--surface,#fff) !important;' 2737 + 'padding:11px var(--osai-sb-pr,14px) 11px var(--osai-sb-pl,14px) !important;' 2738 + 'font-family:inherit !important;font-size:.88rem !important;' 2739 + 'color:var(--txt,#111) !important;
2739outline:none !important;box-shadow:none !important;' 2740 + 'transition:box-shadow .15s,border-color .15s !important}' 2741 + 'input.osai-sb:not([type="range"])::placeholder{color:var(--muted,#5a6d87) !important;opacity:1}' 2742 + 'input.osai-sb:not([type="range"]):hover{border-color:var(--osai-fam,#6366f1) !important;' 2743 + 'box-shadow:0 0 0 3px color-mix(in srgb, var(--osai-fam,#6366f1) 14%, transparent) !important}' 2744 + 'input.osai-sb:not([type="range"]):focus{border-color:var(--osai-fam,#6366f1) !important;' 2745 + 'box-shadow:0 0 0 3px color-mix(in srgb, var(--osai-fam,#6366f1) 26%, transparent) !important}' 2746 /* une loupe posee en fond, pour les champs qui n'en ont pas : le decalage 2747 du texte ne s'applique QUE dans ce cas (classe osai-sb-ic) */ 2748 + 'input.osai-sb.osai-sb-ic:not([type="range"]){padding-left:38px !important;' 2749 + 'background-repeat:no-repeat !important;background-position:13px 50% !important;' 2750 + 'background-size:16px 16px !important}' 2751 + '.ohp-ttl>.osai-ai-tag{margin-top:4px;border-color:rgba(255,255,255,.35);color:#c7d2fe}' 2752 + '.ohp-ttl>.osai-ai-tag b{color:#fff}' 2753 /* [31/07, Romain] « le texte sous le titre ne prend pas toute la largeur, 2754 il retourne a la ligne ». Chaque page bridait son intro a 760px ou 64ch 2755 â un usage typographique classique, mais Romain veut la largeur du 2756 contenu, comme les autres elements. Une seule regle, pour toutes. */ 2757 + '.lead,.page-header p,.hero p,.mcp-lede,.dna-sub,.rel-hero .lead,.sp-hero p{max-width:none !important}' 2758 + '.osai-h1{font-size:clamp(1.85rem,3.2vw,2.25rem) !important;font-weight:800 !important;' 2759 + 'letter-spacing:-.02em !important;line-height:1.15 !important;margin-bottom:12px !important}' 2760 + '.crumbs,.breadcrumb{height:0 !important;overflow:hidden !important;' 2761 + 'padding-bottom:0 !important;margin-bottom:0 !important;visibility:hidden !important}' 2762 /* [02/08/2026, audit] Le bouton de menu mesurait 36x28 : au-dessus du 2763 minimum WCAG (24x24) mais sous les recommandations d'Apple (44) et de 2764 Google (48), sur LE controle le plus utilise du site en mobile. On 2765 agrandit la ZONE TACTILE seulement : les trois barres gardent 24x2, la 2766 barre garde 64px de haut, le centrage ne bouge pas (mesure du 02/08). */ 2767 + '.hamburger{min-width:44px;min-height:44px;align-items:center;justify-content:center}'; 2768 document.head.appendChild(styleEl); 2769 2770 /* âââ Puces de categorie dans LES DEUX menus (30/07/2026) âââââââââââââââ 2771 Le site a deux menus â #moreMenu (bureau) et #osaiMM (mobile) â et la 2772 regle de la maison, apprise a nos depens, est qu'on oublie toujours 2773 l'un des deux. Parade : UNE fonction qui decore tout lien de menu 2774 qu'elle trouve, appelee en differe (setTimeout 0) une fois que le 2775 passage d'initialisation a construit les deux. Impossible d'en 2776 oublier un : il n'y a qu'un seul endroit a maintenir. 2777 La famille se deduit du PREMIER SEGMENT du chemin â a.pathname resout 2778 les hrefs relatifs tout seul â apres retrait du prefixe de langue. 2779 Un segment inconnu ne recoit RIEN : pas de puce plutot qu'une puce 2780 fausse. */ 2781 function osaiCatDots() { 2782 var FAM = { 2783 project: ['projects','project','trending','emerging','releases','top','hf','categories','health','pulse','dna','stacks','countries','spaces','graph'], 2784 tool: ['tools','tool','compare-tools','vs','alternatives','agents','guides','submit','mcp','skills'], 2785 'model-open': ['compare','leaderboard','models-vs','model','models','media','local-ai','makers','maker'], 2786 'model-paid': ['pricing','paid-models'] 2787 }; 2788 var SEG = {}; 2789 for (var f in FAM) for (var i = 0; i < FAM[f].length; i++) SEG[FAM[f][i]] = f; 2790 window.__osaiSEG = SEG; /* la meme table sert au H1 des pages */ 2791 /* le H1 de la page est colore ICI, apres construction de la table : appele 2792 plus tot, il tombait sur l'indigo par defaut (mesure sur /local-ai.html) */ 2793 /* la couleur de la famille, disponible pour toute la page */ 2794 var segCour = (location.pathname.replace(/^\/(fr|de|es|it|pt)(?=\/)/, '').split('/')[1] || '').replace(/\.html$/, ''); 2795 var famCour = SEG[segCour] || null; 2796 document.documentElement.style.setProperty('--osai-fam', 2797 famCour ? getComputedStyle(document.documentElement).getPropertyValue('--cat-' + famCour).trim() || '#6366f1' : '#6366f1'); 2798 if (typeof accentPageTitle === 'function') accentPageTitle(); 2799 if (typeof carteAssistant === 'function') carteAssistant(); /* avant le marquage */ 2800 if (typeof marquerIA === 'function') marquerIA(); 2801 if (typeof uniformiserRecherches === 'function') { 2802 uniformiserRecherches(); 2803 setTimeout(uniformiserRecherches, 1200); /* champs construits a la volee */ 2804 } 2805 /* Le filet : au chargement, puis a chaque fois que la page ajoute des 2806 cartes (les listes se rendent en JavaScript, souvent apres coup). */ 2807 if (typeof osaiRelierSorties === 'function') { 2808 osaiRelierSorties(); 2809 setTimeout(osaiRelierSorties, 1500); 2810 setTimeout(osaiRelierSorties, 4000); 2811 if (window.MutationObserver) { 2812 var relMinuteur = null; 2813 new MutationObserver(function(){ 2814 clearTimeout(relMinuteur); 2815 relMinuteur = setTimeout(function(){ osaiRelierSorties(); }, 350); 2816 }).observe(document.body, { childList: true, subtree: true }); 2817 } 2818 } 2819 /* [30/07 soir, v3] La barre du haut aussi (demande de Romain) : Projects, 2820 AI Tools, Models (/leaderboard), DNA prennent leur famille. Le bouton 2821 Submit (.nav-cta) est exclu : c'est un CTA de marque, pas une entree 2822 de catalogue. Latest et Plans n'appartiennent a aucune famille de 2823 contenu â Latest melange les quatre, Plans est l'offre du site â et 2824 gardent donc l'indigo de marque : le doute ne colore pas. */ 2825 var links = document.querySelectorAll('#moreMenu a, #osaiMM a, #site-nav .nav-links > a:not(.nav-cta)'); 2826 for (var j = 0; j < links.length; j++) { 2827 var a = links[j]; 2828 var path = (a.pathname || '').replace(/^\/(fr|de|es|it|pt)(?=\/)/, ''); 2829 var seg = (path.split('/')[1] || '').replace(/\.html$/, ''); 2830 var fam = SEG[seg]; 2831 /* on teinte l'icone existante ; une entree sans icone ou sans 2832 famille reconnue ne change pas â le doute ne colore pas */ 2833 if (fam && a.querySelector('svg')) a.classList.add('cat-ic-' + fam); 2834 } 2835 } 2836 /* [30/07, nuit] Constat de Romain : teintes visibles sur l'accueil, 2837 indigo partout ailleurs â alors que le serveur sert le meme fichier a 2838 toutes les pages (verifie variante par variante). Quelque chose, sur 2839 certaines pages, reconstruit ou retraduit les menus APRES notre passage 2840 et emporte les classes. Plutot que courir apres le coupable exact : 2841 la pose des teintes est IDEMPOTENTE (classList.add), donc on la 2842 rappelle a chaque moment charniere. La derniere passe a 1,6 s couvre 2843 toute reinjection tardive. */ 2844 setTimeout(osaiCatDots, 0); 2845 setTimeout(osaiCatDots, 400); 2846 setTimeout(osaiCatDots, 1600); 2847 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiCatDots); 2848 window.addEventListener('load', osaiCatDots); 2849 2850 /* âââ Maillage fiches <->
2850 hubs (31/07/2026, nuit) âââââââââââââââââââââââ 2851 Sur la fiche d'un projet qui est un serveur MCP ou un pack de skills, 2852 un encart le dit et renvoie au hub. Injecte ICI et non dans le 2853 generateur des fiches : les 60 000 fiches (6 langues) le recoivent 2854 d'un coup, sans regeneration ni retraduction. Les listes de slugs sont 2855 ecrites par les generateurs de hubs a chaque rafraichissement â 2856 mcp-slugs.json et skills-slugs.json, ~5 Ko chacun, charges UNIQUEMENT 2857 sur les pages /project/. Un 404 (listes pas encore generees) est 2858 silencieux : pas d'encart plutot qu'une erreur. */ 2859 (function () { 2860 var m = location.pathname.match(/^\/(?:(?:fr|de|es|it|pt)\/)?project\/([a-z0-9-]+)\.html$/); 2861 if (!m) return; 2862 var slug = m[1]; 2863 var faits = 0; 2864 function encart(txt, url, fam) { 2865 var h1 = document.querySelector('h1'); 2866 if (!h1 || document.getElementById('hubx-' + fam)) return; 2867 var d = document.createElement('a'); 2868 d.id = 'hubx-' + fam; 2869 d.href = url; 2870 d.className = 'hubx'; 2871 d.innerHTML = '<b>' + txt + '</b><span>See it in the hub \u2192</span>'; 2872 h1.parentNode.insertBefore(d, h1.nextSibling); 2873 } 2874 function charge(fmap, farr, txt, url, fam) { 2875 /* nouveau format : {slug: categorie} -> la banniere DIT quelle 2876 categorie et pointe sur l'ancre de sa section ; repli sur 2877 l'ancien tableau de slugs tant que la carte n'est pas generee */ 2878 fetch(fmap).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }).then(function (m) { 2879 if (m && !Array.isArray(m) && typeof m === 'object') { 2880 if (m[slug]) { 2881 var cat = String(m[slug]); 2882 var ancre = cat.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); 2883 encart(txt + ' \u2014 ' + cat, url + '#' + ancre, fam); 2884 } 2885 return; 2886 } 2887 fetch(farr).then(function (r) { return r.ok ? r.json() : []; }).then(function (l) { 2888 if (l && l.indexOf && l.indexOf(slug) !== -1) encart(txt, url, fam); 2889 }).catch(function () {}); 2890 }); 2891 } 2892 var st = document.createElement('style'); 2893 st.textContent = '.hubx{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:14px 0;' 2894 + 'padding:10px 16px;border:1px solid var(--cat-tool,#f59e0b);border-left:4px solid var(--cat-tool,#f59e0b);' 2895 + 'border-radius:10px;background:rgba(245,158,11,.07);text-decoration:none;color:inherit;font-size:.88rem}' 2896 + '.hubx b{font-weight:600}.hubx span{color:var(--cat-tool,#f59e0b);font-weight:600;white-space:nowrap}' 2897 + '.hubx:hover{background:rgba(245,158,11,.14)}'; 2898 document.head.appendChild(st); 2899 charge('/mcp-map.json', '/mcp-slugs.json', '\u26A1 This project is an MCP server', '/mcp/', 'mcp'); 2900 charge('/skills-map.json', '/skills-slugs.json', '\uD83D\uDCDA This project ships AI skills', '/skills/', 'skills'); 2901 })(); 2902 2903 // Détecter la page d'accueil (multilingue). LP est déjà défini plus haut. 2904 var path = window.location.pathname; 2905 var _pnl = (NAV_LANG === 'en') ? path : (path.replace('/' + NAV_LANG, '') || '/'); 2906 var isHome = (_pnl === '/' || _pnl === '/index.html' || _pnl === ''); 2907 // Préfixe des ancres home : "" si déjà sur l'accueil (même langue), sinon "/xx/" 2908 var H = isHome ? '' : (LP + '/'); 2909 2910 // âââ NAV âââ 2911 function ic(p){ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'+p+'</svg>'; } 2912 var navHTML = '' 2913 + '<nav>' 2914 /* [30/07] Plus de mot-symbole : l'anneau seul, plus grand. Le lien n'ayant 2915 plus de texte, il lui faut un nom accessible explicite. */ 2916 + ' <a href="/" aria-label="olud.ai" title="olud.ai" style="text-decoration:none;display:flex;align-items:center">' 2917 + ' <svg width="40" height="40" viewBox="0 0 100 100" aria-hidden="true">' + '<defs> <linearGradient id="oludGradH" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#2540E8"/><stop offset="1" stop-color="#8B3FEF"/> </linearGradient> <mask id="oludNotchH" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100"> <rect width="100" height="100" fill="#fff"/> <circle cx="86.72" cy="19.91" r="15.91" fill="#000"/> </mask> </defs>
2917 <circle cx="50" cy="50" r="39.61" fill="none" stroke="url(#oludGradH)" stroke-width="20.78" mask="url(#oludNotchH)"/> <circle cx="86.72" cy="19.91" r="10.71" fill="url(#oludGradH)"/>' + '</svg>' 2918 /* [17/08, Romain] Le mot-symbole REVIENT a cote de l'anneau : « olud.ai » 2919 comme sur le mockup de la page promote, puis agrandi et « .ai » fonce a 2920 sa demande. Le style inline bat le .logo span des pages (accent clair) ; 2921 la variable --logo-ai garde le mode nuit lisible (redefinie ci-dessous). */ 2922 + ' <span class="logo" style="margin-left:10px;font-size:1.24rem;font-weight:700;letter-spacing:.01em">olud<span style="color:var(--logo-ai,#4338ca)">.ai</span></span>' 2923 + ' </a>' 2924 /* [19/08] Le blindage de la barre (regles !important des pages) battait le 2925 style inline du bouton Promote : texte gris, icone invisible. La charte 2926 donne la parade : #site-nav + !important, injecte APRES tout le reste. */ 2927 + ' <style>body.dark{--logo-ai:#8b93f8}' 2928 + '#site-nav .nav-soutien{background:linear-gradient(100deg,#5558e8,#8b3fef) !important;color:#fff !important;border:none !important;border-radius:999px !important;padding:6px 14px !important;box-shadow:0 3px 10px rgba(85,88,232,.4) !important}' 2929 + '#site-nav .nav-soutien span{color:#fff !important}' 2930 + '#site-nav .nav-soutien svg{color:#fff !important;stroke:#fff !important}' 2931 + '#site-nav .nav-soutien:hover{filter:brightness(1.08);color:#fff !important}' 2932 + '</style>' 2933 + ' <div class="nav-links">' 2934 + ' <button class="nav-close" id="navClose" aria-label="Close menu">×</button>' 2935 /* [02/08/2026, Romain] « dans la barre du haut on devrait mettre Releases a la 2936 place de Plans ». Releases est du contenu vivant, mis a jour chaque heure â 2937 c'est une raison de revenir. Plans est une page de conversion, consultee une 2938 fois : elle reste dans le menu Explore et dans le pied de page. Meme icone 2939 que l'entree Releases du mega-menu, pour qu'on reconnaisse la meme chose. */ 2940 + ' <a href="/releases.html">'+ic('<path d="M12 2v6"/><path d="m16 6-4 4-4-4"/><rect width="18" height="10" x="3" y="12" rx="2"/>')+'<span>Releases</span></a>' 2941 + ' <a href="/projects.html">'+ic('<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>')+'<span>Projects</span></a>' 2942 + ' <a href="/tools.html">'+ic('<path d="M14.7 6.3a4 4 0 0 1-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 1 5.4-5.4z"/>')+'<span>AI Tools</span></a>' 2943 + ' <a href="/leaderboard.html">'+ic('<line x1="6" y1="20" x2="6" y2="14"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="18" y1="20" x2="18" y2="10"/>')+'<span>Models</span></a>' 2944 + ' <a href="/latest.html">'+ic('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>')+'<span>Latest</span></a>' 2945 + ' <a href="/dna.html">'+ic('<path d="M6 3c0 4 12 5 12 9s-12 5-12 9"/><path d="M18 3c0 4-12 5-12 9s12 5 12 9"/><path d="M8 8h8M8 16h8"/>')+'<span>DNA</span></a>' 2946 /* [17/08, Romain] La pilule Support devient Promote : l'offre payante est 2947 en vente, c'est elle qui merite la place en vitrine. Support reste 2948 accessible par le mega-menu Explore. */ 2949 + ' <a href="/promote.html" class="nav-soutien" style="background:linear-gradient(100deg,#5558e8,#8b3fef);border-radius:999px;padding:6px 14px;color:#fff;box-shadow:0 3px 10px rgba(85,88,232,.4)">'+ic('<path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/>')+'<span>Promote</span></a>' 2950 + ' <div class="nav-dropdown">' 2951 + ' <button class="nav-dropdown-btn" id="moreMenuBtn">Explore ▾</button>' 2952 + ' <div class="nav-dropdown-menu mega" id="moreMenu">' 2953 + ' <div class="mega-grid">' 2954 + ' <div class="mega-col"><div class="mega-h">Tools & Agents</div>' 2955 /* [29/07] Pulse ranking et Security advisories ajoutes ICI aussi. Le site a 2956 DEUX menus : le hamburger mobile (osaiMM) et ce panneau bureau « Explore » 2957 (#moreMenu). Trois heures ont ete perdues a corriger le premier pendant que 2958 Romain regardait le second â sa capture montrait « Community » et « Tools 2959 Directory », absents d'osaiMM, et cet indice n'a pas ete lu. */ 2960 + ' <a href="/pulse.html">'+ic('<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>')+'<span><b>Pulse ranking</b></span></a>' 2961 + ' <a href="/security.html">'+ic('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>')+'<span><b>Security advisories</b></span></a>' 2962 + ' <a href="/agents/">'+ic('<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>')+'<span><b>AI Agents</b><em>Open-source frameworks</em></span></a>' 2963 + ' <a href="/tool/">'+ic('<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>')+'<span><b>Tool Guides</b><em>76 in-depth guides</em></span></a>' 2964 + ' <a href="/tool/">'+ic('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>')+'<span>Tool profiles</span></a>' 2965 + ' <a href="/stack-builder.html">'+ic('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/>')+'<span>Stack Builder</span></a>' 2966 /* [24/07] Entree « Community » (/members) REACTIVEE : on ouvre la decouverte 2967 des membres (Phase A du social). Sans elle, personne ne trouvait personne. */ 2968 + ' <a href="/tools.html">'+ic('<path d="M14.7 6.3a4 4 0 0 1-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 1 5.4-5.4z"/>')+'<span><b>Tools catalog</b><em>225+ tools</em></span></a>' 2969 + ' <a href="/skills/">'+ic('<path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/>')+'<span><b>AI Skills</b><em>199 analyzed</em></span></a>' 2970 + ' <a href="/mcp/">'+ic('<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M6 8h12l-1 7a5 5 0 0 1-10 0z"/>')+'<span><b>MCP Servers</b><em>237 analyzed</em></span></a>' 2971 + ' <a href="/compare-tools.html">'+ic('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>')+'<span><b>Compare Tools</b></span></a>' 2972 + ' <a href="/category.html">'+ic('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>')+'<span><b>Categories</b></span></a>' 2973 + ' </div>' 2974 + ' <div class="mega-col"><div class="mega-h">Models</div>' 2975 + ' <a href="/local-ai.html">'+ic('<rect width=\"16\" height=\"16\" x=\"4\" y=\"4\" rx=\"2\"/><rect width=\"6\" height=\"6\" x=\"9\" y=\"9\"/><path d=\"M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2\"/>')+'<span><b>Local AI</b><em>What can your machine run?</em></span></a>' 2976 + ' <a href="/makers/">'+ic('<path d="M3 21h18"/><path d="M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/>')+'<span><b>Model makers</b></span></a>' 2977 + ' <a href="/pricing.html">'+ic('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>')+'<span><b>AI Pricing</b><em>Live costs + calculator</em></span></a>' 2978 + ' <a href="/models-vs/">'+ic('<circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/>')+'<span><b>Free vs Paid</b><em>Open vs frontier</em></span></a>' 2979 + ' <a href="/leaderboard.html">'+ic('<line x1="6" y1="20" x2="6" y2="14"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="18" y1="20" x2="18" y2="10"/>')+'<span><b>Leaderboard</b></span></a>' 2980 + ' <a href="/media-leaderboard.html">'+ic('<line x1="6" y1="20" x2="6" y2="14"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="18" y1="20" x2="18" y2="10"/>')+'<span><b>Media models</b></span></a>' 2981 + ' <a href="/compare.html">'+ic('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>')+'<span><b>Compare Models</b></span></a>' 2982 + ' <a href="/vs/">'+ic('<path d="m9 7-5 5 5 5"/><path d="m15 7 5 5-5 5"/>')+'<span><b>Versus · X vs Y</b></span></a>' 2983 + ' <a href="/model/">'+ic('<rect width="18" height="14" x="3" y="5" rx="2"/><path d="M7 15h4M7 11h10"/>')+'<span><b>Model profiles</b></span></a>' 2984 + ' <a href="/paid-models.html">'+ic('<path d="M7.5 7.5h.01"/><path d="M19 9.3V5a2 2 0 0 0-2-2h-4.3a2 2 0 0 0-1.4.6l-7.7 7.7a2 2 0 0 0 0 2.8l4.3 4.3a2 2 0 0 0 2.8 0l7.7-7.7a2 2 0 0 0 .6-1.4Z"/>')+'<span><b>Paid Models</b></span></a>' 2985 + ' </div>' 2986 + ' <div class="mega-col"><div class="mega-h">Alternatives</div>' 2987 + ' <a href="/alternatives/">'+ic('<path d="m2 9 3-3 3 3"/><path d="M13 18H7a2 2 0 0 1-2-2V6"/><path d="m22 15-3 3-3-3"/><path d="M11 6h6a2 2 0 0 1 2 2v10"/>')+'<span><b>All Alternatives</b><em>129 tools mapped</em></span></a>' 2988 + ' <a href="/alternatives/jira.html">'+ic('<path d="M3 3h18v18H3z"/><path d="M8 12h8M12 8v8"/>')+'<span><b>Jira alternatives</b></span></a>' 2989 + ' <a href="/alternatives/chatgpt.html">'+ic('<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>')+'<span><b>ChatGPT alternatives</b></span></a>' 2990 + ' <a href="/alternatives/midjourney.html">'+ic('<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>')+'<span><b>Midjourney alternatives</b></span></a>' 2991 + ' <a href="/alternatives/figma.html">'+ic('<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>')+'<span><b>Figma alternatives</b></span></a>' 2992 + ' <a href="/alternatives/slack.html">'+ic('<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>')+'<span><b>Slack alternatives</b></span></a>' 2993 + ' <a href="/alternatives/zoom.html">'+ic('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>')+'<span><b>Zoom alternatives</b></span></a>' 2994 + ' <a href="/blog/alternatives/github-copilot.html">'+ic('<path d="M16 18 22 12 16 6"/><path d="M8 6 2 12l6 6"/>')+'<span><b>Copilot alternatives</b></span></a>' 2995 + ' <a href="/alternatives/google-analytics.html">'+ic('<path d="M3 3v18h18"/><path d="M7 15l4-4 3 3 6-6"/>')+'<span><b>Analytics alternatives</b></span></a>' 2996 + ' <a href="/alternatives/cursor.html">'+ic('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')+'<span><b>Cursor alternatives</b></span></a>' 2997 + ' <a href="/alternatives/notion.html">'+ic('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>')+'<span><b>Notion alternatives</b></span></a>' 2998 + ' <a href="/alternatives/adobe-photoshop.html">'+ic('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>')+'<span><b>Photoshop alternatives</b></span></a>' 2999 + ' </div>' 3000 + ' <div class="mega-col"><div class="mega-h">Discover</div>' 3001 + ' <a href="/projects.html">'+ic('<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>')+'<span><b>Projects</b></span></a>' 3002 + ' <a href="/spaces.html">'+ic('<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>')+'<span><b>Spaces</b></span></a>' 3003 + ' <a href="/vertical/">'+ic('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>')+'<span><b>Industries</b><em>AI by sector: health, financeâ¦</em></span></a>' 3004 + ' <a href="/dna.html">'+ic('<path d="M6 3c0 4 12 5 12 9s-12 5-12 9"/><path d="M18 3c0 4-12 5-12 9s12 5 12 9"/><path d="M8 8h8M8 16h8"/>')+'<span><b>DNA families</b><em>How projects share DNA</em></span></a>' 3005 + ' <a href="/languages.html">'+ic('<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>')+'<span><b>Languages</b><em>Python, Rust, Go\u2026</em></span></a>' 3006 + ' <a href="/countries.html">'+ic('<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><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"/>')+'<span><b>Countries</b><em>Where projects come from</em></span></a>' 3007 + ' </div>' 3008 + ' <div class="mega-col"><div class="mega-h">What's new</div>' 3009 + ' <a href="/latest.html">'+ic('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>')+'<span><b>Latest</b></span></a>' 3010 + ' <a href="/emerging.html">'+ic('<path d="M12 2v8M8 6l4-4 4 4"/><circle cx="12" cy="16" r="5"/>')+'<span><b>Emerging</b><em>Detected by our signals</em></span></a>' 3011 + ' <a href="/releases.html">'+ic('<path d="M12 2v6"/><path d="m16 6-4 4-4-4"/><rect width="18" height="10" x="3" y="12" rx="2"/>')+'<span><b>Releases</b><em>New versions daily</em></span></a>' 3012 + ' <a href="/news.html">'+ic('<path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/>')+'<span><b>News</b></span></a>' 3013 + ' <a href="/blog.html">'+ic('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>')+'<span><b>Blog</b></span></a>' 3014 + ' <a href="/health.html">'+ic('<path d="M19.5 13.6 12 21l-7.5-7.4A5.2 5.2 0 1 1 12 6.3a5.2 5.2 0 1 1 7.5 7.3z"/>')+'<span><b>Project Health</b><em>Alive or abandoned?</em></span></a>' 3015 + ' <a href="/dashboard.html">'+ic('<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>')+'<span><b>Dashboards</b></span></a>' 3016 + ' </div>' 3017 + ' <div class="mega-col"><div class="mega-h">Site & Data</div>' 3018 + ' <a href="/about.html">'+ic('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>')+'<span><b>About</b><em>The project & the maker</em></span></a>' 3019 + ' <a href="/methodology.html">'+ic('<path d="M14.7 6.3a4 4 0 0 1-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 1 5.4-5.4l-2.3 2.3-1-1z"/>')+'<span><b>Methodology</b><em>How we score & rank</em></span></a>' 3020 + ' <a href="/contact.html">'+ic('<path d="M4 4h16v12H5.17L4 17.17V4z"/><path d="M8 9h8"/>
3020<path d="M8 12h5"/>')+'<span><b>Contact</b><em>Corrections, questions, press</em></span></a>' 3021 /* [04/08 pivot] Open Data API, Developer API, Documentation, Connect, Advertise : SUPPRIMES du mega-menu (l'attribut hidden etait ecrase par le display du CSS). */ 3022 + ' <a href="/support.html">'+ic('<path d="M19.5 13.6 12 21l-7.5-7.4A5.2 5.2 0 1 1 12 6.3a5.2 5.2 0 1 1 7.5 7.3z"/>')+'<span><b>Support us</b><em>Keep olud.ai independent</em></span></a>' 3023 + ' <a href="/favorites.html">'+ic('<path d="M12 17.8 5.8 21 7 14.1 2 9.3l7-1L12 2l3 6.3 7 1-5 4.8 1.2 6.9z"/>')+'<span><b>Favorites</b><em>Your saved tools</em></span></a>' 3024 + ' <a href="/my-dashboard.html">'+ic('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 9h6"/><path d="M15 15h6"/>')+'<span><b>My Dashboard</b><em>Compose your own â members</em></span></a>' 3025 + ' <a href="/submit.html">'+ic('<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>')+'<span><b>Submit a tool or project</b><em>Add it to the hub</em></span></a>' 3026 + ' <a href="/badge.html">'+ic('<circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/>')+'<span><b>Get a badge</b><em>For your README</em></span></a>' 3027 + ' </div>' 3028 + ' </div>' 3029 + ' <div class="mega-foot"><span class="mega-foot-t">'+ic('<path d="m12 3 1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3z"/>')+' <b>Featured:</b> Open-source AI agents \u2014 CrewAI, OpenHands, Browser Use</span><a href="/agents/" class="mega-foot-cta">Explore agents →</a></div>' 3030 + ' </div>' 3031 + ' </div>' 3032 /* [29/07] Bouton « Submit a tool or project » dans la barre bureau â demande de Romain 3033 apres la reparation du formulaire : le parcours de soumission merite une 3034 entree visible, pas seulement une ligne dans le mega-menu. La cle « Submit 3035 a tool » existe deja dans les 5 dictionnaires ; NAV_LMAP prefixe le lien. */ 3036 + ' <a href="/submit.html" class="nav-cta" aria-label="Submit a tool or project" title="Submit a tool or project">'+'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">'+'<path d="M12 6v12"/><path d="M6 12h12"/></svg></a>' 3037 + ' <button class="fav-nav-btn" id="favNavBtn" aria-label="My favorites" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openFavorites)openFavorites();},60)" title="My favorites">' 3038 + ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>' 3039 + ' <span class="fav-count-badge" id="favCountBadge">0</span>' 3040 + ' </button>' 3041 + ' <span class="acct-wrap">' 3042 + ' <a class="acct-nav-btn" id="acctNavBtn" href="/account.html" aria-label="Account" title="Sign in">' 3043 + ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>' 3044 + ' </a>' 3045 + ' <div class="acct-menu" id="acctMenu"></div>' 3046 + ' </span>' 3047 + ' <button class="cmdk-trigger" id="cmdkTrigger" aria-label="Search" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openCmdK)openCmdK();},60)">' 3048 + ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' 3049 + ' <span>Search</span><kbd>⌘K</kbd>' 3050 + ' </button>' 3051 + ' <div class="nav-dropdown lang-dd">' 3052 + ' <button class="nav-dropdown-btn" id="langMenuBtn" aria-label="Language / Langue"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><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"/></svg> <span id="langCurrentLbl">EN</span> ▾</button>' 3053 + ' <div class="nav-dropdown-menu" id="langMenu"></div>'
3054 + ' </div>' 3055 + ' <button class="theme-toggle" id="themeToggle" aria-label="Toggle light/dark mode"><div class="theme-toggle-knob"></div></button>' 3056 + ' </div>' 3057 + ' <button class="theme-toggle-nav" id="themeToggleNav" onclick="if(window.applyTheme)applyTheme(!document.body.classList.contains(\'dark\'))" aria-label="Toggle theme">' 3058 + ' <svg class="tt-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>' 3059 + ' <svg class="tt-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>' 3060 + ' </button>' 3061 + ' <button class="hamburger" id="hamburger" aria-label="Menu"><span></span><span></span><span></span></button>' 3062 + '</nav>'; 3063 3064 // âââ FOOTER âââ 3065 /* Une icone au trait de 2px, la meme graisse que celles du menu du site. */ 3066 function FIC(d){ 3067 return '<svg class="fic" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor"' 3068 + ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + d + '</svg>'; 3069 } 3070 var footerHTML = '' 3071 + '<footer><div class="wrap">' 3072 /* [01/08, Romain] « le pied de page utilise des emojis que nous n'utilisons 3073 plus ». Il y en avait vingt-trois, en entites HTML. Ici trois icones au 3074 trait de 2px, comme celles du menu ; ailleurs, plus rien. */ 3075 + ' <div class="loop-strip"><span class="loop-t">Stay in the loop</span>' 3076 + ' <a href="/reports/">' + FIC('<path d="M4 4h16v12H8l-4 4z"/><path d="M8 9h8M8 12.5h5"/>') + 'Weekly report</a>' 3077 + ' <a href="/feed.php">' + FIC('<path d="M5 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/><path d="M4 11a9 9 0 0 1 9 9M4 5a15 15 0 0 1 15 15"/>') + 'RSS</a>' 3078 + ' <a href="/favorites.html">' + FIC('<path d="M12 20s-7-4.5-7-9.2A3.9 3.9 0 0 1 12 8a3.9 3.9 0 0 1 7 2.8C19 15.5 12 20 12 20z"/>') + 'My favorites</a>' 3079 + ' </div>' 3080 + ' <div class="footer-inner">' 3081 + ' <div class="footer-news">' 3082 + ' <div class="footer-news-txt"><h4>Stay in the loop</h4><p>The best of open-source AI in your inbox — new models, trending projects and releases. No spam, unsubscribe anytime.</p></div>' 3083 + ' <form class="footer-news-form" id="newsletterForm">' 3084 + ' <input type="email" id="newsletterEmail" required placeholder="[email protected]" aria-label="Email address">' 3085 + ' <button type="submit">Subscribe</button>' 3086 + ' </form>' 3087 + ' <div class="footer-news-msg" id="newsletterMsg"></div>' 3088 + ' </div>' 3089 + ' <div class="footer-brand">' 3090 + ' <div style="display:flex;align-items:center;gap:8px;margin-bottom:.5rem">' 3091 + ' <svg width="44" height="44" viewBox="0 0 100 100" aria-hidden="true">' + '<defs> <linearGradient id="oludGradF" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#2540E8"/><stop offset="1" stop-color="#8B3FEF"/> </linearGradient> <mask id="oludNotchF" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100"> <rect width="100" height="100" fill="#fff"/> <circle cx="86.72" cy="19.91" r="15.91" fill="#000"/> </mask> </defs> <circle cx="50" cy="50" r="39.61" fill="none" stroke="url(#oludGradF)" stroke-width="20.78" mask="url(#oludNotchF)"/> <circle cx="86.72" cy="19.91" r="10.71" fill="url(#oludGradF)"/>' + '</svg>' 3092 /* [30/07] Pied de page : le mot-symbole etait trop gras et trop petit a cote 3093 d'un anneau de 44 px. Graisse 600 au lieu de l'heritage 800, corps 1.35rem, 3094 et le « .ai » suit a 65 % plutot que d'etre ecrase a .6rem. */ 3095 + ' <div class="logo" style="text-transform:none;letter-spacing:-.02em;font-size:1.35rem;font-weight:600">olud<small style="font-family:\'DM Mono\';font-size:.65em;color:var(--muted);font-weight:400">.ai</small></div>' 3096 + ' </div>' 3097 + ' <p style="margin-top:8px">The AI hub.<br>Open and commercial AI, side by side.<br>Built for the global community.</p>' 3098 + ' <div class="footer-social"><a href="https://x.com/tecOpensourceAI" target="_blank" rel="noopener" aria-label="Follow us on X" title="Follow @tecOpensourceAI on X"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231zm-1.161 17.52h1.833L7.084 4.126H5.117l11.966 15.644z"/></svg></a><a href="https://bsky.app/profile/oludai.bsky.social" target="_blank" rel="noopener" aria-label="Follow us on Bluesky" title="Follow us on Bluesky"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908
30980 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/></svg></a><a href="https://mastodon.social/@opensourceaitech" target="_blank" rel="me noopener" aria-label="Follow us on Mastodon" title="Follow us on Mastodon"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg></a><a href="https://www.linkedin.com/company/136084906" target="_blank" rel="noopener" aria-label="Follow us on LinkedIn" title="Follow us on LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 1 1 0-4.125 2.062 2.062 0 0 1 0 4.125zM7.119 20.452H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"/></svg></a></div>' 3099 + ' </div>' 3100 /* [01/08] Colonnes refaites. Avant : « Explore » melangeait tout avec un 3101 emoji par ligne, et « More » alignait 36 liens dont sept DOUBLONS 3102 (Prompts, Tool profiles, Pulse, Stack Builder, Weekly report, RSS...). 3103 Les pages ouvertes depuis â MCP, Skills, Local AI, constructeurs â n'y 3104 figuraient meme pas, alors que ce sont celles qu'on veut mailler. */ 3105 + ' <div class="footer-col">' 3106 + ' <h4>Explore</h4>' 3107 + ' <a href="/latest.html">Latest</a>' 3108 + ' <a href="/emerging.html">Emerging</a>' 3109 + ' <a href="/releases.html">Releases</a>' 3110 + ' <a href="' + H + '#trending">Trending</a>' 3111 + ' <a href="' + H + '#projects">Top projects</a>' 3112 + ' <a href="/tools.html">AI tools</a>' 3113 + ' <a href="/spaces.html">Hugging Face spaces</a>' 3114 + ' <a href="/category.html">Categories</a>' 3115 + ' <a href="/dna.html">DNA families</a>' 3116 + ' <a href="/languages.html">Languages</a>' 3117 + ' <a href="/trends.html">Trends</a>' 3118 + ' <a href="/news.html">News feed</a>' 3119 + ' </div>' 3120 + ' <div class="footer-col">' 3121 + ' <h4>Compare & rank</h4>' 3122 + ' <a href="/leaderboard.html">LLM leaderboard</a>' 3123 + ' <a href="/media-leaderboard.html">Media leaderboard</a>' 3124 + ' <a href="/compare.html">Compare models</a>' 3125 + ' <a href="/compare-tools.html">Compare tools</a>' 3126 + ' <a href="/vs/">Versus · X vs Y</a>' 3127 + ' <a href="/models-vs/">Free vs paid</a>' 3128 + ' <a href="/pricing.html">AI pricing</a>' 3129 + ' <a href="/paid-models.html">Paid models</a>' 3130 + ' <a href="/makers/">Model makers</a>' 3131 + ' <a href="/pulse.html">Pulse ranking</a>' 3132 + ' <a href="/health.html">Project health</a>'
3133 + ' </div>' 3134 + ' <div class="footer-col">' 3135 + ' <h4>Build</h4>' 3136 + ' <a href="/local-ai.html">Run AI locally</a>' 3137 + ' <a href="/mcp/">MCP servers</a>' 3138 + ' <a href="/skills/">AI skills</a>' 3139 + ' <a href="/agents/">AI agents</a>' 3140 + ' <a href="/alternatives/">Open-source alternatives</a>' 3141 + ' <a href="/stack-builder.html">Stack builder</a>' 3142 + ' <a href="/members">Community stacks</a>' 3143 + ' <a href="/prompts.html">Prompts</a>' 3144 + ' <a href="/model/">Model profiles</a>' 3145 + ' <a href="/tool/">Tool profiles</a>' 3146 + ' </div>' 3147 + ' <div class="footer-col footer-more">' 3148 + ' <h4>olud.ai</h4>' 3149 + ' <a href="/about.html">About</a>' 3150 + ' <a href="/methodology.html">Methodology</a>' 3151 + ' <a href="/blog.html">Blog</a>' 3152 + ' <a href="/reports/">Weekly report</a>' 3153 + ' <a href="/dashboard.html">Dashboard</a>' 3154 + ' <a href="/submit.html">Submit a tool or project</a>' 3155 + ' <a href="/claim.html">Claim your project</a>' 3156 + ' <a href="/badge.html">Get a badge</a>' 3157 + ' <a href="/favorites.html">My favorites</a>' 3158 + ' <a href="/contact.html">Contact</a>' 3159 + ' <a href="/privacy-policy/">Privacy</a>' 3160 + ' <a href="/terms/">Terms</a>' 3161 + ' </div>' 3162 + ' <div class="footer-badges">' 3163 /* Badge de vérification There\'s An AI For That. Leur code impose une image 3164 de 600 px affichée à 300 : on précise width ET height pour que le 3165 navigateur réserve la place et ne fasse pas sauter le pied de page au 3166 chargement. loading="lazy" parce qu\'il est tout en bas. */ 3167 /* [30/07] Badge Product Hunt retire a la demande de Romain : le lancement 3168 visait « opensourceai-tech », un nom que le site ne porte plus. Le lien 3169 et l'image venaient de leur domaine, rien d'autre a nettoyer. */ 3170 + ' </div>' 3171 + ' <div class="footer-copy">' 3172 + ' <span>© 2026 olud.ai â Powered by the open global AI community.</span>' 3173 + ' <span style="color:var(--muted);font-family:\'DM Mono\',monospace;font-size:.62rem">Independent · Open data</span>' 3174 + ' </div>' 3175 + ' </div>' 3176 + '</div></footer>'; 3177 3178 // âââ Injection âââ 3179 // Charger le système de favoris partagé (si pas déjà présent) 3180 if(!window.favBtnHtml && !document.querySelector('script[src*="favorites.js"]')){ 3181 var fs = document.createElement('script'); 3182 fs.src = '/favorites.js?v=6'; 3183 document.head.appendChild(fs); 3184 } 3185 3186 // Surcouche favoris : plafond 5 sans compte, synchro serveur pour les membres. 3187 // Additive â n'altère pas favorites.js, se contente de l'envelopper. 3188 if(!document.querySelector('script[src*="favorites-plus.js"]')){ 3189 var fp = document.createElement('script'); 3190 fp.src = '/favorites-plus.js?v=1'; 3191 document.head.appendChild(fp); 3192 } 3193 3194 function inject(){ 3195 try{ if(window.console) console.log('olud.ai nav: menu mobile autonome v6 actif'); }catch(e){} 3196 var navSlot = document.getElementById('site-nav'); 3197 if(navSlot) navSlot.innerHTML = navHTML; 3198 var footSlot = document.getElementById('site-footer'); 3199 if(footSlot) footSlot.innerHTML = footerHTML; 3200 3201 // Réactiver les comportements du menu après injection 3202 initNavBehaviors(); 3203 initNewsletter(); 3204 localizeNav(); // traduit le nav/footer/mobile si page /fr/ /it/ /es/ /de/ /pt/ (no-op en anglais) 3205 sortMegaColumns(); // colonnes du mega-menu par ordre alphabetique 3206 accentSectionTitles(); // dernier mot des titres de section en indigo 3207 3208 3209 buildLangSelector(); // remplit le sélecteur de langues (desktop + mobile) depuis les hreflang 3210 } 3211 3212 3213 /* âââ Méga-menu : chaque colonne triée par ordre alphabétique âââ 3214 Les entrées s'étaient empilées dans l'ordre où on les avait ajoutées au fil 3215 des mois â six colonnes, une trentaine de liens, et plus moyen de retrouver 3216 quoi que ce soit. On trie APRÃS localizeNav(), donc sur le libellé réellement 3217 affiché : l'ordre est juste dans chacune des six langues, et pas seulement en 3218 anglais. Trier dans le DOM plutôt que dans le HTML source a un second 3219 avantage : une entrée ajoutée demain se range toute seule. */ 3220 function sortMegaColumns(){ 3221 var cols = [].slice.call(document.querySelectorAll('.nav-dropdown-menu.mega .mega-col')); 3222 if(!cols.length) return; 3223 var coll = null; 3224 try { coll = new Intl.Collator(document.documentElement.getAttribute('lang') || 'en', 3225 {sensitivity:'base', numeric:true}); } catch(e){} 3226 function label(a){ 3227 var b = a.querySelector('b'); 3228 return ((b ? b.textContent : a.textContent) || '').replace(/\s+/g,' ').trim(); 3229 }
3230 cols.forEach(function(col){ 3231 var links = [].slice.call(col.children).filter(function(n){ return n.tagName === 'A'; }); 3232 if(links.length < 2) return; 3233 var sorted = links.slice().sort(function(x, y){ 3234 var lx = label(x), ly = label(y); 3235 if(coll) return coll.compare(lx, ly); 3236 lx = lx.toLowerCase(); ly = ly.toLowerCase(); 3237 return lx < ly ? -1 : lx > ly ? 1 : 0; 3238 }); 3239 /* deja trie : on ne touche pas au DOM pour rien */ 3240 var identique = true; 3241 for(var i=0;i<links.length;i++){ if(sorted[i] !== links[i]){ identique = false; break; } } 3242 if(identique) return; 3243 sorted.forEach(function(a){ col.appendChild(a); }); 3244 }); 3245 } 3246 3247 3248 /* âââ Titres de section : le dernier mot en indigo âââ 3249 La convention existait deja sur cinq titres (« Open-Source <em>Alternatives</em> ») 3250 mais pas sur les vingt autres. Romain veut la generaliser. 3251 3252 POURQUOI EN JAVASCRIPT ET NON DANS LE HTML : ecrire 3253 « Trending on <em>GitHub</em> » dans la source couperait le titre en DEUX 3254 noeuds texte, et le traducteur apparie des noeuds ENTIERS. C'est exactement 3255 la faute qui a produit « L'IA open-source hub. » et « $9 0,99 $/mois ». 3256 Ici le titre est traduit d'un bloc par localizeNav(), PUIS habille : le 3257 dernier mot est mis en valeur dans la langue affichee, quelle qu'elle soit. 3258 3259 Un titre d'un seul mot est laisse tel quel â le passer entierement en indigo 3260 n'accentue plus rien. Un titre qui porte deja un <em> n'est pas touche. */ 3261 /* [31/07] Le H1 des pages porte le code couleur sur son DERNIER MOT, comme 3262 les titres de section. Pourquoi ici et pas dans chaque page : decouper le 3263 titre en deux <span> traduits separement casse l'ordre des mots â mesure 3264 en production, « Outils IA open-source » devenait « IA open-source 3265 Outils », « IA locale » devenait « Local IA ». La phrase reste donc 3266 ENTIERE pour le traducteur, et la couleur est posee apres, dans les six 3267 langues. La famille vient du chemin (meme table que les icones du menu). 3268 Les pages qui gerent leur titre elles-memes (un <span> deja present) ne 3269 sont pas touchees. */ 3270 /* âââ [01/08] Marquer les champs alimentes par l'IA âââââââââââââââââââ 3271 Le degrade seul ne dit rien a personne : on ajoute une pastille qui 3272 NOMME la chose, avec une infobulle qui explique quoi. On ne marque que 3273 ce qui est REELLEMENT augmente par l'IA â la palette (qui bascule sur 3274 l'assistant) et le champ de l'assistant lui-meme. Marquer une recherche 3275 texte ordinaire serait un mensonge visuel. */ 3276 /* Lit le rayon REELLEMENT calcule du champ et le repose sur la boite, 3277 augmente de l'epaisseur du lisere : l'anneau epouse le champ au lieu de 3278 parier sur une valeur. On lit les quatre proprietes longues et non le 3279 raccourci : getComputedStyle().borderRadius rend tantot « 8px », tantot 3280 « 8px 8px 8px 8px », tantot une forme elliptique avec une barre oblique. 3281 On n'ecrit jamais un nombre mais un calc() qui reprend la variable 3282 d'epaisseur : changer l'epaisseur dans la feuille suffit. */ 3283 var osaiAiPaires = []; 3284 function osaiAiRayon(champ, boite){ 3285 if (!champ || !boite || !window.getComputedStyle) return; 3286 var cs = getComputedStyle(champ); 3287 var coins = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft']; 3288 var noms = ['--osai-ai-tl', '--osai-ai-tr', '--osai-ai-br', '--osai-ai-bl']; 3289 for (var i = 0; i < 4; i++) { 3290 var v = cs['border' + coins[i] + 'Radius'] || cs.borderRadius || '0px'; 3291 v = String(v).split('/')[0].trim().split(/\s+/)[0] || '0px'; 3292 boite.style.setProperty(noms[i], 'calc(' + v + ' + var(--osai-ai-w, 2px))'); 3293 } 3294 } 3295 function osaiAiRemesure(){ 3296 for (var i = 0; i < osaiAiPaires.length; i++) { 3297 osaiAiRayon(osaiAiPaires[i][0], osaiAiPaires[i][1]); 3298 } 3299 } 3300 /* le rayon d'un champ peut changer avec la largeur de l'ecran (media 3301 queries) : on remesure, sinon l'anneau garde l'ancienne forme. */ 3302 (function(){ 3303 var m = null; 3304 window.addEventListener('resize', function(){ 3305 clearTimeout(m); m = setTimeout(osaiAiRemesure, 160); 3306 }); 3307 if (document.fonts && document.fonts.ready) document.fonts.ready.then(osaiAiRemesure); 3308 })(); 3309 3310 function marquerIA(){
3311 /* [01/08, correction] J'avais marque la palette « AI » : elle ne fait 3312 QUE de la correspondance de texte. C'etait precisement le mensonge 3313 visuel que je disais vouloir eviter. Seul le champ de l'assistant, 3314 qui interroge vraiment le modele, porte la marque. La palette, elle, 3315 propose desormais une PORTE vers l'assistant (voir renderCmdk). */ 3316 var cibles = [ 3317 /* [01/08, correction] L'assistant ne porte plus de pastille du tout. 3318 Dans la barre de saisie elle volait 47px sur 368 au champ ; deplacee 3319 dans l'en-tete, elle y ajoutait une TROISIEME ligne : 107px d'en-tete 3320 sur un panneau de 244px, il ne restait que 36px pour les messages 3321 (mesure en production, fenetre 791x374). Et l'anneau, pose sur le 3322 PARENT du champ, entourait toute la barre du bas, bouton d'envoi 3323 compris : il designait la mauvaise chose. L'IA est dite avec des MOTS 3324 dans le sous-titre â cout en hauteur : zero. */ 3325 /* la grande barre de l'accueil : elle propose « demander a l'assistant » 3326 des que la saisie ressemble a une question (voir index.html) */ 3327 { sel: '#heroSearchInput', mot: 'AI' } 3328 ]; 3329 cibles.forEach(function(c){ 3330 var el = document.querySelector(c.sel); 3331 if (!el || el.getAttribute('data-ia')) return; 3332 el.setAttribute('data-ia', '1'); 3333 var boite = el.parentElement; 3334 /* sur l'accueil, l'objet « alimente par IA » est la CARTE entiere, pas 3335 le seul champ : le lisere doit en faire le tour. */ 3336 var carte = el.closest ? el.closest('.hero-ai') : null; 3337 if (carte) boite = carte; 3338 if (boite && !boite.classList.contains('osai-ai')) boite.classList.add('osai-ai'); 3339 /* [01/08, Romain : « les bordures dans les coins ca va pas du tout »] 3340 On mesurait le rayon du CHAMP. C'etait juste tant que l'anneau 3341 epousait le champ ; depuis que la carte de l'accueil existe, l'anneau 3342 fait le tour de la CARTE (18px) avec des coins calcules sur le champ 3343 (8px) â les quatre coins s'evasaient. On mesure donc toujours 3344 l'element que l'anneau entoure reellement, c'est-a-dire celui qui 3345 porte la classe. */ 3346 osaiAiRayon(boite, boite); 3347 osaiAiPaires.push([boite, boite]); 3348 /* Dans la carte de l'accueil, la pastille ne sert plus a rien : l'en-tete 3349 dit deja « Olud assistant », avec la mascotte et le point en ligne. */ 3350 if (carte) return; 3351 var t = (typeof window.osaiT === 'function') ? window.osaiT : function(x){ return x; }; 3352 var tag = document.createElement('span'); 3353 tag.className = 'osai-ai-tag'; 3354 tag.innerHTML = '\u2728 <b>' + c.mot + '</b>'; 3355 tag.title = t('Ask in your own words â the assistant answers from this site\'s live data, and points you to the right page.'); 3356 var hote = c.hote ? document.querySelector(c.hote) : boite; 3357 if (hote) hote.appendChild(tag); 3358 }); 3359 } 3360 3361 /* âââ Repere les barres de recherche et leur pose la meme forme âââââââ 3362 On ne touche PAS a n'importe quel champ texte : seulement a ceux qui 3363 servent visiblement a chercher ou filtrer (identifiant, classe ou texte 3364 d'invite). Les trois champs qui ont leur propre identite graphique sont 3365 exclus nommement : la grande barre de l'accueil (son anneau IA), la 3366 palette (elle vit dans une modale) et le champ de l'assistant (pastille). 3367 La couleur vient de la famille de la page, comme le titre. */ 3368 function uniformiserRecherches(){ 3369 var EXCLUS = { heroSearchInput: 1, cmdkInput: 1, ohpInput: 1 }; 3370 var MOTS = /(search|recherch|cherch|filtr|filter|buscar|suche|ricerca|pesquis|query)/i; 3371 var champs = document.querySelectorAll('input[type="text"],input[type="search"],input:not([type])'); 3372 for (var i = 0; i < champs.length; i++) { 3373 var e = champs[i]; 3374 if (EXCLUS[e.id] || e.classList.contains('osai-sb')) continue; 3375 var indice = (e.id || '') + ' ' + (e.className || '') + ' ' + (e.getAttribute('placeholder') || '') 3376 + ' ' + (e.getAttribute('aria-label') || ''); 3377 if (!MOTS.test(indice)) continue; 3378 if (e.closest('form[action]')) continue; /* un vrai formulaire garde son style */ 3379 /* Plusieurs champs ont une LOUPE posee en absolu par-dessus, et se 3380 menagent la place avec un padding gauche. Forcer le meme retrait 3381 partout collait le texte sous l'icone (vu sur leaderboard). On releve 3382 donc le retrait D'ORIGINE avant de changer quoi que ce soit, et on le 3383 garde s'il servait a quelque chose. Idem a droite pour les champs qui 3384 portent une croix d'effacement. */ 3385 var av = getComputedStyle(e); 3386 var pl = parseFloat(av.paddingLeft) || 0, pr = parseFloat(av.paddingRight) || 0; 3387 if (pl > 20) e.style.setProperty('--osai-sb-pl', Math.round(pl) + 'px'); 3388 if (pr > 20) e.style.setProperty('--osai-sb-pr', Math.round(pr) + 'px'); 3389 e.classList.add('osai-sb'); 3390 } 3391 } 3392 3393 var MASCOTTE_32 = '<svg class="oludbot" viewBox="0 0 32 32" width="30" height="30" aria-hidden="true">' 3394 + '<defs><linearGradient id="oludbot-cg" x1="0" y1="1" x2="1" y2="0">' 3395 + '<stop offset="0" stop-color="#3B57FF"/><stop offset="1" stop-color="#A257FF"/></linearGradient>' 3396 + '<mask id="oludbot-cm"><rect width="32" height="32" fill="#fff"/>' 3397 + '<circle cx="26.2" cy="5.8" r="4.4" fill="#000"/></mask></defs>' 3398 + '<g mask="url(#oludbot-cm)">
3398<circle cx="14.4" cy="17.6" r="11.2" fill="none" stroke="url(#oludbot-cg)" stroke-width="2.5"/></g>' 3399 + '<rect x="9.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-cg)"/>' 3400 + '<rect x="17.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="url(#oludbot-cg)"/>' 3401 + '<circle cx="26.2" cy="5.8" r="2.6" fill="url(#oludbot-cg)"/></svg>'; 3402 3403 /* La meme tete, mais posee sur la pastille degradee de la bulle d'accueil : 3404 elle y est tracee en BLANC, sinon l'indigo sur l'indigo ne se lit pas. */ 3405 var MASCOTTE_B = '<svg viewBox="0 0 32 32" width="19" height="19" aria-hidden="true">' 3406 + '<defs><mask id="oludbot-bm"><rect width="32" height="32" fill="#fff"/>' 3407 + '<circle cx="26.2" cy="5.8" r="4.4" fill="#000"/></mask></defs>' 3408 + '<g mask="url(#oludbot-bm)"><circle cx="14.4" cy="17.6" r="11.2" fill="none" stroke="#fff" stroke-width="2.7"/></g>' 3409 + '<rect x="9.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="#fff"/>' 3410 + '<rect x="17.4" y="15.6" width="4.4" height="3.8" rx="1.7" fill="#fff"/>' 3411 + '<circle cx="26.2" cy="5.8" r="2.6" fill="#fff"/></svg>'; 3412 3413 /* âââ [01/08, maquettes de Romain] LA CARTE ASSISTANT DE L'ACCUEIL âââââââ 3414 La barre du hero devient une carte qui se presente : la mascotte, le nom, 3415 un point « en ligne », une bulle d'accueil qui dit ce qu'on peut demander, 3416 et un bouton d'envoi. On ne REMPLACE rien : on ENVELOPPE la barre et sa 3417 liste de resultats, qui gardent leurs identifiants et tout leur code. 3418 La recherche texte continue donc de fonctionner exactement comme avant ; 3419 la carte ajoute le cadre, l'accueil et l'envoi. 3420 Le lisere anime passe de la barre a la CARTE : c'est elle, desormais, 3421 l'objet « alimente par IA ». */ 3422 function carteAssistant(){ 3423 var champ = document.getElementById('heroSearchInput'); 3424 if (!champ) return null; /* pas l'accueil */ 3425 var boite = champ.closest('.hero-search-box'); 3426 var liste = document.getElementById('heroSearchResults'); 3427 if (!boite || !liste || document.querySelector('.hero-ai')) return null; 3428 3429 var t = function(x){ return (typeof window.osaiT === 'function') ? window.osaiT(x) : x; }; 3430 var carte = document.createElement('div'); 3431 carte.className = 'hero-ai'; 3432 carte.innerHTML = 3433 '<div class="hero-ai-head">' 3434 + '<span class="hero-ai-av">' + MASCOTTE_32 + '</span>' 3435 + '<div class="hero-ai-id"><b>' + t('Olud assistant') + '</b>' 3436 + '<span class="hero-ai-on">' + t('Online') + '</span></div>' 3437 + '</div>' 3438 /* [01/08, Romain] « a la place de l'icone avec les etoiles il faut mettre 3439 notre icone olud assistant, ca parait logique ». Oui : c'est lui qui 3440 parle dans cette bulle, l'etincelle etait un generique. */ 3441 + '<div class="hero-ai-greet" id="heroAiGreet"><span class="hero-ai-spark">' + MASCOTTE_B + '</span>' 3442 + '<div>' + t('I can help you find AI tools, compare models and explore projects \u2014 or just search.') 3443 + '<b>' + t('Ask anything.') + '</b></div></div>'; 3444 3445 boite.parentNode.insertBefore(carte, boite); 3446 carte.appendChild(boite); 3447 carte.appendChild(liste); 3448 3449 /* le bouton d'envoi de la maquette */ 3450 var envoi = document.createElement('button'); 3451 envoi.type = 'button'; 3452 envoi.className = 'hero-ai-send'; 3453 envoi.setAttribute('aria-label', t('Ask the assistant')); 3454 envoi.innerHTML = '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor"' 3455 + ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20 21 12 4 4l3 8z"/></svg>'; 3456 boite.appendChild(envoi); 3457 3458 var demander = function(){ 3459 var q = (champ.value || '').trim(); 3460 if (!q) { champ.focus(); return; } 3461 liste.classList.add('open'); 3462 if (window.osaiAskInto) window.osaiAskInto(q, liste); 3463 else if (window.osaiAsk) window.osaiAsk(q); 3464 }; 3465 envoi.addEventListener('click', demander); 3466 champ.addEventListener('keydown', function(e){ 3467 /* Entree n'avait aucune action sur cette barre : les resultats 3468 s'affichent deja a la frappe. Elle sert donc a DEMANDER, ce que le 3469 champ propose maintenant en toutes lettres. */ 3470 if (e.key === 'Enter') { e.preventDefault(); demander(); } 3471 }); 3472 3473 /* [01/08] Sur telephone, le bouton flottant de l'assistant tombait 3474 EXACTEMENT sur le bouton d'envoi de la carte : le geste principal de la 3475 page devenait intapable. Et tant que la carte est a l'ecran, le bouton 3476 fait doublon â c'est le meme assistant. Il s'efface donc pendant que la 3477 carte est visible et revient des qu'on l'a depassee. */ 3478 /* Une simple mesure au defilement, et non un IntersectionObserver : je 3479 n'ai pas pu verifier ce dernier (il ne se declenche pas dans le 3480 navigateur d'essai, meme cree a la main), et un correctif que je ne 3481 peux pas voir fonctionner n'en est pas un. La mesure est calee sur 3482 requestAnimationFrame : une seule lecture par image, au pire. */ 3483 var enCours = false;
3484 function placerFlottant(){ 3485 enCours = false; 3486 var flottant = document.getElementById('osaiHelpBtn'); 3487 if (!flottant) return; 3488 if (!flottant.style.transition) flottant.style.transition = 'opacity .22s, transform .22s'; 3489 var r = carte.getBoundingClientRect(); 3490 var vue = r.bottom > 90 && r.top < (window.innerHeight || 0) - 60; 3491 flottant.style.opacity = vue ? '0' : ''; 3492 flottant.style.pointerEvents = vue ? 'none' : ''; 3493 flottant.style.transform = vue ? 'scale(.85)' : ''; 3494 } 3495 function planifier(){ 3496 if (enCours) return; 3497 enCours = true; 3498 setTimeout(placerFlottant, 16); /* et non requestAnimationFrame : il ne 3499 se declenche pas dans tous les 3500 contextes, et je veux pouvoir le 3501 verifier */ 3502 } 3503 window.addEventListener('scroll', planifier, { passive: true }); 3504 window.addEventListener('resize', planifier); 3505 setTimeout(placerFlottant, 60); 3506 setTimeout(placerFlottant, 900); /* le bouton arrive apres la carte */ 3507 3508 var accueil = carte.querySelector('#heroAiGreet'); 3509 var majAccueil = function(){ accueil.style.display = (champ.value || '').trim() ? 'none' : ''; }; 3510 champ.addEventListener('input', majAccueil); 3511 majAccueil(); 3512 return carte; 3513 } 3514 3515 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3516 LE FILET : PLUS AUCUN LIEN NE SORT DU SITE QUAND LA FICHE EXISTE 3517 3518 [01/08, Romain] « il faut ABSOLUMENT empecher les utilisateurs de sortir 3519 du site ». Je corrigeais moteur par moteur depuis le 31/07 â la palette, 3520 la barre du hero, la palette de l'accueil, le bandeau du jour â et il en 3521 reapparaissait un a chaque fois. Mesure qui a arrete les rustines : sur 3522 la SEULE page leaderboard, 326 liens partaient chez openrouter.ai. Ce 3523 n'etait pas quelques sections, c'etait le site. 3524 3525 Un seul filet, pose une fois, sur toutes les pages : on relit les liens 3526 sortants et on les remplace par NOTRE fiche quand elle existe. Ce qui n'a 3527 pas de fiche chez nous n'est pas touche â on ne fabrique pas de lien mort. 3528 3529 Cout : model-slugs.json fait 6 Ko et ne se charge que sur une page qui 3530 porte des liens de modeles ; project-slugs.json fait 757 Ko et ne se 3531 charge QUE si la page porte des liens GitHub et qu'aucune table n'est 3532 deja en memoire (l'accueil la charge deja pour son propre compte). */ 3533 var OSAI_SLUGS_M = null, OSAI_SLUGS_P = null, osaiEnVol = {}; 3534 function osaiPrefixe(){ 3535 var m = location.pathname.match(/^\/(fr|it|es|de|pt)(\/|$)/); 3536 return m ? '/' + m[1] : ''; 3537 } 3538 function osaiSlugifie(x){ 3539 return String(x || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); 3540 } 3541 function osaiCharge(nom, cle){ 3542 if (osaiEnVol[cle]) return osaiEnVol[cle]; 3543 osaiEnVol[cle] = fetch(nom).then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }); 3544 return osaiEnVol[cle]; 3545 } 3546 function osaiPose(a, url){ 3547 a.setAttribute('href', url); 3548 a.removeAttribute('target'); 3549 a.removeAttribute('rel'); 3550 a.setAttribute('data-osai-interne', '1'); 3551 } 3552 function osaiRelierSorties(racine){ 3553 var R = racine || document; 3554 if (!R.querySelectorAll) return; 3555 var pfx = osaiPrefixe(); 3556 3557 /* ââ les modeles ââ */ 3558 var mods = [].slice.call(R.querySelectorAll('a[href*="openrouter.ai/"]:not([data-osai-interne])')); 3559 if (mods.length) { 3560 (OSAI_SLUGS_M ? Promise.resolve(OSAI_SLUGS_M) : osaiCharge('/model-slugs.json', 'm')).then(function(t){ 3561 if (!t) return; 3562 OSAI_SLUGS_M = t; 3563 mods.forEach(function(a){ 3564 var h = a.getAttribute('href') || ''; 3565 var q = h.split('openrouter.ai/')[1]; 3566 if (!q) return; 3567 var brut = q.split(/[?#]/)[0].replace(/\/+$/, '').split('/').pop(); 3568 var s1 = brut.toLowerCase(), s2 = osaiSlugifie(brut); 3569 var s = t[s1] ? s1 : (t[s2] ? s2 : null); 3570 if (s) osaiPose(a, pfx + '/model/' + s + '.html'); 3571 }); 3572 }); 3573 } 3574 3575 /* ââ les projets ââ */ 3576 var gits = [].slice.call(R.querySelectorAll('a[href*="github.com/"]:not([data-osai-interne])')) 3577 .filter(function(a){ return /github\.com\/[^\/]+\/[^\/?#]+/.test(a.getAttribute('href') || ''); }); 3578 if (gits.length) { 3579 /* [01/08, apres mesure] project-slugs.json pese 757 Ko. Le telecharger 3580 au demarrage pour reecrire trois liens etait un mauvais marche : le 3581 site est deja lourd, et Romain le trouve lent a s'afficher. On ne le 3582 va chercher QUE si la page en a vraiment besoin (plus de trois liens 3583 GitHub), et seulement quand le navigateur n'a plus rien d'urgent a 3584 faire. Les liens ne bougent donc plus au chargement mais un instant 3585 apres, ce qui ne change rien pour le visiteur : il n'a pas encore eu 3586 le temps de cliquer. */ 3587 var pret; 3588 if (window.__FICHE_SLUGS && Object.keys(window.__FICHE_SLUGS).length > 50) { 3589 pret = Promise.resolve(window.__FICHE_SLUGS); 3590 } else if (OSAI_SLUGS_P) { 3591 pret = Promise.resolve(OSAI_SLUGS_P); 3592 } else if (gits.length > 3) { 3593 pret = new Promise(function(ok){ 3594 var lance = function(){ osaiCharge('/project-slugs.json', 'p').then(ok); }; 3595 if (window.requestIdleCallback) requestIdleCallback(lance, { timeout: 4000 }); 3596 else setTimeout(lance, 2500); 3597 }); 3598 } else { 3599 return; /* deux ou trois liens : pas 757 Ko pour ca */ 3600 } 3601 pret.then(function(t){ 3602 if (!t) return; 3603 OSAI_SLUGS_P = t;
3604 gits.forEach(function(a){ 3605 var h = (a.getAttribute('href') || '').split(/[?#]/)[0].replace(/\/+$/, ''); 3606 var s = t[h] || t[h + '/'] || t[h.replace(/^http:/, 'https:')]; 3607 if (s) osaiPose(a, pfx + '/project/' + s + '.html'); 3608 }); 3609 }); 3610 } 3611 } 3612 3613 /* le fond gris ne vaut que pour les fiches et leurs hubs â pose tot pour 3614 eviter un eclair blanc */ 3615 try{ 3616 if (/^\/(fr|de|es|it|pt)?\/?(project|tool|language|country|languages\.html|countries\.html)/.test(location.pathname.replace(/^\/(fr|de|es|it|pt)(?=\/)/,'')) || 3617 /^\/(project|tool|language|country)\//.test(location.pathname.replace(/^\/(fr|de|es|it|pt)(?=\/)/,''))) { 3618 document.body.classList.add('osai-fond-gris'); 3619 } 3620 }catch(e){} 3621 3622 function accentPageTitle(){ 3623 var seg = (location.pathname.replace(/^\/(fr|de|es|it|pt)(?=\/)/, '').split('/')[1] || '').replace(/\.html$/, ''); 3624 var fam = (window.__osaiSEG || {})[seg] || null; 3625 var col = fam ? 'var(--cat-' + fam + ')' : 'var(--accent,#6366f1)'; 3626 var h = document.querySelector('.wrap h1, main h1, .mwrap h1, .twrap h1, header h1'); 3627 if (!h || h.getAttribute('data-accented')) return; 3628 /* [31/07, Romain] « aucun n'a la meme taille ni la meme graisse » : mesure 3629 faite, les titres allaient de 2,1rem/800 (leaderboard) a clamp(2.4-4rem) 3630 /600 (tools) en passant par 600 sur projects. On pose ici une classe 3631 d'uniformisation â une seule regle pour tout le site, plutot que de 3632 corriger trente feuilles de style. Les FICHES generees (project, model, 3633 tool, alt, blog, maker : 60 000 pages deja coherentes entre elles) et le 3634 hero de l'accueil gardent leur propre echelle. */ 3635 var chemin = location.pathname.replace(/^\/(fr|de|es|it|pt)(?=\/)/, ''); 3636 /* Les fiches generees ont leur echelle propre, et le TABLEAU DE BORD est 3637 une interface, pas une page de contenu : son titre vit dans une barre 3638 d'indicateurs (LIVE, DATA, SYNC) alignes sur la meme ligne de base. 3639 Lui imposer le gabarit editorial cassait cette rangee (releve par 3640 Romain le 01/08). */ 3641 var fiche = /^\/(?:project|model|tool|alt|alt-models|alternatives|blog|maker|u)\//.test(chemin) 3642 || /^\/(?:dashboard|account|my-dashboard)\.html$/.test(chemin); 3643 if (!fiche && !h.classList.contains('hero-title')) { 3644 h.classList.add('osai-h1'); 3645 /* Une simple classe ne suffit pas : les pages ont des selecteurs plus 3646 specifiques (« .hero h1 », « .page-header h1 »), parfois en 3647 !important â mesure faite, tools restait en graisse 600. On ecrit 3648 donc le gabarit EN LIGNE avec priorite, ce que rien ne peut battre. */ 3649 h.style.setProperty('font-size', 'clamp(1.85rem,3.2vw,2.25rem)', 'important'); 3650 h.style.setProperty('font-weight', '800', 'important'); 3651 h.style.setProperty('letter-spacing', '-.02em', 'important'); 3652 h.style.setProperty('line-height', '1.15', 'important'); 3653 h.style.setProperty('font-family', "'Poppins','Poppins-fb',sans-serif", 'important'); 3654 /* MEME DISTANCE AVEC LE MENU (demande de Romain, mesure du 31/07 : 3655 98px sur leaderboard et latest, 104 sur releases, 108 sur dna, 110 3656 sur pricing, 174 sur projects/tools/spaces/local-ai). Chaque page a 3657 son propre rembourrage d'en-tete ; plutot que d'en corriger trente, 3658 on mesure la position REELLE du titre et on la ramene a la valeur 3659 voulue. Recalcule apres le chargement des polices, qui deplace tout. */ 3660 var caler = function(){ 3661 var VOULU = (window.innerWidth < 640) ? 96 : 128; 3662 /* On repart de la position NATURELLE : sans cela, chaque nouvel appel 3663 (polices chargees, minuterie) se cumulait a la marge precedente. */ 3664 h.style.removeProperty('margin-top'); 3665 var t = h.getBoundingClientRect().top + (window.scrollY || 0); 3666 var mt = parseFloat(getComputedStyle(h).marginTop) || 0; 3667 var d = VOULU - t; 3668 /* [02/08/2026 â AUDIT] GARDE-FOU. Remonter le titre a une hauteur fixe 3669 le faisait passer PAR-DESSUS ce qui le precede : sur les pages qui 3670 ont un sur-titre visible (« maintainers », « geography · updated 3671 daily »...), le titre le recouvrait, illisible. Mesure du 02/08 : 3672 une quinzaine de pages touchees, dont claim, contact, plans, 3673 newsletter, countries, archive, advertise. 3674 On ne remonte donc jamais au-dessus du bas de ce qui est DEJA 3675 au-dessus, 6 px de respiration compris. Les elements en position 3676 fixe (la barre) ou absolue (halos decoratifs) ne comptent pas, et 3677 ce qui est SOUS le titre non plus. */ 3678 if (d < 0) { 3679 var plancher = null, tous = document.body.querySelectorAll('*'); 3680 for (var q = 0; q < tous.length; q++) { 3681 var e2 = tous[q]; 3682 if (e2 === h) break; /* ordre du document : on s'arrete au titre */ 3683 if (e2.contains(h)) continue; /* un ancetre n'est pas « au-dessus » */ 3684 var c2 = getComputedStyle(e2); 3685 if (c2.position === 'fixed' || c2.position === 'absolute') continue; 3686 if (c2.visibility === 'hidden' || c2.display === 'none') continue; 3687 var r2 = e2.getBoundingClientRect(); 3688 if (r2.height <= 0 || r2.width <= 0) continue; 3689 var b2 = r2.bottom + (window.scrollY || 0); 3690 if (b2 > t + 1) continue; /* pas au-dessus : sans objet */ 3691 if (plancher === null || b2 > plancher) plancher = b2; 3692 } 3693 if (plancher !== null) { var mini = plancher + 6 - t; if (d < mini) d = mini; } 3694 } 3695 if (Math.abs(d) > 1) h.style.setProperty('margin-top', (mt + d) + 'px', 'important'); 3696 }; 3697 caler(); 3698 if (document.fonts && document.fonts.ready) document.fonts.ready.then(caler); 3699 setTimeout(caler, 400); 3700 } 3701 /* [31/07, urgent] La page d'accueil a mange l'espace : son titre est 3702 « The AI <em>hub.</em> », le dernier NOEUD TEXTE est « The AI » et la 3703 fonction a colore « AI » puis supprime l'espace final (« AIhub »). 3704 Regle : des qu'un H1 contient un element autre qu'une icone (em, span, 3705 b...), la page gere son titre elle-meme â on ne touche a rien. */ 3706 var kids = [].slice.call(h.childNodes), node = null; 3707 for (var e = 0; e < kids.length; e++) { 3708 var el = kids[e];
3709 if (el.nodeType !== 1) continue; 3710 var cl = ' ' + (el.getAttribute('class') || '') + ' '; 3711 var estIcone = el.tagName === 'SVG' || el.tagName === 'svg' 3712 || cl.indexOf(' h1-ic ') > -1 || cl.indexOf(' sec-ic ') > -1; 3713 if (!estIcone) return; 3714 } 3715 for (var i = kids.length - 1; i >= 0; i--) { 3716 if (kids[i].nodeType === 3 && kids[i].nodeValue && kids[i].nodeValue.trim() !== '') { node = kids[i]; break; } 3717 } 3718 if (!node) return; 3719 /* [02/08, Romain] Sur une FICHE le titre est un nom propre, souvent en un 3720 seul mot : « le dernier mot » = tout le nom, peint aux couleurs de sa 3721 famille â awesome en vert, whisper.cpp en orange, sous le seuil de 3722 contraste AA (2,15:1 et 2,54:1 mesures). La couleur de categorie vit 3723 desormais dans la puce au-dessus du titre ; le nom reste en encre. */ 3724 if (fiche) { h.setAttribute('data-accented', '1'); return; } 3725 var brut = node.nodeValue; 3726 var fin = (brut.match(/\s+$/) || [''])[0]; /* l'espace final est RENDU */ 3727 var m = brut.replace(/\s+$/, '').match(/^([\s\S]*?)(\S+)$/); 3728 if (!m) return; 3729 node.nodeValue = m[1]; 3730 var sp = document.createElement('span'); 3731 sp.className = 'osai-ttx-h1'; 3732 sp.style.color = col; 3733 sp.textContent = m[2]; 3734 node.parentNode.insertBefore(sp, node.nextSibling); 3735 if (fin) node.parentNode.insertBefore(document.createTextNode(fin), sp.nextSibling); 3736 h.setAttribute('data-accented', '1'); 3737 } 3738 3739 function accentSectionTitles(){ 3740 var path = location.pathname.replace(/index\.html$/, ''); 3741 /* page d'accueil, et la page Connect qui suit la meme charte de titres */ 3742 if (!/^\/(?:[a-z]{2}\/)?(?:connect\.html|docs\.html)?$/.test(path)) return; 3743 /* .featured-title n'est pas un titre de niveau mais un div : il porte quand 3744 meme un intitule de section, et Romain veut le meme traitement. */ 3745 var sel = 'h2.section-title, h2.embed-section-title, h3.section-title, h3.embed-section-title, .featured-title'; 3746 [].slice.call(document.querySelectorAll(sel)).forEach(function(h){ 3747 /* [10/08/2026, demande de Romain] LE MOT ACCENTUE ET LE BOUTON SUIVENT L ICONE. 3748 L icone de chaque section portait deja sa couleur de famille - ambre pour les 3749 outils, ciel pour les modeles, emeraude pour les projets - mais le mot mis en 3750 valeur juste a cote restait indigo, et le bouton de la section aussi : trois 3751 couleurs sur une meme ligne. 3752 Ce bloc est AVANT les deux sorties anticipees ci-dessous, et c'est deliberé : 3753 un titre deja accentue dans le HTML (« Featured Tools ») en sortait avant 3754 d'avoir recu sa couleur. La colorisation ne depend pas de l'accentuation. 3755 --sic est pose sur le titre ET sur la section : le bouton .sec-cta vit hors du 3756 titre et n'en heriterait pas. Les sections dont l icone est deja indigo 3757 recoivent la meme couleur qu'elles avaient : rien ne bouge pour elles. */ 3758 var pastille = h.querySelector('.sec-ic') || h.querySelector('svg'); 3759 /* .featured-title est un div dont la pastille est posee A COTE, pas dedans. */ 3760 if (!pastille && h.parentElement) pastille = h.parentElement.querySelector('.sec-ic, svg'); 3761 if (pastille) { 3762 var teinte = ''; 3763 try { teinte = getComputedStyle(pastille).color || ''; } catch (e) {} 3764 if (teinte && teinte !== 'rgba(0, 0, 0, 0)') { 3765 h.style.setProperty('--sic', teinte); 3766 var sect = h.closest ? h.closest('section') : null; 3767 if (sect) { 3768 sect.style.setProperty('--sic', teinte); 3769 /* Les boutons sont peints ICI, en style direct, et non par une regle CSS : 3770 une feuille non lisible depuis le script (chargee d'une autre origine) 3771 battait une regle pourtant plus specifique ET en !important. Constate a 3772 la mesure : --sic arrivait bien jusqu'au bouton, la regle correspondait, 3773 et le fond restait indigo. Un style direct en !important tranche, et 3774 reste au meme endroit que le reste de la logique. */ 3775 [].slice.call(sect.querySelectorAll('.sec-cta')).forEac
3775h(function (b) { 3776 b.style.setProperty('background', teinte, 'important'); 3777 if (!b.getAttribute('data-teinte')) { 3778 b.setAttribute('data-teinte', '1'); 3779 b.addEventListener('mouseenter', function () { 3780 b.style.setProperty('box-shadow', 'inset 0 0 0 999px rgba(0,0,0,.14)', 'important'); 3781 }); 3782 b.addEventListener('mouseleave', function () { b.style.removeProperty('box-shadow'); }); 3783 } 3784 }); 3785 } 3786 /* Un <em> deja present peut porter sa couleur en style DIRECT (« Alternatives », 3787 « DNA », « vs Paid Models »...), ce qui bat toute feuille de style. On le 3788 renvoie vers --sic au lieu de le laisser imposer l'indigo. */ 3789 /* On peint TOUS les <em> du titre, pas seulement ceux qui portaient deja 3790 une couleur en style direct. « Featured Tools » avait son propre 3791 selecteur .featured-title em, a specificite egale et en !important lui 3792 aussi : a egalite, la derniere regle du document gagne, et ce n'etait 3793 pas la notre. Un style direct met fin au concours. */
3794 [].slice.call(h.querySelectorAll('em')).forEach(function (mot) { 3795 if (!mot.style) return; 3796 mot.style.setProperty('color', teinte, 'important'); 3797 mot.style.setProperty('-webkit-text-fill-color', teinte, 'important'); 3798 }); 3799 } 3800 } 3801 3802 if (h.getAttribute('data-accented')) return; 3803 if (h.querySelector('.osai-ttx')) return; 3804 3805 /* TOUT ce qui suit la pastille d'icone est regroupe dans UN SEUL element. 3806 Ces titres passent en display:flex sur mobile : chaque noeud reste alors 3807 un element flex distinct, avec trois consequences vues en production â 3808 l'espace entre deux noeuds disparait (on lisait « FeaturedTools »), un 3809 espacement de flex s'insere la ou le texte n'en a pas (« 1000 + »), et 3810 les lignes suivantes s'alignent sur le debut du DERNIER noeud au lieu du 3811 debut du titre (« Open-Source AI » cale sous le « + » et non sous le 3812 « 1000 »). Un seul bloc, et le texte se replie proprement sous lui-meme. */ 3813 var kids = [].slice.call(h.childNodes), start = 0; 3814 for (var k = 0; k < kids.length; k++){ 3815 var c = kids[k]; 3816 if (c.nodeType === 1 && (' ' + (c.getAttribute('class') || '') + ' ').indexOf(' sec-ic ') > -1) start = k + 1; 3817 } 3818 3819 var body = kids.slice(start); 3820 if (!body.length) return; 3821 3822 /* dernier noeud texte non vide du bloc : c'est lui qui porte l'accent */ 3823 var node = null; 3824 for (var i = body.length - 1; i >= 0; i--) { 3825 var n = body[i]; 3826 if (n.nodeType === 3 && n.nodeValue && n.nodeValue.replace(/[\s ]+/g, '') !== '') { node = n; break; } 3827 if (n.nodeType === 1) break; /* un element ferme la marche : on n'y touche pas */ 3828 } 3829 var m = null, tailP = ''; 3830 if (node) { 3831 var txt = node.nodeValue; 3832 /* Une ponctuation finale detachee n'est pas un mot. En francais le « ? » 3833 est precede d'une espace fine insecable : « Quoi de neuf ? » se serait 3834 donc termine par le seul caractere « ? », trop court pour etre accentue, 3835 et le titre n'aurait recu aucun indigo. On la met de cote, on accentue 3836 le vrai dernier mot, et on la remet derriere. */ 3837 var mp = txt.match(/^([\s\S]*?)([\s â¯]*[?!:;»]+\s*)$/); 3838 if (mp && mp[1].replace(/[\s â¯]+/g, '') !== '') { txt = mp[1]; tailP = mp[2]; } 3839 m = txt.match(/^([\s\S]*?[\s ])([^\s ]+)([\s ]*)$/); 3840 if (!m || m[2].length < 2) { m = null; tailP = ''; } /* un seul mot : pas d'accent */ 3841 } 3842 /* un titre qui porte deja son accent dans le HTML garde le sien : on le 3843 regroupe pour la mise en page, sans lui en ajouter un second */ 3844 if (h.querySelector('em')) m = null; 3845 /* rien a accentuer ET rien a regrouper : on laisse le titre tranquille */ 3846 if (!m && body.length < 2) return; 3847 3848 var wrap = document.createElement('span'); 3849 wrap.className = 'osai-ttx'; 3850 h.replaceChild(wrap, body[0]); 3851 for (var b = 0; b < body.length; b++) { 3852 var nb = body[b]; 3853 if (m && nb === node) { 3854 /* le noeud d'origine est remplace par ses morceaux : il faut le retirer 3855 du titre, sinon le texte apparait deux fois */ 3856 if (nb.parentNode === h) h.removeChild(nb); 3857 wrap.appendChild(document.createTextNode(m[1])); 3858 var em = document.createElement('em'); 3859 em.textContent = m[2]; 3860 wrap.appendChild(em); 3861 if (m[3]) wrap.appendChild(document.createTextNode(m[3])); 3862 if (tailP) wrap.appendChild(document.createTextNode(tailP)); 3863 } else { 3864 wrap.appendChild(nb); /* appendChild deplace le noeud */ 3865 } 3866 } 3867 h.setAttribute('data-accented', '1'); 3868 }); 3869 } 3870 3871 // âââ Newsletter (footer) â soumission AJAX vers Web3Forms âââ 3872 function initNewsletter(){ 3873 var forms = [].slice.call(document.querySelectorAll('#newsletterForm, form.osai-sub')); 3874 forms.forEach(function(form){ wireSub(form); }); 3875 } 3876 function wireSub(form){ 3877 if(!form || form.__wired) return; 3878 form.__wired = true; 3879 form.addEventListener('submit', function(e){ 3880 e.preventDefault(); 3881 var email = form.querySelector('input[type=email]'); 3882 var msg = form.parentNode.querySelector('.footer-news-msg, .sub-msg'); 3883 var btn = form.querySelector('button'); 3884 if(!email || !email.value) return; 3885 var src = form.getAttribute('data-src') || 'footer'; 3886 btn.disabled = true; var orig = btn.textContent; btn.textContent = 'Subscribing\u2026'; 3887 /* V94 â les inscriptions partaient vers Web3Forms (service tiers) : elles 3888 n'arrivaient JAMAIS en base, d'où 0 abonné dans l'admin, aucun weekly 3889 report possible, et un transfert de données non déclaré. On poste 3890 désormais sur notre propre API, en double opt-in. */ 3891 /* la cadence est facultative : un formulaire sans <select name=freq> 3892 reste valide et vaut hebdomadaire */ 3893 var fq = form.querySelector('[name=freq]'); 3894 var payload = { email: email.value, src: src, freq: (fq && fq.value) || 'weekly' }; 3895 fetch('/api/subscribe.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) }) 3896 .then(function(r){ return r.json(); }) 3897 .then(function(j){ 3898 if(j && j.ok){ 3899 if(msg){ 3900 msg.style.color='var(--accent)'; 3901 msg.textContent = j.already 3902 ? "You're already on the list." 3903 : osaiT('Check your inbox â one click to confirm.'); 3904 } 3905 form.reset(); 3906 } 3907 else { if(msg){ msg.style.color='#e0245e'; msg.textContent = (j && j.error) ? j.error : "Hmm, that didn't work \u2014 please try again."; } } 3908 }) 3909 .catch(function(){ if(msg){ msg.style.color='#e0245e'; msg.textContent='Network error \u2014 please try again.'; } }) 3910 .then(function(){ btn.disabled=false; btn.textContent=orig; }); 3911 }); 3912 } 3913 3914 function initNavBehaviors(){ 3915 // Dropdown "More" 3916 var moreBtn = document.getElementById('moreMenuBtn'); 3917 var moreMenu = document.getElementById('moreMenu'); 3918 if(moreBtn && moreMenu){ 3919 moreBtn.addEventListener('click', function(e){ e.stopPropagation(); moreMenu.classList.toggle('open'); }); 3920 document.addEventListener('click', function(){ moreMenu.classList.remove('open'); }); 3921 } 3922 var langBtn = document.getElementById('langMenuBtn'); 3923 var langMenu = document.getElementById('langMenu'); 3924 if(langBtn && langMenu){ 3925 langBtn.addEventListener('click', function(e){ e.stopPropagation(); langMenu.classList.toggle('open'); }); 3926 document.addEventListener('click', function(){ langMenu.classList.remove('open'); }); 3927 } 3928 // Hamburger (menu mobile) : ouverture/fermeture robustes 3929 var ham = document.getElementById('hamburger'); 3930 if(ham){ 3931 // Menu mobile AUTONOME : un overlay est créé comme enfant direct de <body>, 3932 // donc indépendant du <nav> (contexte d'empilement) et du CSS de chaque page. 3933 var mm = document.getElementById('osaiMM'); 3934 if(!mm){ 3935 mm = document.createElement('div'); 3936 mm.id = 'osaiMM'; 3937 mm.innerHTML = 3938 '<div class="osaiMM-bd"></div>' 3939 + '<div class="osaiMM-panel">' 3940 + '<button type="button" class="osaiMM-close" aria-label="Close menu">×</button>' 3941 + '<div class="osaiMM-quick">' 3942 + '<a class="osaiMM-q" href="' + LP + '/account.html">' + ic('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>') + '<span>Account</span></a>' 3943 + '<button type="button" class="osaiMM-q" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openFavorites)openFavorites();},60)">' + ic('<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/>') + '<span>Favorites</span></button>' 3944 + '<button type="button" class="osaiMM-q" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openCmdK)openCmdK();},60)">' + ic('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>') + '<span>Search</span></button>' 3945 + '</div>' 3946 + '<a href="/promote.html" style="color:var(--accent,#6366f1);font-weight:600;">'+ic('<path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/>')+'Promote your project</a>' 3947 + '<a href="/my-dashboard.html" style="color:var(--accent);font-weight:600;">'+ic('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 9h6"/><path d="M15 15h6"/>')+' My Dashboard</a>' 3948 + '<a href="/projects.html">Projects</a>' 3949 + '<a href="/tools.html">Catalog</a>' 3950 + '<a href="/tools.html">AI Tools</a>' 3951 + '<a href="/latest.html" style="color:var(--accent);font-weight:600;">'+ic('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>')+' Latest</a>' 3952 + '<a href="/emerging.html" style="color:var(--accent);font-weight:600;">'+ic('<path d="M12 2v8M8 6l4-4 4 4"/><circle cx="12" cy="16" r="5"/>')+' Emerging</a>' 3953 + '<a href="/vertical/">'+ic('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>')+' Industries</a>' 3954 + '<a href="/releases.html" style="color:var(--accent);font-weight:600;">'+ic('<path d="M12 2v6"/><path d="m16 6-4 4-4-4"/><rect width="18" height="10" x="3" y="12" rx="2"/>')+' Releases</a>' 3955 + '<a href="/dashboard.html" style="color:var(--accent);font-weight:600;">'+ic('<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>')+' Dashboards</a>' 3956 + '<div class="osaiMM-sec">Tools & Agents</div>' 3957 + '<a href="/skills/">'+ic('<path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/>')+' AI Skills</a>' 3958 + '<a href="/mcp/">'+ic('<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M6 8h12l-1 7a5 5 0 0 1-10 0z"/>')+' MCP Servers</a>' 3959 + '<a href="/local-ai.html">'+ic('<rect width=\"16\" height=\"16\" x=\"4\" y=\"4\" rx=\"2\"/><rect width=\"6\" height=\"6\" x=\"9\" y=\"9\"/><path d=\"M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2\"/>')+' Local AI</a>' 3960 + '<a href="/makers/">'+ic('<path d="M3 21h18"/><path d="M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/>')+' Model makers</a>' 3961 + '<a href="/pulse.html">'+ic('<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>')+'Pulse ranking</a>' 3962 + '<a href="/security.html">'+ic('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>')+'Security advisories</a>' 3963 + '<a href="/agents/">'+ic('<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>')+' AI Agents</a>' 3964 + '<a href="/tool/">'+ic('<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>')+' Tool Guides</a>' 3965 + '<a href="/compare-tools.html">'+ic('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>')+' Compare Tools</a>' 3966 + '<a href="/category.html">'+ic('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>')+' Categories</a>' 3967 + '<a href="/stack-builder.html">'+ic('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
3967')+'Stack Builder</a>' 3968 + '<div class="osaiMM-sec">Models</div>' 3969 + '<a href="/pricing.html" style="color:var(--accent);font-weight:600;">'+ic('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>')+' AI Pricing</a>' 3970 + '<a href="/models-vs/" style="color:var(--accent);font-weight:600;">'+ic('<circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/>')+' Models · Free vs Paid</a>' 3971 + '<a href="/leaderboard.html">'+ic('<line x1="6" y1="20" x2="6" y2="14"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="18" y1="20" x2="18" y2="10"/>')+' Leaderboard</a>' 3972 + '<a href="/media-leaderboard.html">'+ic('<line x1="6" y1="20" x2="6" y2="14"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="18" y1="20" x2="18" y2="10"/>')+'Media models</a>' 3973 + '<a href="/compare.html" style="color:var(--accent);font-weight:600;">'+ic('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>')+' Compare Models</a>' 3974 + '<a href="/vs/" style="color:var(--accent);font-weight:600;">'+ic('<path d="m9 7-5 5 5 5"/><path d="m15 7 5 5-5 5"/>')+' Versus · X vs Y</a>' 3975 + '<a href="/model/">'+ic('<rect width="18" height="14" x="3" y="5" rx="2"/><path d="M7 15h4M7 11h10"/>')+'Model profiles</a>' 3976 + '<a href="/paid-models.html">'+ic('<path d="M7.5 7.5h.01"/><path d="M19 9.3V5a2 2 0 0 0-2-2h-4.3a2 2 0 0 0-1.4.6l-7.7 7.7a2 2 0 0 0 0 2.8l4.3 4.3a2 2 0 0 0 2.8 0l7.7-7.7a2 2 0 0 0 .6-1.4Z"/>')+' Paid Models</a>' 3977 + '<div class="osaiMM-sec">Alternatives</div>' 3978 + '<a href="/alternatives/">'+ic('<path d="m2 9 3-3 3 3"/><path d="M13 18H7a2 2 0 0 1-2-2V6"/><path d="m22 15-3 3-3-3"/><path d="M11 6h6a2 2 0 0 1 2 2v10"/>')+' Alternatives</a>' 3979 + '<div class="osaiMM-sec">Discover</div>' 3980 + '<a href="/spaces.html">'+ic('<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>')+' Spaces</a>' 3981 + '<a href="/dna.html">'+ic('<path d="M6 3c0 4 12 5 12 9s-12 5-12 9"/><path d="M18 3c0 4-12 5-12 9s12 5 12 9"/><path d="M8 8h8M8 16h8"/>')+'DNA families</a>' 3982 + '<a href="/languages.html">'+ic('<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>')+'Languages</a>' 3983 + '<a href="/countries.html">'+ic('<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><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"/>')+' Countries</a>' 3984 + '<div class="osaiMM-sec">What's new</div>' 3985 + '<a href="/trends.html">'+ic('<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>')+'Trends</a>' 3986 + '<a href="/news.html">'+ic('<path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/>')+' News</a>' 3987 + '<a href="/blog.html">'+ic('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>')+' Blog</a>' 3988 + '<a href="/health.html">'+ic('<path d="M19.5 13.6 12 21l-7.5-7.4A5.2 5.2 0 1 1 12 6.3a5.2 5.2 0 1 1 7.5 7.3z"/>')+' Project Health</a>' 3989 + '<a href="/reports/">'+ic('<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>')+'Weekly report</a>' 3990 + '<div class="osaiMM-sec">Site & Data</div>' 3991 + '<a href="/about.html">'+ic('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>')+'About</a>' 3992 + '<a href="/methodology.html">'+ic('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>')+'Methodology</a>' 3993 + '<a href="/contact.html">'+ic('<path d="M4 4h16v12H5.17L4 17.17V4z"/><path d="M8 9h8"/><path d="M8 12h5"/>')+' Contact</a>' 3994 + '<a href="/favorites.html">'+ic('<path d="M12 17.8 5.8 21 7 14.1 2 9.3l7-1L12 2l3 6.3 7 1-5 4.8 1.2 6.9z"/>')+'Favorites</a>' 3995 + '<a href="/submit.html" style="color:var(--accent);font-weight:600;">'+ic('<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/>
3995<line x1="8" y1="12" x2="16" y2="12"/>')+' Submit a tool or project</a>' 3996 + '<a href="/badge.html">'+ic('<circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/>')+' Get a badge</a>' 3997 + '<div class="osaiMM-sec">Language</div>' 3998 + '<div id="osaiMMLang"></div>' 3999 + '<div class="osaiMM-sec">Tools</div>' 4000 + '<button type="button" class="osaiMM-act" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openFavorites)openFavorites();},60)">'+ic('<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/>')+' My favorites</button>' 4001 + '<a class="osaiMM-act" href="' + ((location.pathname.match(/^\/(fr|it|es|de|pt)\//)||[,''])[1] ? '/' + location.pathname.split('/')[1] : '') + '/account.html">'+ic('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>')+' Account</a>' 4002 + '<button type="button" class="osaiMM-act" onclick="if(window.osaiMMClose)window.osaiMMClose();setTimeout(function(){if(window.openCmdK)openCmdK();},60)">'+ic('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')+' Search</button>' 4003 + '</div>'; 4004 document.body.appendChild(mm); 4005 } 4006 var mmOpen = function(){ mm.classList.add('osaiMM-open'); ham.classList.add('active'); document.body.style.overflow='hidden'; }; 4007 var mmClose = function(){ mm.classList.remove('osaiMM-open'); ham.classList.remove('active'); document.body.style.overflow=''; }; 4008 /* #osaiMM porte un z-index de 2 147 483 600 : la palette de recherche et 4009 le panneau Favoris (9999) s'ouvraient DERRIÃRE lui, donc invisibles. 4010 On expose la fermeture pour que ces boutons referment le menu d'abord. */ 4011 window.osaiMMClose = mmClose; 4012 ham.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); mm.classList.contains('osaiMM-open') ? mmClose() : mmOpen(); }); 4013 mm.querySelector('.osaiMM-bd').addEventListener('click', mmClose); 4014 mm.querySelector('.osaiMM-close').addEventListener('click', mmClose); 4015 // Taper un lien/bouton : on laisse la navigation se faire puis on referme. 4016 mm.querySelectorAll('.osaiMM-panel a, .osaiMM-act').forEach(function(el){ el.addEventListener('click', function(){ setTimeout(mmClose, 80); }); }); 4017 document.addEventListener('keydown', function(e){ if(e.key === 'Escape') mmClose(); }); 4018 window.addEventListener('resize', function(){ if(window.innerWidth > 900) mmClose(); }); 4019 } 4020 // Thème : si la page a déjà applyTheme, le bouton l'utilise via onclick. 4021 // Sinon, fallback minimal : 4022 if(!window.applyTheme){ 4023 window.applyTheme = function(dark){ 4024 document.body.classList.toggle('dark', dark); 4025 try{ localStorage.setItem('osai_theme', dark?'dark':'light'); }catch(e){} 4026 }; 4027 } 4028 var themeToggle = document.getElementById('themeToggle'); 4029 if(themeToggle){ themeToggle.addEventListener('click', function(){ window.applyTheme(!document.body.classList.contains('dark')); }); } 4030 // Appliquer le thème : le choix explicite prime ; sinon on suit le réglage 4031 // jour/nuit du système du visiteur (prefers-color-scheme). Doit rester aligné 4032 // avec le snippet inline en tête de page, sinon la nav réécrase l'auto. 4033 try{ 4034 var saved = localStorage.getItem('osai_theme'); 4035 var dark = saved ? (saved === 'dark') 4036 : (window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches); 4037 document.body.classList.toggle('dark', !!dark); 4038 // Bascule en direct si le système passe jourânuit (tant qu'aucun choix manuel) 4039 if(window.matchMedia && !saved){ 4040 var _mq = window.matchMedia('(prefers-color-scheme:dark)'); 4041 if(_mq.addEventListener) _mq.addEventListener('change', function(e){ 4042 if(!localStorage.getItem('osai_theme')) document.body.classList.toggle('dark', e.matches); 4043 }); 4044 } 4045 }catch(e){ document.body.classList.add('dark'); } 4046 // Mettre à jour le badge favoris si la fonction existe 4047 if(window.favUpdateBadge) window.favUpdateBadge(); 4048 } 4049 4050 if(document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', inject); } 4051 else { inject(); } 4052 4053 /* âââ V17 â Badge live du hero (home uniquement) : "N new AI projects today" âââ */ 4054 function osaiLivePill(){ 4055 /* Romain 31/07/2026 : pastille retiree de l accueil â avec le badge 4056 Olud Index a cote du titre, deux pilules autour du hero faisaient 4057 trop charge. Coupee ICI et pas seulement via le slot : le repli 4058 ci-dessous reinjecte la pastille avant le h1 quand le slot manque.
4059 Pour la remettre un jour : supprimer ce return. */ 4060 return; 4061 if (!isHome || document.querySelector('.osai-live-pill')) return; 4062 var h1 = document.querySelector('#heroSection .hero-title') || document.querySelector('.hero-title'); 4063 if (!h1) return; 4064 fetch('/today-data.json?d=' + new Date().toISOString().slice(0,13)).then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }).then(function(d){ 4065 var c = d && d.counts; 4066 if (!c || !c.projects_today) return; 4067 var pill = document.createElement('a'); 4068 pill.className = 'osai-live-pill'; 4069 pill.href = '/latest.html'; 4070 pill.setAttribute('aria-label', 'See the latest AI projects'); 4071 pill.innerHTML = '<span class="olp-dot"></span><b>' + parseInt(c.projects_today, 10) + '</b> <span>new AI projects today</span> <span class="olp-go">→</span>'; 4072 var slot = document.getElementById('osaiPillSlot'); 4073 if (slot) { slot.appendChild(pill); } /* slot réservé â zéro décalage de mise en page */ 4074 else { h1.parentNode.insertBefore(pill, h1); } /* repli : anciennes pages sans slot */ 4075 if (typeof localizeTextTree === 'function') localizeTextTree(pill); 4076 }); 4077 } 4078 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiLivePill); else osaiLivePill(); 4079 4080 /* âââ V23b â Section EMERGING sous le hero (home uniquement). 4081 Les 3 découvertes du jour en cartes, injectées APRÃS #heroSection : 4082 zéro modification d'index.html, zéro régénération des 6 langues. 4083 Données : /emerging-public.json (build-graph, cron 9h45). */ 4084 function osaiEmergingHome(){ 4085 if (!isHome || document.querySelector('.osai-em-home')) return; 4086 fetch('/emerging-public.json?d=' + new Date().toISOString().slice(0,10)).then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }).then(function(d){ 4087 var P = d && d.projects; 4088 if (!P || P.length < 3) return; 4089 var hero = document.getElementById('heroSection') || (function(){ var t = document.querySelector('.hero-title'); return t ? t.closest('section') : null; })(); 4090 if (!hero || !hero.parentNode) return; 4091 4092 function av(p){ 4093 var initial = (p.name || '?').trim().charAt(0).toUpperCase() || '?'; 4094 var h = 0, n = p.name || ''; for (var i = 0; i < n.length; i++){ h = n.charCodeAt(i) + ((h << 5) - h); } 4095 var cols = ['#6366f1','#0071e3','#e0245e','#9b59d9','#f24f00','#c98a00','#1a56db','#16a394']; 4096 var img = p.avatar ? '<img src="' + String(p.avatar).replace(/"/g,'"') + '" alt="" loading="lazy" onerror="this.remove();" onload="this.parentNode.style.background=\'transparent\'">' : ''; 4097 return '<span class="oeh-av" style="background:' + cols[Math.abs(h) % cols.length] + '">' + initial + img + '</span>'; 4098 } 4099 function esc(x){ return String(x == null ? '' : x).replace(/</g, '<'); } 4100 function kf(n){ n = +n || 0; return n >= 1000 ? (Math.round(n / 100) / 10) + 'k' : String(n); } 4101 4102 var sec = document.createElement('section'); 4103 sec.className = 'osai-em-home'; 4104 sec.innerHTML = '<div class="oeh-wrap"><div class="oeh-panel">' 4105 + '<div class="oeh-head">' 4106 + '<h2 class="oeh-title"><span class="sec-ic" aria-hidden="true">' 4107 + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' 4108 + '<path d="M12 2v8M8 6l4-4 4 4"/><circle cx="12" cy="16" r="5"/></svg></span>' 4109 + '<span>Emerging</span></h2>' /* [02/08/2026, Romain] etait un <span> avec un emoji fusee : les autres sections ont un h2 et une icone au trait. Meme icone que l'entree « Emerging » du menu. */ 4110 + '<span class="oeh-sub"><span>Detected by our signals</span> · <span>updated daily</span></span>' 4111 + '</div>' 4112 + '<div class="oeh-grid">' 4113 + P.slice(0, 3).map(function(p){ 4114 var href = p.page || p.url; 4115 return '<a class="oeh-card" href="' + String(href).replace(/"/g,'"') + '">' 4116 + av(p) 4117 + '<span class="oeh-body"><b>' + esc(p.name) + '</b>' 4118 + '<span class="oeh-line"><span class="oeh-mom">+' + kf(p.d7 || 0) + '★<i>/7d</i></span>' 4119 + (p.accel ? '<span class="oeh-fire"><span>accelerating</span></span>' : '') 4120 + '</span>' 4121 + (p.desc ? '<span class="oeh-desc">' + esc(String(p.desc).slice(0, 90)) + '</span>' : '') 4122 + '</span></a>'; 4123 }).join('') 4124 + '</div>' 4125 /* [09/08/2026] Le lien vivait en pastille dans l'en-tete, colle au titre. 4126 Il descend sous la grille, en bouton plein, comme toutes les autres 4127 sections de l'accueil depuis le 08/08. */ 4128 + '<div class="oeh-cta"><a href="/emerging.html">See all ' + P.length + ' emerging projects →</a></div>' 4129 + '</div></div>'; 4130 hero.parentNode.insertBefore(sec, hero.nextSibling); 4131 /* largeur : calée sur le conteneur réel du hero (la home n'utilise pas 4132 la même max-width que les pages internes) â mesurée, pas devinée. */
4133 try { 4134 var ref = hero.firstElementChild; 4135 var vw = document.documentElement.clientWidth || window.innerWidth; 4136 /* Mobile : le CSS par défaut suffit â et le hero peut contenir des 4137 éléments plus larges que l'écran (ticker) qui fausseraient la mesure. */ 4138 if (ref && vw > 760) { 4139 var rw = Math.min(Math.round(ref.getBoundingClientRect().width), vw); 4140 if (rw > 700) { 4141 var rcs = getComputedStyle(ref), wr = sec.querySelector('.oeh-wrap'); 4142 wr.style.maxWidth = rw + 'px'; 4143 wr.style.paddingLeft = rcs.paddingLeft; 4144 wr.style.paddingRight = rcs.paddingRight; 4145 wr.style.boxSizing = 'border-box'; 4146 } 4147 } 4148 } catch(e) {} 4149 if (typeof localizeTextTree === 'function') localizeTextTree(sec); 4150 }); 4151 } 4152 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiEmergingHome); else osaiEmergingHome(); 4153 4154 /* âââ V114 â « Maintained by @x â » sur les fiches /project/ réclamées, 4155 et l'invitation à réclamer sur les autres. Données : /claims-public.json 4156 (écrit par api/claim.php à chaque claim). Zéro régénération de fiches. */ 4157 function osaiClaim(){ 4158 var path = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 4159 var m = path.match(/^\/project\/([^\/]+)\.html$/); 4160 if (!m || document.querySelector('.osai-claim')) return; 4161 var slug = decodeURIComponent(m[1]); 4162 4163 function findRepo(){ 4164 var links = document.querySelectorAll('a[href*="github.com/"]'); 4165 for (var j = 0; j < links.length; j++){ 4166 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; 4167 var mm = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 4168 if (mm && ['topics','sponsors','features','orgs','search','trending'].indexOf(mm[1].toLowerCase()) < 0) 4169 return (mm[1] + '/' + mm[2]).toLowerCase(); 4170 } 4171 return null; 4172 } 4173 function place(el){ 4174 var tries = 0; 4175 (function tick(){ 4176 var h = document.querySelector('.osai-health'); 4177 var anchor = h || document.querySelector('.cta-row') || document.querySelector('main h1, .hero h1, h1'); 4178 if (anchor){ 4179 if (h && h.insertAdjacentElement) h.insertAdjacentElement('afterend', el); 4180 else if (anchor.insertAdjacentElement) anchor.insertAdjacentElement('afterend', el); 4181 if (typeof localizeTextTree === 'function') localizeTextTree(el); 4182 return; 4183 } 4184 if (++tries < 10) setTimeout(tick, 300); 4185 })(); 4186 } 4187 function css(){ 4188 if (document.getElementById('osaiClaimCSS')) return; 4189 var st = document.createElement('style'); st.id = 'osaiClaimCSS'; 4190 st.textContent = 4191 '.osai-claim{margin:10px 0 6px;font-family:Poppins,system-ui,sans-serif;}' + 4192 '.oc-pill{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;border:2px solid #6366f1;color:#6366f1;font-size:.84rem;font-weight:600;text-decoration:none;}' + 4193 'body.dark .oc-pill{border-color:#818cf8;color:#818cf8;}' + 4194 '.oc-pill b{font-family:"DM Mono",monospace;font-weight:500;}' + 4195 '.oc-cta{display:inline-block;margin:4px 0 0;font-family:"DM Mono",monospace;font-size:.7rem;color:var(--muted,#5a6d87);text-decoration:none;}' + 4196 '.oc-cta:hover{color:#6366f1;text-decoration:underline;}'; 4197 document.head.appendChild(st); 4198 } 4199 4200 fetch('/claims-public.json?d=' + new Date().toISOString().slice(0,15)).then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }).then(function(d){ 4201 var claims = (d && d.claims) || {}; 4202 var repo = findRepo(); 4203 var hit = claims[slug] || (repo ? claims[repo] : null); 4204 css(); 4205 var box = document.createElement('div'); 4206 box.className = 'osai-claim'; 4207 /* [31/07] t() etait declare plus bas, DANS la branche « else if » : par 4208 hoisting du var il existait ici mais valait undefined, et l'appeler 4209 faisait planter la fonction sans un mot dans la console â badge 4210 invisible malgre une revendication verifiee et publiee. */ 4211 var t = function(x){ return (typeof window.osaiT === 'function') ? window.osaiT(x) : x; }; 4212 /* [31/07, 2e passe] Le badge n'apparaissait QUE si le membre avait lie 4213 GitHub. Un mainteneur verifie par le marqueur README (donc sans 4214 OAuth) n'obtenait rien apres avoir pousse un commit dans son depot. 4215 On affiche desormais la meilleure identite disponible, et a defaut 4216 le simple fait â verifie â qu'un mainteneur tient cette page. */ 4217 if (hit){ 4218 var gh = String(hit.gh || '').replace(/[^A-Za-z0-9-]/g, ''); 4219 var hd = String(hit.handle || '').replace(/[^A-Za-z0-9_-]/g, ''); 4220 var lg = (NAV_LANG && NAV_LANG !== 'en') ? '/' + NAV_LANG : ''; 4221 if (gh) { 4222 box.innerHTML = '<a class="oc-pill" href="https://github.com/' + gh + '" target="_blank" rel="noopener">' 4223 + '🛡️ <span>' + t('Maintained by') + '</span> <b>@' + gh + '</b> ✓</a>'; 4224 } else if (hd) { 4225 box.innerHTML = '<a class="oc-pill" href="' + lg + '/u/' + hd + '">' 4226 + '🛡️ <span>' + t('Maintained by') + '</span> <b>@' + hd + '</b> ✓</a>'; 4227 } else { 4228 box.innerHTML = '<span class="oc-pill">🛡️ <span>' 4229 + t('Claimed by its maintainer') + '</span> ✓</span>'; 4230 } 4231 } else if (repo){ 4232 /* le verdict du claim, s'il revient en ?claimed=⦠*/ 4233 var flag = (location.search.match(/[?&]claimed=([a-z_]+)/) || [])[1]; 4234 if (flag) { 4235 try { history.replaceState(null, '', location.pathname); } catch(e2){} 4236 var okC = (flag === 'ok' || flag === 'already'); 4237 var MSG = { 4238 ok: t('Verified â you are now listed as the maintainer of this project.'), 4239 already: t('Already verified â you are listed as the maintainer.'), 4240 link_github_first: t('To claim a project, your account must be signed in with GitHub.'), 4241 not_the_owner: t('This repository belongs to another GitHub account.'), 4242 not_public_org_member: t('You must be a public member of the organization that owns this repository.'), 4243 not_a_contributor: t('You must appear among the contributors of this repository.') 4244 }; 4245 var vb = document.createElement('div'); 4246 vb.style.cssText = 'margin:10px 0;padding:12px 16px;border-radius:12px;font-size:.85rem;font-weight:600;' 4247 + (okC ? 'background:rgba(13,158,112,.1);border:1px solid #0d9e70;color:#0d9e70;' 4248 : 'background:rgba(217,119,6,.09);border:1px solid #d97706;color:#b45309;'); 4249 vb.innerHTML = (okC ? '\u2713 ' : '\u26A0 ') + (MSG[flag] || t('Claim failed â contact [email protected] if you believe this is wrong.')) 4250 + (flag === 'link_github_first' 4251 ? ' <a href="/api/github-auth.php" style="display:inline-block;margin-left:8px;background:#1c2030;color:#fff;border-radius:8px;padding:5px 12px;font-size:.78rem;text-decoration:none">
4251' + t('Continue with GitHub') + ' \u2192</a>' 4252 : ''); 4253 box.appendChild(vb); 4254 if (okC) { try { fetch('/claims-public.json?d=' + new Date().toISOString().slice(0,15)); } catch(e3){} } 4255 } 4256 /* [31/07] Ce lien pointait sur /api/claim.php : le mainteneur qui 4257 cliquait « Are you the maintainer? » recevait {"error":"Not signed 4258 in."} en JSON brut, sur 60 000 fiches. Il mene desormais a la page 4259 de revendication, qui explique et connecte. */ 4260 box.innerHTML += '<a class="oc-cta" href="/claim.html?repo=' + encodeURIComponent(repo) + '">' 4261 + '🔑 <span>' + t('Are you the maintainer?') + '</span> <span>' + t('Claim this project') + '</span> →</a>'; 4262 /* le claim se joue ICI, en POST : l'anonyme part se connecter, le 4263 connecté voit TOUJOURS un verdict â plus jamais de JSON brut ni de 4264 clic silencieux. (Le lien ?go=1 reste en filet si JS meurt.) */ 4265 /* [31/07/2026 â AUDIT] Ce bouton interceptait le clic et postait vers 4266 /api/claim.php SANS action : l'API repondait le statut (sans champ 4267 « ok »), le script lisait donc une erreur et affichait « Claim 4268 failed â contact [email protected] » a TOUT mainteneur connecte qui 4269 cliquait. Un refus imaginaire, sur toutes les fiches. Le clic suit 4270 desormais simplement le lien vers /claim.html, qui pre-remplit le 4271 depot, explique la preuve et gere la connexion : une seule voie de 4272 revendication, celle qui marche. */ 4273 } else { return; } 4274 place(box); 4275 }); 4276 } 4277 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiClaim); else osaiClaim(); 4278 4279 /* âââ V115 â avatar du membre dans la barre (Google ou GitHub). 4280 /api/me.php en cache sessionStorage 10 min : une requête par session, 4281 et les anonymes (401) sont mémorisés aussi pour ne pas marteler l'API. */ 4282 function osaiNavAvatar(){ 4283 var btn = document.getElementById('acctNavBtn'); 4284 if (!btn) return; 4285 function paint(url, plan){ 4286 if (url) { 4287 btn.innerHTML = '<img src="' + String(url).replace(/"/g, '"') + '" alt="" ' + 4288 'style="width:26px;height:26px;border-radius:50%;object-fit:cover;display:block" ' + 4289 'onerror="this.remove()">'; 4290 btn.setAttribute('title', 'Your account'); 4291 } 4292 /* chip de plan VISIBLE, à côté du bouton compte (payants uniquement) */ 4293 if (plan && plan !== 'free' && !document.querySelector('.osai-plan-chip')) { 4294 if (!document.getElementById('osaiPlanCSS')) { 4295 var pst = document.createElement('style'); pst.id = 'osaiPlanCSS'; 4296 /* Romain 31/07 : l ancienne version (1,5px + halo + etoile \u2726 en 4297 .58rem monospace) faisait gadget. Liseres 2 px = la charte ; 4298 DM Mono comme le reste de la barre ; hover rempli indigo. */ 4299 pst.textContent = '.osai-plan-chip{display:inline-flex;align-items:center;margin-left:8px;' + 4300 'background:transparent;color:var(--accent,#6366f1);border:2px solid var(--accent,#6366f1);border-radius:999px;' + 4301 'padding:3px 12px;font-weight:700;font-size:.62rem;letter-spacing:.1em;' + 4302 'font-family:\'DM Mono\',ui-monospace,monospace;line-height:1.7;text-decoration:none;' + 4303 'transition:background .15s,color .15s}' + 4304 '#site-nav a.osai-plan-chip:hover, a.osai-plan-chip:hover{background:var(--accent,#6366f1) !important;color:#fff !important}'; 4305 document.head.appendChild(pst); 4306 } 4307 var chip = document.createElement('a'); 4308 chip.className = 'osai-plan-chip'; 4309 chip.href = '/account.html#secDev'; 4310 chip.textContent = String(plan).toUpperCase(); 4311 chip.setAttribute('title', 'Your API plan â manage in your account'); 4312 /* [04/08 pivot, Romain : « personne n'a d'offre, y'a que moi »] 4313 la pastille de plan (DEV/PRO) n'est plus inseree dans la barre. */ 4314 void chip; 4315 btn.setAttribute('title', 'Your account â ' + plan + ' plan'); 4316 } 4317 } 4318 /* [30/07] LE CACHE MONTRAIT L'AVATAR DU COMPTE PRECEDENT. 4319 Il etait indexe sur une seule cle, 'osai_me1', valable 10 minutes. Se
4320 deconnecter puis se reconnecter avec un AUTRE compte dans cette fenetre 4321 affichait donc la photo du premier â constate par Romain en passant de 4322 [email protected] a [email protected]. Sur un poste partage, 4323 c'est le visage de quelqu'un d'autre sur son propre compte. 4324 Le cache porte desormais une empreinte de la SESSION : un cookie 4325 different â autre compte, ou deconnexion â ne correspond plus a la 4326 cle, et on redemande a /api/me.php. */ 4327 function sessFingerprint(){ 4328 var m = document.cookie.match(/(?:^|;\s*)osai_sess=([^;]*)/); 4329 var v = m ? m[1] : ''; 4330 var h = 0; 4331 for (var i = 0; i < v.length; i++) h = (h * 31 + v.charCodeAt(i)) >>> 0; 4332 return String(h); 4333 } 4334 var fp = sessFingerprint(); 4335 var cached = null; 4336 try { cached = JSON.parse(sessionStorage.getItem('osai_me1') || 'null'); } catch(e) {} 4337 if (cached && cached.fp === fp && (Date.now() - cached.t) < 600000){ paint(cached.avatar, cached.plan); return; } 4338 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 4339 .then(function(j){ 4340 var avatar = (j && j.avatar) || ''; 4341 var plan = (j && j.plan) || ''; 4342 var logged = !!(j && j.email); 4343 try { sessionStorage.setItem('osai_me1', JSON.stringify({ t: Date.now(), fp: fp, avatar: avatar, plan: plan, logged: logged })); } catch(e) {} 4344 paint(avatar, plan); 4345 }); 4346 } 4347 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiNavAvatar); else osaiNavAvatar(); 4348 4349 /* âââ V121 â L'ENCART COMPTE + FRICTION INTELLIGENTE âââ 4350 Pour les visiteurs anonymes, sur les pages clés : un encart discret dont 4351 le TITRE est l'action contextuelle de la page (la friction intelligente) 4352 et le corps l'argumentaire du compte gratuit. Refermable 7 jours. 4353 Jamais pour les connectés, jamais sur les pages compte/auth. */ 4354 function osaiSignupNudge(){ 4355 var path = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, '') || '/'; 4356 var CTX = { 4357 '/dashboards.html': ['\u2B50', 'Save this dashboard'], 4358 '/latest.html': ['\uD83D\uDD14', 'Get notified when a new project appears'], 4359 '/releases.html': ['\uD83D\uDCE6', 'Follow these projects'], 4360 '/emerging.html': ['\uD83D\uDE80', 'Save discoveries to your portfolio'], 4361 '/health.html': ['\uD83D\uDC9A', 'Track health evolution over time'], 4362 '/leaderboard.html':['\uD83D\uDCCA', 'Follow your favorite models'], 4363 '/trends.html': ['\uD83D\uDCC8', 'Watch these trends from your account'], 4364 '/category.html': ['\uD83E\uDDF0', 'Build your own AI toolbox'], 4365 '/pricing.html': ['\uD83D\uDCB0', 'Get price-drop alerts on the models you use'] 4366 }; 4367 var ctx = CTX[path]; 4368 if (!ctx && /^\/vertical\//.test(path)) ctx = ['\uD83C\uDFED', 'Follow this industry\u2019s projects']; 4369 if (!ctx) return; 4370 try { if (localStorage.getItem('osai_nudge_off') && Date.now() - (+localStorage.getItem('osai_nudge_off')) < 6048e5) return; } catch(e) {} 4371 4372 /* connecté ? (même cache que l'avatar : zéro requête ajoutée) */ 4373 var me = null; 4374 try { me = JSON.parse(sessionStorage.getItem('osai_me1') || 'null'); } catch(e) {} 4375 function show(){ 4376 if (document.querySelector('.osai-nudge')) return; 4377 var host = document.querySelector('main .wrap, main, .wrap'); 4378 var h1 = host ? host.querySelector('h1') : null; 4379 var after = h1 ? (h1.closest('header') || h1) : null; 4380 if (!after) return; 4381 var css = document.createElement('style'); 4382 css.textContent = '.osai-nudge{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:18px 0 6px;padding:13px 16px;border:1.5px dashed var(--accent,#6366f1);border-radius:13px;background:var(--accent-2-soft,rgba(99,102,241,.06));font-family:Poppins,system-ui,sans-serif;position:relative;}' + 4383 '.osai-nudge b{font-size:.9rem;}' + 4384 '.osai-nudge .on-ticks{display:flex;gap:12px;flex-wrap:wrap;font-size:.72rem;color:var(--muted,#5a6d87);}' + 4385 '.osai-nudge .on-ticks span::before{content:"\\2713 ";color:var(--accent,#6366f1);font-weight:700;}' + 4386 '.osai-nudge a.on-cta{margin-left:auto;flex:none;font-weight:600;font-size:.8rem;background:var(--accent,#6366f1);color:#fff;border-radius:9px;padding:9px 16px;text-decoration:none;}' + 4387 '.osai-nudge .on-x{position:absolute;top:6px;right:9px;background:none;border:none;color:var(--muted,#5a6d87);cursor:pointer;font-size:.9rem;line-height:1;padding:2px;}' + 4388 '@media(max-width:700px){.osai-nudge a.on-cta{margin-left:0;}}'; 4389 document.head.appendChild(css); 4390 var box = document.createElement('aside'); 4391 box.className = 'osai-nudge'; 4392 box.innerHTML = '<div><b>' + ctx[0] + ' <span>' + ctx[1] + '</span></b>' + 4393 '<div class="on-ticks"><span>Save projects</span><span>Build AI portfolios</span><span>Follow emerging tools</span><span>Get daily alerts</span></div></div>' + 4394 '<a class="on-cta" href="/account.html"><span>Create your free account</span> →</a>' + 4395 '<button class="on-x" aria-label="Dismiss" title="Hide for a week">✕</button>'; 4396 box.querySelector('.on-x').addEventListener('click', function(){ 4397 box.remove(); 4398 try { localStorage.setItem('osai_nudge_off', String(Date.now())); } catch(e) {} 4399 }); 4400 if (after.insertAdjacentElement) after.insertAdjacentElement('afterend', box); 4401 if (typeof localizeTextTree === 'function') localizeTextTree(box); 4402 } 4403 if (me && (Date.now() - me.t) < 600000) { 4404 if (!me.logged) show(); /* cache dit anonyme (fraîcheur ⤠10 min) */ 4405 return; 4406 } 4407 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 4408 .then(function(j){ if (!j || !j.email) show(); }); 4409 } 4410 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiSignupNudge); else osaiSignupNudge(); 4411 4412 /* âââ V122 â L'INVITATION CONTEXTUELLE âââ 4413 [27/08/2026, demande de Romain] Une sollicitation a s'inscrire, revendiquer 4414 ou deposer un outil. L'encart V121 ci-dessus ne couvre que neuf pages de 4415 listes ; il ignore les 10 000 fiches projet, qui sont l'essentiel du trafic 4416 et le SEUL endroit ou « ce projet est le votre ? » veut dire quelque chose. 4417 4418 Trois partis pris : 4419 . JAMAIS un interstitiel plein ecran. Google degrade le classement mobile 4420 des pages qui masquent le contenu a l'arrivee ; une carte en bas d'ecran 4421 n'est pas concernee, et ne coute pas le referencement qu'on cherche. 4422 . JAMAIS a l'ouverture. Elle attend un signe d'interet reel â trente 4423 secondes, la moitie de la page, ou la souris qui part vers la barre 4424 d'adresse. Un visiteur qui rebondit en trois secondes n'est pas un 4425 inscrit potentiel, c'est quelqu'un qu'on derange. 4426 . JAMAIS deux fois. Refermee, elle se tait quatorze jours ; et si l'encart 4427 V121 occupe deja la page, elle ne s'affiche pas du tout. */ 4428 function osaiInvite(){ 4429 var p = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, '') || '/'; 4430 /* la ou l'action est deja engagee, insister serait absurde */ 4431 /* token.html est exclue depuis le 27/08 : sa raison d'etre est qu'on lise 4432 une adresse de 44 caracteres et deux avertissements sur des fonds 4433 perdus. Une carte qui recouvre le coin pendant cette lecture travaille 4434 exactement contre la page. Vu a l'ecran, elle masquait « The rule has 4435 no exception ». */ 4436 if (/^\/(account|claim|submit|login|signin|auth|unsub|legal|privacy|terms|cgu|token)/.test(p)) return; 4437 if (document.querySelector('.osai-nudge, .osai-inv')) return; 4438 try { 4439 var off = +localStorage.getItem('osai_inv_off'); 4440 if (off && Date.now() - off < 12096e5) return; /* 14 jours */ 4441 if (sessionStorage.getItem('osai_inv_seen')) return; /* une fois par visite */ 4442 } catch(e) {} 4443 4444 var T = function(x){ return (typeof window.osaiT === 'function' && window.osaiT(x)) || x; }; 4445 4446 /* ââ quel repo, s'il y en a un ââ */ 4447 function repoIci(){ 4448 var e = document.querySelector('[data-repo]'); 4449 var v = e ? (e.getAttribute('data-repo') || '').trim() : ''; 4450 return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(v) ? v : ''; 4451 } 4452 4453 /* ââ que proposer ici ââ */ 4454 var repo = /^\/(tool|project)\//.test(p) ? repoIci() : ''; 4455 var msg; 4456 if (repo) { 4457 msg = { t: 'Is this your project?', 4458 s: 'Claim it to write the description in your own words, see your pageâs audie
4458nce, and get alerted when the signals move.', 4459 c: 'Claim ' + repo.split('/')[1], 4460 u: '/claim.html?repo=' + encodeURIComponent(repo).replace(/%2F/gi, '/') }; 4461 } else if (/^\/(category|categories|alternatives|compare|latest|emerging|trends|leaderboard)/.test(p)) { 4462 msg = { t: 'You maintain an open-source AI project?', 4463 s: 'Add it to olud.ai. It gets a page that keeps itself up to date â stars, releases, health, in five languages.', 4464 c: 'Submit your project', 4465 u: '/submit.html' }; 4466 } else { 4467 msg = { t: 'Create a free account', 4468 s: 'Save projects, build your own AI toolbox, and get a daily digest of what actually moved.', 4469 c: 'Create your account', 4470 u: '/account.html' }; 4471 } 4472 4473 function css(){ 4474 if (document.getElementById('osaiInvCSS')) return; 4475 var st = document.createElement('style'); st.id = 'osaiInvCSS'; 4476 /* Tout passe par les variables du theme : la carte doit vivre en clair 4477 comme en sombre sans qu'aucune couleur ne soit figee ici. */ 4478 st.textContent = 4479 '.osai-inv{position:fixed;z-index:9998;right:18px;bottom:18px;width:340px;max-width:calc(100vw - 24px);' + 4480 'background:var(--surface,#fff);border:1px solid var(--border,rgba(15,23,36,.10));border-radius:16px;' + 4481 'padding:16px 18px 15px;font-family:Poppins,system-ui,sans-serif;color:var(--txt,#0f1424);' + 4482 'box-shadow:0 2px 6px rgba(15,23,36,.06),0 14px 40px rgba(15,23,36,.14);' + 4483 'opacity:0;transform:translateY(14px);transition:opacity .28s ease,transform .28s ease;}' + 4484 '.osai-inv.on{opacity:1;transform:none;}' + 4485 '.osai-inv .oi-t{font-weight:600;font-size:.95rem;line-height:1.35;padding-right:20px;}' + 4486 '.osai-inv .oi-s{margin-top:6px;font-size:.79rem;line-height:1.6;color:var(--muted,#5a6d87);}' + 4487 '.osai-inv .oi-go{display:inline-block;margin-top:13px;background:var(--accent,#6366f1);color:#fff;' + 4488 'font-weight:600;font-size:.8rem;border-radius:10px;padding:9px 16px;text-decoration:none;}' + 4489 '.osai-inv .oi-go:hover{filter:brightness(1.08);}' + 4490 '.osai-inv .oi-x{position:absolute;top:8px;right:10px;background:none;border:none;cursor:pointer;' + 4491 'color:var(--muted,#5a6d87);font-size:.95rem;line-height:1;padding:4px;border-radius:6px;}' + 4492 '.osai-inv .oi-x:hover{background:var(--accent-2-soft,rgba(99,102,241,.10));}
4492' + 4493 '.osai-inv .oi-x:focus-visible,.osai-inv .oi-go:focus-visible{outline:2px solid var(--accent,#6366f1);outline-offset:2px;}' + 4494 /* En dessous de 640px la carte devient un bandeau : plus de place pour 4495 le texte, et le pouce tombe naturellement sur le bouton. */ 4496 /* 80px et non 10 : #osaiHelpBtn occupe le coin bas-gauche avec un 4497 z-index superieur, et recouvrait le bouton d'action. Verifie a 4498 l'ecran le 27/08 â la carte passe au-dessus de lui, pas dessous. */ 4499 '@media(max-width:640px){.osai-inv{right:10px;left:10px;bottom:80px;width:auto;border-radius:14px;}}' + 4500 '@media(prefers-reduced-motion:reduce){.osai-inv{transition:none;}}'; 4501 document.head.appendChild(st); 4502 } 4503 4504 /* ââ le coin bas est deja occupe ââââââââââââââââââââââââââââââââââââââââ 4505 Releve a l'ecran le 27/08, accueil en 1280x820 : #backToTop tient le 4506 coin bas-droite (44x44), #osaiHelpBtn le bas-gauche, et #cookieBanner 4507 barre toute la largeur sur 89 px avec un z-index SUPERIEUR au notre. 4508 On mesure au moment d'afficher plutot que de figer une valeur : ces 4509 elements se deplacent selon la page, le defilement et le consentement. */ 4510 function decalageBas(){ 4511 var d = 18, vw = window.innerWidth, vh = window.innerHeight, etroit = vw <= 640; 4512 var ids = etroit ? ['backToTop', 'osaiHelpBtn'] : ['backToTop']; 4513 for (var i = 0; i < ids.length; i++) { 4514 var e = document.getElementById(ids[i]); 4515 if (!e) continue; 4516 var st = getComputedStyle(e); 4517 if (st.display === 'none' || st.visibility === 'hidden') continue; 4518 /* On ne saute PAS un element transparent : #backToTop est a opacity 0 4519 tant qu'on n'a pas defile, et notre carte s'affiche justement APRES 4520 un defilement. L'ignorer reviendrait a le voir surgir sous elle. */ 4521 var r = e.getBoundingClientRect(); 4522 if (!r.height) continue; 4523 /* Seulement ce qui est REELLEMENT ancre en bas. Mesure du 27/08 en 4524 375x812 : #backToTop y passe a bottom:324px, en plein milieu de 4525 l'ecran â le compter comme un obstacle du bas propulsait la carte 4526 a 384 px du sol. Au-dela de 160 px, ce n'est plus le coin bas. */ 4527 if (vh - r.bottom > 160) continue; 4528 if (!etroit && r.right < vw - 400) continue; /* hors de notre colonne */ 4529 d = Math.max(d, vh - r.top + 16); /* 16 et non 12 : a 12 les deux bords se touchaient */ 4530 } 4531 return d; 4532 } 4533 4534 /* Le consentement passe AVANT nous : c'est une obligation, pas une offre. 4535 Tant que le bandeau est la, on se tait â et on repasse plus tard, parce 4536 qu'un visiteur qui vient d'accepter est justement disponible. */ 4537 function consentementEnCours(){ 4538 var c = document.getElementById('cookieBanner'); 4539 if (!c) return false; 4540 var st = getComputedStyle(c); 4541 return st.display !== 'none' && st.visibility !== 'hidden' && c.getBoundingClientRect().height > 0; 4542 } 4543 4544 var affichee = false, reports = 0; 4545 function montrer(){ 4546 if (affichee || document.querySelector('.osai-inv')) return; 4547 if (consentementEnCours()) { 4548 if (++reports > 6) return; /* deux minutes, puis on renonce */ 4549 setTimeout(montrer, 20000); 4550 return; 4551 } 4552 affichee = true; 4553 try { sessionStorage.setItem('osai_inv_seen', '1'); } catch(e) {} 4554 css(); 4555 var b = document.createElement('aside'); 4556 b.className = 'osai-inv'; 4557 b.setAttribute('role', 'complementary'); 4558 b.setAttribute('aria-label', T(msg.t)); 4559 b.innerHTML = '<button class="oi-x" type="button" aria-label="' + T('Dismiss') + '">✕</button>' + 4560 '<div class="oi-t"></div><div class="oi-s"></div>' + 4561 '<a class="oi-go" href="' + msg.u + '"></a>'; 4562 b.querySelector('.oi-t').textContent = T(msg.t); 4563 b.querySelector('.oi-s').textContent = T(msg.s); 4564 b.querySelector('.oi-go').textContent = T(msg.c) + ' â'; 4565 function fermer(){ 4566 b.classList.remove('on'); 4567 try { localStorage.setItem('osai_inv_off', String(Date.now())); } catch(e) {} 4568 setTimeout(function(){ if (b.parentNode) b.parentNode.removeChild(b); }, 300); 4569 document.removeEventListener('keydown', echap); 4570 } 4571 function echap(e){ if (e.key === 'Escape' || e.keyCode === 27) fermer(); } 4572 b.querySelector('.oi-x').addEventListener('click', fermer); 4573 document.addEventListener('keydown', echap); 4574 b.style.bottom = decalageBas() + 'px'; 4575 document.body.appendChild(b); 4576 if (typeof localizeTextTree === 'function') localizeTextTree(b); 4577 requestAnimationFrame(function(){ requestAnimationFrame(function(){ b.classList.add('on'); }); }); 4578 } 4579 4580 /* ââ ne jamais doubler ce que la page dit deja ââââââââââââââââââââââââââ 4581 Releve du 27/08 : une fiche projet porte DEJA trois invitations dans son 4582 flux (« Are you the maintainer? » a 2 532 px, « Claim this page » a 4583 3 146, « Get email alerts » a 3 834), et l'accueil en porte deux. Elles 4584 sont enterrees â l'accueil fait 24 746 px de haut â donc la carte garde 4585 son utilite pour qui ne descend pas. Mais si le visiteur en a croise 4586 une, insister devient du harcelement : on se retire. 4587 Les liens a taille nulle sont ignores : ce sont ceux des menus replies. */ 4588 function guetterDoublon(renoncer){ 4589 if (!window.IntersectionObserver) return; 4590 var liens = []; 4591 var tous = document.querySelectorAll('a[href*="/claim.html"],a[href*="/account.html"],a[href*="/submit.html"]'); 4592 for (var i = 0; i < tous.length; i++) { 4593 var a = tous[i]; 4594 if (a.closest('#site-nav,#site-footer,.osaiMM,#moreMenu,.osai-inv')) continue; 4595 var r = a.getBoundingClientRect(); 4596 if (!r.width || !r.height) continue; 4597 liens.push(a); 4598 } 4599 if (!liens.length) return; 4600 var io = new IntersectionObserver(function(ents){ 4601 for (var k = 0; k < ents.length; k++) { 4602 if (ents[k].isIntersecting) { io.disconnect(); renoncer(); return; } 4603 } 4604 }, { threshold: 0.5 }); 4605 for (var j = 0; j < liens.length; j++) io.observe(liens[j]); 4606 } 4607 4608 /* ââ les trois signes d'interet ââ */ 4609 function armer(){ 4610 var minuteur = setTimeout(montrer, 30000); 4611 guetterDoublon(function(){ 4612 clearTimeout(minuteur); 4613 affichee = true; /* verrou : la page a fait le travail */ 4614 var d = document.querySelector('.osai-inv'); 4615 if (d && d.parentNode) d.parentNode.removeChild(d); 4616 }); 4617 function auScroll(){ 4618 var h = document.documentElement; 4619 var total = (h.scrollHeight - h.clientHeight); 4620 if (total > 400 && (h.scrollTop || document.body.scrollTop) / total > 0.55) { 4621 clearTimeout(minuteur); montrer(); 4622 window.removeEventListener('scroll', auScroll); 4623 } 4624 } 4625 window.addEventListener('scroll', auScroll, { passive: true }); 4626 /* Intention de sortie : la souris franchit le haut de la fenetre. Sans 4627 objet au doigt, donc reserve aux pointeurs fins. */ 4628 if (window.matchMedia && window.matchMedia('(pointer:fine)').matches) { 4629 document.addEventListener('mouseout', function(e){ 4630 if (!e.relatedTarget && e.clientY <= 0) { clearTimeout(minuteur); montrer(); } 4631 }); 4632 } 4633 } 4634 4635 /* ââ connecte ? on se tait. Meme cache que l'avatar : zero requete ajoutee. ââ */ 4636 var me = null; 4637 try { me = JSON.parse(sessionStorage.getItem('osai_me1') || 'null'); } catch(e) {} 4638 if (me && (Date.now() - me.t) < 600000) { if (!me.logged) armer(); return; } 4639 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 4640 .then(function(j){ if (!j || !j.email) armer(); }); 4641 } 4642 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiInvite); else osaiInvite(); 4643 4644 /* âââ V22 â BADGE SANTà sur les fiches /tool/ et /project/ âââ
4645 Répond à « ce projet est-il vivant ? » directement sur la fiche, sans 4646 régénérer une seule page : données de /health-data.json (cron quotidien). 4647 Détection du repo : attribut [data-repo] de la fiche, sinon premier lien 4648 GitHub owner/repo du contenu (hors nav/footer). Pas de donnée â pas de badge. */ 4649 function osaiHealth(){ 4650 var hpath = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 4651 if (!/^\/(tool|project)\/[^\/]+\.html$/.test(hpath)) return; 4652 if (/\/index\.html$/.test(hpath) || document.querySelector('.osai-health')) return; 4653 4654 function findRepo(){ 4655 var els = document.querySelectorAll('[data-repo]'); 4656 for (var i = 0; i < els.length; i++){ 4657 var v = (els[i].getAttribute('data-repo') || '').trim().toLowerCase(); 4658 if (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/.test(v)) return v; 4659 } 4660 /* priorité aux boutons d'action de la fiche (View on GitHub), puis n'importe quel lien du contenu */ 4661 var scopes = ['.btn-row a[href*="github.com/"], .cta-row a[href*="github.com/"]', 'a[href*="github.com/"]']; 4662 for (var s = 0; s < scopes.length; s++){ 4663 var links = document.querySelectorAll(scopes[s]); 4664 for (var j = 0; j < links.length; j++){ 4665 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; // liens du menu/footer : ignorés 4666 var m = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 4667 if (m && ['topics','sponsors','features','orgs','search','trending'].indexOf(m[1].toLowerCase()) < 0) 4668 return (m[1] + '/' + m[2]).toLowerCase(); 4669 } 4670 } 4671 return null; 4672 } 4673 4674 var HCOLORS = { great:'#22c55e', good:'#4ade80', ok:'#eab308', warn:'#f97316', bad:'#ef4444', dead:'#9ca3af' }; 4675 4676 function hCSS(){ 4677 if (document.getElementById('osaiHealthCSS')) return; 4678 var s = document.createElement('style'); s.id = 'osaiHealthCSS'; 4679 /* La carte du mockup (02/08/2026) : anneau a gauche, composantes a 4680 droite. Tout passe par les variables du theme â aucun fond fige. */ 4681 s.textContent = 4682 '.osai-health{margin:18px 0 8px;font-family:Poppins,system-ui,sans-serif;}' + 4683 '.oh-card{display:grid;grid-template-columns:290px 1fr;background:var(--surface,#fff);' + 4684 'border:1px solid var(--border,rgba(0,0,0,.08));border-radius:18px;overflow:hidden;' + 4685 'box-shadow:0 1px 2px rgba(15,23,36,.04),0 6px 20px rgba(15,23,36,.05);}' + 4686 '.oh-left{padding:22px 20px 24px;border-right:1px solid var(--border,rgba(0,0,0,.08));' + 4687 'display:flex;flex-direction:column;align-items:center;}' + 4688 '.oh-t{display:flex;gap:9px;align-items:center;align-self:flex-start;font-weight:700;' + 4689 'font-size:1rem;color:var(--txt,#111);}' + 4690 '.oh-t svg{width:19px;height:19px;color:var(--accent,#6366f1);flex:none;}' + 4691 '.oh-ring{position:relative;width:150px;height:150px;margin-top:16px;}' + 4692 '.oh-ring svg{display:block;transform:rotate(-90deg);}' + 4693 '.oh-tr{fill:none;stroke:var(--surface2,#f2f4f9);stroke-width:11;}' + 4694 '.oh-va{fill:none;stroke-width:11;stroke-linecap:round;}' + 4695 '.oh-mid{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;' + 4696 'align-items:center;justify-content:center;line-height:1;}' + 4697 '.oh-mid b{font-size:2.25rem;font-weight:700;letter-spacing:-.03em;color:var(--txt,#111);' + 4698 'font-variant-numeric:tabular-nums;}' + 4699 '.oh-mid i{font-style:normal;font-size:.85rem;color:var(--muted,#5a6d87);margin-top:3px;}' + 4700 '.oh-verdict{margin-top:15px;font-weight:700;font-size:1rem;display:flex;align-items:center;gap:8px;}' + 4701 '.oh-verdict i{width:9px;height:9px;border-radius:50%;background:currentColor;font-style:normal;}' + 4702 '.oh-right{padding:20px 24px 16px;min-width:0;}' + 4703 '.oh-why{margin:0 0 14px;font-size:.92rem;color:var(--txt2,#3a4460);line-height:1.5;}' + 4704 '.oh-row{display:grid;grid-template-columns:132px minmax(0,1fr) 180px 92px;gap:12px;' + 4705 'align-items:center;margin:11px 0;}' + 4706 '.oh-nm{display:flex;gap:9px;align-items:center;font-weight:700;font-size:.88rem;color:var(--txt,#111);}' + 4707 '.oh-nm svg{width:17px;height:17px;color:var(--muted,#5a6d87);flex:none;}' + 4708 '.oh-txt{font-size:.8rem;color:var(--muted,#5a6d87);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' + 4709 '.oh-bar{display:block;height:7px;border-radius:99px;background:var(--surface2,#eef0f5);overflow:hidden;min-width:44px;}' + 4710 '.oh-fill{display:block;height:100%;border-radius:99px;}' + 4711 '.oh-pillr{justify-self:end;font-size:.76rem;font-weight:600;padding:4px 12px;border-radius:8px;white-space:nowrap;}
4711' + 4712 '.oh-meta{margin-top:12px;padding-top:12px;border-top:1px solid var(--border,rgba(0,0,0,.08));' + 4713 'font-size:.84rem;color:var(--muted,#5a6d87);display:flex;gap:6px 18px;flex-wrap:wrap;align-items:center;}' + 4714 '.oh-meta b{color:var(--txt,#111);}' + 4715 '.oh-meta a{color:var(--accent,#6366f1);text-decoration:none;font-weight:600;}' + 4716 '@media(max-width:900px){' + 4717 '.oh-card{grid-template-columns:1fr;}' + 4718 '.oh-left{border-right:0;border-bottom:1px solid var(--border,rgba(0,0,0,.08));}' + 4719 '.oh-row{grid-template-columns:1fr 92px;row-gap:5px;}' + 4720 '.oh-nm{grid-column:1;}' + 4721 '.oh-pillr{grid-row:1;grid-column:2;}' + 4722 '.oh-bar{grid-column:1/-1;}' + 4723 '.oh-txt{grid-column:1/-1;white-space:normal;}' + 4724 '}'; 4725 document.head.appendChild(s); 4726 } 4727 4728 function esc(t){ return String(t).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } 4729 4730 /* icones au trait, meme grammaire que le mega-menu */ 4731 function hIc(d){ 4732 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' + 4733 'stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>'; 4734 } 4735 var H_ICONS = { 4736 shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>', 4737 Activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>', 4738 Momentum: '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>', 4739 Community: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>', 4740 Maintenance: '<path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/>' 4741 }; 4742 4743 /* l'appreciation d'une composante, deduite de son taux */ 4744 function hRate(pct){ 4745 if (pct >= 85) return ['Excellent', '#15803d', 'rgba(34,197,94,.13)']; 4746 if (pct >= 60) return ['Good', '#1d4ed8', 'rgba(59,130,246,.13)']; 4747 if (pct >= 35) return ['Fair', '#b45309', 'rgba(245,158,11,.14)']; 4748 return ['Low', '#b91c1c', 'rgba(239,68,68,.13)']; 4749 } 4750 4751 function render(h){ 4752 hCSS(); 4753 var col = HCOLORS[h.cls] || HCOLORS.ok; 4754 var wrap = document.createElement('div'); 4755 wrap.className = 'osai-health'; 4756 var rows = ''; 4757 var order = ['Activity', 'Momentum', 'Community', 'Maintenance']; 4758 for (var i = 0; i < order.length; i++){ 4759 var p = h.parts && h.parts[order[i]]; 4760 if (!p) continue; 4761 var pct = p.max ? Math.round(p.v / p.max * 100) : 0; 4762 var trend = p.trend === 'up' ? ' ↗' : (p.trend === 'down' ? ' ↘' : ''); 4763 var r = hRate(pct); 4764 rows += '<div class="oh-row">' 4765 + '<span class="oh-nm">' + hIc(H_ICONS[order[i]] || '') + order[i] + '</span>' 4766 + '<span class="oh-txt">' + esc(p.txt || '') + trend + '</span>' 4767 + '<span class="oh-bar"><span class="oh-fill" style="width:' + pct + '%;background:' + col + ';"></span></span>' 4768 + '<span class="oh-pillr" style="color:' + r[1] + ';background:' + r[2] + ';">' + r[0] + '</span>' 4769 + '</div>'; 4770 } 4771 var score = (h.parts && h.score > 0) ? parseInt(h.score, 10) : 0; 4772 /* anneau : rayon 64, circonference 402,1 */ 4773 var dash = Math.max(0, Math.min(100, score)) / 100 * 402.1; 4774 wrap.innerHTML = 4775 '<div class="oh-card">' 4776 + '<div class="oh-left">' 4777 + '<span class="oh-t">' + hIc(H_ICONS.shield) + '<span>Project health</span></span>' 4778 + '<span class="oh-ring"><svg width="150" height="150" viewBox="0 0 150 150">' 4779 + '<circle class="oh-tr" cx="75" cy="75" r="64"></circle>' 4780 + '<circle class="oh-va" cx="75" cy="75" r="64" stroke="' + col + '" stroke-dasharray="' + dash.toFixed(1) + ' 402.1"></circle>' 4781 + '</svg><span class="oh-mid"><b>' + score + '</b><i>/100</i></span></span>' 4782 + '<span class="oh-verdict" style="color:' + col + ';"><span>' + esc(h.label || '') + '</span><i></i></span>' 4783 + '</div>' 4784 + '<div class="oh-right">' 4785 + '<p class="oh-why">' + esc(String(h.why || '')) + '</p>' + rows 4786 + '<div class="oh-meta"><span><span>Checked</span> <b>' + esc(h.checked || '') + '</b></span>' 4787 + '<a href="/health.html"><span>See all project health</span> →</a></div>' 4788 + '</div>' 4789 + '</div>'; 4790 var slot = document.getElementById('osaiHealthSlot'); 4791 var badges = document.querySelector('.wrap > .badges') || document.querySelector('.badges'); 4792 var cta = document.querySelector('.hero-main .cta-row') || document.querySelector('.cta-row'); 4793 var h1 = document.querySelector('main h1, .hero h1, article h1, h1'); 4794 if (slot) slot.appendChild(wrap); 4795 /* ORDRE DU MOCKUP : etiquettes -> sante -> boutons. Sur la fiche outil la 4796 rangee de badges precede les boutons, on se pose apres elle ; sur la 4797 fiche projet on se glisse AVANT la rangee de boutons â l'ancien code se 4798 posait apres, et la sante arrivait sous les boutons. */ 4799 else if (badges && badges.insertAdjacentElement) badges.insertAdjacentElement('afterend', wrap); 4800 else if (cta && cta.insertAdjacentElement) cta.insertAdjacentElement('beforebegin', wrap); 4801 else if (h1 && h1.parentNode) h1.parentNode.insertBefore(wrap, h1.nextSibling); 4802 else return; 4803 if (typeof localizeTextTree === 'function') localizeTextTree(wrap); 4804 if (typeof localizeAllLinks === 'function') localizeAllLinks(wrap
4804); 4805 } 4806 4807 var repo = findRepo(); 4808 if (!repo) return; 4809 var dayKey = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // cache-bust quotidien (données quotidiennes) 4810 fetch('/health-data.json?d=' + dayKey) 4811 .then(function(r){ return r.ok ? r.json() : null; }) 4812 .catch(function(){ return null; }) 4813 .then(function(d){ 4814 var h = d && d.data && d.data[repo]; 4815 if (h && h.label) render(h); 4816 }); 4817 } 4818 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiHealth); else osaiHealth(); 4819 4820 /* âââ V24 â TICKER TERMINAL sur la home : bandeau défilant (movers, prix, nouveaux 4821 modèles) qui mène au /dashboard.html. Même philosophie que le live pill : zéro 4822 modification d'index.html, dégradation propre si une source manque. âââ */ 4823 function osaiHomeTape(){ 4824 if (!isHome || document.getElementById('osaiTape')) return; 4825 /* Ancrage, du plus précis au filet ultime â mêmes repères que le live pill V17 : 4826 1. #heroSection · 2. le conteneur de .hero-title · 3. .hero · 4. le conteneur 4827 du slot pill · 5. juste après le nav injecté (le bandeau apparaît TOUJOURS). */ 4828 var ht = document.querySelector('#heroSection .hero-title') || document.querySelector('.hero-title'); 4829 /* [02/08/2026, Romain] « la barre Terminal ne devrait-elle pas etre au 4830 niveau de la section Terminal ? » Si : un bandeau qui s'annonce 4831 « olud.ai TERMINAL » et qui flotte a dix sections de la section du meme 4832 nom se lit comme un element decoratif. Colle a elle, il devient sa 4833 ligne vivante â la preuve que ce qu'annonce la section bouge vraiment. 4834 #termHome d'abord, le hero seulement s'il n'existe pas. */ 4835 var anchor = document.getElementById('termHome') 4836 || document.getElementById('heroSection') 4837 || (ht && ht.closest && ht.closest('section,header')) 4838 || document.querySelector('.hero') 4839 || (document.getElementById('osaiPillSlot') && document.getElementById('osaiPillSlot').parentNode) 4840 || document.getElementById('site-nav'); 4841 if (!anchor) return; 4842 function j(u){ return fetch(u + '?t=' + Date.now()).then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }); } 4843 function esc(t){ return String(t == null ? '' : t).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } 4844 function kk(n){ n = +n || 0; return n >= 1000 ? (n/1000).toFixed(n >= 10000 ? 0 : 1) + 'k' : String(n); } 4845 Promise.all([j('/today-data.json'), j('/projects-velocity.json'), j('/price-history.json'), j('/paid-models-data.json')]).then(function(R){ 4846 var today = R[0], vel = R[1], prices = R[2], paid = R[3], tks = []; 4847 if (vel){ 4848 var mv = []; 4849 for (var u in vel){ var m = String(u).match(/github\.com\/[^\/]+\/([^\/#?]+)/i); if (m) mv.push({ n: m[1], d1: +vel[u].d1 || 0 }); } 4850 mv.sort(function(a, b){ return b.d1 - a.d1; }); 4851 mv.slice(0, 6).forEach(function(x){ tks.push('<span class="ot-up">▲</span><b>' + esc(x.n) + '</b> +' + kk(x.d1) + ' ★ 24h'); }); 4852 } 4853 if (prices && prices.models){ 4854 var pc = []; 4855 for (var id in prices.models){ 4856 var pm = prices.models[id], h = pm.history || []; 4857 if (h.length >= 2 && (h[h.length-1]['in'] !== h[h.length-2]['in'] || h[h.length-1].out !== h[h.length-2].out)) 4858 pc.push({ n: pm.name, a: h[h.length-2]['in'], b2: h[h.length-1]['in'], d: h[h.length-1].d }); 4859 } 4860 pc.sort(function(a, b){ return a.d < b.d ? 1 : -1; }); 4861 pc.slice(0, 4).forEach(function(x){ 4862 var dn = x.b2 < x.a; 4863 tks.push('<span class="' + (dn ? 'ot-up' : 'ot-down') + '">' + (dn ? '▼' : '▲') + '</span><b>' + esc(x.n) + '</b> $' + x.a + ' → $' + x.b2); 4864 }); 4865 } 4866 if (today && today.models){ today.models.slice(0, 3).forEach(function(m){ tks.push('<span class="ot-new">●</span><b>' + esc(m.name) + '</b> ' + esc(m.provider)); }); } 4867 if (paid && paid.models){ 4868 paid.models.filter(function(m){ return m.created && (Date.now()/1000 - m.created) < 14*86400; }) 4869 .sort(function(a, b){ return b.created - a.created; }).slice(0, 3) 4870 .forEach(function(m){ tks.push('<span class="ot-new">●</span><b>' + esc(m.name) + '</b> ' + esc(m.provider) + ' · paid'); }); 4871 } 4872 /* zip open/paid dans les NEW : un lancement payant n'attend pas derrière tous les open */ 4873 (function(){ 4874 var po=[], pp=[]; 4875 tks.forEach(function(t){ if(t.indexOf('NEW')>-1 || t.indexOf('ot-new')>-1){ (t.indexOf('paid')>-1?pp:po).push(t); } }); 4876 if(pp.length&&po.length){ 4877 var z=[], i2=0; while(po.length||pp.length){ if(po.length) z.push(po.shift()); if(pp.length) z.push(pp.shift()); } 4878 var zi=0; tks=tks.map(function(t){ return (t.indexOf('NEW')>-1||t.indexOf('ot-new')>-1)?z[zi++]:t; }); 4879 } 4880 })(); 4881 /* entrelacement : un NEW toutes les 2 positions â visibles dès les premières secondes */ 4882 (function(){ 4883 var news = [], rest = []; 4884 tks.forEach(function(t){ (t.indexOf('ot-new') > -1 ? news : rest).push(t); }); 4885 if (news.length){ 4886 var mixed = []; 4887 while (rest.length || news.length){ 4888 if (rest.length) mixed.push(rest.shift()); 4889 if (rest.length) mixed.push(rest.shift()); 4890 if (news.length) mixed.push(news.shift()); 4891 } 4892 tks = mixed; 4893 } 4894 })(); 4895 if (tks.length < 4) return; /* pas assez de matière â pas de bandeau */ 4896 if (!document.getElementById('osaiTapeCSS')){ 4897 var s = document.createElement('style'); s.id = 'osaiTapeCSS'; 4898 s.textContent = 4899 '#osaiTape{margin:14px auto 4px;max-width:1180px;padding:0 22px;}' + 4900 '#osaiTape .ot-box{display:flex;align-items:stretch;border:1px solid rgba(128,128,150,.25);border-radius:10px;overflow:hidden;background:rgba(128,128,160,.05);}' + 4901 'body.dark #osaiTape .ot-box{background:rgba(255,255,255,.03);border-color:rgba(255,255,255,.1);}
4901' + 4902 '#osaiTape .ot-lb{display:flex;align-items:center;gap:7px;padding:8px 14px;font-family:"DM Mono",monospace;font-size:.66rem;font-weight:500;letter-spacing:.1em;white-space:nowrap;color:inherit;text-decoration:none;border-right:1px solid rgba(128,128,150,.25);}' + 4903 '#osaiTape .ot-lb b{color:#6366f1;font-weight:500;}' + 4904 'body.dark #osaiTape .ot-lb b{color:#818cf8;}' + 4905 '#osaiTape .ot-dot{width:7px;height:7px;border-radius:50%;background:#10b981;animation:otp 2s infinite;}' + 4906 '@keyframes otp{0%,100%{opacity:1}50%{opacity:.3}}' + 4907 '#osaiTape .ot-vp{flex:1;overflow:hidden;position:relative;}' + 4908 '#osaiTape .ot-tape{display:flex;width:max-content;animation:otk 55s linear infinite;}' + 4909 '#osaiTape .ot-vp:hover .ot-tape{animation-play-state:paused;}' + 4910 '@keyframes otk{from{transform:translateX(0)}to{transform:translateX(-50%)}}' + 4911 '#osaiTape .ot-i{display:inline-flex;align-items:center;gap:6px;padding:8px 18px;font-family:"DM Mono",monospace;font-size:.7rem;white-space:nowrap;color:inherit;text-decoration:none;opacity:.85;border-right:1px dashed rgba(128,128,150,.25);}' + 4912 '#osaiTape .ot-i:hover{opacity:1;}' + 4913 '#osaiTape .ot-i b{font-weight:500;}' + 4914 '#osaiTape .ot-up{color:#10b981;} body.dark #osaiTape .ot-up{color:#34d399;}' + 4915 '#osaiTape .ot-down{color:#ef4444;} body.dark #osaiTape .ot-down{color:#f87171;}' + 4916 '#osaiTape .ot-new{color:#6366f1;} body.dark #osaiTape .ot-new{color:#818cf8;}' + 4917 '@media(prefers-reduced-motion:reduce){#osaiTape .ot-tape{animation:none;flex-wrap:wrap;width:auto;} #osaiTape .ot-dot{animation:none;}}' + 4918 '@media(max-width:640px){#osaiTape .ot-lb span.ot-t{display:none;}}'; 4919 document.head.appendChild(s); 4920 } 4921 var items = tks.map(function(t){ return '<a class="ot-i" href="/dashboard.html">' + t + '</a>'; }).join(''); 4922 var wrap = document.createElement('div'); 4923 wrap.id = 'osaiTape'; 4924 wrap.innerHTML = '<div class="ot-box">' 4925 + '<a class="ot-lb" href="/dashboard.html"><span class="ot-dot"></span><b>olud.ai</b><span class="ot-t">TERMINAL</span></a>' /* [02/08/2026] etait « OSAI » : l'ancien nom de marque, visible en clair */ 4926 + '<div class="ot-vp"><div class="ot-tape">' + items + items + '</div></div></div>'; 4927 anchor.insertAdjacentElement('afterend', wrap); 4928 if (typeof localizeAllLinks === 'function') localizeAllLinks(wrap); 4929 if (window.osaiTrack){ wrap.addEventListener('click', function(){ osaiTrack('home_tape_click', {}); }); } 4930 }); 4931 } 4932 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiHomeTape); else osaiHomeTape(); 4933 4934 4935 4936 /* âââ V21 â Harmonisation dark : fonds verts â .osai-mint (menthe + texte sombre), textes verts 4937 en dur â .osai-mint-txt, SVG fill/stroke verts ou blancs-sur-menthe corrigés, et re-passe 4938 débouncée (MutationObserver) pour le contenu rendu en JS après le chargement. âââ */ 4939 var OSAI_GREEN = 'rgb(13, 158, 112)', OSAI_MINT = 'rgb(61, 255, 181)'; 4940 /* classes d'état : un fond vert porté par ces classes est transitoire â ne jamais figer */ 4941 var OSAI_STATE = ['active','selected','checked','current','is-active','on','open']; 4942 function osaiIsStateful(el){ for (var i = 0; i < OSAI_STATE.length; i++){ if (el.classList.contains(OSAI_STATE[i])) return true; } return false; } 4943 function osaiIsWhite(v){ v = String(v||'').toLowerCase().replace(/\s/g,''); return v==='#fff'||v==='#ffffff'||v==='white'||v==='rgb(255,255,255)'; } 4944 function osaiIsGreen(v){ v = String(v||'').toLowerCase().replace(/\s/g,''); return v==='#6366f1'||v==='rgb(13,158,112)'; } 4945 /* flèches/icônes des boutons verts : blanc ou vert (attribut OU feuille de style) â currentColor */ 4946 function osaiFixSvg(root){ 4947 try { 4948 var nodes = root.querySelectorAll('svg, svg *'); 4949 for (var i = 0; i < nodes.length; i++){ 4950 var n = nodes[i], f = n.getAttribute('fill'), s = n.getAttribute('stroke'); 4951 var cs = getComputedStyle(n); 4952 var fBad = osaiIsWhite(f) || osaiIsGreen(f) || ((f===null||f==='') && (osaiIsWhite(cs.fill) || osaiIsGreen(cs.fill))); 4953 var sBad = osaiIsWhite(s) || osaiIsGreen(s) || ((s===null||s==='') && (osaiIsWhite(cs.stroke) || osaiIsGreen(cs.stroke))); 4954 if (fBad && String(n.style.fill).toLowerCase() !== 'currentcolor') n.style.fill = 'currentColor'; 4955 if (sBad && String(n.style.stroke).toLowerCase() !== 'currentcolor') n.style.stroke = 'currentColor'; 4956 } 4957 } catch(e){} 4958 } 4959 function osaiMintify(){ 4960 try { 4961 /* 1) fonds vert foncé OU menthe â .osai-mint (couvre aussi le blanc-sur-menthe, ex. bouton remonter) 4962 â ï¸ jamais sur un fond TRANSITOIRE (état coché, survol) : la classe est permanente et 4963 figerait le bouton en "coché" (bug des filtres de /compare/). Ces cas sont couverts 4964 par le filet CSS global (--accent menthe en dark). */ 4965 var els = document.querySelectorAll('a,button,input[type="submit"],[class*="btn"],[class*="cta"],[class*="badge"],[class*="pill"],[class*="top"],[class*="Top"],[class*="fab"],[class*="scroll"],[class*="Scroll"],[class*="up"],[class*="Up"],[id*="top"],[id*="Top"],[id*="scroll"],[id*="Scroll"]'); 4966 for (var i = 0; i < els.length; i++){ 4967 var el = els[i]; 4968 if (el.classList.contains('osai-mint')){ 4969 if (osaiIsStateful(el)) { el.classList.remove('osai-mint'); }
4969 // répare les boutons déjà figés 4970 else { osaiFixSvg(el); } 4971 continue; 4972 } 4973 if (osaiIsStateful(el)) continue; // état coché/ouvert : ne pas figer 4974 try { if (el.matches(':hover')) continue; } catch(e){} // survol : fond transitoire 4975 var bg = getComputedStyle(el).backgroundColor; 4976 if (bg === OSAI_GREEN || bg === OSAI_MINT){ el.classList.add('osai-mint'); osaiFixSvg(el); } 4977 } 4978 /* 2) textes vert foncé en dur â .osai-mint-txt (menthe en dark) */ 4979 var tEls = document.querySelectorAll('a,button,span,b,strong,em,small,p,li,td,th,h1,h2,h3,h4,h5,h6,label,dt,dd'); 4980 for (var j = 0; j < tEls.length; j++){ 4981 var t = tEls[j]; 4982 if (t.classList.contains('osai-mint-txt')) continue; 4983 if (getComputedStyle(t).color === OSAI_GREEN && !t.closest('.osai-mint')) t.classList.add('osai-mint-txt'); 4984 } 4985 /* 3) SVG verts en dur hors boutons (icônes, courbes) â suivent le thème via --osai-green */ 4986 var sv = document.querySelectorAll('svg[stroke="#6366f1"], svg [stroke="#6366f1"], svg[fill="#6366f1"], svg [fill="#6366f1"]'); 4987 for (var k = 0; k < sv.length; k++){ 4988 var n = sv[k]; 4989 if (n.getAttribute('stroke') === '#6366f1' && n.style.stroke !== 'var(--osai-green)') n.style.stroke = 'var(--osai-green)'; 4990 if (n.getAttribute('fill') === '#6366f1' && n.style.fill !== 'var(--osai-green)') n.style.fill = 'var(--osai-green)'; 4991 } 4992 } catch(e){} 4993 } 4994 function osaiMintifyRuns(){ osaiMintify(); setTimeout(osaiMintify, 1200); setTimeout(osaiMintify, 3200); } 4995 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', osaiMintifyRuns); else osaiMintifyRuns(); 4996 /* contenu rendu en JS après coup (grilles, résultats de rechercheâ¦) : re-passe débouncée. 4997 childList uniquement â nos propres ajouts de classes/styles ne re-déclenchent pas l'observer. */ 4998 try { 4999 var osaiMintT = null; 5000 new MutationObserver(function(){ clearTimeout(osaiMintT); osaiMintT = setTimeout(osaiMintify, 350); }) 5001 .observe(document.body, { childList:true, subtree:true }); 5002 } catch(e){} 5003})(); 5004/* âââ V27 â Palette âK GLOBALE : fonctionne sur toutes les pages. 5005 Sur la home (qui a sa propre palette plus riche), ce module s'efface. 5006 Données chargées à la PREMIÃRE ouverture seulement (aucun coût au chargement). âââ */ 5007(function(){ 5008 if (document.getElementById('cmdkOverlay')) return; // la home garde la sienne 5009 5010 /* utilitaires de recherche partagés (les mêmes que la home) */ 5011 window.__osaiSearchUtil = window.__osaiSearchUtil || (function(){ 5012 function norm(s){ s=String(s||'').toLowerCase(); try{ s=s.normalize('NFD').replace(/[\u0300-\u036f]/g,''); }catch(e){} return s; } 5013 function hay(it){ return it._q || (it._q = norm((it.name||'')+' '+(it.desc||'')+' '+(it.tag||'')+' '+(it.extra||''))); } 5014 function tokens(qn){ var t=qn.split(/\s+/).filter(Boolean); if(t.length>1){ var b=t.filter(function(w){return w.length>2;}); if(b.length) t=b; } return t; } 5015 function match(it,tk){ var h=hay(it); for(var i=0;i<tk.length;i++){ if(h.indexOf(tk[i])<0) return false; } return true; } 5016 return {norm:norm, hay:hay, tokens:tokens, match:match}; 5017 })(); 5018 5019 /* modèles + alternatives : même promesse défensive que la home (créée ici si absente) */ 5020 window.__searchExtrasPromise = window.__searchExtrasPromise || (function(){ 5021 var norm = window.__osaiSearchUtil.norm; 5022 function jget(u){ return fetch(u+'?d='+new Date().toISOString().slice(0,10)).then(function(r){ return r.ok?r.json():null; }).catch(function(){ return null; }); } 5023 function asList(d,keys){ if(Array.isArray(d)) return d; if(d){ for(var i=0;i<keys.length;i++){ if(Array.isArray(d[keys[i]])) return d[keys[i]]; } } return []; } 5024 function cap(s){ s=String(s||''); return s?s.charAt(0).toUpperCase()+s.slice(1):s; } 5025 function modelSlug(name){ return String(name||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''); } 5026 function modelEntries(data,fallback,slugSet){ 5027 var out=[];
5028 asList(data,['models','data','items']).forEach(function(m){ 5029 if(!m) return; 5030 var id=String(m.id||m.slug||''), name=String(m.name||id); if(!name) return; 5031 var provider=String(m.provider||m.author||m.vendor||m.company||(id.indexOf('/')>0?id.split('/')[0]:'')); 5032 var desc=String(m.desc||m.description||''); if(!desc) desc=provider?('AI model by '+cap(provider)):'AI model'; 5033 /* route vers la FICHE du modèle si elle existe, sinon page générique */ 5034 var sl=modelSlug(name), murl=(slugSet && slugSet[sl]) ? ('/model/'+sl+'.html') : fallback; 5035 out.push({name:name,desc:desc,tag:'Model',url:murl,type:'model',extra:(id+' '+provider)}); 5036 }); 5037 return out; 5038 } 5039 return Promise.all([jget('/models-data.json'), jget('/paid-models-data.json'), jget('/alternatives-index.json'), jget('/model-slugs.json')]) 5040 .then(function(res){ 5041 var extras=[], seen={}, slugSet=res[3]||{}; 5042 modelEntries(res[0],'/compare/',slugSet).concat(modelEntries(res[1],'/pricing.html',slugSet)).forEach(function(e){ 5043 var k=norm(e.name).replace(/^[a-z0-9 .\-]+:\s*/,''); if(!seen[k]){ seen[k]=1; extras.push(e); } 5044 });
5045 asList(res[2],['products','items']).forEach(function(p){ 5046 if(p&&p.name&&p.url){ extras.push({name:String(p.name),desc:String(p.desc||p.domain||'Open-source alternatives'),tag:'Alternatives',url:String(p.url),type:'alt',extra:String(p.domain||'')}); } 5047 }); 5048 return extras; 5049 }); 5050 })(); 5051 5052 var CMDK_CSS = '' 5053 +'.cmdk-overlay{position:fixed;inset:0;background:rgba(10,15,25,.5);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:100000;display:none;align-items:flex-start;justify-content:center;padding-top:12vh;}' 5054 +'.cmdk-overlay.open{display:flex;}' 5055 +'.cmdk-box{width:90%;max-width:620px;background:var(--surface,#fff);border:1px solid var(--border2,rgba(0,0,0,.15));border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.3);overflow:hidden;animation:cmdkIn .15s ease;}' 5056 +'@keyframes cmdkIn{from{opacity:0;transform:translateY(-8px) scale(.98);}to{opacity:1;transform:none;}}' 5057 +'.cmdk-input-row{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid var(--border,rgba(0,0,0,.08));}' 5058 +'.cmdk-search-icon{width:20px;height:20px;color:var(--muted,#5a6d87);flex-shrink:0;}' 5059 +'#cmdkInput{flex:1;border:none;background:none;font-family:inherit;font-size:1.05rem;color:var(--txt,#0f1724);outline:none;}' 5060 +'#cmdkInput::placeholder{color:var(--muted,#5a6d87);}' 5061 +'.cmdk-esc{font-family:monospace;font-size:.64rem;background:var(--surface2,#f7f9fd);border:1px solid var(--border2,rgba(0,0,0,.15));border-radius:5px;padding:2px 7px;color:var(--muted,#5a6d87);}' 5062 +'.cmdk-results{max-height:50vh;overflow-y:auto;padding:8px;}' 5063 +'.cmdk-group-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted,#5a6d87);padding:10px 12px 6px;font-weight:600;}' 5064 +'.cmdk-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:10px;cursor:pointer;text-decoration:none;color:var(--txt,#0f1724);}' 5065 +'.cmdk-item.active{background:#6366f1;color:#fff;}' 5066 +'body.dark .cmdk-item.active{background:#818cf8;color:#ffffff;}' 5067 +'.cmdk-item.active .cmdk-item-desc,.cmdk-item.active .cmdk-item-tag{color:rgba(255,255,255,.85);}' 5068 +'body.dark .cmdk-item.active .cmdk-item-desc,body.dark .cmdk-item.active .cmdk-item-tag{color:rgba(6,40,27,.8);}' 5069 +'.cmdk-item-icon{width:30px;height:30px;flex-shrink:0;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:600;color:#fff;font-size:.95rem;overflow:hidden;position:relative;}' 5070 +'.cmdk-item-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}' 5071 +'.cmdk-item-info{min-width:0;flex:1;}' 5072 +'.cmdk-item-name{font-weight:600;font-size:.88rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' 5073 +'.cmdk-item-desc{font-size:.76rem;color:var(--muted,#5a6d87);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' 5074 +'.cmdk-item-tag{font-size:.66rem;color:var(--accent,#6366f1);flex-shrink:0;font-weight:500;}' 5075 +'.cmdk-empty{padding:32px;text-align:center;color:var(--muted,#5a6d87);font-size:.88rem;}' 5076 +'.cmdk-foot{display:flex;gap:18px;padding:10px 18px;border-top:1px solid var(--border,rgba(0,0,0,.08));}' 5077 +'.cmdk-foot span{font-size:.7rem;color:var(--muted,#5a6d87);display:flex;align-items:center;gap:5px;}' 5078 +'.cmdk-foot kbd{font-family:monospace;font-size:.62rem;background:var(--surface2,#f7f9fd);border:1px solid var(--border2,rgba(0,0,0,.15));border-radius:4px;padding:1px 5px;}' 5079 +'body.dark .cmdk-box{background:#161c28 !important;border-color:rgba(255,255,255,.14) !important;}' 5080 +'body.dark .cmdk-input-row,body.dark .cmdk-foot{border-color:rgba(255,255,255,.08) !important;}' 5081 +'@media(max-width:768px){.cmdk-foot{display:none;}}'; 5082 5083 var navItems = [ 5084 {type:'page', name:'Home', desc:'Back to the homepage', tag:'Page', url:'/'}, 5085 {type:'page', name:'Tools catalog', desc:'Curated open-source AI tools', tag:'Page', url:'/tools.html'}, 5086 {type:'page', name:'Compare Models', desc:'Side-by-side model comparison', tag:'Page', url:'/compare.html'}, 5087 {type:'page', name:'AI Pricing', desc:'Live model pricing', tag:'Page', url:'/pricing.html'}, 5088 {type:'page', name:'Paid Models', desc:'Commercial model pricing', tag:'Page', url:'/paid-models.html'}, 5089 {type:'page', name:'Leaderboard', desc:'Top models ranking', tag:'Page', url:'/leaderboard.html'}, 5090 {type:'page', name:'Latest', desc:'Newest AI projects', tag:'Page', url:'/latest.html'}, 5091 {type:'page', name:'Industries', desc:'AI by sector: healthcare, financeâ¦', tag:'Page', url:'/vertical/'}, 5092 {type:'page', name:'Releases', desc:'New tool versions daily', tag:'Page', url:'/releases.html'}, 5093 {type:'page', name:'Dashboards', desc:'Open-source AI dashboard', tag:'Page', url:'/dashboard.html'}, 5094 {type:'page', name:'News', desc:'Latest AI news', tag:'Page', url:'/news.html'}, 5095 {type:'page', name:'Prompts', desc:'Prompt library', tag:'Page', url:'/prompts.html'}, 5096 {type:'page', name:'AI Agents', desc:'Open-source AI agents', tag:'Page', url:'/agents/'}, 5097 {type:'page', name:'Projects', desc:'Trending GitHub AI projects', tag:'Page', url:'/projects/'}, 5098 {type:'page', name:'Alternatives to top tools', desc:'Open-source replacements', tag:'Page', url:'/alternatives/'}, 5099 {type:'page', name:'Blog', desc:'Articles and guides', tag:'Page', url:'/blog/'}, 5100 {type:'page', name:'Submit a tool or project', desc:'Suggest an AI tool or project', tag:'Page', url:'/submit.html'}, 5101 {type:'page', name:'About', desc:'About this project', tag:'Page', url:'/about.html'}, 5102 {type:'page', name:'Methodology', desc:'How our data is collected', tag:'Page', url:'/methodology.html'} 5103 ]; 5104 5105 var built=false, overlay=null, input=null, results=null, activeIdx=0, currentList=[], cmdkIndex=null; 5106 5107 function iconColor(str){ 5108 var h=0; for(var i=0;i<str.length;i++){ h=str.charCodeAt(i)+((h<<5)-h); } 5109 var c=['#6366f1','#0071e3','#e0245e','#9b59d9','#f24f00','#c98a00','#1a56db','#16a394']; 5110 return c[Math.abs(h)%c.length]; 5111 } 5112 function realIcon(m){ 5113 var initial=(m.name||'?').trim().charAt(0).toUpperCase()||'?'; 5114 var img=''; var gh=(m.url||'').match(/github\.com\/([^\/]+)/); 5115 if(gh){ img='<img src="https://github.com/'+gh[1]+'.png?size=160" alt="" loading="lazy" onerror="this.remove();">'; } 5116 return '<div class="cmdk-item-icon" style="background:'+iconColor(m.name||'?')+';">'+initial+img+'</div>'; 5117 } 5118 function esc(s){ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } 5119 function itemHtml(it){ 5120 return '<a class="cmdk-item" href="'+esc(it.url)+'" data-url="'+esc(it.url)+'">' 5121 + realIcon(it) 5122 + '<div class="cmdk-item-info"><div class="cmdk-item-name">'+esc(it.name)+'</div>' 5123 + '<div class="cmdk-item-desc">'+esc(it.desc||'')+'</div></div>' 5124 + '<span class="cmdk-item-tag">'+esc(it.tag||'')+'</span></a>'; 5125 } 5126 5127 /* [04/08/2026] LA RECHERCHE VOIT ENFIN TOUT, ET PESE DIX FOIS MOINS. 5128 Avant : projets + modeles + alternatives, au prix de projects-data.json 5129 entier (5 116 Ko a chaque ouverture de la palette). Les 232 outils, les 5130 700 serveurs MCP, les 277 skills et les pages du site etaient
5131 introuvables. Desormais un seul fichier compact, search-index.json 5132 (~600 Ko), fabrique par generate-search-index.php. 5133 Le chemin d'avant reste en secours : si l'index manque â pas encore 5134 genere, erreur reseau â la palette retombe sur l'ancienne methode 5135 plutot que de ne rien trouver. */ 5136 var TYPES_IDX = { p:'Project', t:'Tool', m:'Model', c:'MCP server', 5137 s:'Skill', a:'Alternatives', v:'Compare', g:'Page' }; 5138 function loadIndexCompact(){ 5139 return fetch('/search-index.json?d=' + new Date().toISOString().slice(0,10)) 5140 .then(function(r){ return r.ok ? r.json() : null; }) 5141 .catch(function(){ return null; }) 5142 .then(function(j){ 5143 var L = j && j.e; 5144 if (!L || !L.length) return false; 5145 for (var i = 0; i < L.length; i++) { 5146 var it = L[i]; 5147 cmdkIndex.push({ type: it[2], name: it[0], desc: it[3] || '', 5148 tag: TYPES_IDX[it[2]] || 'Page', url: it[1] }); 5149 } 5150 if (input && input.value) renderCmdk(input.value); 5151 return true; 5152 }); 5153 } 5154 5155 function loadIndex(){ 5156 if(cmdkIndex) return; 5157 cmdkIndex=[]; 5158 /* l'index compact d'abord ; l'ancien chemin seulement s'il manque */ 5159 loadIndexCompact().then(function(ok){ if (!ok) loadIndexAncien(); }); 5160 } 5161 5162 function loadIndexAncien(){ 5163 try{ 5164 window.__searchExtrasPromise.then(function(extras){ extras.forEach(function(e){ cmdkIndex.push(e); }); if(input&&input.value) renderCmdk(input.value); }).catch(function(){}); 5165 }catch(e){} 5166 try{ 5167 (window.__projectsPromise || (window.__projectsPromise = fetch('/projects-data.json?d='+new Date().toISOString().slice(0,10)).then(function(r){ if(!r.ok) throw 0; return r.json(); }))) 5168 .then(function(data){ 5169 var list=(data&&data.projects)?data.projects:[]; if(!list.length) return; 5170 /* [29/07] Le .slice(0,1500) qui etait ici rendait 85 % du catalogue 5171 INTROUVABLE : il fallait plus de 6 426 etoiles pour exister dans la 5172 recherche. SeekStorm (rang 3818) et neuron-ai (rang 3663) etaient 5173 invisibles. Le plafond n'economisait rien : le JSON entier est deja 5174 telecharge, seule la boucle de filtrage grandit â quelques 5175 millisecondes sur 10 000 entrees. Tri conserve : les plus etoiles 5176 restent en tete des resultats. */ 5177 list.slice().sort(function(a,b){ return (+b.stars||0)-(+a.stars||0); }).forEach(function(p){ 5178 if(p&&p.name&&p.url){ cmdkIndex.push({type:'project',name:p.name,desc:p.desc||'',tag:(p.tags&&p.tags[0])||'Project',url:p.url}); } 5179 }); 5180 if(input&&input.value) renderCmdk(input.value); 5181 }).catch(function(){}); 5182 }catch(e){} 5183 } 5184 5185 /* Les liens affiches pointent l'URL du catalogue (GitHub). On les reecrit 5186 vers nos fiches une fois la liste rendue : le survol, le clic-milieu et 5187 « ouvrir dans un nouvel onglet » restent chez nous, pas seulement le 5188 clic simple. Verification HEAD memoisee, au plus 20 liens visibles. */ 5189 function ramenerLesLiens(){ 5190 if (!window.osaiFicheForAsync || !results) return; 5191 [].slice.call(results.querySelectorAll('.cmdk-item[data-url*="github.com/"]')).forEach(function(a){ 5192 var u = a.getAttribute('data-url'); 5193 window.osaiFicheForAsync(u).then(function(f){ 5194 if (f && f !== u) { a.setAttribute('href', f); a.setAttribute('data-url', f); } 5195 }); 5196 }); 5197 } 5198 5199 function renderCmdk(query){ 5200 var q=(query||'').trim().toLowerCase(), html=''; 5201 currentList=[]; 5202 if(!q){ 5203 html+='<div class="cmdk-group-label">Pages</div>'; 5204 navItems.forEach(function(it){ currentList.push(it); }); 5205 html+=navItems.map(itemHtml).join(''); 5206 } else { 5207 var U=window.__osaiSearchUtil, qn=U.norm(q), toks=U.tokens(qn); 5208 var pages=navItems.filter(function(it){ return U.norm(it.name).indexOf(qn)>=0; }); 5209 /* [04/08] CLASSEMENT PAR PERTINENCE. Avant, on prenait les 20 premiers 5210 de l'index dans l'ordre d'insertion : les 4 000 projets saturaient la 5211 liste et les outils, modeles ou serveurs MCP n'apparaissaient jamais. 5212 « ollama » remontait quatre projets et pas la fiche outil. On note 5213 desormais chaque resultat â nom exact, debut de nom, debut de mot, 5214 contenu â et on garde les 20 MEILLEURS, pas les 20 premiers. */ 5215 var scoreCmdk = function(it){ 5216 var nom = U.norm(it.name || ''); 5217 var sc = 0; 5218 if (nom === qn) sc = 1000; 5219 else if (nom.indexOf(qn) === 0) sc = 600; 5220 else if ((' ' + nom).indexOf(' ' + qn) >= 0) sc = 400; /* debut d'un mot */ 5221 else if (nom.indexOf(qn) >= 0) sc = 200; 5222 else sc = 40; /* trouve via desc */ 5223 /* a pertinence egale, on montre d'abord ce qui est cure : une page du 5224 site, un outil du catalogue ou un modele plutot qu'un depot brut */ 5225 var bonus = { g: 30, t: 25, m: 20, a: 15, c: 10, s: 10, p: 0 }; 5226 return sc + (bonus[it.type] || 0); 5227 }; 5228 var tools = []; 5229 if (cmdkIndex) { 5230 tools = cmdkIndex.filter(function(it){ return U.match(it, toks); });
5231 tools.forEach(function(it){ it._sc = scoreCmdk(it); }); 5232 tools.sort(function(a, b){ return b._sc - a._sc; }); 5233 tools = tools.slice(0, 20); 5234 } 5235 /* [01/08] LA PORTE VERS L'ASSISTANT. La recherche texte ne comprend pas 5236 « un modele pour transcrire du francais sur 16 Go » : elle cherche ces 5237 mots-la. Des que la saisie ressemble a une question (plusieurs mots, 5238 ou peu de resultats), on propose en TETE de la poser a l'assistant, 5239 qui lui repond avec nos donnees. C'est la seule facon honnete de 5240 relier la recherche a l'IA sans pretendre que la recherche EST une IA. */ 5241 var TT = function(x){ return (typeof window.osaiT === 'function') ? window.osaiT(x) : x; }; 5242 var motsQ = q.split(/\s+/).filter(Boolean).length; 5243 var maigre = (tools.length + pages.length) < 3; 5244 var question = /[?\uFF1F]/.test(String(query || '')); 5245 var ligneIA = ''; 5246 if (question || motsQ >= 3 || maigre) { 5247 ligneIA = '<div class="cmdk-group-label">' + TT('Ask the assistant') + '</div>' 5248 + '<a class="cmdk-item cmdk-ia" href="#" data-ia-q="' + esc(query) + '">' 5249 + '<span class="cmdk-ia-ic">\u2728</span>' 5250 + '<div class="cmdk-item-info"><div class="cmdk-item-name">' + esc(query) + '</div>' 5251 + '<div class="cmdk-item-desc">' + TT('Answered with this site\'s live data') + '</div></div>' 5252 + '<span class="cmdk-item-tag">AI</span></a>'; 5253 } 5254 if(pages.length){ html+='<div class="cmdk-group-label">Pages</div>'; pages.forEach(function(it){ currentList.push(it); }); html+=pages.map(itemHtml).join(''); } 5255 if(tools.length){ html+='<div class="cmdk-group-label">Tools & Projects</div>'; tools.forEach(function(it){ currentList.push(it); }); html+=tools.map(itemHtml).join(''); } 5256 html += ligneIA; /* apres les resultats, pas avant */ 5257 /* Zero resultat texte = le moment ou l'assistant sert le PLUS. 5258 On garde donc la ligne IA au lieu de tout ecraser. */ 5259 if(!pages.length&&!tools.length){ 5260 html = '<div class="cmdk-empty">' + TT('No text match') + ' \u2014 "' + esc(query) + '"</div>' + ligneIA; 5261 } 5262 } 5263 results.innerHTML=html; activeIdx=0; highlight(); 5264 ramenerLesLiens(); 5265 } 5266 function highlight(){ 5267 var items=results.querySelectorAll('.cmdk-item'); 5268 for(var i=0;i<items.length;i++){ items[i].classList.toggle('active', i===activeIdx); } 5269 var act=items[activeIdx]; if(act&&act.scrollIntoView) act.scrollIntoView({block:'nearest'}); 5270 } 5271 function go(el){ 5272 if(!el) return; 5273 var iaQ = el.getAttribute('data-ia-q'); 5274 if (iaQ) { 5275 if (window.osaiAskInto) { window.osaiAskInto(iaQ, results); } 5276 else { closeGlobalCmdk(); if (window.osaiAsk) window.osaiAsk(iaQ); } 5277 return; 5278 } 5279 var url=el.getAttribute('data-url'); closeGlobalCmdk(); 5280 /* [31/07, Romain] « chercher compass skills m'a amene sur GitHub » : 5281 l'index de recherche pousse l'URL du catalogue, qui EST l'adresse 5282 GitHub du depot. On envoyait donc nos visiteurs chez GitHub depuis 5283 notre propre moteur. Le meme resolveur que le partage ramene la fiche 5284 olud.ai quand elle existe (verification HEAD memoisee) ; sinon on 5285 garde l'adresse d'origine plutot qu'un lien mort. */ 5286 if (window.osaiFicheForAsync) { 5287 window.osaiFicheForAsync(url).then(function(u){ window.location.href = u || url; }); 5288 } else { 5289 window.location.href = url; 5290 } 5291 } 5292 function buildOnce(){ 5293 if(built) return; built=true; 5294 var st=document.createElement('style'); st.textContent=CMDK_CSS; document.head.appendChild(st); 5295 var d=document.createElement('div'); 5296 d.className='cmdk-overlay'; d.id='cmdkOverlay'; 5297 d.innerHTML='' 5298 +'<div class="cmdk-box" role="dialog" aria-modal="true" aria-label="Search">' 5299 +'<div class="cmdk-input-row">' 5300 +'<svg class="cmdk-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' 5301 +'<input type="text" id="cmdkInput" placeholder="Search tools, projects, models…" autocomplete="off" spellcheck="false">' 5302 +'<kbd class="cmdk-esc">ESC</kbd>' 5303 +'</div>' 5304 +'<div class="cmdk-results" id="cmdkResults"></div>' 5305 +'<div class="cmdk-foot"><span><kbd>↑</kbd><kbd>↓</kbd> to navigate</span><span><kbd>↵</kbd> to open</span><span><kbd>esc</kbd> to close</span></div>' 5306 +'</div>'; 5307 d.addEventListener('click', function(ev){ if(ev.target===d) closeGlobalCmdk(); }); 5308 document.body.appendChild(d); 5309 overlay=d; input=d.querySelector('#cmdkInput'); results=d.querySelector('#cmdkResults'); 5310 input.addEventListener('input', function(){ renderCmdk(input.value); }); 5311 input.addEventListener('keydown', function(e){ 5312 var items=results.querySelectorAll('.cmdk-item'); 5313 if(e.key==='ArrowDown'){ e.preventDefault(); activeIdx=Math.min(activeIdx+1, items.length-1); highlight(); } 5314 else if(e.key==='ArrowUp'){ e.preventDefault(); activeIdx=Math.max(activeIdx-1, 0); highlight(); } 5315 else if(e.key==='Enter'){ e.preventDefault(); go(items[activeIdx]); } 5316 }); 5317 results.addEventListener('click', function(ev){ 5318 var a=ev.target && ev.target.closest ? ev.target.closest('.cmdk-item') : null; 5319 if(a){ ev.preventDefault(); go(a); } 5320 }); 5321 } 5322 function closeGlobalCmdk(){ if(overlay) overlay.classList.remove('open'); } 5323 5324 window.openCmdK = function(){ 5325 buildOnce(); loadIndex(); 5326 overlay.classList.add('open'); 5327 input.value=''; renderCmdk(''); 5328 try{ input.focus(); }catch(e){} 5329 }; 5330 window.closeCmdK = window.closeCmdK || closeGlobalCmdk; 5331 5332 document.addEventListener('keydown', function(e){ 5333 if((e.metaKey||e.ctrlKey) && (e.key==='k'||e.key==='K')){ 5334 e.preventDefault(); 5335 if(overlay && overlay.classList.contains('open')) closeGlobalCmdk(); else window.openCmdK(); 5336 } else if(e.key==='Escape' && overlay && overlay.classList.contains('open')){ closeGlobalCmdk(); } 5337 }); 5338})(); 5339 5340 5341/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5342 PARTAGE UNIVERSEL â un seul système pour tout le site. 5343 5344 Fini Facebook et LinkedIn (que personne n'utilise pour partager un outil dev). 5345 Les réseaux qui comptent pour ce public : X, Bluesky, Mastodon, WhatsApp, 5346 Reddit â et surtout « Copier le lien », de loin le plus utilisé. 5347 5348 USAGE â n'importe quelle carte du site devient partageable avec ça : 5349 <button class="osai-share" data-url="/releases.html#x" data-text="Ollama v0.31 is out">â¦</button> 5350 ou en JS : osaiShare(url, text) 5351 5352 Les éléments ajoutés dynamiquement (cartes rendues en JS) sont pris en charge : 5353 on écoute au niveau du document, pas sur chaque bouton. 5354 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 5355(function(){ 5356 if (window.__osaiShareReady) return; 5357 window.__osaiShareReady = true; 5358 5359 var NETS = [ 5360 {k:'x', label:'X', ico:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>', 5361 url:function(u,t){ return 'https://x.com/intent/post?text=' + encodeURIComponent(t) + '&url=' + encodeURIComponent(u); }}, 5362 {k:'bluesky', label:'Bluesky', ico:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908
53620 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/></svg>', 5363 url:function(u,t){ return 'https://bsky.app/intent/compose?text=' + encodeURIComponent(t + ' ' + u); }}, 5364 {k:'mastodon', label:'Mastodon', ico:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.066-.051 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545z"/></svg>', 5365 url:function(u,t){ return 'https://mastodonshare.com/?text=' + encodeURIComponent(t) + '&url=' + encodeURIComponent(u); }}, 5366 {k:'reddit', label:'Reddit', ico:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-6.995 4.87-3.865 0-6.994-2.176-6.994-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>', 5367 url:function(u,t){ return 'https://www.reddit.com/submit?url=' + encodeURIComponent(u) + '&title=' + encodeURIComponent(t); }}, 5368 {k:'whatsapp', label:'WhatsApp', ico:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0 0 20.465 3.488"/></svg>', 5369 url:function(u,t){ return 'https://wa.me/?text=' + encodeURIComponent(t + ' ' + u); }} 5370 ]; 5371 5372 /* CSS injecté une fois */ 5373 var st = document.createElement('style'); 5374 st.textContent = '' 5375 + '.osai-sheet{position:fixed;inset:0;z-index:2147483640;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.55);backdrop-filter:blur(3px)}' 5376 + '.osai-sheet.open{display:flex}' 5377 + '.osai-sheet-in{background:var(--surface,#fff);border:1px solid var(--border2,rgba(0,0,0,.1));border-radius:18px;padding:22px;width:min(360px,92vw);box-shadow:0 20px 60px rgba(0,0,0,.35)}' 5378 + '.osai-sheet h4{margin:0 0 4px;font-family:Poppins,sans-serif;font-size:1rem;font-weight:700;color:var(--txt,#111)}' 5379 + '.osai-sheet p{margin:0 0 16px;font-size:.8rem;color:var(--muted,#667);word-break:break-all;line-height:1.4}' 5380 + '.osai-nets{display:grid;grid-template-columns:repeat(auto-fit,minmax(92px,1fr));gap:8px}' 5381 + '.osai-net{display:flex;flex-direction:column;align-items:center;gap:6px;background:var(--surface2,#f6f7f9);border:1px solid var(--border2,rgba(0,0,0,.08));border-radius:12px;padding:12px 8px;cursor:pointer;text-decoration:none;color:var(--txt,#111);font-family:Poppins,sans-serif;font-size:.72rem;font-weight:600;transition:border-color .15s,background .15s}' 5382 + '.osai-net:hover{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1)}' 5383 + '.osai-net svg{width:20px;height:20px}' 5384 + '.osai-copy{grid-column:1/-1;background:var(--accent,#6366f1);border-color:var(--accent,#6366f1);color:#fff;flex-direction:row;gap:8px;padding:12px}' 5385 + '.osai-copy:hover{color:#fff;opacity:.9}' 5386 + '.osai-close{width:100%;margin-top:12px;background:none;border:none;color:var(--muted,#667);font-family:Poppins,sans-serif;font-size:.78rem;cursor:pointer;padding:8px}' 5387 + '.osai-close:hover{color:var(--txt,#111)}' 5388 /* le bouton lui-même, réutilisable partout */ 5389 + '.osai-share{display:inline-flex;align-items:center;justify-content:center;gap:5px;background:none;border:none;cursor:pointer;color:var(--muted,#888);padding:4px 6px;border-radius:6px;font-size:.72rem;font-family:inherit;transition:color .15s,background .15s}' 5390 + '.osai-share:hover{color:var(--accent,#6366f1);background:var(--card-hover,rgba(99,102,241,.06))}' 5391 + '.osai-share svg{width:14px;
5391height:14px}'; 5392 document.head.appendChild(st); 5393 5394 var sheet = null; 5395 function build(){ 5396 sheet = document.createElement('div'); 5397 sheet.className = 'osai-sheet'; 5398 sheet.innerHTML = '<div class="osai-sheet-in" role="dialog" aria-label="Share">' 5399 + '<h4>Share</h4><p id="osaiShareUrl"></p><div class="osai-nets" id="osaiNets"></div>' 5400 + '<button class="osai-close">Close</button></div>'; 5401 document.body.appendChild(sheet); 5402 sheet.addEventListener('click', function(e){ if(e.target === sheet || e.target.classList.contains('osai-close')) close(); }); 5403 document.addEventListener('keydown', function(e){ if(e.key === 'Escape') close(); }); 5404 } 5405 function close(){ if(sheet) sheet.classList.remove('open'); } 5406 5407 window.osaiShare = function(url, text){ 5408 url = url || location.href; 5409 text = text || document.title; 5410 if(url.charAt(0) === '/') url = location.origin + url; 5411 /* [04/08] un partage de plus pour cette page â anonyme, comme les vues */ 5412 try { 5413 var pu = '/api/pv.php?ev=share&p=' + encodeURIComponent(location.pathname || '/'); 5414 if (navigator.sendBeacon) navigator.sendBeacon(pu); 5415 } catch(e){} 5416 5417 /* mobile : on utilise le partage natif du système, bien meilleur */ 5418 if(navigator.share && /Android|iPhone|iPad/i.test(navigator.userAgent)){ 5419 navigator.share({title: text, text: text, url: url}).catch(function(){}); 5420 if(window.osaiTrack) osaiTrack('share', {net:'native'}); 5421 return; 5422 } 5423 if(!sheet) build(); 5424 sheet.querySelector('#osaiShareUrl').textContent = url; 5425 var box = sheet.querySelector('#osaiNets'); 5426 box.innerHTML = NETS.map(function(n){ 5427 return '<a class="osai-net" href="' + n.url(url, text) + '" target="_blank" rel="noopener" data-n="' + n.k + '">' 5428 + n.ico + '<span>' + n.label + '</span></a>'; 5429 }).join('') 5430 + '<button class="osai-net osai-copy" id="osaiCopy">' 5431 + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>' 5432 + '<span>Copy link</span></button>'; 5433 5434 box.querySelectorAll('.osai-net[data-n]').forEach(function(a){ 5435 a.addEventListener('click', function(){ if(window.osaiTrack) osaiTrack('share', {net:a.getAttribute('data-n')}); close(); }); 5436 }); 5437 var cp = box.querySelector('#osaiCopy'); 5438 cp.addEventListener('click', function(){ 5439 var done = function(){ 5440 var sp = cp.querySelector('span'); sp.textContent = 'Copied!'; 5441 setTimeout(function(){ sp.textContent = 'Copy link'; close(); }, 900); 5442 }; 5443 if(navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(url).then(done).catch(function(){}); 5444 if(window.osaiTrack) osaiTrack('share', {net:'copy'}); 5445 }); 5446 sheet.classList.add('open'); 5447 }; 5448 5449 /* âââ V87 â bouton compte : pastille rouge/verte + mini-menu âââ */ 5450 (function(){ 5451 var T = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 5452 function lang(){ return (location.pathname.match(/^\/(fr|it|es|de|pt)\//) || [, ''])[1]; } 5453 function lp(u){ var l = lang(); return (l ? '/' + l : '') + u; } 5454 function logged(){ return /(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie); } 5455 5456 var IC = { 5457 user: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', 5458 grid: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>', 5459 card: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>', 5460 out: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/>
5460<path d="M21 12H9"/></svg>' 5461 }; 5462 5463 function closeMenu(){ var m = document.getElementById('acctMenu'); if (m) m.classList.remove('open'); } 5464 5465 function buildMenu(me){ 5466 var m = document.getElementById('acctMenu'); 5467 if (!m) return; 5468 var html = ''; 5469 if (me && me.email) html += '<div class="who">' + me.email.replace(/[<>&]/g, '') + '</div>'; 5470 html += '<a href="' + lp('/account.html') + '">' + IC.card + '<span>' + T('Your account') + '</span></a>'; 5471 html += '<a href="' + lp('/my-dashboard.html') + '">' + IC.grid + '<span>' + T('My Dashboard') + '</span></a>'; 5472 if (me && me.handle && Number(me.pf_public)) { 5473 html += '<a href="/u/' + encodeURIComponent(me.handle) + '">' + IC.user + '<span>' + T('My public page') + '</span></a>'; 5474 } 5475 html += '<button type="button" class="out" id="acctOut">' + IC.out + '<span>' + T('Sign out') + '</span></button>'; 5476 m.innerHTML = html; 5477 var out = document.getElementById('acctOut'); 5478 if (out) out.onclick = function(){ 5479 fetch('/api/logout.php', { method: 'POST' }).catch(function(){}) 5480 .then(function(){ 5481 try { document.cookie = 'osai_m=; Max-Age=0; path=/'; } catch(e){} 5482 location.reload(); 5483 }); 5484 }; 5485 } 5486 5487 function paint(){ 5488 var b = document.getElementById('acctNavBtn'); 5489 if (!b) return; 5490 var inn = logged(); 5491 b.setAttribute('href', lp('/account.html')); 5492 b.classList.toggle('is-in', inn); 5493 b.setAttribute('title', T(inn ? 'Your account' : 'Sign in / create account')); 5494 b.setAttribute('aria-label', b.getAttribute('title')); 5495 5496 b.onclick = function(e){ 5497 if (!logged()) return; /* déconnecté : lien direct vers la page compte */ 5498 e.preventDefault(); 5499 e.stopPropagation(); 5500 var m = document.getElementById('acctMenu'); 5501 if (!m) return; 5502 if (m.classList.contains('open')) { closeMenu(); return; } 5503 buildMenu(null); /* ouverture immédiate */ 5504 m.classList.add('open'); 5505 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 5506 .then(function(me){ if (me && document.getElementById('acctMenu').classList.contains('open')) buildMenu(me); }); 5507 }; 5508 } 5509 5510 document.addEventListener('click', function(e){ 5511 var m = document.getElementById('acctMenu'); 5512 if (m && m.classList.contains('open') && !e.target.closest('.acct-wrap')) closeMenu(); 5513 }); 5514 document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeMenu(); }); 5515 5516 paint(); 5517 document.addEventListener('DOMContentLoaded', paint); 5518 window.addEventListener('load', paint); 5519 })(); 5520 5521 /* Ãcoute globale : marche AUSSI pour les cartes créées dynamiquement en JS */ 5522 document.addEventListener('click', function(e){ 5523 var b = e.target.closest('.osai-share, [data-share]'); 5524 if(!b) return; 5525 e.preventDefault(); 5526 e.stopPropagation(); 5527 var u0 = b.getAttribute('data-url') || b.getAttribute('data-share') || ''; 5528 var t0 = b.getAttribute('data-text') || ''; 5529 window.osaiFicheForAsync(u0).then(function(u){ osaiShare(u, t0); }); 5530 }); 5531 5532 /* ââ partage vers NOS fiches : une URL GitHub devient la fiche projet 5533 quand elle existe. Resolution AU CLIC (1 requete HEAD par partage 5534 reel, memoisee) â pas de gros mapping charge sur chaque page.
5535 [31/07] osaiFicheFor n'a jamais ete defini nulle part : le repointage 5536 promis par shareUrl()/refreshShares() ne fonctionnait sur AUCUNE page. */ 5537 var _ficheCache = {}; 5538 window.osaiFicheForAsync = function(url){ 5539 var m = String(url || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 5540 if (!m) return Promise.resolve(url); 5541 var slug = (m[1] + '-' + m[2]).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); 5542 if (_ficheCache[slug] !== undefined) return Promise.resolve(_ficheCache[slug] || url); 5543 var p = '/project/' + slug + '.html'; 5544 return fetch(p, { method: 'HEAD' }) 5545 .then(function(r){ _ficheCache[slug] = r.ok ? (location.origin + p) : ''; return _ficheCache[slug] || url; }) 5546 .catch(function(){ return url; }); 5547 }; 5548 5549 /* Icône prête à l'emploi pour construire un bouton */ 5550 window.osaiShareIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>'; 5551})(); 5552 5553 5554/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5555 UNIFICATION DU PARTAGE â on écrase les anciennes fonctions de chaque page. 5556 5557 Le site avait accumulé une dizaine de systèmes de partage différents 5558 (doShare, doShareTool, shareProject, lbShareToggle, tlDoShare, shareOnâ¦), 5559 chacun avec son petit menu maison et ses réseaux (Facebook, LinkedInâ¦). 5560 Résultat : un comportement différent sur chaque page. 5561 5562 Ce script se charge APRÃS les scripts des pages, donc ces redéfinitions 5563 gagnent. Toutes ces fonctions ouvrent désormais LA MÃME feuille : 5564 X · Bluesky · Mastodon · Reddit · WhatsApp · Copy link. 5565 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 5566(function(){ 5567 function txt(name){ 5568 return name ? ('Check out ' + name + ' \u2014 found on olud.ai') 5569 : (document.title || 'olud.ai'); 5570 } 5571 function go(url, name){ 5572 if(!window.osaiShare) return false; 5573 var u0 = url || location.href; 5574 if (window.osaiFicheForAsync) window.osaiFicheForAsync(u0).then(function(u){ osaiShare(u, txt(name)); }); 5575 else osaiShare(u0, txt(name)); 5576 return true; 5577 } 5578 5579 function unify(){ 5580 if(!window.osaiShare) return; 5581 5582 /* ââ Les OUVREURS (ils affichaient un petit menu) â feuille universelle ââ */ 5583 /* signature (e, name, url) */ 5584 ['shareProject','shareSpace','shareTool','shareModel','shareRelease'].forEach(function(fn){ 5585 window[fn] = function(e, name, url){ 5586 if(e && e.preventDefault) e.preventDefault(); 5587 if(e && e.stopPropagation) e.stopPropagation(); 5588 go(url, name); 5589 }; 5590 }); 5591 /* signature (e, name, url, kind) */ 5592 window._openShareMenu = function(e, name, url){ 5593 if(e && e.preventDefault) e.preventDefault(); 5594 if(e && e.stopPropagation) e.stopPropagation(); 5595 go(url, name); 5596 }; 5597 /* toggles sans url : on partage la page courante */ 5598 ['lbShareToggle','tlShareToggle','shareToggle','_shareToggle'].forEach(function(fn){ 5599 window[fn] = function(e){ 5600 if(e && e.preventDefault) e.preventDefault(); 5601 if(e && e.stopPropagation) e.stopPropagation(); 5602 go(location.href, null); 5603 }; 5604 }); 5605 5606 /* ââ Les EXÃCUTEURS (ils envoyaient direct sur un réseau) â feuille aussi ââ 5607 On ignore le réseau demandé : l'utilisateur choisit dans la feuille. */ 5608 /* signature (network, name, url) */ 5609 ['doShare','doShareSpace','doShareTool','doShareModel','tlDoShare'].forEach(function(fn){ 5610 window[fn] = function(network, name, url){ go(url, name); }; 5611 }); 5612 /* signature (net) â l'url était stockée ailleurs â page courante */ 5613 ['_doGlobalShare','lbDoShare','shareOn'].forEach(function(fn){ 5614 window[fn] = function(){ go(location.href, null); }; 5615 }); 5616 } 5617 5618 unify(); /* maintenant */ 5619 document.addEventListener('DOMContentLoaded', unify); /* et après le DOM, au cas où */ 5620 window.addEventListener('load', unify); /* et après tout, au cas où */ 5621 5622 /* âââ V81 â ADOPTION GÃNÃRIQUE DES MENUS HÃRITÃS âââ 5623 L'absorption par NOM de fonction (ci-dessus) rate toutes les pages dont la 5624 fonction s'appelle autrement (tools.html, fiches /project/ avec 5625 ficheShareToggleâ¦) : elles gardent leur vieux menu « X + copier ». 5626 Ici on intercepte au niveau du DOCUMENT, en phase de CAPTURE : le clic 5627 n'atteint jamais le handler inline de la page, quel que soit son nom. 5628 Marche aussi pour les cartes rendues en JS (aucun observateur requis). */ 5629 5630 /* on ne montre plus jamais un menu local : la feuille universelle le remplace */ 5631 var hide = document.createElement('style'); 5632 hide.textContent = '[class*="share-menu"]{display:none !important}'; 5633 document.head.appendChild(hide); 5634 5635 /* extrait les arguments entre apostrophes d'un onclick hérité */ 5636 function argsOf(el){ 5637 var oc = (el && el.getAttribute) ? (el.getAttribute('onclick') || '') : ''; 5638 var out = [], re = /'((?:[^'\\]|\\.)*)'/g, m; 5639 while((m = re.exec(oc))) out.push(m[1].replace(/\\'/g, "'")); 5640 return out; 5641 } 5642 var NETW = /^(twitter|x|bluesky|mastodon|reddit|whatsapp|facebook|linkedin|telegram|copy|email)$/i; 5643 function pick(args){ 5644 var url = null, name = null; 5645 for (var i = 0; i < args.length; i++){ 5646 var a = args[i]; 5647 if (/^(https?:\/\/|\/)/.test(a)) { if (!url) url = a; } 5648 else if (a && !NETW.test(a) && !name) name = a; 5649 } 5650 return { url: url, name: name }; 5651 }
5652 /* l'ouvreur ne porte pas toujours l'URL (ex. ficheShareToggle(event)) : 5653 on va la chercher dans les boutons du menu qu'il commande. */ 5654 function itemOf(el){ 5655 var got = pick(argsOf(el)); 5656 if (got.url) return got; 5657 var wrap = (el.closest && (el.closest('[class*="share-wrap"]') || el.parentElement)) || null; 5658 var menu = wrap ? wrap.querySelector('[class*="share-menu"]') : null; 5659 if (menu){ 5660 var btns = menu.querySelectorAll('button, a'); 5661 for (var i = 0; i < btns.length; i++){ 5662 var g = pick(argsOf(btns[i])); 5663 if (g.url) return { url: g.url, name: g.name || got.name }; 5664 } 5665 } 5666 return got; /* rien trouvé â page courante */ 5667 } 5668 5669 document.addEventListener('click', function(e){ 5670 if (!window.osaiShare || !e.target || !e.target.closest) return; 5671 var t = e.target; 5672 if (t.closest('.osai-sheet')) return; /* la feuille elle-même */ 5673 if (t.closest('.osai-share, [data-share]')) return; /* déjà géré proprement */ 5674 5675 /* un clic sur un ouvreur hérité, ou dans un menu hérité resté ouvert */ 5676 var hit = t.closest('button[class*="share"], a[class*="share"], [class*="share-menu"] button, [class*="share-menu"] a'); 5677 if (!hit) return; 5678 5679 e.preventDefault(); 5680 e.stopPropagation(); /* le handler inline ne verra rien */ 5681 5682 var it = itemOf(hit.closest('[class*="share-menu"]') ? hit : hit); 5683 if (!it.url){ /* menu ouvert : chercher dans le menu */ 5684 var mn = hit.closest('[class*="share-menu"]'); 5685 if (mn) it = itemOf(mn.querySelector('button, a') || hit); 5686 } 5687 osaiShare(it.url || location.href, txt(it.name)); 5688 }, true); /* â CAPTURE */ 5689})(); 5690 5691/* âââ V75 â bouton â Follow sur les fiches /project/ (watchlist, comptes) âââ 5692 Anonyme â renvoi vers /account.html (magic link). Connecté â toggle via 5693 /api/watch.php. Dégradation totale si l'API est absente (site statique pur). */ 5694(function(){ 5695 var hpath = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 5696 if (!/^\/(tool|project)\/[^\/]+\.html$/.test(hpath) || /\/index\.html$/.test(hpath)) return; 5697 5698 function findRepo(){ 5699 var links = document.querySelectorAll('a[href*="github.com/"]'); 5700 for (var j = 0; j < links.length; j++){ 5701 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; 5702 var m = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 5703 if (m && ['topics','sponsors','features','orgs','search','trending'].indexOf(m[1].toLowerCase()) < 0) 5704 return (m[1] + '/' + m[2]).toLowerCase(); 5705 } 5706 return null; 5707 } 5708 var t = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 5709 5710 function boot(){ 5711 var repo = findRepo(); 5712 if (!repo || document.getElementById('osaiWatchBtn')) return; 5713 var anchor = document.querySelector('.hero-main .cta-row') || document.querySelector('.cta-row') 5714 || document.querySelector('.wrap > .badges') || document.querySelector('.badges') 5715 || document.querySelector('main h1, .hero h1, h1'); 5716 if (!anchor) return; 5717 5718 if (!document.getElementById('osaiWatchCSS')){ 5719 var s = document.createElement('style'); s.id = 'osaiWatchCSS'; 5720 s.textContent = '#osaiWatchBtn{display:inline-flex;align-items:center;gap:8px;height:46px;padding:0 18px;' 5721 + 'border-radius:12px;border:2px solid var(--border2,rgba(0,0,0,.15));background:var(--surface2,transparent);' 5722 + 'color:var(--muted,#5a6d87);font:inherit;font-size:.88rem;font-weight:600;cursor:pointer;' 5723 + 'transition:color .2s,border-color .2s,transform .15s}' 5724 + '#osaiWatchBtn:hover{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1);transform:scale(1.04)}' 5725 + '#osaiWatchBtn.on{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1);background:rgba(99,102,241,.08)}' 5726 + '#osaiWatchBtn .st{font-size:1.05rem;line-height:1}' 5727 + '@media(max-width:640px){#osaiWatchBtn{height:42px;padding:0 14px}}'; 5728 document.head.appendChild(s); 5729 } 5730 var btn = document.createElement('button'); 5731 btn.id = 'osaiWatchBtn'; btn.type = 'button'; 5732 var watching = false, logged = false;
5733 function paint(){ 5734 btn.innerHTML = '<span class="st">' + (watching ? '\u2605' : '\u2606') + '</span><span>' 5735 + (watching ? t('Following') : t('Follow')) + '</span>'; 5736 btn.classList.toggle('on', watching); 5737 btn.title = logged ? '' : t('Sign in to follow projects'); 5738 } 5739 paint(); 5740 if (anchor.classList && anchor.classList.contains('cta-row')) anchor.appendChild(btn); 5741 else anchor.insertAdjacentElement('afterend', btn); 5742 5743 /* le badge README : chaque mainteneur qui l'affiche relaie la fiche. 5744 L'état « copié » persiste (localStorage) â le bouton reste coloré 5745 au retour sur la fiche pour signaler qu'on a déjà pris le badge. */ 5746 if (!document.getElementById('osaiBadgeBtn')) { 5747 var bb = document.createElement('button'); 5748 bb.id = 'osaiBadgeBtn'; bb.type = 'button'; 5749 bb.style.cssText = 'display:inline-flex;align-items:center;gap:7px;height:46px;padding:0 15px;margin-left:8px;' 5750 + 'border-radius:12px;border:2px solid var(--border2,rgba(0,0,0,.15));background:var(--surface2,transparent);' 5751 + 'color:var(--muted,#5a6d87);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer;' 5752 + 'transition:color .2s,border-color .2s,background .2s'; 5753 function badgeTaken(){ 5754 try { return !!(JSON.parse(localStorage.getItem('osai_badge_md') || '{}')[repo]); } catch(e){ return false; } 5755 } 5756 function rememberBadge(){ 5757 try { var m = JSON.parse(localStorage.getItem('osai_badge_md') || '{}'); m[repo] = 1; 5758 localStorage.setItem('osai_badge_md', JSON.stringify(m)); } catch(e){} 5759 } 5760 function paintB(flash){ 5761 var on = badgeTaken(); 5762 if (flash) { 5763 bb.innerHTML = '<span style="font-size:1rem">✓</span><span>' + t('Markdown copied!') + '</span>'; 5764 } else { 5765 bb.innerHTML = '<span style="font-size:1rem">' + (on ? '✓' : '🔖') + '</span><span>' + t('Health badge') + '</span>'; 5766 } 5767 bb.style.borderColor = on ? 'var(--accent,#6366f1)' : 'var(--border2,rgba(0,0,0,.15))'; 5768 bb.style.color = on ? 'var(--accent,#6366f1)' : 'var(--muted,#5a6d87)'; 5769 bb.style.background = on ? 'rgba(99,102,241,.08)' : 'var(--surface2,transparent)'; 5770 bb.title = on ? t('Badge copied â click to copy again') : t('Copy the Markdown badge for your README'); 5771 } 5772 paintB(false); 5773 bb.addEventListener('mouseenter', function(){ bb.style.borderColor = 'var(--accent,#6366f1)'; bb.style.color = 'var(--accent,#6366f1)'; }); 5774 bb.addEventListener('mouseleave', function(){ paintB(false); }); 5775 bb.addEventListener('click', function(){ 5776 var md = '[]' 5777 + '(https://olud.ai' + hpath + ')'; 5778 var done = function(){ 5779 rememberBadge(); 5780 paintB(true); 5781 setTimeout(function(){ paintB(false); }, 2200); 5782 }; 5783 if (navigator.clipboard && navigator.clipboard.writeText) { 5784 navigator.clipboard.writeText(md).then(done, function(){ window.prompt(t('Copy the Markdown badge for your README'), md); done(); }); 5785 } else { window.prompt(t('Copy the Markdown badge for your README'), md); done(); } 5786 }); 5787 btn.insertAdjacentElement('afterend', bb); 5788 } 5789 /* [17/08, Romain, 2e passe : « en haut a cote du nom du projet »] 5790 Le bouton quitte la rangee de boutons pour une pilule compacte collee 5791 au H1 â visible des le premier ecran, sans pousser les vrais boutons. */ 5792 if (!document.getElementById('osaiPromoBtn')) { 5793 var pb = document.createElement('a'); 5794 pb.id = 'osaiPromoBtn'; pb.href = '/promote.html'; 5795 pb.style.cssText = 'display:inline-flex;align-items:center;gap:5px;vertical-align:middle;margin-left:14px;' 5796 + 'padding:5px 13px;border-radius:999px;border:2px solid var(--accent,#6366f1);' 5797 + 'color:var(--accent,#6366f1);background:transparent;font-family:Poppins,system-ui,sans-serif;' 5798 + 'font-size:.72rem;font-weight:600;letter-spacing:.04em;text-decoration:none;white-space:nowrap;' 5799 + 'transition:background .2s,color .2s'; 5800 pb.innerHTML = '<span style="font-size:.85rem;line-height:1">📢</span><span>' + t('Promote') + '</span>'; 5801 pb.addEventListener('mouseenter', function(){ pb.style.background = 'var(--accent,#6366f1)'; pb.style.color = '#fff'; }); 5802 pb.addEventListener('mouseleave', function(){ pb.style.background = 'transparent'; pb.style.color = 'var(--accent,#6366f1)'; }); 5803 var h1f = document.querySelector('.hero-main h1') || document.querySelector('.wrap h1') || document.querySelector('h1'); 5804 if (h1f) h1f.appendChild(pb); 5805 else { var anc2 = document.getElementById('osaiBadgeBtn') || btn; anc2.insertAdjacentElement('afterend', pb); } 5806 } 5807 5808 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 5809 .then(function(me){ 5810 if (!me) return; 5811 logged = true; 5812 watching = (me.watch || []).some(function(w){ return w.repo === repo; });
5813 paint(); 5814 }); 5815 5816 btn.addEventListener('click', function(){ 5817 if (!logged) { 5818 /* nudge au lieu d'une redirection seche : le visiteur garde sa fiche, 5819 et on lui dit ce que le compte apporte (voir osaiSignupNudge). */ 5820 if (window.osaiSignupNudge) { osaiSignupNudge(btn, 'follow'); return; } 5821 var lg = (location.pathname.match(/^\/(fr|it|es|de|pt)\//) || [,''])[1]; 5822 location.href = (lg ? '/' + lg : '') + '/account.html'; 5823 return; 5824 } 5825 var next = !watching; 5826 fetch('/api/watch.php', { method:'POST', headers:{'Content-Type':'application/json'}, 5827 body: JSON.stringify({ repo: repo, action: next ? 'add' : 'remove' }) }) 5828 .then(function(r){ return r.ok ? r.json() : null; }) 5829 .then(function(res){ 5830 if (res && res.ok) { watching = res.watching; paint(); 5831 if (window.osaiTrack) osaiTrack('watch_toggle', { repo: repo, on: watching }); 5832 /* il vient de demander a etre tenu au courant : proposer le navigateur */ 5833 if (watching && window.osaiPushNudge) osaiPushNudge(btn); } 5834 }); 5835 }); 5836 } 5837 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); 5838})(); 5839/* âââ V86 â PAGE D'ACCUEIL PERSONNALISÃE âââââââââââââââââââââââââââââââââââ 5840 Le membre choisit ses sections depuis /account.html. On applique d'abord le 5841 miroir localStorage (zéro clignotement, marche même hors ligne), puis on 5842 resynchronise depuis le compte (multi-appareils). Aucun effet pour les 5843 visiteurs anonymes : la home reste complète pour eux et pour Google. */ 5844(function(){ 5845 var p = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/|$)/, ''); 5846 if (!(p === '/' || p === '' || p === '/index.html')) return; 5847 5848 function apply(list){ 5849 if (!Array.isArray(list)) return; 5850 var all = document.querySelectorAll('section[id]'); 5851 /* On ne réaffiche QUE ce que ce module a caché lui-même : certaines 5852 sections (ex-annuaire Spaces #tools) sont volontairement masquées en 5853 inline par la page â les « réinitialiser » les ressusciterait vides. */ 5854 for (var i = 0; i < all.length; i++) { 5855 if (all[i].getAttribute('data-osai-hidden') === '1') { 5856 all[i].style.display = ''; 5857 all[i].removeAttribute('data-osai-hidden'); 5858 } 5859 } 5860 list.forEach(function(id){ 5861 if (!/^[A-Za-z][A-Za-z0-9_-]{1,30}$/.test(id)) return; 5862 var el = document.getElementById(id); 5863 if (el && el.tagName === 'SECTION') { el.style.display = 'none'; el.setAttribute('data-osai-hidden', '1'); } 5864 }); 5865 } 5866 function fromLocal(){ 5867 try { return JSON.parse(localStorage.getItem('osai_home_hide') || '[]'); } catch(e){ return []; } 5868 } 5869 5870 var cached = fromLocal(); 5871 if (cached.length) { 5872 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ apply(cached); }); 5873 else apply(cached); 5874 } 5875 5876 /* membre connecté : le compte fait foi */ 5877 if (/(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie)) { 5878 fetch('/api/me.php').then(function(r){ return r.ok ? r.json() : null; }).catch(function(){ return null; }) 5879 .then(function(me){ 5880 if (!me) return; 5881 var h = me.home_hide || [];
5882 try { localStorage.setItem('osai_home_hide', JSON.stringify(h)); } catch(e){} 5883 apply(h); 5884 }); 5885 } else if (cached.length) { 5886 /* déconnecté : on oublie les préférences du compte */ 5887 try { localStorage.removeItem('osai_home_hide'); } catch(e){} 5888 apply([]); 5889 } 5890})(); 5891 5892/* âââ V84 â INCITATION DOUCE à L'INSCRIPTION ââââââââââââââââââââââââââââââââ 5893 Principe : on ne masque AUCUN contenu. Un bandeau discret apparaît après 5894 quelques pages visitées, propose un compte, et disparaît si on le ferme 5895 (30 jours de silence). Zéro contenu caché = zéro risque de cloaking, donc 5896 le SEO â qui amène justement ces visiteurs â reste intact. */ 5897(function(){ 5898 if (/(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie)) return; /* déjà membre */ 5899 var p = location.pathname; 5900 if (/account\.html|\/u\.php|\/c\.php|privacy|terms/.test(p)) return; /* pages hors-sujet */ 5901 5902 var SEUIL = 4, SILENCE = 30 * 86400000; 5903 var pv = 0, dis = 0; 5904 try { 5905 dis = parseInt(localStorage.getItem('osai_cta_off') || '0', 10); 5906 if (dis && Date.now() - dis < SILENCE) return; 5907 pv = parseInt(localStorage.getItem('osai_pv') || '0', 10) + 1; 5908 localStorage.setItem('osai_pv', String(pv)); 5909 if (sessionStorage.getItem('osai_cta_seen')) return; /* 1 fois par session */ 5910 } catch(e){ return; } 5911 if (pv < SEUIL) return; 5912 5913 var t = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 5914 function acct(){ 5915 var lg = (location.pathname.match(/^\/(fr|it|es|de|pt)\//) || [, ''])[1]; 5916 return (lg ? '/' + lg : '') + '/account.html'; 5917 } 5918 5919 function show(){ 5920 if (document.getElementById('osaiCta')) return; 5921 try { sessionStorage.setItem('osai_cta_seen', '1'); } catch(e){} 5922 var st = document.createElement('style'); 5923 st.textContent = '#osaiCta{position:fixed;right:20px;bottom:20px;z-index:99998;width:min(340px,calc(100vw - 32px));' 5924 + 'padding:18px 18px 16px;border-radius:16px;background:var(--surface,#141a29);color:var(--txt,#eef1f7);' 5925 + 'border:1px solid var(--border2,rgba(255,255,255,.12));box-shadow:0 18px 50px rgba(0,0,0,.32);' 5926 + 'font-family:inherit;animation:osaiCtaIn .3s ease-out}' 5927 + '@keyframes osaiCtaIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}' 5928 + '#osaiCta h4{margin:0 0 6px;font-size:.98rem}' 5929 + '#osaiCta p{margin:0 0 13px;font-size:.83rem;line-height:1.5;color:var(--muted,#8a99ad)}' 5930 + '#osaiCta a.cta{display:inline-block;padding:10px 16px;border-radius:10px;background:#6366f1;color:#fff;' 5931 + 'text-decoration:none;font-weight:600;font-size:.84rem}' 5932 + '#osaiCta button{position:absolute;top:10px;right:12px;background:none;border:0;color:var(--muted,#8a99ad);' 5933 + 'font-size:1.15rem;cursor:pointer;line-height:1;padding:2px 4px}' 5934 + '#osaiCta .free{margin-left:10px;font-size:.76rem;color:var(--muted,#8a99ad)}'; 5935 document.head.appendChild(st); 5936 5937 var d = document.createElement('div'); 5938 d.id = 'osaiCta'; 5939 d.innerHTML = '<button aria-label="Close">×</button>' 5940 + '<h4>' + t('Keep track of what matters') + '</h4>' 5941 + '<p>' + t('Follow projects and get an email when their health drops, a new release ships or the license changes. Free, no password.') + '</p>' 5942 + '<a class="cta" href="' + acct() + '">' + t('Create a free account') + '</a>' 5943 + '<span class="free">' + t('30 seconds') + '</span>'; 5944 d.querySelector('button').onclick = function(){
5945 try { localStorage.setItem('osai_cta_off', String(Date.now())); } catch(e){} 5946 d.remove(); 5947 }; 5948 document.body.appendChild(d); 5949 } 5950 5951 /* on attend un signe d'intérêt : 25 s de lecture, ou un défilement conséquent */ 5952 var armed = false; 5953 function arm(){ if (!armed) { armed = true; show(); } } 5954 setTimeout(arm, 25000); 5955 window.addEventListener('scroll', function(){ 5956 if (!armed && (window.scrollY > document.body.scrollHeight * 0.45)) arm(); 5957 }, { passive: true }); 5958})(); 5959 5960/* âââ V83 â bouton « + Collection » sur les fiches /project/ et /tool/ âââââââ 5961 Le chaînon manquant : les collections étaient créables depuis la page compte, 5962 mais RIEN n'y menait depuis le site. Ce bouton s'installe à côté de â Follow. 5963 Anonyme â page compte. Connecté â panneau : cocher/décocher ses collections, 5964 ou en créer une à la volée. Dégradation totale si l'API est absente. */ 5965(function(){ 5966 var hp = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 5967 if (!/^\/(tool|project)\/[^\/]+\.html$/.test(hp) || /\/index\.html$/.test(hp)) return; 5968 5969 var t = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 5970 function logged(){ return /(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie); } 5971 function acctUrl(){ 5972 var lg = (location.pathname.match(/^\/(fr|it|es|de|pt)\//) || [, ''])[1]; 5973 return (lg ? '/' + lg : '') + '/account.html'; 5974 } 5975 function findRepo(){ 5976 var links = document.querySelectorAll('a[href*="github.com/"]'); 5977 for (var j = 0; j < links.length; j++){ 5978 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; 5979 var m = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 5980 if (m && ['topics','sponsors','features','orgs','search','trending'].indexOf(m[1].toLowerCase()) < 0) 5981 return (m[1] + '/' + m[2]).toLowerCase(); 5982 } 5983 return null; 5984 } 5985 5986 var repo = null, panel = null, btn = null; 5987 5988 function css(){ 5989 if (document.getElementById('osaiColCSS')) return; 5990 var s = document.createElement('style'); s.id = 'osaiColCSS'; 5991 s.textContent = 5992 '#osaiColBtn{display:inline-flex;align-items:center;gap:8px;height:46px;padding:0 18px;margin-left:8px;' 5993 + 'border-radius:12px;border:2px solid var(--border2,rgba(0,0,0,.15));background:var(--surface2,transparent);' 5994 + 'color:var(--muted,#5a6d87);font:inherit;font-size:.88rem;font-weight:600;cursor:pointer;' 5995 + 'transition:color .2s,border-color .2s,transform .15s}' 5996 + '#osaiColBtn:hover{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1);transform:scale(1.04)}' 5997 + '#osaiColBtn.on{border-color:var(--accent,#6366f1);color:var(--accent,#6366f1);background:rgba(99,102,241,.08)}' 5998 + '#osaiColPanel{position:absolute;z-index:9999;width:290px;padding:12px;border-radius:14px;' 5999 + 'background:var(--surface,#fff);border:1px solid var(--border2,rgba(0,0,0,.12));' 6000 + 'box-shadow:0 16px 44px rgba(0,0,0,.22);font-size:.86rem;color:var(--txt,#1a2540)}' 6001 + '#osaiColPanel .hd{font-weight:700;margin-bottom:9px;font-size:.82rem}' 6002 + '#osaiColPanel .ls{max-height:190px;overflow:auto;display:flex;flex-direction:column;gap:2px}' 6003 + '#osaiColPanel label{display:flex;align-items:center;gap:9px;padding:6px 4px;border-radius:8px;cursor:pointer}' 6004 + '#osaiColPanel label:hover{background:var(--card-hover,rgba(99,102,241,.06))}' 6005 + '#osaiColPanel input[type=checkbox]{accent-color:var(--accent,#6366f1);cursor:pointer;flex:none;margin:0}' 6006 + '#osaiColPanel .nw{display:flex;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid var(--border2,rgba(0,0,0,.1))}' 6007 + '#osaiColPanel .nw input[type=text]{flex:1;min-width:0;padding:7px 9px;border-radius:8px;font:inherit;font-size:.82rem;' 6008 + 'border:1px solid var(--border2,rgba(0,0,0,.15));background:var(--bg,#fff);color:var(--txt,#1a2540)}' 6009 + '#osaiColPanel .nw button{padding:7px 11px;border-radius:8px;border:0;background:var(--accent,#6366f1);color:#fff;' 6010 + 'font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}' 6011 + '#osaiColPanel .em{color:var(--muted,#7a8fa8);font-size:.8rem;padding:6px 2px}'; 6012 document.head.appendChild(s); 6013 } 6014 6015 function api(body){ 6016 return fetch('/api/collections.php', { method:'POST', headers:{'Content-Type':'application/json'}, 6017 body: JSON.stringify(body) }).then(function(r){ return r.ok ? r.json() : null; }) 6018 .catch(function(){ return null; }); 6019 } 6020 function close(){ if (panel){ panel.remove(); panel = null; } } 6021 6022 function open(){ 6023 close(); 6024 css(); 6025 panel = document.createElement('div'); 6026 panel.id = 'osaiColPanel'; 6027 panel.innerHTML = '<div class="hd">' + t('Add to collection') + '</div><div class="ls em">â¦</div>'; 6028 document.body.appendChild(panel); 6029 var r = btn.getBoundingClientRect(); 6030 panel.style.top = (r.bottom + window.scrollY + 8) + 'px'; 6031 panel.style.left = Math.max(10, Math.min(r.left + window.scrollX, window.innerWidth - 302)) + 'px'; 6032 panel.addEventListener('click', function(e){ e.stopPropagation(); }); 6033 6034 fetch('/api/collections.php').then(function(r2){ return r2.ok ? r2.json() : null; }) 6035 .then(function(j){ 6036 if (!panel) return; 6037 var cols = (j && j.collections) || []; 6038 var ls = document.createElement('div'); ls.className = 'ls'; 6039 if (!cols.length) { 6040 var em = document.createElement('div'); em.className = 'em'; 6041 em.textContent = t('No collection yet â create your first one below.'); 6042 ls.appendChild(em); 6043 }
6044 cols.forEach(function(c){ 6045 var l = document.createElement('label'); 6046 var cb = document.createElement('input'); cb.type = 'checkbox'; 6047 cb.checked = (c.items || []).indexOf(repo) >= 0; 6048 cb.onchange = function(){ 6049 api({ action: cb.checked ? 'add' : 'remove', id: c.id, repo: repo }).then(function(){ mark(); }); 6050 }; 6051 l.appendChild(cb); l.appendChild(document.createTextNode(c.title)); 6052 ls.appendChild(l); 6053 }); 6054 var nw = document.createElement('div'); nw.className = 'nw'; 6055 var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = t('New collectionâ¦'); inp.maxLength = 80; 6056 var go = document.createElement('button'); go.textContent = t('Create'); 6057 function create(){ 6058 var title = (inp.value || '').trim(); 6059 if (!title) return; 6060 api({ action:'create', title: title }).then(function(j2){ 6061 if (!j2 || !j2.ok) return; 6062 api({ action:'add', id: j2.id, repo: repo }).then(function(){ open(); mark(); }); 6063 }); 6064 } 6065 go.onclick = create; 6066 inp.addEventListener('keydown', function(e){ if (e.key === 'Enter') create(); }); 6067 nw.appendChild(inp); nw.appendChild(go); 6068 panel.innerHTML = '<div class="hd">' + t('Add to collection') + '</div>'; 6069 panel.appendChild(ls); panel.appendChild(nw); 6070 }); 6071 } 6072 6073 /* le bouton s'allume si le projet est déjà dans au moins une collection */ 6074 function mark(){ 6075 if (!logged()) return; 6076 fetch('/api/collections.php').then(function(r){ return r.ok ? r.json() : null; }) 6077 .then(function(j){ 6078 if (!j || !btn) return; 6079 var inAny = (j.collections || []).some(function(c){ return (c.items || []).indexOf(repo) >= 0; }); 6080 btn.classList.toggle('on', inAny); 6081 btn.querySelector('.lb').textContent = inAny ? t('In collection') : t('Collection'); 6082 }); 6083 } 6084 6085 function boot(){ 6086 repo = findRepo(); 6087 if (!repo || document.getElementById('osaiColBtn')) return; 6088 var follow = document.getElementById('osaiWatchBtn'); 6089 var anchor = follow || document.querySelector('.hero-main .cta-row') || document.querySelector('.cta-row'); 6090 if (!anchor) return; 6091 css(); 6092 btn = document.createElement('button'); 6093 btn.id = 'osaiColBtn'; btn.type = 'button'; 6094 btn.innerHTML = '<span style="font-size:1.05rem;line-height:1">+</span><span class="lb">' + t('Collection') + '</span>'; 6095 if (follow) follow.insertAdjacentElement('afterend', btn); 6096 else anchor.appendChild(btn); 6097 6098 btn.addEventListener('click', function(e){ 6099 e.stopPropagation(); 6100 if (!logged()) { if (window.osaiSignupNudge) { osaiSignupNudge(btn, 'collection'); } else { location.href = acctUrl(); } return; } 6101 if (panel) { close(); return; } 6102 open(); 6103 }); 6104 /* fermeture sur clic extérieur â motif robuste : on teste la cible plutôt 6105 que de compter sur la propagation (un repaint peut décaler le bouton) */ 6106 document.addEventListener('click', function(e){ 6107 if (!panel) return; 6108 if (panel.contains(e.target) || btn.contains(e.target) || e.target === btn) return; 6109 close(); 6110 }); 6111 document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); }); 6112 mark(); 6113 } 6114 6115 /* le bouton â Follow s'installe au chargement : on se greffe juste après */ 6116 var n = 0; 6117 (function wait(){ 6118 if (document.getElementById('osaiWatchBtn') || n > 25) { boot(); return; } 6119 n++; setTimeout(wait, 200); 6120 })(); 6121})(); 6122 6123/* âââ V95 â CÅURS ET PARTAGE SUR LES PAGES /alternatives/ âââââââââââââââââââ 6124 Ces pages n'avaient ni favori ni partage. Plutôt que de régénérer 6 langues 6125 à N produits, on les équipe DEPUIS LE NAV : le cÅur est injecté dans chaque 6126 .alt-card (avec l'URL du dépôt, pour rester cohérent avec les favoris posés 6127 ailleurs sur le site), et un bouton de partage rejoint le hero. */ 6128(function(){ 6129 var hp = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 6130 if (!/^\/alternatives\/[^\/]+\.html$/.test(hp)) return; 6131 6132 function txt(el){ return el ? String(el.textContent || '').trim() : ''; } 6133 6134 /* Ãtiquette du favori = le produit remplacé. On la tire du SLUG plutôt que du 6135 titre : le titre est traduit dans 6 langues, et un regex sur les tirets 6136 mordait dans « Open-source ». Le slug, lui, est stable. */ 6137 function pageTag(){ 6138 var m = hp.match(/^\/alternatives\/([^\/]+)\.html$/); 6139 if (!m) return 'Alternative'; 6140 return m[1].split('-').map(function(w){ 6141 return w ? w.charAt(0).toUpperCase() + w.slice(1) : ''; 6142 }).join(' ').trim().slice(0, 32); 6143 } 6144 6145 /* URL du favori : le dépôt GitHub d'abord (cohérent avec les autres pages), 6146 sinon le site officiel, sinon la fiche interne. */ 6147 function cardUrl(card){ 6148 var as = card.querySelectorAll('.ac-links a'); 6149 var gh = null, site = null, fiche = null; 6150 for (var i = 0; i < as.length; i++){ 6151 var href = as[i].getAttribute('href') || ''; 6152 if (/^https?:\/\/(www\.)?github\.com\//i.test(href)) { if (!gh) gh = href; } 6153 else if (/^https?:\/\//i.test(href)) { if (!site) site = href; } 6154 else if (href.charAt(0) === '/') { if (!fiche) fiche = href; } 6155 } 6156 return gh || site || fiche || null; 6157 } 6158 6159 function boot(){ 6160 if (typeof window.favBtnHtml !== 'function') return false;
6161 var tag = pageTag(); 6162 6163 /* ââ un cÅur par carte ââ */ 6164 var cards = document.querySelectorAll('.alt-card'); 6165 for (var i = 0; i < cards.length; i++){ 6166 var card = cards[i]; 6167 if (card.querySelector('.fav-btn')) continue; 6168 var name = txt(card.querySelector('.ac-head b')).replace(/\s*âº\s*$/, ''); 6169 var desc = txt(card.querySelector('.ac-desc')); 6170 var url = cardUrl(card); 6171 if (!name || !url) continue; 6172 6173 var head = card.querySelector('.ac-head'); 6174 if (!head) continue; 6175 var slot = document.createElement('span'); 6176 slot.className = 'alt-fav'; 6177 slot.innerHTML = window.favBtnHtml(name, desc, tag, url); 6178 head.appendChild(slot); 6179 } 6180 6181 /* ââ partage de la page, dans le hero ââ */ 6182 var hero = document.querySelector('.alt-hero'); 6183 if (hero && !document.getElementById('altShare')) { 6184 var b = document.createElement('button'); 6185 b.id = 'altShare'; 6186 b.className = 'osai-share alt-share'; 6187 b.setAttribute('data-url', location.pathname); 6188 b.setAttribute('data-text', txt(document.querySelector('.alt-hero h1')) || document.title); 6189 b.setAttribute('aria-label', 'Share'); 6190 b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg><span>Share</span>'; 6191 hero.appendChild(b); 6192 } 6193 6194 if (!document.getElementById('altFavCSS')) { 6195 var s = document.createElement('style'); 6196 s.id = 'altFavCSS'; 6197 s.textContent = 6198 '.alt-card .ac-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap}' 6199 + '.alt-card .alt-fav{margin-left:auto;flex:none;display:inline-flex}' 6200 + '.alt-share{margin-left:auto;display:inline-flex;align-items:center;gap:7px;flex:none;' 6201 + 'padding:9px 15px;border-radius:10px;border:1px solid var(--border2);background:var(--surface);' 6202 + 'color:var(--muted);font:inherit;font-size:.82rem;font-weight:600;cursor:pointer}' 6203 + '.alt-share:hover{border-color:var(--accent);color:var(--accent)}' 6204 + '.alt-share svg{width:15px;height:15px}' 6205 + '@media(max-width:620px){.alt-share{margin:12px auto 0}}'; 6206 document.head.appendChild(s); 6207 } 6208 if (typeof window.favUpdateBadge === 'function') window.favUpdateBadge(); 6209 return true; 6210 } 6211 6212 /* favorites.js est injecté par le nav : on attend son API */ 6213 var n = 0; 6214 (function wait(){ 6215 if (boot() || ++n > 40) return; 6216 setTimeout(wait, 200); 6217 })(); 6218})(); 6219 6220/* âââ V99 â PREUVE SOCIALE SUR LES FICHES âââââââââââââââââââââââââââââââââââ 6221 Tes 16 000 fiches reçoivent le trafic SEO et sont des culs-de-sac : personne 6222 n'y apprend que la communauté existe. Ce bloc les transforme en portes 6223 d'entrée vers les stacks et collections â et donc vers les comptes. 6224 6225 Règle absolue : si les compteurs sont à zéro, on n'affiche RIEN. Un bloc 6226 « 0 membre » fait plus de mal que pas de bloc du tout. */ 6227(function(){ 6228 var hp = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 6229 if (!/^\/(tool|project)\/[^\/]+\.html$/.test(hp) || /\/index\.html$/.test(hp)) return; 6230 6231 var T = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 6232 function lp(u){ 6233 var l = (location.pathname.match(/^\/(fr|it|es|de|pt)\//) || [, ''])[1]; 6234 return (l ? '/' + l : '') + u; 6235 } 6236 function findRepo(){ 6237 var links = document.querySelectorAll('a[href*="github.com/"]'); 6238 for (var j = 0; j < links.length; j++){ 6239 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; 6240 var m = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 6241 if (m && ['topics','sponsors','features','orgs','search','trending'].indexOf(m[1].toLowerCase()) < 0) 6242 return (m[1] + '/' + m[2]).toLowerCase(); 6243 } 6244 return null; 6245 } 6246 6247 function css(){ 6248 if (document.getElementById('osaiSocialCSS')) return; 6249 var s = document.createElement('style'); 6250 s.id = 'osaiSocialCSS'; 6251 s.textContent = 6252 '#osaiSocial{margin:22px 0;padding:16px 18px;border:1px solid var(--border,rgba(0,0,0,.1));' 6253 + 'border-radius:16px;background:var(--surface,#fff)}' 6254 + '#osaiSocial .os-stats{display:flex;gap:20px;flex-wrap:wrap;align-items:center}' 6255 + '#osaiSocial .os-stat{display:inline-flex;align-items:center;gap:8px;font-size:.86rem;color:var(--txt2,#4a5568)}' 6256 + '#osaiSocial .os-stat b{color:var(--txt,#1a2540);font-size:1.02rem}' 6257 + '#osaiSocial .os-stat svg{width:16px;height:16px;flex:none;opacity:.75}' 6258 + '#osaiSocial .os-sep{width:1px;height:20px;background:var(--border,rgba(0,0,0,.1))}' 6259 + '#osaiSocial .os-t{font-size:.68rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;' 6260 + 'color:var(--muted,#7a8fa8);margin:16px 0 9px}' 6261 + '#osaiSocial .os-list{display:grid;gap:4px}' 6262 + '#osaiSocial .os-list a{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;' 6263 + 'text-decoration:none;color:var(--txt,#1a2540);font-size:.88rem}' 6264 + '#osaiSocial .os-list a:hover{background:var(--surface2,rgba(0,0,0,.03))}' 6265 + '#osaiSocial .os-list img{width:24px;height:24px;border-radius:7px;background:var(--surface2,#eee);flex:none}' 6266 + '#osaiSocial .os-list b{flex:none;font-weight:600}' 6267 + '#osaiSocial .os-list .by{flex:1;min-width:0;color:var(--muted,#7a8fa8);font-size:.8rem;' 6268 + 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' 6269 + '#osaiSocial .os-list .k{flex:none;font-size:.6rem;font-weight:800;letter-spacing:.05em;text-transform:uppercase;' 6270 + 'padding:2px 7px;border-radius:20px;background:rgba(99,102,241,.13);color:var(--accent,#6366f1)}' 6271 + '#osaiSocial .os-list em{font-style:normal;color:var(--muted,#7a8fa8);flex:none}' 6272 + '#osaiSocial .os-faces{display:flex;align-items:center;gap:-6px;flex-wrap:wrap}' 6273 + '#osaiSocial .os-faces a{display:block;width:34px;height:34px;border-radius:11px;overflow:hidden;' 6274 + 'margin-right:-6px;border:2px solid var(--surface,#fff);transition:transform .15s,z-index 0s}' 6275 + '#osaiSocial .os-faces a:hover{transform:translateY(-2px) scale(1.08);position:relative;z-index:2}' 6276 + '#osaiSocial .os-faces img{width:100%;height:100%;display:block;background:var(--surface2,#eee)}' 6277 + '#osaiSocial .os-faces .ph{display:flex;width:100%;height:100%;align-items:center;justify-content:center;' 6278 + 'background:var(--accent,#6366f1);color:#fff;font-weight:800;font-size:.82rem}' 6279 + '#osaiSocial .os-more{margin-left:12px;font-size:.8rem;font-weight:700;color:var(--muted,#7a8fa8)}' 6280 + '@media(max-width:560px){#osaiSocial .os-sep{display:none}#osaiSocial .os-stats{gap:12px}}'; 6281 document.head.appendChild(s); 6282 } 6283 6284 var IC = { 6285 heart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21.2l7.8-7.8 1-1.1a5.5 5.5 0 0 0 0-7.7z"/></svg>', 6286 stack: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>', 6287 folder:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' 6288 }; 6289 6290 function render(j){ 6291 var total = (j.favorites || 0) + (j.stacks || 0) + (j.collections || 0); 6292 if (!total) return; /* rien à montrer : on n'affiche rien */ 6293 6294 var anchor = document.querySelector('.hero-main .cta-row') || document.querySelector('.cta-row') 6295 || document.querySelector('main h1, .hero h1, h1'); 6296 if (!anchor || document.getElementById('osaiSocial')) return; 6297 css(); 6298 6299 var box = document.createElement('div'); 6300 box.id = 'osaiSocial'; 6301 6302 var stats = document.createElement('div'); 6303 stats.className = 'os-stats'; 6304 var parts = []; 6305 if (j.favorites) parts.push([IC.heart, j.favorites, j.favorites === 1 ? T('member saved this') : T('members saved this')]); 6306 if (j.stacks) parts.push([IC.stack, j.stacks, j.stacks === 1 ? T('public stack') : T('public stacks')]); 6307 if (j.collections) parts.push([IC.folder, j.collections, j.collections === 1 ? T('collection') : T('collections')]);
6308 parts.forEach(function(p, i){ 6309 if (i) { var sp = document.createElement('span'); sp.className = 'os-sep'; stats.appendChild(sp); } 6310 var s = document.createElement('span'); 6311 s.className = 'os-stat'; 6312 s.innerHTML = p[0] + '<b>' + p[1] + '</b><span>' + p[2] + '</span>'; 6313 stats.appendChild(s); 6314 }); 6315 box.appendChild(stats); 6316 6317 /* Les membres qui exposent publiquement ce projet (favoris ou watchlist). 6318 C'est ainsi qu'on se découvre : par les projets qu'on partage, jamais par 6319 un annuaire de trombines. Aucune donnée privée ici â seuls apparaissent 6320 ceux qui ont explicitement rendu leurs favoris ou leur stack publics. */ 6321 if (j.members && j.members.length) { 6322 var mt = document.createElement('div'); 6323 mt.className = 'os-t'; 6324 mt.textContent = T('Members who use it'); 6325 box.appendChild(mt); 6326 var mrow = document.createElement('div'); 6327 mrow.className = 'os-faces'; 6328 j.members.forEach(function(m){ 6329 var a = document.createElement('a'); 6330 a.href = lp('/u/' + encodeURIComponent(m.handle)); 6331 a.title = (m.name || ('@' + m.handle)); 6332 if (m.gh) { 6333 a.innerHTML = '<img loading="lazy" alt="" src="https://github.com/' + encodeURIComponent(m.gh) 6334 + '.png?size=64" onerror="this.outerHTML=\'<span class="ph">' 6335 + String(m.name || m.handle).charAt(0).toUpperCase() + '</span>\'">'; 6336 } else { 6337 a.innerHTML = '<span class="ph">' + String(m.name || m.handle).charAt(0).toUpperCase() + '</span>'; 6338 } 6339 mrow.appendChild(a); 6340 }); 6341 if (j.members_more > 0) { 6342 var more = document.createElement('span'); 6343 more.className = 'os-more'; 6344 more.textContent = '+' + j.members_more; 6345 mrow.appendChild(more); 6346 } 6347 box.appendChild(mrow); 6348 } 6349 6350 if (j.items && j.items.length) { 6351 var t = document.createElement('div'); 6352 t.className = 'os-t'; 6353 t.textContent = T('Featured in'); 6354 box.appendChild(t); 6355 var list = document.createElement('div'); 6356 list.className = 'os-list'; 6357 j.items.forEach(function(it){ 6358 var who = (it.name && it.name !== '') ? it.name : '@' + it.handle; 6359 var a = document.createElement('a'); 6360 a.href = lp('/c/' + encodeURIComponent(it.slug)); 6361 var img = ''; 6362 if (it.gh) img = '<img loading="lazy" alt="" src="https://github.com/' + encodeURIComponent(it.gh) 6363 + '.png?size=48" onerror="this.style.visibility=\'hidden\'">'; 6364 var used = Number(it.copies) > 0 6365 ? ' · <b style="color:var(--accent,#6366f1)">' + it.copies + '</b> ' + T('use it') 6366 : ''; 6367 a.innerHTML = img + '<b>' + String(it.title).replace(/[<>&]/g, '') + '</b>' 6368 + (it.kind === 'stack' ? '<span class="k">' + T('Stack') + '</span>' : '') 6369 + '<span class="by">' + T('by') + ' ' + String(who).replace(/[<>&]/g, '') + ' · ' 6370 + it.n + ' ' + (Number(it.n) === 1 ? T('project') : T('projects')) + used + '</span><em>â</em>'; 6371 list.appendChild(a); 6372 }); 6373 box.appendChild(list); 6374 } 6375 6376 if (anchor.classList && anchor.classList.contains('cta-row')) anchor.insertAdjacentElement('afterend', box); 6377 else anchor.insertAdjacentElement('afterend', box); 6378 } 6379 6380 function boot(){ 6381 var repo = findRepo(); 6382 if (!repo) return; 6383 fetch('/api/project-stats.php?repo=' + encodeURIComponent(repo)) 6384 .then(function(r){ return r.ok ? r.json() : null; }) 6385 .then(function(j){ if (j) render(j); }) 6386 .catch(function(){}); 6387 } 6388 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); 6389})(); 6390 6391 6392/* âââ V106 â CÅUR + PARTAGE SUR LES CARTES DE VEILLE (.tcard) ââââââââââââââ 6393 /latest.html (et toute page bâtie sur .tcard) affichait des cartes sans 6394 aucune prise : impossible de garder un projet repéré, impossible de le 6395 partager. Or c'est LA page où l'on découvre â celle où l'envie d'agir naît. 6396 6397 Ãquipé DEPUIS LE NAV, comme les pages /alternatives/ : aucune régénération 6398 des 6 langues, et toute page qui utilisera .tcard demain en héritera. 6399 6400 Les cartes étant elles-mêmes des <a>, les actions sont posées en overlay et 6401 coupent la propagation ; l'intercepteur de liens ci-dessus les ignore. */ 6402(function(){ 6403 /* .tgrid est présent statiquement même quand les cartes arrivent en JS après fetch */ 6404 if (!document.querySelector('.tcard, .tgrid, #gridModels, #gridProjects')) return; 6405 6406 /* Ãtiquette du favori : dérivée de la grille, pas du titre (traduit en 6 langues) */ 6407 var TAGS = { 6408 gridModels: 'Model', 6409 gridProjects: 'Project', 6410 gridSpaces: 'Space', 6411 gridReleases: 'Release', 6412 gridMovers: 'Trending', 6413 gridEmerging: 'Emerging' 6414 }; 6415 6416 function txt(el){ return el ? String(el.textContent || '').trim() : ''; } 6417 6418 /* Clé du favori : on retire le préfixe de langue pour qu'une carte sauvegardée 6419 depuis /fr/latest.html soit LA MÃME que depuis /latest.html. */ 6420 function favKey(href){ 6421 if (!href) return null; 6422 if (href.charAt(0) === '/') return href.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 6423 return href; 6424 } 6425 6426 /* URL de partage : la fiche du site quand elle existe (favorites.js sait faire 6427 le lien dépôt â /project/<slug>.html), sinon la source. */ 6428 function shareUrl(href){ 6429 var u = href; 6430 if (typeof window.osaiFicheFor === 'function') u = window.osaiFicheFor(href) || href; 6431 if (u && u.charAt(0) === '/') u = location.origin + u; 6432 return u; 6433 } 6434 6435 function tagFor(card){ 6436 var g = card.closest('[id]'); 6437 while (g && !TAGS[g.id]) g = g.parentElement && g.parentElement.closest ? g.parentElement.closest('[id]') : null; 6438 return (g && TAGS[g.id]) || 'Saved'; 6439 } 6440 6441 function decorate(){ 6442 if (typeof window.favBtnHtml !== 'function') return false;
6443 var cards = document.querySelectorAll('a.tcard'); 6444 for (var i = 0; i < cards.length; i++){ 6445 var card = cards[i]; 6446 if (card.querySelector('.tc-actions')) continue; 6447 6448 var href = card.getAttribute('href') || ''; 6449 var name = txt(card.querySelector('.tcard-name')); 6450 if (!name || !href || href === '#') continue; 6451 6452 var desc = txt(card.querySelector('.tcard-desc')) 6453 || txt(card.querySelector('.tcard-by')); 6454 var key = favKey(href); 6455 var tag = tagFor(card); 6456 6457 var box = document.createElement('div'); 6458 /* Sur les cartes a image banniere (OpenGraph GitHub), l'avatar est en haut a 6459 droite : on descend l'overlay en bas a droite pour ne pas le masquer. */ 6460 box.className = card.querySelector('.tcard-img') ? 'tc-actions on-img' : 'tc-actions'; 6461 if (!card.querySelector('.tcard-img')) card.classList.add('has-acts'); 6462 box.innerHTML = window.favBtnHtml(name, desc, tag, key) 6463 + '<button class="osai-share tc-share" type="button" aria-label="Share" title="Share"' 6464 + ' data-url="' + shareUrl(href).replace(/"/g, '"') + '"' 6465 + ' data-text="' + name.replace(/"/g, '"') + '">' 6466 + (window.osaiShareIcon || '') + '</button>'; 6467 card.appendChild(box); 6468 } 6469 refreshShares(); 6470 syncFavState(); 6471 if (typeof window.favUpdateBadge === 'function') window.favUpdateBadge(); 6472 return true; 6473 } 6474 6475 /* Le cÅur allumé garde son étui visible (pas seulement au survol) */ 6476 function syncFavState(){ 6477 var as = document.querySelectorAll('a.tcard .tc-actions'); 6478 for (var i = 0; i < as.length; i++){ 6479 as[i].classList.toggle('has-fav', !!as[i].querySelector('.fav-btn.is-fav')); 6480 } 6481 } 6482 document.addEventListener('click', function(e){ 6483 if (e.target && e.target.closest && e.target.closest('.tc-actions .fav-btn')) setTimeout(syncFavState, 0); 6484 }); 6485 6486 /* project-slugs.json arrive en asynchrone : quand il atterrit, les cartes déjà 6487 équipées doivent repointer leur partage vers la fiche du site. */ 6488 function refreshShares(){ 6489 var bs = document.querySelectorAll('a.tcard .tc-share'); 6490 for (var i = 0; i < bs.length; i++){ 6491 var card = bs[i].closest('a.tcard'); 6492 if (card) bs[i].setAttribute('data-url', shareUrl(card.getAttribute('href') || '')); 6493 } 6494 } 6495 6496 function css(){ 6497 if (document.getElementById('tcActCSS')) return; 6498 var s = document.createElement('style'); 6499 s.id = 'tcActCSS'; 6500 s.textContent = 6501 'a.tcard{position:relative}' 6502 + '.tc-actions{position:absolute;top:8px;right:8px;z-index:2;display:flex;align-items:center;gap:2px;' 6503 + 'padding:2px;border-radius:9px;background:var(--surface,#fff);border:1px solid var(--border,rgba(0,0,0,.08));' 6504 /* [31/07] visibles en PERMANENCE (Romain) : au survol seulement, la 6505 moitie des visiteurs ignorait que le coeur et le partage existaient */ 6506 + 'opacity:1;transition:opacity .18s}' 6507 /* et la carte equipee reserve la place du bloc : il ne recouvre plus 6508 la fin des noms longs */ 6509 + 'a.tcard.has-acts .tcard-name{padding-right:66px}' 6510 /* un favori posé reste visible sans survol â sinon on ne voit plus ce qu'on a gardé. 6511 Règle séparée : groupée avec :has(), un vieux navigateur jetterait tout le bloc. */ 6512 + '.tc-actions.has-fav{opacity:1;transform:none}' 6513 + '@media(hover:none){.tc-actions{opacity:1;transform:none}}' 6514 + '.tc-actions .fav-btn{padding:5px}' 6515 + '.tc-actions .fav-btn svg{width:16px;height:16px}' 6516 + '.tc-share{padding:5px 6px}' 6517 + '.tc-share svg{width:15px;height:15px}' 6518 + '.tc-actions.on-img{top:auto;bottom:10px;right:10px}' 6519 + 'body.dark .tc-actions{background:var(--surface,#141a29);border-color:var(--border2,rgba(255,255,255,.18))}'; 6520 document.head.appendChild(s); 6521 } 6522 6523 function boot(){ 6524 css(); 6525 if (typeof window.osaiLoadFiches === 'function') window.osaiLoadFiches(function(){ decorate(); refreshShares(); }); 6526 var ok = decorate(); 6527 6528 /* Les grilles sont réécrites en JS (fetch â innerHTML) : on redécore après coup. */ 6529 if (window.MutationObserver){ 6530 var t = null; 6531 new MutationObserver(function(){ 6532 clearTimeout(t); 6533 t = setTimeout(decorate, 60); 6534 }).observe(document.body, {childList:true, subtree:true}); 6535 } 6536 return ok; 6537 } 6538 6539 /* favorites.js est injecté par le nav : on attend son API */ 6540 var n = 0; 6541 (function wait(){ 6542 if (boot() || ++n > 40) return; 6543 setTimeout(wait, 200); 6544 })(); 6545})(); 6546/* WebMCP : expose les actions du site aux agents qui pilotent un navigateur.
6547 Charge en differe et sans bloquer - la quasi-totalite des navigateurs 6548 n'implemente pas encore la norme, et le script sort alors sans rien faire. */ 6549(function(){ 6550 try{ 6551 var s = document.createElement('script'); 6552 s.src = '/webmcp.js'; 6553 s.defer = true; 6554 (document.body || document.head).appendChild(s); 6555 }catch(e){} 6556})(); 6557 6558/* âââ [24/07] Google One Tap sur l'ACCUEIL pour les visiteurs deconnectes âââââââ 6559 Le login Google existe deja (account.html + /api/google-auth.php + client_id). 6560 Ici on le PROPOSE directement : la fenetre One Tap apparait en haut a droite 6561 des l'arrivee sur l'accueil, uniquement si l'utilisateur n'est pas connecte. 6562 Reutilise le meme client_id et le meme endpoint -> aucune nouvelle infra. */ 6563(function(){ 6564 try{ 6565 var isHome = /^\/([a-z]{2}\/)?(index\.html)?$/.test(location.pathname); 6566 if (!isHome) return; 6567 fetch('/api/me.php', { credentials:'same-origin' }) 6568 .then(function(r){ return r.ok ? r.json() : null; }) 6569 .then(function(me){ 6570 if (me && me.email) return; /* deja connecte -> ne rien proposer */ 6571 var g = document.createElement('script'); 6572 g.src = 'https://accounts.google.com/gsi/client'; g.async = true; g.defer = true; 6573 g.onload = function(){ 6574 if (!(window.google && google.accounts && google.accounts.id)) return; 6575 google.accounts.id.initialize({ 6576 client_id: '221130680652-dcpjc9ajo0vgd8sbal4k768pbosa8km1.apps.googleusercontent.com', 6577 auto_select: false, 6578 cancel_on_tap_outside: true, 6579 callback: function(resp){ 6580 if (!resp || !resp.credential) return; 6581 fetch('/api/google-auth.php', { method:'POST', headers:{'Content-Type':'application/json'}, 6582 body: JSON.stringify({ credential: resp.credential }) }) 6583 .then(function(r){ return r.ok; }) 6584 .then(function(ok){ if (ok) location.reload(); }) 6585 .catch(function(){}); 6586 } 6587 }); 6588 google.accounts.id.prompt(); /* affiche One Tap en haut a droite */ 6589 }; 6590 document.head.appendChild(g); 6591 }) 6592 .catch(function(){}); 6593 }catch(e){} 6594})(); 6595 6596/* âââ [24/07] Commentaires Giscus sur les ARTICLES de blog âââââââââââââââââââ 6597 Login GitHub, aucun backend ni spam a moderer (modere sur GitHub). Injecte 6598 UNIQUEMENT sur /blog/<slug>/ (pas l'index, pas les alternatives, pas les 60k 6599 fiches -> pas d'effet ville-fantome). Theme synchronise clair/sombre, langue 6600 = celle de la page. Depot: opensourceaitech/comments. */ 6601(function(){ 6602 try{ 6603 var m = location.pathname.match(/^\/([a-z]{2}\/)?blog\/([^\/]+)\/$/); 6604 if (!m || m[2] === 'alternatives') return; 6605 if (document.getElementById('giscus-wrap')) return; 6606 var anchor = document.getElementById('site-footer') || document.querySelector('footer'); 6607 if (!anchor || !anchor.parentNode) return; 6608 var lang = (document.documentElement.getAttribute('lang') || 'en').slice(0,2).toLowerCase(); 6609 if (!/^(en|fr|de|es|it|pt)$/.test(lang)) lang = 'en'; 6610 var titles = { en:'Comments', fr:'Commentaires', de:'Kommentare', es:'Comentarios', it:'Commenti', pt:'Comentários' }; 6611 var wrap = document.createElement('section'); 6612 wrap.id = 'giscus-wrap'; 6613 wrap.style.cssText = 'max-width:820px;margin:2.5rem auto 1rem;padding:0 1rem'; 6614 wrap.innerHTML = '<h2 style="font-family:Poppins,sans-serif;font-size:1.25rem;font-weight:600;margin:0 0 1rem;color:var(--txt)">' + titles[lang] + '</h2>'; 6615 var g = document.createElement('script'); 6616 g.src = 'https://giscus.app/client.js'; 6617 var attrs = { 6618 'data-repo':'opensourceaitech/comments', 6619 'data-repo-id':'R_kgDOTiEmZQ', 6620 'data-category':'Announcements', 6621 'data-category-id':'DIC_kwDOTiEmZc4DB3CK', 6622 'data-mapping':'pathname', 6623 'data-strict':'0', 6624 'data-reactions-enabled':'1', 6625 'data-emit-metadata':'0', 6626 'data-input-position':'bottom', 6627 'data-theme': document.body.classList.contains('dark') ? 'dark' : 'light', 6628 'data-lang': lang, 6629 'crossorigin':'anonymous' 6630 }; 6631 for (var k in attrs) g.setAttribute(k, attrs[k]); 6632 g.async = true; 6633 wrap.appendChild(g); 6634 anchor.parentNode.insertBefore(wrap, anchor); 6635 /* Sync du theme quand l'utilisateur bascule le mode clair/sombre du site */ 6636 var mo = new MutationObserver(function(){ 6637 var f = document.querySelector('iframe.giscus-frame'); 6638 if (f && f.contentWindow) f.contentWindow.postMessage( 6639 { giscus: { setConfig: { theme: document.body.classList.contains('dark') ? 'dark' : 'light' } } }, 6640 'https://giscus.app'); 6641 }); 6642 mo.observe(document.body, { attributes:true, attributeFilter:['class'] }); 6643 }catch(e){} 6644})(); 6645 6646/* âââ [24/07] Like / Dislike sur les fiches projet ââââââââââââââââââââââââââ 6647 Signal de popularite anonyme via /api/vote.php (score = somme des votes). 6648 Injecte cote client dans la .cta-row -> aucune regeneration des 60k fiches. 6649 â² vert = like, â¼ rouge = dislike, score gris a 0 (vert si +, rouge si -). */ 6650(function(){ 6651 try{ 6652 if (!/^\/([a-z]{2}\/)?project\/[^\/]+\.html$/.test(location.pathname)) return; 6653 var repoEl = document.querySelector('[data-repo]'); 6654 var row = document.querySelector('.cta-row'); 6655 if (!repoEl || !row || document.getElementById('osai-vote')) return; 6656 var repo = (repoEl.getAttribute('data-repo') || '').toLowerCase(); 6657 if (!/^[^\/]+\/[^\/]+$/.test(repo)) return; 6658 6659 var st = document.createElement('style'); 6660 st.textContent = '.osai-votes{display:inline-flex;align-items:center;gap:5px}' 6661 + '.osai-vb{display:inline-flex;align-items:center;justify-content:center;width:32px;height:30px;' 6662 + 'border:1px solid var(--border2,rgba(0,0,0,.15));background:var(--surface2,#f5f5f7);border-radius:8px;' 6663 + 'cursor:pointer;font-size:.82rem;line-height:1;' 6664 + 'transition:transform .15s,background .15s,border-color .15s}' 6665 + '.osai-vb:hover{transform:scale(1.12)}' 6666 + '.osai-vb.up.on{background:rgba(22,163,74,.16);border-color:#16a34a}' 6667 + '.osai-vb.down.on{background:rgba(224,36,94,.16);border-color:#e0245e}' 6668 + '.osai-vb:disabled{opacity:.5;cursor:default}' 6669 + '.osai-score{min-width:24px;text-align:center;font-weight:700;font-size:.9rem;color:var(--muted,#8a99ad)}' 6670 + '.osai-score.pos{color:#16a34a}.osai-score.neg{color:#e0245e}'; 6671 document.head.appendChild(st); 6672 6673 var box = document.createElement('span'); 6674 box.id = 'osai-vote'; box.className = 'osai-votes'; 6675 box.innerHTML = '<button type="button" class="osai-vb up" aria-label="Like" title="Like">👍</button>' 6676 + '<span class="osai-score">·</span>' 6677 + '<button type="button" class="osai-vb down" aria-label="Dislike" title="Dislike">👎
6677</button>'; 6678 row.appendChild(box); 6679 var up = box.querySelector('.up'), down = box.querySelector('.down'), sc = box.querySelector('.osai-score'); 6680 6681 /* [29/07] Le « chiffre de depart » fabrique est SUPPRIME. Il affichait 6682 5 a 80 pouces leves sur des fiches ou PERSONNE n'avait vote (46 sur 6683 SeekStorm, vrai compteur en base : zero) â repere par Romain au premier 6684 regard. Regle du site : aucun chiffre invente. Score reel uniquement ; 6685 un point median tant que personne n'a vote. */ 6686 function render(score, dir){ 6687 sc.textContent = (score === 0) ? '\u00B7' : score; 6688 sc.className = 'osai-score' + (score > 0 ? ' pos' : score < 0 ? ' neg' : ''); 6689 up.classList.toggle('on', dir === 1); 6690 down.classList.toggle('on', dir === -1); 6691 } 6692 fetch('/api/vote.php?kind=project&ids=' + encodeURIComponent(repo)) 6693 .then(function(r){ return r.ok ? r.json() : null; }) 6694 .then(function(j){ var v = (j && j.votes && j.votes[repo]) || { score:0, dir:0 }; render(v.score||0, v.dir||0); }) 6695 .catch(function(){ render(0, 0); }); 6696 6697 function vote(dir){ 6698 up.disabled = down.disabled = true; 6699 fetch('/api/vote.php', { method:'POST', headers:{'Content-Type':'application/json'}, 6700 credentials:'same-origin', body: JSON.stringify({ kind:'project', id: repo, dir: dir }) }) 6701 .then(function(r){ 6702 if (r.status === 401){ location.href = '/account.html'; return null; } /* login requis pour voter */ 6703 return r.ok ? r.json() : null; 6704 }) 6705 .then(function(j){ if (j && typeof j.score === 'number') render(j.score, j.dir); }) 6706 .catch(function(){}) 6707 .then(function(){ up.disabled = down.disabled = false; }); 6708 } 6709 up.addEventListener('click', function(){ vote(1); }); 6710 down.addEventListener('click', function(){ vote(-1); }); 6711 }catch(e){} 6712})(); 6713 6714/* âââ [24/07] Commentaires Giscus sur les FICHES projet â CHARGEMENT AU CLIC âââ 6715 Disponible sur TOUTES les fiches, mais Giscus ne se charge qu'au clic sur le 6716 bouton -> pas de section vide (ville-fantome) ni de cout de perf par defaut. 6717 Chaque fiche a son propre fil (mapping pathname), cree seulement au 1er message. */ 6718(function(){ 6719 try{ 6720 if (!/^\/([a-z]{2}\/)?project\/[^\/]+\.html$/.test(location.pathname)) return; 6721 if (document.getElementById('giscus-wrap')) return; 6722 var anchor = document.getElementById('site-footer') || document.querySelector('footer'); 6723 if (!anchor || !anchor.parentNode) return; 6724 var lang = (document.documentElement.getAttribute('lang') || 'en').slice(0,2).toLowerCase(); 6725 if (!/^(en|fr|de|es|it|pt)$/.test(lang)) lang = 'en'; 6726 var L = { en:['Comments','ð¬ View / add a comment','Share your take and see what others say.'], 6727 fr:['Commentaires','ð¬ Voir / laisser un commentaire','Donnez votre avis et découvrez celui des autres.'], 6728 de:['Kommentare','ð¬ Kommentare ansehen / schreiben','Teile deine Meinung und sieh, was andere sagen.'], 6729 es:['Comentarios','ð¬ Ver / dejar un comentario','Comparte tu opinión y descubre la de los demás.'], 6730 it:['Commenti','ð¬ Vedi / lascia un commento','Condividi la tua opinione e scopri quella degli altri.'], 6731 pt:['Comentários','ð¬ Ver / deixar um comentário','Partilhe a sua opinião e veja a dos outros.'] }[lang]; 6732 var wrap = document.createElement('section'); 6733 wrap.id = 'giscus-wrap'; 6734 wrap.style.cssText = 'max-width:820px;margin:2.5rem auto 3rem;padding:0 1rem'; 6735 wrap.innerHTML = '<div style="text-align:center;border:1px solid var(--border2,rgba(0,0,0,.15));border-radius:16px;padding:26px 22px;background:var(--surface,#fff)">' 6736 + '<div style="font-size:1.7rem;line-height:1;margin-bottom:8px">ð¬</div>' 6737 + '<div style="font-family:Poppins,sans-serif;font-size:1.15rem;font-weight:600;color:var(--txt);margin-bottom:4px">' + L[0] + '</div>' 6738 + '<div style="color:var(--muted,#8a99ad);font-size:.9rem;margin-bottom:16px">' + L[2] + '</div>' 6739 + '<button type="button" class="osai-load-comments" style="font-family:inherit;font-size:.92rem;font-weight:600;color:#fff;background:var(--accent,#6366f1);border:none;border-radius:12px;padding:12px 26px;cursor:pointer">' + L[1] + '</button>' 6740 + '</div>'; 6741 anchor.parentNode.insertBefore(wrap, anchor); 6742 var btn = wrap.querySelector('.osai-load-comments'); 6743 btn.addEventListener('click', function(){ 6744 if (wrap.getAttribute('data-loaded')) return; 6745 wrap.setAttribute('data-loaded', '1'); 6746 wrap.innerHTML = '<h2 style="font-family:Poppins,sans-serif;font-size:1.25rem;font-weight:600;margin:0 0 1rem;color:var(--txt)">' + L[0] + '</h2>'; 6747 var g = document.createElement('script'); 6748 g.src = 'https://giscus.app/client.js'; 6749 var a = { 'data-repo':'opensourceaitech/comments','data-repo-id':'R_kgDOTiEmZQ', 6750 'data-category':'Announcements','data-category-id':'DIC_kwDOTiEmZc4DB3CK', 6751 'data-mapping':'pathname','data-strict':'0','data-reactions-enabled':'1', 6752 'data-emit-metadata':'0','data-input-position':'bottom', 6753 'data-theme': document.body.classList.contains('dark') ? 'dark' : 'light', 6754 'data-lang': lang, 'crossorigin':'anonymous' }; 6755 for (var k in a) g.setAttribute(k, a[k]); 6756 g.async = true; 6757 wrap.appendChild(g); 6758 var mo = new MutationObserver(function(){ 6759 var f = document.querySelector('iframe.giscus-frame'); 6760 if (f && f.contentWindow) f.contentWindow.postMessage( 6761 { giscus: { setConfig: { theme: document.body.classList.contains('dark') ? 'dark' : 'light' } } }, 6762 'https://giscus.app'); 6763 }); 6764 mo.observe(document.body, { attributes:true, attributeFilter:['class'] }); 6765 }); 6766 }catch(e){} 6767})(); 6768 6769 6770/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6771 FICHE RÃCLAMÃE â LIEN VERS LE PROFIL DU MAINTENEUR (Phase 0, p0-3) 6772 6773 Le flux de revendication existait de bout en bout â marqueur dans le README,
6774 vérification, enrichissement â mais rien n'était VISIBLE : claims-public.json 6775 restait vide (personne ne l'écrivait, corrigé côté api/) et aucune fiche ne 6776 montrait qui maintient le projet. Ce bloc ferme la boucle : si la fiche est 6777 revendiquée, une pastille « Maintenu par @pseudo » pointe vers /u/pseudo. 6778 C'est aussi la récompense qui donne envie de revendiquer. */ 6779(function(){ 6780 try{ 6781 if (!/^\/(?:[a-z]{2}\/)?project\/[^\/]+\.html$/.test(location.pathname)) return; 6782 var L = { en:'Maintained by', fr:'Maintenu par', de:'Betreut von', 6783 es:'Mantenido por', it:'Mantenuto da', pt:'Mantido por' }; 6784 var lab = L[(document.documentElement.lang || 'en').slice(0,2)] || L.en; 6785 6786 function repoOnPage(){ 6787 var links = document.querySelectorAll('a[href*="github.com/"]'); 6788 for (var j = 0; j < links.length; j++){ 6789 if (links[j].closest && links[j].closest('#site-nav,#site-footer,.osaiMM')) continue; 6790 var mm = (links[j].getAttribute('href') || '').match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[\/#?].*)?$/); 6791 if (mm && ['topics','sponsors','features','orgs','search','trending'].indexOf(mm[1].toLowerCase()) < 0) 6792 return (mm[1] + '/' + mm[2]).toLowerCase(); 6793 } 6794 return null; 6795 } 6796 6797 fetch('/claims-public.json?d=' + new Date().toISOString().slice(0,10)) 6798 .then(function(r){ return r.ok ? r.json() : null; }) 6799 .then(function(j){ 6800 if (!j || !j.claims) return; 6801 var tries = 0; 6802 (function tick(){ 6803 var repo = repoOnPage(); 6804 var row = document.querySelector('.cta-row'); 6805 if ((!repo || !row) && ++tries < 20) { setTimeout(tick, 300); return; } 6806 if (!repo || !row) return; 6807 var c = j.claims[repo]; 6808 if (!c || !c.handle || document.getElementById('osaiClaimChip')) return; 6809 var a = document.createElement('a'); 6810 a.id = 'osaiClaimChip'; 6811 a.href = '/u/' + encodeURIComponent(c.handle); 6812 /* liseres 2 px : charte graphique */ 6813 a.style.cssText = 'display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 14px;' 6814 + 'border-radius:12px;border:2px solid var(--accent,#6366f1);background:rgba(99,102,241,.08);' 6815 + 'color:var(--accent,#6366f1);font:inherit;font-size:.82rem;font-weight:600;text-decoration:none;order:4'; 6816 a.innerHTML = '<span style="font-size:.95rem">✓</span><span>' + lab + ' @' 6817 + String(c.handle).replace(/[<>&"]/g, '') + '</span>'; 6818 a.title = (c.name && c.name !== c.handle) ? c.name : ''; 6819 row.appendChild(a); 6820 if (window.osaiTrack) osaiTrack('claim_chip_shown', { repo: repo }); 6821 })(); 6822 }) 6823 .catch(function(){}); 6824 }catch(e){} 6825})(); 6826 6827 6828/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6829 NOTIFICATIONS NAVIGATEUR â le nudge au bon moment (Phase 3, p3-6) 6830 6831 Toute la pile existe depuis le 25/07 : abonnements (api/push.php), envoyeur 6832 VAPID « tickle » (api/osai-push.php), worker (osai-push-sw.js) et envoi reel 6833 dans send-alerts.php. Mais le bouton « Enable » vivait uniquement dans les 6834 reglages du compte, ou personne ne va. Le bon moment pour proposer les 6835 notifications, c'est la seconde ou le membre vient de suivre un projet : 6836 il vient LITTERALEMENT de demander a etre tenu au courant. 6837 6838 Regles : membres seulement, permission encore non demandee, pas deja abonne, 6839 une proposition par session, jamais d'auto-popup â le prompt natif du 6840 navigateur n'est déclenché que par le clic sur « Activer ». */ 6841(function(){ 6842 window.osaiPushNudge = function(anchor){ 6843 try{ 6844 if (!('serviceWorker' in navigator) || !('PushManager' in window) 6845 || !('Notification' in window) || !window.isSecureContext) return; 6846 if (Notification.permission !== 'default') return; 6847 try { if (sessionStorage.getItem('osai_pushnudge') === '1') return; } catch(e){}
6848 try { sessionStorage.setItem('osai_pushnudge', '1'); } catch(e){} 6849 6850 var L = { 6851 en:['Get these alerts in your browser too?','Enable','Not now'], 6852 fr:['Recevoir ces alertes aussi dans le navigateur ?','Activer','Plus tard'], 6853 de:['Diese Alerts auch im Browser erhalten?','Aktivieren','Spaeter'], 6854 es:['Recibir estas alertas tambien en el navegador?','Activar','Ahora no'], 6855 it:['Ricevere questi avvisi anche nel browser?','Attiva','Non ora'], 6856 pt:['Receber estes alertas tambem no navegador?','Ativar','Agora nao'] }; 6857 var t = L[(document.documentElement.lang || 'en').slice(0,2)] || L.en; 6858 6859 var card = document.createElement('div'); 6860 card.id = 'osaiPushNudge'; 6861 /* liseres 2 px : charte graphique */ 6862 card.style.cssText = 'position:absolute;z-index:2147483000;max-width:300px;padding:14px 16px;' 6863 + 'border:2px solid var(--accent,#6366f1);border-radius:14px;background:var(--surface,#fff);' 6864 + 'box-shadow:0 14px 40px rgba(0,0,0,.20);font-family:Poppins,system-ui,sans-serif;' 6865 + 'font-size:.85rem;line-height:1.45;color:var(--txt,#111)'; 6866 card.innerHTML = '<div style="margin-bottom:10px">' + t[0] + '</div>' 6867 + '<div style="display:flex;gap:8px">' 6868 + '<button type="button" data-on style="background:var(--accent,#6366f1);color:#fff;border:none;' 6869 + 'border-radius:10px;padding:8px 14px;font:inherit;font-weight:600;cursor:pointer">' + t[1] + '</button>' 6870 + '<button type="button" data-off style="background:none;border:2px solid var(--border2,rgba(0,0,0,.15));' 6871 + 'border-radius:10px;padding:8px 12px;font:inherit;color:var(--muted,#888);cursor:pointer">' + t[2] + '</button></div>'; 6872 document.body.appendChild(card); 6873 var r = anchor.getBoundingClientRect(); 6874 card.style.top = (r.bottom + window.scrollY + 10) + 'px'; 6875 card.style.left = Math.max(10, Math.min(r.left + window.scrollX, window.scrollX + window.innerWidth - 320)) + 'px'; 6876 6877 function kill(){ card.remove(); document.removeEventListener('keydown', esc); } 6878 function esc(e){ if (e.key === 'Escape') kill(); } 6879 document.addEventListener('keydown', esc); 6880 card.querySelector('[data-off]').addEventListener('click', kill); 6881 6882 function b64u(str){ 6883 str = String(str).replace(/-/g, '+').replace(/_/g, '/'); 6884 str += '=='.slice(0, (4 - str.length % 4) % 4); 6885 var raw = atob(str), arr = new Uint8Array(raw.length); 6886 for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i); 6887 return arr; 6888 } 6889 card.querySelector('[data-on]').addEventListener('click', function(){ 6890 /* meme flux que account.html : meme worker, memes endpoints */ 6891 navigator.serviceWorker.register('/osai-push-sw.js').then(function(reg){ 6892 return (reg.pushManager ? Promise.resolve(reg) : navigator.serviceWorker.ready); 6893 }).then(function(reg){ 6894 return fetch('/api/push.php').then(function(x){ return x.json(); }).then(function(j){ 6895 if (!j || !j.publicKey) throw new Error('no key'); 6896 return reg.pushManager.subscribe({ userVisibleOnly:true, applicationServerKey: b64u(j.publicKey) }); 6897 }); 6898 }).then(function(sub){ 6899 return fetch('/api/push.php', { method:'POST', headers:{'Content-Type':'application/json'}, 6900 body: JSON.stringify({ action:'subscribe', subscription: sub.toJSON ? sub.toJSON() : sub }) }); 6901 }).then(function(){ 6902 if (window.osaiTrack) osaiTrack('push_enabled', { from:'follow' }); 6903 kill(); 6904 }).catch(function(){ kill(); }); 6905 }); 6906 }catch(e){} 6907 }; 6908})(); 6909 6910/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6911 NUDGE D'INSCRIPTION (Phase 2, p2-4) â au moment de l'intention, jamais avant. 6912 6913 Avant : un visiteur non connecte qui cliquait « Follow » ou « Collection » 6914 etait REDIRIGE d'un coup vers /account.html. Il perdait sa fiche, sans un mot 6915 d'explication â le pire des nudges, au seul moment ou le visiteur venait de 6916 manifester un interet. 6917 6918 Maintenant : une petite carte s'ouvre a cote du bouton. Elle dit ce que le 6919 compte apporte, propose de le creer, et se referme d'un clic ailleurs ou d'un 6920 Echap. Pas d'interstitiel, pas d'apparition spontanee : elle ne repond qu'a un 6921 geste du visiteur â c'est la contrainte posee au plan (penalite Google mobile 6922 sur les interstitiels, et simple politesse). 6923 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 6924(function(){ 6925 var M = { 6926 follow: { 6927 en:'Follow projects and get alerts when they move â with a free account.', 6928 fr:'Suivez des projets et recevez des alertes quand ils bougent â avec un compte gratuit.', 6929 de:'Folge Projekten und erhalte Alerts, wenn sich etwas tut â mit einem kostenlosen Konto.', 6930 es:'Sigue proyectos y recibe alertas cuando se muevan â con una cuenta gratuita.', 6931 it:'Segui i progetti e ricevi avvisi quando si muovono â con un account gratuito.', 6932 pt:'Siga projetos e receba alertas quando algo mudar â com uma conta gratuita.' 6933 }, 6934 collection: { 6935 en:'Save projects into collections â with a free account.', 6936 fr:'Rangez des projets en collections â avec un compte gratuit.', 6937 de:'Speichere Projekte in Sammlungen â mit einem kostenlosen Konto.',
6938 es:'Guarda proyectos en colecciones â con una cuenta gratuita.', 6939 it:'Salva i progetti in raccolte â con un account gratuito.', 6940 pt:'Guarde projetos em colecoes â com uma conta gratuita.' 6941 }, 6942 cta: { en:'Create a free account', fr:'Creer un compte gratuit', de:'Kostenloses Konto erstellen', 6943 es:'Crear una cuenta gratuita', it:'Crea un account gratuito', pt:'Criar uma conta gratuita' } 6944 }; 6945 function lang(){ var l=(document.documentElement.lang||'en').slice(0,2); return M.cta[l]?l:'en'; } 6946 function acct(){ var lg=(location.pathname.match(/^\/(fr|it|es|de|pt)\//)||[,''])[1]; return (lg?'/'+lg:'')+'/account.html'; } 6947 6948 window.osaiSignupNudge = function(anchor, kind){ 6949 try{ 6950 var old = document.getElementById('osaiNudge'); 6951 if (old) old.remove(); 6952 var l = lang(); 6953 var card = document.createElement('div'); 6954 card.id = 'osaiNudge'; 6955 card.setAttribute('role','dialog'); 6956 /* liseres 2 px : charte graphique */ 6957 card.style.cssText = 'position:absolute;z-index:2147483000;max-width:290px;padding:14px 16px;' 6958 + 'border:2px solid var(--accent,#6366f1);border-radius:14px;background:var(--surface,#fff);' 6959 + 'box-shadow:0 14px 40px rgba(0,0,0,.20);font-family:Poppins,system-ui,sans-serif;' 6960 + 'font-size:.85rem;line-height:1.45;color:var(--txt,#111)'; 6961 card.innerHTML = '<div style="display:flex;gap:8px;align-items:flex-start">' 6962 + '<span style="flex:1">' + (M[kind]||M.follow)[l] + '</span>' 6963 + '<button type="button" aria-label="close" style="flex:none;background:none;border:none;font-size:1.1rem;' 6964 + 'line-height:1;color:var(--muted,#888);cursor:pointer;padding:0 2px">×</button></div>' 6965 + '<a href="' + acct() + '" style="display:inline-block;margin-top:10px;background:var(--accent,#6366f1);' 6966 + 'color:#fff;text-decoration:none;font-weight:600;border-radius:10px;padding:8px 14px;font-size:.83rem">' 6967 + M.cta[l] + '</a>'; 6968 document.body.appendChild(card); 6969 var r = anchor.getBoundingClientRect(); 6970 var top = r.bottom + window.scrollY + 10; 6971 var left = Math.max(10, Math.min(r.left + window.scrollX, window.scrollX + window.innerWidth - 310)); 6972 card.style.top = top + 'px'; card.style.left = left + 'px'; 6973 function kill(){ card.remove(); 6974 document.removeEventListener('pointerdown', out, true); 6975 document.removeEventListener('keydown', esc); } 6976 function out(e){ if (!card.contains(e.target) && e.target !== anchor) kill(); } 6977 function esc(e){ if (e.key === 'Escape') kill(); } 6978 card.querySelector('button').addEventListener('click', kill); 6979 document.addEventListener('pointerdown', out, true); 6980 document.addEventListener('keydown', esc); 6981 if (window.osaiTrack) osaiTrack('signup_nudge', { kind: kind }); 6982 }catch(e){ location.href = acct(); } /* au pire : l'ancien comportement */ 6983 }; 6984})(); 6985 6986/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6987 COMPTEUR DE FICHES â CONSTRUIT, MAIS DÃSACTIVà 6988 6989 Demande de Romain : « préparer une solution technique, sans l'activer dans 6990 l'immédiat pour ne pas faire fuir les visiteurs, permettant de limiter la 6991 consultation à 10 fiches pour les utilisateurs sans compte ». 6992 6993 Trois principes tenus, parce qu'ils décident si ça rapporte ou si ça coûte : 6994 6995 1. ÃA NE BLOQUE JAMAIS. Le contenu reste entier et lisible. Un interstitiel 6996 qui masque la page est pénalisé par Google sur mobile, et il fait fuir 6997 justement le visiteur qu'on voulait convertir. On affiche un bandeau 6998 discret en bas, refermable â une invitation, pas un péage. 6999 2. RIEN N'EST CACHà à L'INDEXATION. Aucun texte, aucun lien n'est retiré du 7000 DOM : ce que voit Google reste ce que voit le visiteur. 7001 3. ÃA NE HARCÃLE PAS. Une fois refermé, on ne le remontre plus (mémorisé) ; 7002 jamais pour un membre ; au plus une fois par session. 7003 7004 POUR ACTIVER : passer ON à true ci-dessous. LIMIT règle le seuil. 7005 Le compteur, lui, tourne déjà : quand tu activeras, tu sauras combien de 7006 visiteurs atteignent réellement le seuil au lieu de le deviner. 7007 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 7008(function(){ 7009 try{ 7010 var ON = false; /* â passer à true pour activer le bandeau */ 7011 var LIMIT = 10; 7012 var KEY = 'osai_seen_fiches', DISMISS = 'osai_softwall_off'; 7013 7014 /* on ne compte que les fiches projet, en anglais comme en traduction */ 7015 var m = location.pathname.match(/^\/(?:[a-z]{2}\/)?project\/([^\/]+)\.html$/); 7016 if (!m) return; 7017 if (/(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie)) return; /* membre : jamais */ 7018 7019 var seen = []; 7020 try { seen = JSON.parse(localStorage.getItem(KEY) || '[]') || []; } catch(e){ seen = []; } 7021 if (!Array.isArray(seen)) seen = []; 7022 if (seen.indexOf(m[1]) < 0) { 7023 seen.push(m[1]); 7024 if (seen.length > 200) seen = seen.slice(-200); /* borne : c'est un compteur, pas un historique */
7025 try { localStorage.setItem(KEY, JSON.stringify(seen)); } catch(e){} 7026 } 7027 window.osaiFichesSeen = seen.length; /* lisible en console pour mesurer */ 7028 7029 if (!ON || seen.length < LIMIT) return; 7030 try { if (localStorage.getItem(DISMISS) === '1') return; } catch(e){} 7031 try { if (sessionStorage.getItem(DISMISS + '_s') === '1') return; } catch(e){} 7032 7033 function tt(en, fr){ return (document.documentElement.lang || 'en').slice(0,2) === 'fr' ? fr : en; } 7034 var bar = document.createElement('div'); 7035 bar.setAttribute('role', 'complementary'); 7036 bar.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:2147483000;' 7037 + 'display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center;' 7038 + 'max-width:min(680px,94vw);padding:13px 18px;border-radius:14px;' 7039 + 'border:2px solid var(--accent,#6366f1);background:var(--surface,#fff);' 7040 + 'box-shadow:0 14px 40px rgba(0,0,0,.22);font-family:Poppins,system-ui,sans-serif;' 7041 + 'font-size:.88rem;color:var(--txt,#111)'; 7042 bar.innerHTML = '<span>' + tt('You have explored ' + seen.length + ' projects. Save them, follow their health and get alerts â free.', 7043 'Vous avez exploré ' + seen.length + ' projets. Enregistrez-les, suivez leur santé et recevez des alertes â gratuit.') + '</span>' 7044 + '<a href="/account.html" style="flex:none;background:var(--accent,#6366f1);color:#fff;text-decoration:none;' 7045 + 'font-weight:600;border-radius:10px;padding:9px 16px">' + tt('Create a free account', 'Créer un compte gratuit') + '</a>' 7046 + '<button type="button" aria-label="' + tt('Close','Fermer') + '" style="flex:none;background:none;border:none;' 7047 + 'font-size:1.2rem;line-height:1;color:var(--muted,#888);cursor:pointer;padding:4px 6px">×</button>'; 7048 bar.querySelector('button').addEventListener('click', function(){ 7049 bar.remove(); 7050 try { localStorage.setItem(DISMISS, '1'); } catch(e){} 7051 try { sessionStorage.setItem(DISMISS + '_s', '1'); } catch(e){} 7052 if (window.osaiTrack) osaiTrack('softwall_dismiss', { seen: seen.length }); 7053 }); 7054 (document.body || document.documentElement).appendChild(bar); 7055 try { sessionStorage.setItem(DISMISS + '_s', '1'); } catch(e){} 7056 if (window.osaiTrack) osaiTrack('softwall_shown', { seen: seen.length }); 7057 }catch(e){} 7058})(); 7059 7060/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7061 [16/08/2026] NEWSLETTER SUR LES FICHES â piste n°1 de la feuille de route 7062 7063 Le formulaire d'inscription existait UNIQUEMENT dans le pied de page, 7064 3e colonne, tout en bas de pages de plusieurs milliers de pixels â 7065 invisible par construction, meme lecon que le lien « Claim » du footer. 7066 Ce bloc remonte l'inscription sur les fiches, la ou les visiteurs 7067 atterrissent, sans regenerer une seule page : c'est la nav qui l'injecte. 7068 7069 Regles tenues : 7070 . jamais pour un membre connecte (cookie osai_m=1) : il a deja le 7071 reglage dans son compte ; 7072 . jamais apres une inscription reussie (localStorage osai_nl=1) ; 7073 . les TROIS chaines affichees existent deja dans les dictionnaires plus 7074 haut (« Stay in the loop », le pitch, « Subscribe ») : les six langues 7075 sont couvertes sans une traduction de plus ; 7076 . le POST recopie le contrat de wireSub() (api/subscribe.php, double 7077 opt-in). Recopie et non reutilise : initNewsletter() ne cable que les 7078 formulaires presents au moment ou il passe, et ce bloc peut naitre 7079 apres lui â un formulaire jamais cable serait un bouton mort. 7080 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 7081(function(){ 7082 try{ 7083 var m = location.pathname.match(/^\/(?:[a-z]{2}\/)?(project|model|tool)\/[^\/]+\.html$/); 7084 if (!m) return; 7085 if (/(?:^|;\s*)osai_m=1(?:;|$)/.test(document.cookie)) return; /* membre : jamais */ 7086 try { if (localStorage.getItem('osai_nl') === '1') return; } catch(e){} 7087 var t = function(s){ return (typeof window.osaiT === 'function') ? window.osaiT(s) : s; }; 7088 7089 function build(){ 7090 if (document.getElementById('oNlBand')) return; 7091 var band = document.createElement('div'); 7092 band.id = 'oNlBand'; 7093 band.style.cssText = 'margin:26px auto;max-width:760px;padding:20px 22px;border-radius:14px;' 7094 + 'border:1px solid var(--border2,rgba(99,102,241,.25));background:var(--surface2,rgba(99,102,241,.05));' 7095 + 'font-family:inherit;box-sizing:border-box'; 7096 band.innerHTML = 7097 '<div style="font-weight:700;font-size:1rem;margin:0 0 4px;color:var(--txt,#111)">' + t('Stay in the loop') + '</div>' 7098 + '<p style="margin:0 0 12px;font-size:.88rem;line-height:1.55;color:var(--muted,#5a6d87)">' 7099 + t('The best of open-source AI in your inbox â new models, trending projects and releases. No spam, unsubscribe anytime.') + '</p>' 7100 + '<form class="osai-sub" data-src="fiche" style="display:flex;gap:8px;flex-wrap:wrap;margin:0">' 7101 + '<input type="email" required placeholder="[email protected]" aria-label="Email address" ' 7102 + 'style="flex:1 1 200px;min-width:0;height:42px;padding:0 13px;border-radius:10px;' 7103 + 'border:1px solid var(--border2,rgba(0,0,0,.18));background:var(--surface,#fff);color:var(--txt,#111);font:inherit;font-size:.9rem">' 7104 + '<button type="submit" style="flex:none;height:42px;padding:0 18px;border:none;border-radius:10px;' 7105 + 'background:var(--accent,#6366f1);color:#fff;font:inherit;font-size:.9rem;font-weight:600;cursor:pointer">' 7106 + t('Subscribe') + '</button>' 7107 + '</form>' 7108 + '<div class="sub-msg" style="margin:8px 0 0;font-size:.85rem;min-height:1em"></div>'; 7109 7110 /* ANCRE â verifie dans un vrai navigateur le 16/08 : PAS une <section>. 7111 Les sections des fiches vivent dans .fwrap, qui porte l'animation 7112 d'apparition au defilement : tout element etranger qu'on y insere 7113 reste ecrase (46 px mesures) car la revelation ne cible que les .sec. 7114 On accroche donc le bloc en FIN DE HEROS (.hero-main, flux normal, 7115 ~20 % de profondeur, visible sans defiler beaucoup) ; a defaut, 7116 juste avant le pied de page â moins bien, mais jamais casse. */ 7117 var hero = document.querySelector('.hero-main'); 7118 var foot = document.getElementById('site-footer'); 7119 if (hero) { band.style.margin = '18px 0 0'; hero.appendChild(band); } 7120 else if (foot && foot.parentNode) foot.parentNode.insertBefore(band, foot); 7121 else return; 7122 7123 var form = band.querySelector('form'); 7124 form.__wired = true; /* si initNewsletter() passe apres nous, il verra 7125 le drapeau et n'attachera pas un 2e gestionnaire 7126 â sans lui, chaque clic posterait DEUX fois */ 7127 form.addEventListener('submit', function(e){ 7128 e.preventDefault(); 7129 var email = form.querySelector('input[type=email]'); 7130 var msg = band.querySelector('.sub-msg'); 7131 var btn = form.querySelector('button'); 7132 if (!email || !email.value) return; 7133 btn.disabled = true; var orig = btn.textContent; btn.textContent = 'Subscribingâ¦'; 7134 fetch('/api/subscribe.php', { method:'POST', headers:{'Content-Type':'application/json'}, 7135 body: JSON.stringify({ email: email.value, src: 'fiche', freq: 'weekly' }) }) 7136 .then(function(r){ return r.json(); }) 7137 .then(function(j){ 7138 if (j && j.ok){ 7139 msg.style.color = 'var(--accent,#6366f1)'; 7140 msg.textContent = j.already ? "You're already on the list." : t('Check your inbox â one click to confirm.'); 7141 try { localStorage.setItem('osai_nl', '1'); } catch(e){} 7142 if (window.osaiTrack) osaiTrack('nl_subscribe', { src: 'fiche' }); 7143 form.reset(); 7144 } else { 7145 msg.style.color = '#e0245e'; 7146 msg.textContent = (j && j.error) ? j.error : "Hmm, that didn't work â please try again."; 7147 } 7148 }) 7149 .catch(function(){ msg.style.color = '#e0245e'; msg.textContent = 'Network error â please try again.'; }) 7150 .then(function(){ btn.disabled = false; btn.textContent = orig; }); 7151 }); 7152 if (window.osaiTrack) osaiTrack('nl_seen', { page: m[1] }); 7153 } 7154 7155 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', build); 7156 else build(); 7157 }catch(e){} 7158})(); 7159 7160/* ââ [04/08] compteur de visites maison (api/pv.php) : une date, un chemin,
7161 un nombre â RIEN sur le visiteur (pas d'IP, pas de cookie). Ne doit 7162 jamais bloquer la page : tout echec est avale. ââ */ 7163(function(){ 7164 try{ 7165 if (/localhost|127\.0\.0\.1/.test(location.hostname)) return; 7166 var chemin = location.pathname || '/'; 7167 var drapeaux = ''; 7168 try { 7169 /* [04/08 - corrige apres « 1 vue et 0 visiteur ? » de Romain] 7170 DEUX choses differentes, que la v1 confondait : 7171 s=1 : premiere page de la session -> une VISITE du site 7172 u=1 : premiere fois que CETTE page est vue dans la session 7173 -> un VISITEUR de plus pour cette page. 7174 Sans u, une page atteinte en 2e clic recoltait une vue et zero 7175 visiteur. Le navigateur se souvient, pas nous : rien ne sort d'ici. */ 7176 if (!sessionStorage.getItem('osai_v')) { sessionStorage.setItem('osai_v', '1'); drapeaux += '&s=1'; } 7177 var cle = 'osai_p:' + chemin; 7178 if (!sessionStorage.getItem(cle)) { sessionStorage.setItem(cle, '1'); drapeaux += '&u=1'; } 7179 } catch(e){} 7180 var u = '/api/pv.php?p=' + encodeURIComponent(chemin) + drapeaux; 7181 if (navigator.sendBeacon) { navigator.sendBeacon(u); } 7182 else { var img = new Image(); img.src = u; } 7183 }catch(e){} 7184})(); 7185 7186/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7187 TELECHARGEMENTS SUR LES FICHES PROJET [09/08/2026] 7188 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7189 Une etoile ne redescend jamais : un projet arrete depuis deux ans garde 7190 chacune de celles qu'il a gagnees. Les telechargements, eux, baissent quand 7191 les gens s'en vont. C'est la seule mesure d'usage REEL du catalogue, et on 7192 la repete dans nos articles depuis ce matin sans l'afficher nulle part. 7193 7194 Pourquoi ici et pas dans le generateur : baker le chiffre dans le HTML 7195 demanderait de regenerer 10 000 fiches x 6 langues, un marathon de plusieurs 7196 heures avec les traducteurs. L'injection cote navigateur donne le meme 7197 resultat visible aujourd'hui, sur toutes les langues d'un coup. Le chiffre 7198 passera dans le HTML a la prochaine regeneration naturelle. 7199 7200 L'index fait 40 Ko et n'est demande QUE sur une fiche projet, une fois par 7201 session (le navigateur le garde ensuite). 1 413 projets sur 6 402 en ont un : 7202 l'absence est la norme, et une fiche sans chiffre ne montre rien plutot 7203 qu'un zero qui laisserait croire a l'abandon. 7204 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 7205(function(){ 7206 try{ 7207 var chemin = location.pathname.replace(/^\/(fr|it|es|de|pt)(?=\/)/, ''); 7208 var m = chemin.match(/^\/project\/([a-z0-9-]+)\.html$/); 7209 if (!m) return; 7210 var slug = m[1]; 7211 var grille = document.querySelector('.facts'); 7212 if (!grille) return; 7213 7214 var abrege = function(n){ 7215 if (n >= 1e9) return (Math.round(n/1e8)/10).toString().replace(/\.0$/,'') + 'B'; 7216 if (n >= 1e6) return (Math.round(n/1e5)/10).toString().replace(/\.0$/,'') + 'M'; 7217 if (n >= 1e3) return (Math.round(n/100)/10).toString().replace(/\.0$/,'') + 'k'; 7218 return String(n); 7219 }; 7220 7221 fetch('/downloads-index.json', {cache:'force-cache'}) 7222 .then(function(r){ return r.ok ? r.json() : null; }) 7223 .then(function(j){ 7224 var n = j && j.t && j.t[slug]; 7225 if (!n) return; /* pas de paquet : on n'affiche rien */ 7226 var d = document.createElement('div'); 7227 d.className = 'fact'; 7228 d.innerHTML = 7229 '<svg class="fk-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"' 7230 + ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' 7231 + '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/>' 7232 + '<line x1="12" y1="15" x2="12" y2="3"/></svg>' 7233 + '<div class="fb"><div class="v">' + abrege(n) + '</div>' 7234 + '<div class="k">Downloads / month</div></div>';
7235 d.title = n.toLocaleString('en-US') + ' downloads in the last month (npm or PyPI)'; 7236 grille.appendChild(d); 7237 7238 /* ââ Le badge propose au mainteneur âââââââââââââââââââââââââââââââ 7239 La fiche ne proposait que le badge etoiles. Or GitHub affiche deja 7240 le compteur d'etoiles juste a cote : le repeter n'apporte rien, et 7241 c'est probablement pourquoi deux badges seulement ont ete poses en 7242 un mois. Le nombre de telechargements, lui, n'est visible nulle 7243 part ailleurs sur sa page. 7244 Quand le projet a un paquet, on propose donc CELUI-LA par defaut, 7245 avec un lien pour revenir aux etoiles : le choix reste au 7246 mainteneur, mais le meilleur des deux est devant. */ 7247 try{ 7248 var apercu = document.querySelector('.badge-preview'); 7249 var code = document.getElementById('osaiBadgeSnip'); 7250 var texte = document.querySelector('.badge-p'); 7251 if (!apercu || !code) return; 7252 7253 var urlEt = '/badge.php?tool=' + slug; 7254 var urlDl = urlEt + '&dl=1'; 7255 var mdEt = code.textContent; 7256 var mdDl = mdEt.replace('/badge.php?tool=' + slug, '/badge.php?tool=' + slug + '&dl=1'); 7257 var txtEt = texte ? texte.textContent : ''; 7258 var txtDl = 'Add this live badge to your README \u2014 how many times your package was ' 7259 + 'downloaded last month, refreshed every week. GitHub already shows your stars; ' 7260 + 'this is the number nobody else displays.'; 7261 7262 var bascule = document.createElement('a'); 7263 bascule.href = '#'; 7264 bascule.style.cssText = 'display:inline-block;font-size:.82rem;' 7265 + 'color:var(--muted,#667);text-decoration:underline;cursor:pointer'; 7266 var mode = 'dl'; 7267 var poser = function(){ 7268 var dl = (mode === 'dl'); 7269 apercu.src = dl ? urlDl : urlEt; 7270 code.textContent = dl ? mdDl : mdEt; 7271 if (texte) texte.textContent = dl ? txtDl : txtEt; 7272 bascule.textContent = dl ? 'Prefer the stars badge?' : 'Show the downloads badge instead'; 7273 }; 7274 bascule.addEventListener('click', function(e){ 7275 e.preventDefault(); mode = (mode === 'dl') ? 'et' : 'dl'; poser(); 7276 }); 7277 poser(); 7278 /* Pas dans la rangee de l'extrait : celle-ci est un flex en 7279 align-items:stretch, le lien s'y etirait sur toute la hauteur du 7280 bouton Copy et tombait de travers. Les deux actions secondaires se 7281 tiennent mieux cote a cote, sous l'extrait. 7282 Une rangee souple plutot qu'un point de separation : a 375 px les 7283 deux liens passent a la ligne, et un point tout seul restait 7284 orphelin au bout de la premiere. Le gap fait le meme travail sans 7285 rien laisser trainer. */ 7286 var plus = document.querySelector('.badge-more'); 7287 if (plus && plus.parentNode) { 7288 var rangee = document.createElement('div'); 7289 rangee.style.cssText = 'display:flex;flex-wrap:wrap;align-items:baseline;' 7290 + 'gap:4px 18px;margin-top:.6rem'; 7291 plus.parentNode.insertBefore(rangee, plus); 7292 plus.style.marginTop = '0'; 7293 rangee.appendChild(plus); 7294 rangee.appendChild(bascule); 7295 } else { 7296 bascule.style.marginTop = '.5rem'; 7297 (code.parentNode || apercu.parentNode).appendChild(bascule); 7298 } 7299 }catch(e){} 7300 }) 7301 .catch(function(){}); 7302 }catch(e){} 7303})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.