PageSourceSearch

https://chatbot-478611.web.app/

html chatbot-478611.web.app collected 2026-10-03 12:19:36 UTC 22,047 bytes, 526 lines download raw bytes

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.