1<!DOCTYPE html> 2<html lang="it"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> 6<title>ASSISTENTE APISTICO TERRITORIALE â NECTAR RADAR</title> 7<link rel="icon" href="/favicon.png"> 8<link rel="stylesheet" href="api-style.css"> 9<meta property="og:image" content="https://chatbot-478611.web.app/preview.jpg"> 10<link rel="stylesheet" href="style.css">
vendor: 69 bytes, lines 10-11
10 11<script src="https://www.google.com/recaptcha/enterprise.js?render=
116LeaX5osAAAAACMPGp3EDqfipJ5lFw7MB0JAMEBx
vendor: 13 bytes, line 11
11"></script>
12<script src="tailwindcss.js"></script>
vendor: 2 bytes, line 12
12
13<script src="auth-logic.js"></script>
vendor: 2 bytes, line 13
13
14<script src="react.production.min.js"></script>
vendor: 2 bytes, line 14
14
15<script src="react-dom.production.min.js"></script>
vendor: 2 bytes, line 15
15
16<script src="babel.min.js"></script>
vendor: 2 bytes, line 16
16
17<script src="marked.min.js"></script>
17 18<link rel="stylesheet" href="leaflet.css" />
19<script src="leaflet.js"></script>
vendor: 2 bytes, line 19
19
20<script src="hive-logic.js"></script>
20 21<link rel="stylesheet" href="Control.Geocoder.css" />
22<script src="Control.Geocoder.js"></script>
22 23 24<link rel="stylesheet" type="text/css" href="cookieconsent.css" />
25<script src="cookieconsent.js" data-cfasync="false"></script>
vendor: 2 bytes, line 25
25
26<script> 27window.addEventListener("load", function(){ 28 window.cookieconsent.initialise({ 29 "palette": { 30 "popup": { "background": "#020617", "text": "#ffffff" }, 31 "button": { "background": "#facc15", "text": "#020617" } 32 }, 33 "theme": "classic", 34 "position": "bottom-right", 35 "content": { 36 "message": "Utilizziamo i cookie per migliorare l'esperienza su Nectar Radar.", 37 "dismiss": "Accetto", 38 "link": "Privacy Policy", 39 "href": "privacy.html" 40 } 41 })}); 42</script>
42 43 44<style> 45 body, html, #root { height: 100%; margin: 0; overflow: hidden; font-family: sans-serif; background: #000; } 46 #map { position: absolute; top: 0; bottom: 0; width: 100%; z-index: 1; } 47 48 .chat-overlay { 49 position: absolute; 50 bottom: 0; 51 left: 0; 52 right: 0; 53 display: flex; 54 flex-direction: column; 55 transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 56 } 57 58 .is-open { height: 50vh; transform: translateY(0); z-index: 100; } 59 .is-closed { height: 45vh; transform: translateY(calc(45vh - 160px)); z-index: 100; } 60 .is-max { height: 90vh; transform: translateY(0); z-index: 120; } 61 62 .glass { background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(15px); border-top: 1px solid rgba(0,0,0,0.1); border-radius: 28px 28px 0 0; display: flex; flex-direction: column; box-shadow: 0 -10px 30px rgba(0,0,0,0.3); height: 100%; } 63 64 .bot-box { width: 98%; background: #facc15; color: black; font-weight: 600; border-radius: 14px; padding: 16px; margin: 4px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.1); line-height: 1.5; } 65 .user-bubble { background: #f1f5f9; color: #1e293b; padding: 10px 16px; border-radius: 18px; max-width: 80%; font-size: 0.875rem; align-self: flex-end; margin-right: 4px; border: 1px solid #e2e8f0; } 66 67 .leaflet-top { top: 150px !important; right: 20px !important; } 68 .leaflet-bar a { width: 44px !important; height: 44px !important; line-height: 44px !important; font-size: 22px !important; } 69 .leaflet-control-geocoder { min-width: 44px !important; min-height: 44px !important; } 70 71 .map-tools { position: absolute; top: 150px; left: 20px; z-index: 105; display: flex; flex-direction: column; gap: 12px; } 72 .gps-container { display: flex; align-items: center; gap: 10px; } 73 .tool-btn { background: white; width: 46px; height: 46px; border-radius: 12px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: pointer; font-size: 22px; border: none; transition: transform 0.2s; } 74 .live-tag { background: rgba(0, 0, 0, 0.75); color: #22c55e; font-size: 10px; font-weight: 900; padding: 5px 10px; border-radius: 30px; border: 1px solid #22c55e; text-transform: uppercase; letter-spacing: 1px; animation: pulse-live 2s infinite; pointer-events: none; } 75 76 @keyframes pulse-live { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } 77 78 @keyframes float { 79 0%, 100% { transform: translateY(0px); } 80 50% { transform: translateY(-15px); } 81 } 82 .animate-float { animation: float 4s ease-in-out infinite; } 83 84 .helper-bubble { position: absolute; top: 145px; left: 20px; z-index: 105; background: #facc15; color: black; padding: 12px; border-radius: 14px; font-weight: bold; font-size: 12px; border: 2px solid black; max-width: 210px; line-height: 1.4; box-shadow: 0 6px 20px rgba(0,0,0,0.4); } 85 86 .chat-toggle-handle { 87 width: 95%; 88 background: #f1f5f9; 89 padding: 12px; 90 display: flex; 91 flex-direction: column; 92 align-items: center; 93 border-radius: 16px; 94 cursor: pointer; 95 border: 2px solid #e2e8f0; 96 97 /* aggiunte per posizionamento */ 98 position: sticky; /* o absolute se vuoi fissarlo sulla chat */ 99 top: 60px; /* più alto = più basso sullo schermo */ 100 z-index: 150; 101 } /* <-- chiusura corretta */ 102 103/* --- NUOVO: STILE BOTTONE ANALIZZA --- */ 104 .custom-api-popup .leaflet-popup-content-wrapper { 105 background: transparent !important; 106 box-shadow: none !important; 107 padding: 0 !important; 108 } 109 .custom-api-popup .leaflet-popup-tip { display: none; } 110 111 /* AGGIUNGI QUESTO QUI SOTTO */ 112 .custom-api-popup { 113 z-index: 2000 !important; 114 } 115 116 .btn-mappa-analizza { 117 background: #facc15; 118 color: black; 119 padding: 10px 22px; 120 border-radius: 12px; 121 font-weight: 800; 122 font-size: 14px; 123 text-transform: uppercase; 124 border: 2px solid black; 125 box-shadow: 0 6px 15px rgba(0,0,0,0.4); 126 cursor: pointer; 127 transition: transform 0.1s; 128 } 129 .btn-mappa-analizza:active { transform: scale(0.9); } 130 /* ------------------------------------- */ 131</style> 132</head> 133<body> 134<div id="root"></div>
135<script type="text/babel"> 136const { useState, useRef, useEffect } = React; 137const BASE_URL = "https://chatbot-gemini-350964511180.europe-west1.run.app"; 138 139function App() { 140 const [started, setStarted] = useState(false); 141 const [messages, setMessages] = useState([]); 142 const [input, setInput] = useState(''); 143 const [sessionId, setSessionId] = useState(null); 144 const [waiting, setWaiting] = useState(false); 145 const [coords, setCoords] = useState(null); 146 const [chatState, setChatState] = useState(1); 147 const [showHelper, setShowHelper] = useState(false); 148 const [firstInteraction, setFirstInteraction] = useState(false); 149 150 const mapRef = useRef(null); 151 const markerRef = useRef(null); 152 const circleRef = useRef(null); 153 const endRef = useRef(null); 154 const tokenId = useRef(localStorage.getItem("tokenId")); 155 156 // --- LOGICA DI AVVIO SESSIONE (FONDAMENTALE) --- 157 useEffect(() => { 158 if (started) { 159 localStorage.setItem("tokenId", tokenId.current); 160 initSession(); 161 } 162 }, [started]); 163 164 // Scroll automatico chat 165 useEffect(() => { 166 endRef.current?.scrollIntoView({behavior:'smooth'}); 167 }, [messages, waiting]); 168 169 useEffect(() => { 170 if (started && !mapRef.current) { 171 // Definiamo il "recinto" (Solo Italia) 172 const italyBounds = [[32.0, 2.0], [50.0, 22.0]]; 173 174 // Creiamo i layer dicendogli di caricare SOLO i pezzi dentro il recinto 175 const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { 176 attribution: 'Esri', 177 bounds: italyBounds 178 }); 179 const labels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', { 180 bounds: italyBounds 181 }); 182 183 // Creiamo la mappa bloccata sull'Italia 184 mapRef.current = L.map('map', { 185 center: [42.0, 12.5], 186 zoom: 6, 187 minZoom: 6, // Non fa rimpicciolire troppo la mappa 188 maxBounds: italyBounds, // Non fa uscire l'utente dall'Italia 189 maxBoundsViscosity: 1.0, // Effetto "muro" ai bordi 190 layers: [satellite, labels], 191 zoomControl: false 192 }); 193 194 L.control.zoom({ position: 'topright' }).addTo(mapRef.current); 195 196 // Configuriamo la ricerca (Geocoder) per cercare SOLO in Italia 197 L.Control.geocoder({ 198 defaultMarkGeocode: false, 199 position: 'topright', 200 geocoder: L.Control.Geocoder.nominatim({ 201 geocodingQueryParams: { countrycodes: 'it' } 202 }) 203 }) 204 .on('markgeocode', (e) => { updateMarker(e.geocode.center.lat, e.geocode.center.lng); }) 205 .addTo(mapRef.current); 206 207 mapRef.current.on('click', (e) => { 208 updateMarker(e.latlng.lat, e.latlng.lng); 209 setShowHelper(false); 210 }); 211 } 212 }, [started]); 213 214 const updateMarker = (lat, lng) => { 215 const nLat = parseFloat(lat); 216 const nLng = parseFloat(lng); 217 if (isNaN(nLat) || isNaN(nLng)) return; 218 const pos = [nLat, nLng]; 219 setCoords(pos); 220 221 // 1. Gestione Cerchio Giallo (Resta invariato) 222 if (circleRef.current) circleRef.current.setLatLng(pos); 223 else circleRef.current = L.circle(pos, {radius: 1000, color: '#facc15', weight: 2, fillOpacity: 0.1, interactive: false}).addTo(mapRef.current); 224 225 // 2. Gestione Gruppo ARNIE (Layer Inferiore) 226 // Usiamo una variabile dedicata per non fare confusione 227 if (!window.hivesLayer) { 228 window.hivesLayer = L.layerGroup().addTo(mapRef.current); 229 } else { 230 window.hivesLayer.clearLayers(); // Pulisce solo le vecchie arnie 231 } 232 233 // 3. Disegna le Arnie (Hive Logic) nel loro layer dedicato 234 if (window.renderHives) { 235 window.renderHives(mapRef.current, window.hivesLayer, nLat, nLng); 236 } 237 238 // 4. Gestione Marker/Popup per il Bottone (Layer Superiore) 239 // Usiamo markerRef per il bottone, così sta sopra 240 if (markerRef.current) mapRef.current.removeLayer(markerRef.current); 241 242 // Creiamo il marker invisibile che tiene il bottone 243 markerRef.current = L.marker(pos, { opacity: 0, zIndexOffset: 1000 }).addTo(mapRef.current); 244 245 const content = ` 246<button class="btn-mappa-analizza" onclick="window.startAnalisi(${nLat}, ${nLng})"> 247 CLICCA PER<br>ANALIZZARE<br>â O â<br>SCRIVI IN CHAT 248</button>`; 249 250 markerRef.current.bindPopup(content, {
251 closeButton: false, 252 autoClose: false, 253 closeOnClick: false, 254 offset: [0, 225], // Spostato a sinistra (-40) e in basso (45) 255 className: 'custom-api-popup', 256 autoPan: false 257 }).openPopup(); 258 }; 259 260 const handleGps = () => { 261 const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 262 if (!isMobile) alert("â ï¸ NOTA: Per una precisione perfetta usa questa app dal CELLULARE.\n\nð» Su PC il GPS è approssimativo: se vedi un punto errato, clicca direttamente sulla mappa."); 263 if (!navigator.geolocation) return alert("GPS non supportato"); 264 navigator.geolocation.getCurrentPosition((pos) => { 265 updateMarker(pos.coords.latitude, pos.coords.longitude); 266 setShowHelper(false); 267 }, () => alert("Attiva la posizione."), {enableHighAccuracy: true}); 268 }; 269 270 const initSession = async () => { 271 try { 272 // --- GENERAZIONE TOKEN RECAPTCHA --- 273 const recaptchaToken = await new Promise((resolve) => { 274 grecaptcha.enterprise.ready(async () => { 275 const token = await grecaptcha.enterprise.execute('6LeaX5osAAAAACMPGp3EDqfipJ5lFw7MB0JAMEBx', { action: 'start_session' }); 276 resolve(token); 277 }); 278 }); 279 280 const res = await fetch(`${BASE_URL}/start_session`, { 281 method: "POST", 282 headers: { 283 "token-id": tokenId.current, 284 "x-recaptcha-token": recaptchaToken // <--- Passaggio del token 285 } 286 }); 287 const data = await res.json(); 288 if (res.status === 403) { 289 addMsg(data.response || "â ï¸ Accesso negato dal sistema.", "bot"); 290 return; // Blocca l'esecuzione ed evita il messaggio di benvenuto 291 } 292 setSessionId(data.session_id); 293 addMsg(`NECTAR RADAR [BETA] Progetto scientifico in fase di validazione territoriale. 294Seleziona un punto sulla mappa in corrispondenza del tuo apiario (o dellâarea di interesse) per avviare lâanalisi della zona, oppure scegli un punto e inizia a porre le tue domande.`, "bot"); 295 } catch { addMsg("Errore connessione.", "bot"); } 296}; 297 298 const addMsg = (text, sender) => { setMessages(prev => [...prev, {text, sender}]); }; 299 300 const send = async (e) => { 301 if (e) e.preventDefault(); 302 if (!firstInteraction) { setChatState(2); setFirstInteraction(true); } 303 if (!input.trim() || waiting || !sessionId) return; 304 305 // Salviamo l'input originale prima di pulirlo per l'interfaccia 306 const userQuery = input.trim(); 307 308 // LOGICA ORIGINARIA: Concateniamo le coordinate al messaggio per Gemini 309 const lat = coords?.[0]; 310 const lon = coords?.[1]; 311 const msgPerGemini = coords 312 ? `[PUNTO MAPPA: ${lat}, ${lon}] ${userQuery}` 313 : userQuery; 314 315 addMsg(userQuery, 'user'); // L'utente vede solo il suo messaggio 316 setInput(''); 317 setWaiting(true); 318 319 try { 320 // 1. Otteniamo il token reCAPTCHA Enterprise 321 const recaptchaToken = await new Promise((resolve, reject) => { 322 if (typeof grecaptcha === 'undefined') { 323 reject(new Error("reCAPTCHA non caricato")); 324 return; 325 } 326 grecaptcha.enterprise.ready(async () => { 327 try { 328 const token = await grecaptcha.enterprise.execute('6LeaX5osAAAAACMPGp3EDqfipJ5lFw7MB0JAMEBx', { action: 'chat_query' }); 329 resolve(token); 330 } catch (err) { 331 reject(err); 332 } 333 }); 334 }); 335 336 // 2. Eseguiamo la fetch con il token negli headers 337 const res = await fetch(`${BASE_URL}/chat`, { 338 method: "POST", 339 headers: { 340 "Content-Type": "application/json", 341 "session-id": sessionId, 342 "token-id": tokenId.current, 343 "x-recaptcha-token": recaptchaToken // Nuovo header di sicurezza 344 }, 345 body: JSON.stringify({ 346 query: msgPerGemini, 347 latitude: coords?.[0], 348 longitude: coords?.[1], 349 date_time: new Date().toISOString() 350 }) 351 }); 352 353 const data = await res.json(); 354 355 if (res.status === 403) { 356 // Usa il messaggio che arriva dal server (es: "Piano Scaduto"), 357 // se non c'è, usa il reCAPTCHA come ruota di scorta. 358 const messaggioErrore = data.response || "â ï¸ Accesso negato dal sistema di
358sicurezza (reCAPTCHA)."; 359 addMsg(messaggioErrore, "bot"); 360 } else { 361 addMsg(data.response, 'bot'); 362 } 363 364 } catch (error) { 365 console.error("Errore invio:", error); 366 addMsg("Errore di connessione o sicurezza.", "bot"); 367 } finally { 368 setWaiting(false); 369 } 370 }; 371 372 const handleToggle = () => { 373 if (chatState === 1) setChatState(0); 374 else if (chatState === 0) setChatState(2); 375 else setChatState(0); 376 }; 377 378 return ( 379 <div className="relative h-full w-full"> 380 {started && ( 381 <> 382 <header className="absolute top-0 left-0 right-0 z-[105] bg-black/85 backdrop-blur-md text-white p-8 flex justify-between items-center border-b border-white/10"> 383 <div className="flex items-center gap-3"> 384 <img src="./ferrari.png" className="h-8"/> 385 </div> 386 <div className="flex items-center gap-4"> 387 <div className="text-[10px] font-black px-2 py-1 bg-green-500/20 text-green-400 rounded-md border border-green-500/30 uppercase tracking-widest text-center"> 388 Live System 389 </div> 390 <button 391 onClick={() => window.NectarAuth.logout()} 392 className="text-[10px] font-bold text-red-500 border border-red-500/30 px-2 py-1 rounded ml-2 uppercase hover:bg-red-500/10 transition-all" 393 > 394 ESCI 395 </button> 396 </div> 397 </header> 398 399 <div className="map-tools"> 400 {/* RIGA 1: GPS, LABEL E AUDIO */} 401 <div className="flex items-center gap-3"> 402 <button onClick={handleGps} className="tool-btn shadow-green-500/20">ð</button> 403 <div className="live-tag">GPS Real-Time</div> 404 <button id="mute-btn" onClick={() => window.toggleAudio()} className="tool-btn">ð</button> 405 </div> 406 407 {/* RIGA 2: IL PUNTO INTERROGATIVO SOTTO IL GPS */} 408 <div className="flex"> 409 <button 410 onClick={() => setShowHelper(!showHelper)} 411 className="tool-btn" 412 style={{ 413 width: '46px', /* Stessa larghezza del tasto GPS sopra */ 414 height: '46px', 415 fontSize: '22px', 416 background: '#facc15', 417 border: '2px solid black' 418 }} 419 > 420 â 421 </button> 422 </div> 423</div> 424 {showHelper && ( 425 <div className="helper-bubble"> 426 {/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) 427 ? "ð± GPS Attivo! In alternativa, tocca un punto sulla mappa per spostare l'analisi." 428 : "â ï¸ NOTA: Su PC il GPS può sbagliare città . Usa il CELLULARE o clicca sulla mappa per correggere."} 429 </div> 430 )} 431 </> 432 )} 433 434 {!started ? ( 435 <div className="h-full flex flex-col items-center justify-center gap-6 bg-slate-950 text-white p-6 text-center"> 436 <div className="relative mb-4"> 437 <div className="absolute inset-0 bg-yellow-400/20 blur-[60px] rounded-full animate-pulse"></div> 438 <img src="nectar.webp" className="relative w-80 h-80 object-contain animate-float drop-shadow-[0_0_15px_rgba(250,204,21,0.5)]" alt="Nectar Logo" /> 439 </div> 440 <h2 className="text-4xl font-black italic tracking-tighter uppercase text-yellow-400">NECTAR RADAR</h2> 441 <p className="text-slate-500 text-sm max-w-xs font-medium uppercase tracking-widest">ASSISTENTE APISTICO TERRITORIALE</p> 442 <button onClick={()=>setStarted(true)} className="mt-4 bg-yellow-400 text-black px-14 py-5 rounded-full font-black text-xl shadow-xl hover:scale-105 active:scale-95 transition-all">AVVIA SISTEMA</button> 443 </div> 444 ) : ( 445 <> 446 <div id="map"></div> 447 <div className={`chat-overlay ${chatState === 1 ? 'is-open' : chatState === 2 ? 'is-max' : 'is-closed'}`}> 448 <div className="glass"> 449 <div className="pt-5 pb-2 px-2 flex justify-center" onClick={handleToggle}> 450 <div className="chat-toggle-handle shadow-sm"> 451 <div className="w-14 h-1.5 bg-slate-300 rounded-full mb-2"></div>
452 <span className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]"> 453 {chatState === 0 ? " CLICCA PER MOSTRARE CHAT" : "CLICCA PER MOSTRARE MAPPA"} 454 </span> 455 </div> 456 </div> 457 <div className="flex-1 overflow-y-auto px-4 py-2 flex flex-col gap-5"> 458 {messages.map((m, i) => ( 459 <div key={i} className={m.sender === 'bot' ? "bot-box" : "user-bubble"}> 460 {m.sender === 'bot' ? <div dangerouslySetInnerHTML={{ __html: marked.parse(m.text) }} /> : m.text} 461 </div> 462 ))} 463 {waiting && ( 464 <div className="flex flex-col items-center py-6"> 465 <div className="w-10 h-10 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin"></div> 466 <div className="mt-2 text-[10px] text-yellow-400 font-bold uppercase"> 467 Scansione... 468 </div> 469 </div> 470)} 471 <div ref={endRef}></div> 472 </div> 473 <footer className="p-4 bg-white border-t pb-10"> 474 <form onSubmit={send} className="flex gap-2"> 475 <input 476 value={input} 477 onChange={e=>setInput(e.target.value)} 478 onFocus={() => { }} 479 placeholder="Domanda sulla zona..." 480 className="flex-1 bg-slate-100 rounded-2xl px-5 py-4 text-sm outline-none border-2 border-transparent focus:border-yellow-400 transition-all font-medium" 481 disabled={waiting} 482 /> 483 <button className="bg-yellow-400 px-7 rounded-2xl font-black text-lg active:scale-90 transition-all shadow-lg" disabled={waiting}>â¤</button> 484 </form> 485 </footer> 486 </div> 487 </div> 488 </> 489 )} 490 </div> 491 ); 492} 493 494window.startAnalisi = (lat, lon) => { 495 // Apre la chat 496 const chatHandle = document.querySelector('.chat-toggle-handle'); 497 if (chatHandle) chatHandle.click(); 498 499 const inputEl = document.querySelector('footer input'); 500 const submitBtn = document.querySelector('footer button'); 501 502 if (inputEl && submitBtn) { 503 // MESSAGGIO: Manteniamo la tua logica di concatenazione 504 const testo = `Punto ${lat}, ${lon}: Analisi zona ......`; 505 506 // TRUCCO PER REACT: Questo forza React a leggere il testo inserito 507 const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; 508 setter.call(inputEl, testo); 509 inputEl.dispatchEvent(new Event('input', { bubbles: true })); 510 511 // Clicca invio dopo un attimo 512 setTimeout(() => { 513 submitBtn.click(); 514 }, 500); 515 } 516}; 517 518// --- MODIFICA QUI: AVVIO CON CONTROLLO LOGIN --- 519window.NectarAuth.checkAccess().then(() => { 520 // Questo codice viene eseguito SOLO se l'utente è loggato 521 ReactDOM.createRoot(document.getElementById('root')).render(<App />); 522}); 523// ---------------------------------------------- 524</script>
524 525</body> 526</html>
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.