PageSourceSearch

https://umowy.ai/js/main.js?v=35

js umowy.ai collected 2026-10-05 14:48:38 UTC 34,545 bytes, 933 lines download raw bytes

1/* ──────────────────────────────────────
2   Umowy.AI — Main JS
3   ────────────────────────────────────── */
4
5(function () {
6  'use strict';
7
8  // ── i18n ──
9  // Wersje inne niż polska są tymczasowo wyłączone (tłumaczenia niekompletne).
10  // Przywrócenie: true tutaj + odkomentowanie przełączników w index.html
11  // + przywrócenie tagów hreflang w <head> i wpisów w sitemap.xml.
12  const MULTILANG_ENABLED = false;
13
14  const ALL_LANGS = ['pl', 'en', 'ua', 'ru'];
15  const SUPPORTED_LANGS = MULTILANG_ENABLED ? ALL_LANGS : ['pl'];
16  // Kod pliku 'ua' to kod kraju; atrybut lang/hreflang wymaga ISO 639-1 ('uk').
17  const HTML_LANG = { pl: 'pl', en: 'en', ua: 'uk', ru: 'ru' };
18
19  // Przy wyłączonych tłumaczeniach wymuszamy 'pl' zamiast czytać localStorage —
20  // inaczej wracający użytkownik z zapisanym 'en' dalej dostawałby tę wersję.
21  // Gdy włączone, ?lang= musi wygrywać z localStorage, żeby udostępniony link
22  // otwierał się w języku nadawcy, a nie odbiorcy.
23  let currentLang = MULTILANG_ENABLED
24    ? (langFromUrl() || localStorage.getItem('umowy-ai-lang') || detectLang())
25    : 'pl';
26  let translations = {};
27  // Czy strona została już nadpisana tłumaczeniem z JSON (język inny niż polski)
28  let translationsApplied = false;
29
30  function langFromUrl() {
31    const value = new URLSearchParams(location.search).get('lang');
32    return SUPPORTED_LANGS.includes(value) ? value : null;
33  }
34
35  function detectLang() {
36    const nav = navigator.language?.slice(0, 2) || 'pl';
37    return SUPPORTED_LANGS.includes(nav) ? nav : 'pl';
38  }
39
40  // Utrzymuje ?lang=, <html lang>, canonical i og:url w zgodzie z aktualnym
41  // językiem. Bez aktualizacji canonical każda wersja ?lang= wskazywałaby na
42  // polski adres i hreflang zostałby przez wyszukiwarki zignorowany.
43  const OG_LOCALE = { pl: 'pl_PL', en: 'en_US', ua: 'uk_UA', ru: 'ru_RU' };
44
45  function syncLangUrl(lang) {
46    document.documentElement.lang = HTML_LANG[lang] || 'pl';
47
48    const url = new URL(location.href);
49    if (lang === 'pl') url.searchParams.delete('lang');
50    else url.searchParams.set('lang', lang);
51    url.hash = '';
52    history.replaceState(null, '', url.pathname + url.search + location.hash);
53
54    const absolute = 'https://umowy.ai/' + (lang === 'pl' ? '' : '?lang=' + lang);
55    const canonical = document.querySelector('link[rel="canonical"]');
56    if (canonical) canonical.href = absolute;
57    const ogUrl = document.querySelector('meta[property="og:url"]');
58    if (ogUrl) ogUrl.content = absolute;
59    const ogLocale = document.querySelector('meta[property="og:locale"]');
60    if (ogLocale) ogLocale.content = OG_LOCALE[lang] || 'pl_PL';
61  }
62
63  async function loadTranslations(lang) {
64    // Zabezpieczenie na wypadek wywołania z pominięciem UI (stary localStorage,
65    // link z ?lang=, konsola) — przy wyłączonych tłumaczeniach zawsze polski.
66    if (!MULTILANG_ENABLED) lang = 'pl';
67
68    // Polski jest językiem źródłowym strony — treść siedzi w HTML i jest
69    // zawsze aktualna. Nie nadpisujemy jej danymi z pl.json (plik bywa
70    // starszy niż HTML). Tłumaczenia z JSON aplikujemy tylko dla innych języków.
71    if (lang === 'pl') {
72      localStorage.setItem('umowy-ai-lang', 'pl');
73      if (translationsApplied) {
74        // Strona była przełączona na inny język — przeładowanie przywraca
75        // oryginalny polski HTML w całości. Ładujemy adres bez ?lang, żeby
76        // po odświeżeniu parametr nie przywrócił poprzedniego języka.
77        const url = new URL(location.href);
78        url.searchParams.delete('lang');
79        location.replace(url);
80        return;
81      }
82      currentLang = 'pl';
83      syncLangUrl('pl');
84      updateLangButtons();
85      return;
86    }
87    try {
88      const res = await fetch(`i18n/${lang}.json?v=4`);
89      if (!res.ok) throw new Error();
90      translations = await res.json();
91      currentLang = lang;
92      localStorage.setItem('umowy-ai-lang', lang);
93      applyTranslations();
94      translationsApplied = true;
95      syncLangUrl(lang);
96      updateLangButtons();
97    } catch {
98      if (lang !== 'pl') loadTranslations('pl');
99    }
100  }
101
102  function applyTranslations() {
103    // Simple text nodes via data-i18n
104    document.querySelectorAll('[data-i18n]').forEach(function(el) {
105      const key = el.getAttribute('data-i18n');
106      const val = getNestedValue(translations, key);
107      if (val) el.textContent = val;
108    });
109
110    // Hero title — built from safe text values
111    buildHeroTitle();
112
113    // Features
114    applyFeatureTranslations();
115
116    // Showcase tabs
117    applyShowcaseTranslations();
118
119    // Pricing
120    applyPricingTranslations();
121
122    // Implementation terminal
123    buildTerminal();
124
125    // FAQ
126    applyFaqTranslations();
127
128    // Footer
129    applyFooterTranslations();
130  }
131
132  // ── Hero title typewriter rotation ──
133  var _heroTypewriterTimer = null;
134
135  function buildHeroTitle() {
136    var heroTitle = document.getElementById('hero-title');
137    if (!heroTitle || !translations.hero) return;
138
139    // Cancel any running typewriter before rebuilding (e.g. on lang switch)
140    if (_heroTypewriterTimer !== null) {
141      clearTimeout(_heroTypewriterTimer);
142      _heroTypewriterTimer = null;
143    }
144
145    var headlines = translations.hero.rotating_headlines || [];
146    // Prepend the primary headline; accent is optional (empty string = no accent span)
147    var accentText = translations.hero.title_accent || '';
148    var primary = {
149      accent: accentText.length > 0 ? accentText : null,
150      main: translations.hero.title_main || 'Wybierz, co potrzebujesz.'
151    };
152
153    var allHeadlines = [primary].concat(
154      headlines.map(function(h) { return { accent: null, main: h }; })
155    );
156
157    // Start with empty title — typewriter writes first headline from scratch on load
158    _renderHeroTitle(heroTitle, primary, 0);
159    _startHeroTypewriter(heroTitle, allHeadlines);
160  }
161
162  function _getHeadlineFullLength(headline) {
163    return (headline.accent ? headline.accent.length : 0) + headline.main.length;
164  }
165
166  function _renderHeroTitle(heroTitle, headline, charCount) {
167    heroTitle.textContent = '';
168
169    if (headline.accent) {
170      var accentLen = headline.accent.length;
171      var accentSpan = document.createElement('span');
172      accentSpan.className = 'accent';
173      if (charCount <= accentLen) {
174        accentSpan.textContent = headline.accent.substring(0, charCount);
175        heroTitle.appendChild(accentSpan);
176      } else {
177        accentSpan.textContent = headline.accent;
178        heroTitle.appendChild(accentSpan);
179        var mainSlice = headline.main.substring(0, charCount - accentLen);
180        mainSlice.split('\n').forEach(function(part, i, arr) {
181          heroTitle.appendChild(document.createTextNode(part));
182          if (i < arr.length - 1) heroTitle.appendChild(document.createElement('br'));
183        });
184      }
185    } else {
186      var textSlice = headline.main.substring(0, charCount);
187      textSlice.split('\n').forEach(function(part, i, arr) {
188        heroTitle.appendChild(document.createTextNode(part));
189        if (i < arr.length - 1) heroTitle.appendChild(document.createElement('br'));
190      });
191    }
192
193    var cursor = document.createElement('span');
194    cursor.className = 'hero__cursor';
195    heroTitle.appendChild(cursor);
196  }
197
198  function _startHeroTypewriter(heroTitle, headlines) {
199    var TYPE_SPEED   = 55;   // ms per character when typing
200    var DELETE_SPEED = 28;   // ms per character when deleting (faster = more natural)
201    var PAUSE_AFTER  = 3000; // ms pause after fully typed
202    var PAUSE_BEFORE = 320;  // ms pause before typing next headline
203
204    var idx       = 0;
205    var charCount = 0;  // start from empty — types in on load
206    var deleting  = false;
207
208    function tick() {
209      var headline = headlines[idx];
210      var fullLen  = _getHeadlineFullLength(headline);
211
212      if (!deleting) {
213        charCount++;
214        _renderHeroTitle(heroTitle, headline, charCount);
215        if (charCount >= fullLen) {
216          if (headlines.length === 1) return; // single headline — stop after typing
217          deleting = true;
218          _heroTypewriterTimer = setTimeout(tick, PAUSE_AFTER);
219        } else {
220          _heroTypewriterTimer = setTimeout(tick, TYPE_SPEED);
221        }
222      } else {
223        charCount--;
224        _renderHeroTitle(heroTitle, headline, charCount);
225        if (charCount <= 0) {
226          deleting = false;
227          idx = (idx + 1) % headlines.length;
228          charCount = 0;
229          _heroTypewriterTimer = setTimeout(tick, PAUSE_BEFORE);
230        } else {
231          _heroTypewriterTimer = setTimeout(tick, DELETE_SPEED);
232        }
233      }
234    }
235
236    // Short initial delay so the cursor blinks once before typing starts
237    _heroTypewriterTimer = setTimeout(tick, 600);
238  }
239
240  function applyFeatureTranslations() {
241    if (!translations.features?.items) return;
242    translations.features.items.forEach(function(item, i) {
243      var card = document.querySelector('.feature-card[data-index="' + i + '"]');
244      if (!card) return;
245      var num = card.querySelector('.feature-card__num');
246      var title = card.querySelector('.feature-card__title');
247      var desc = card.querySelector('.feature-card__desc');
248      var bullets = card.querySelectorAll('.feature-card__bullets li');
249      if (num) num.textContent = item.num;
250      if (title) title.textContent = item.title;
251      if (desc) desc.textContent = item.desc;
252      bullets.forEach(function(li, j) {
253        if (item.bullets[j]) li.textContent = item.bullets[j];
254      });
255    });
256  }
257
258  function applyShowcaseTranslations() {
259    if (!translations.showcase?.tabs) return;
260    document.querySelectorAll('.showcase__tab').forEach(function(tab, i) {
261      if (translations.showcase.tabs[i]) tab.textContent = translations.showcase.tabs[i];
262    });
263  }
264
265  function applyPricingTranslations() {
266    if (!translations.pricing?.plans) return;
267    translations.pricing.plans.forEach(function(plan, i) {
268      var card = document.querySelector('.pricing-card[data-index="' + i + '"]');
269      if (!card) return;
270      card.querySelector('.pricing-card__name').textContent = plan.name;
271      card.querySelector('.pricing-card__price-value').textContent = plan.price;
272      card.querySelector('.pricing-card__tokens-value').textContent = plan.tokens;
273      var cta = card.querySelector('.pricing-card__cta');
274      if (cta) cta.textContent = plan.cta;
275      plan.features.forEach(function(f, j) {
276        var fEl = card.querySelectorAll('.pricing-card__feature')[j];
277        if (fEl) {
278          var textEl = fEl.querySelector('.text');
279          if (textEl) textEl.textContent = f.text;
280        }
281      });
282    });
283
284    // Token table
285    if (translations.pricing.token_rows) {
286      document.querySelectorAll('.pricing__token-row').forEach(function(row, i) {
287        var data = translations.pricing.token_rows[i];
288        if (!data) return;
289        var label = row.querySelector('.label');
290        var value = row.querySelector('.value');
291        if (label) label.textContent = data[0];
292        if (value) value.textContent = data[1];
293      });
294    }
295  }
296
297  function buildTerminal() {
298    if (!translations.implementation?.terminal_lines) return;
299    var body = document.querySelector('.terminal__body');
300    if (!body) return;
301
302    // Clear existing content
303    body.textContent = '';
304
305    translations.implementation.terminal_lines.forEach(function(line) {
306      if (!line) {
307        body.appendChild(document.createElement('br'));
308        return;
309      }
310      var span = document.createElement('span');
311      if (line.startsWith('$')) {
312        span.className = 'terminal__line--prompt';
313      } else if (line.includes('\u2713')) { // ✓
314        span.className = 'terminal__line--success';
315      } else if (line.includes('\u2593')) { // ▓
316        span.className = 'terminal__line--progress';
317      } else if (line.includes('oczekuje') || line.includes('pending')) {
318        span.className = 'terminal__line--pending';
319      } else if (line.startsWith('>')) {
320        span.className = 'terminal__line--info';
321      }
322      span.textContent = line;
323      body.appendChild(span);
324      body.appendChild(document.createTextNode('\n'));
325    });
326
327    var cursor = document.createElement('span');
328    cursor.className = 'terminal__cursor';
329    body.appendChild(cursor);
330  }
331
332  function applyFaqTranslations() {
333    if (!translations.faq?.items) return;
334    document.querySelectorAll('.faq__item').forEach(function(item, i) {
335      var data = translations.faq.items[i];
336      if (!data) return;
337      var q = item.querySelector('.faq__question-text');
338      var a = item.querySelector('.faq__answer-inner');
339      if (q) q.textContent = data.q;
340      if (a) a.textContent = data.a;
341    });
342  }
343
344  function applyFooterTranslations() {
345    if (!translations.footer) return;
346    var f = translations.footer;
347    document.querySelectorAll('.footer__col').forEach(function(col, i) {
348      var titleKey = ['col1_title', 'col2_title', 'col3_title'][i];
349      var linksKey = ['col1_links', 'col2_links', 'col3_links'][i];
350      var titleEl = col.querySelector('.footer__col-title');
351      if (titleEl && f[titleKey]) titleEl.textContent = f[titleKey];
352      var links = col.querySelectorAll('.footer__col-links a');
353      if (f[linksKey]) links.forEach(function(a, j) {
354        if (f[linksKey][j]) a.textContent = f[linksKey][j];
355      });
356    });
357  }
358
359  function getNestedValue(obj, path) {
360    return path.split('.').reduce(function(o, k) { return o?.[k]; }, obj);
361  }
362
363  function updateLangButtons() {
364    document.querySelectorAll('[data-lang]').forEach(function(btn) {
365      btn.classList.toggle('active', btn.dataset.lang === currentLang);
366    });
367  }
368
369  // ── Scroll reveal ──
370  function initScrollReveal() {
371    var observer = new IntersectionObserver(function(entries) {
372      entries.forEach(function(entry) {
373        if (entry.isIntersecting) {
374          entry.target.classList.add('visible');
375          observer.unobserve(entry.target);
376        }
377      });
378    }, { threshold: 0.15 });
379
380    document.querySelectorAll('.reveal').forEach(function(el) {
381      observer.observe(el);
382    });
383  }
384
385  // ── Scroll spy ──
386  function initScrollSpy() {
387    var sections = document.querySelectorAll('section[id]');
388    var links = document.querySelectorAll('.nav__link[href^="#"]');
389
390    var observer = new IntersectionObserver(function(entries) {
391      entries.forEach(function(entry) {
392        if (entry.isIntersecting) {
393          var id = entry.target.id;
394          links.forEach(function(link) {
395            link.classList.toggle('active', link.getAttribute('href') === '#' + id);
396          });
397        }
398      });
399    }, { rootMargin: '-20% 0px -80% 0px' });
400
401    sections.forEach(function(section) {
402      observer.observe(section);
403    });
404  }
405
406  // ── Mobile menu ──
407  function initMobileMenu() {
408    var hamburger = document.querySelector('.nav__hamburger');
409    var menu = document.querySelector('.nav__mobile-menu');
410    if (!hamburger || !menu) return;
411
412    hamburger.addEventListener('click', function() {
413      hamburger.classList.toggle('open');
414      menu.classList.toggle('open');
415      document.body.style.overflow = menu.classList.contains('open') ? 'hidden' : '';
416    });
417
418    menu.querySelectorAll('a').forEach(function(link) {
419      link.addEventListener('click', function() {
420        hamburger.classList.remove('open');
421        menu.classList.remove('open');
422        document.body.style.overflow = '';
423      });
424    });
425  }
426
427  // ── FAQ accordion ──
428  function initFaqAccordion() {
429    document.querySelectorAll('.faq__question').forEach(function(btn) {
430      btn.addEventListener('click', function() {
431        var item = btn.closest('.faq__item');
432        var answer = item.querySelector('.faq__answer');
433        var isOpen = item.classList.contains('open');
434
435        // Close all
436        document.querySelectorAll('.faq__item.open').forEach(function(openItem) {
437          openItem.classList.remove('open');
438          openItem.querySelector('.faq__question').setAttribute('aria-expanded', 'false');
439          openItem.querySelector('.faq__answer').style.maxHeight = '0';
440        });
441
442        // Open clicked (if was closed)
443        if (!isOpen) {
444          item.classList.add('open');
445          btn.setAttribute('aria-expanded', 'true');
446          answer.style.maxHeight = answer.scrollHeight + 'px';
447        }
448      });
449
450      btn.addEventListener('keydown', function(e) {
451        if (e.key === 'Enter' || e.key === ' ') {
452          e.preventDefault();
453          btn.click();
454        }
455      });
456    });
457  }
458
459  // ── Showcase tabs ──
460  function initShowcaseTabs() {
461    var tabs = document.querySelectorAll('.showcase__tab');
462    var preview = document.querySelector('.showcase__preview-inner');
463    if (!tabs.length || !preview) return;
464    var placeholders = ['// analysis.output', '// document.output', '// verification.output'];
465
466    function activateTab(index) {
467      tabs.forEach(function(t) {
468        t.classList.remove('active');
469        t.setAttribute('aria-selected', 'false');
470        t.setAttribute('tabindex', '-1');
471      });
472      tabs[index].classList.add('active');
473      tabs[index].setAttribute('aria-selected', 'true');
474      tabs[index].setAttribute('tabindex', '0');
475      tabs[index].focus();
476      preview.textContent = placeholders[index] || '// preview';
477    }
478
479    tabs.forEach(function(tab, i) {
480      tab.addEventListener('click', function() { activateTab(i); });
481      tab.addEventListener('keydown', function(e) {
482        if (e.key === 'ArrowRight') { e.preventDefault(); activateTab((i + 1) % tabs.length); }
483        if (e.key === 'ArrowLeft')  { e.preventDefault(); activateTab((i - 1 + tabs.length) % tabs.length); }
484      });
485    });
486  }
487
488  // ── Language switch ──
489  function initLangSwitch() {
490    document.addEventListener('click', function(e) {
491      var btn = e.target.closest('[data-lang]');
492      if (btn) {
493        loadTranslations(btn.dataset.lang);
494      }
495    });
496
497    // Nav dropdown
498    var langBtn = document.querySelector('.nav__lang');
499    var langDropdown = document.querySelector('.nav__lang-dropdown');
500    if (langBtn && langDropdown) {
501      langBtn.addEventListener('click', function(e) {
502        e.stopPropagation();
503        langDropdown.classList.toggle('open');
504      });
505      document.addEventListener('click', function(e) {
506        if (!e.target.closest('.nav__lang-wrap')) {
507          langDropdown.classList.remove('open');
508        }
509      });
510    }
511  }
512
513  // ── Theme toggle ──
514  // Click handler is inline onclick on the button (no duplicate addEventListener)
515
516  // ── Features Carousel ──
517  function initFeaturesPersonas() {
518    var el = document.getElementById('featuresPersona');
519    if (!el) return;
520    var words = ['firm', 'kancelarii', 'prawników', 'działów prawnych', 'przedsiębiorców'];
521    var prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
522    if (prefersReduced) { el.textContent = words[0]; return; }
523
524    var typeSpeed = 55;
525    var deleteSpeed = 30;
526    var holdAfterType = 1100;
527    var holdAfterDelete = 180;
528    var wordIndex = 0;
529    var charIndex = 0;
530    var isDeleting = false;
531
532    function tick() {
533      var word = words[wordIndex];
534      if (!isDeleting) {
535        charIndex++;
536        el.textContent = word.slice(0, charIndex);
537        if (charIndex === word.length) {
538          isDeleting = true;
539          setTimeout(tick, holdAfterType);
540          return;
541        }
542        setTimeout(tick, typeSpeed);
543      } else {
544        charIndex--;
545        el.textContent = word.slice(0, charIndex);
546        if (charIndex === 0) {
547          isDeleting = false;
548          wordIndex = (wordIndex + 1) % words.length;
549          setTimeout(tick, holdAfterDelete);
550          return;
551        }
552        setTimeout(tick, deleteSpeed);
553      }
554    }
555    tick();
556  }
557
558  function initFoundersCarousel() {
559    var track = document.getElementById('foundersTrack');
560    if (!track) return;
561    var section = document.getElementById('founders');
562    var dots = document.querySelectorAll('.founders-carousel__dot');
563    var slides = track.querySelectorAll('.founders-carousel__slide');
564    var current = 0;
565    var autoplayTimer = null;
566    var autoplayDelay = 6000;
567    var startTime = 0;
568    var remainingTime = autoplayDelay;
569    var isVisible = false;
570    var isHovering = false;
571
572    function setDotStates(restart) {
573      dots.forEach(function(d, i) {
574        d.classList.remove('founders-carousel__dot--active', 'founders-carousel__dot--completed');
575        if (i < current) d.classList.add('founders-carousel__dot--completed');
576      });
577      var activeDot = dots[current];
578      if (activeDot) {
579        if (restart) { void activeDot.offsetWidth; }
580        activeDot.classList.add('founders-carousel__dot--active');
581      }
582    }
583
584    function goTo(index) {
585      current = (index + slides.length) % slides.length;
586      track.style.transform = 'translateX(-' + (current * 100) + '%)';
587      remainingTime = autoplayDelay;
588      setDotStates(true);
589    }
590
591    function clearTimer() {
592      if (autoplayTimer) { clearTimeout(autoplayTimer); autoplayTimer = null; }
593    }
594
595    function startAutoplay() {
596      clearTimer();
597      if (!isVisible || isHovering || slides.length < 2) return;
598      if (section) section.classList.remove('is-paused');
599      startTime = Date.now();
600      autoplayTimer = setTimeout(function() {
601        goTo(current + 1);
602        startAutoplay();
603      }, remainingTime);
604    }
605
606    function pauseAutoplay() {
607      if (autoplayTimer) {
608        clearTimeout(autoplayTimer);
609        autoplayTimer = null;
610        remainingTime = Math.max(0, remainingTime - (Date.now() - startTime));
611      }
612      if (section) section.classList.add('is-paused');
613    }
614
615    dots.forEach(function(dot, i) {
616      dot.addEventListener('click', function() {
617        goTo(i);
618        startAutoplay();
619      });
620    });
621
622    if (section) {
623      section.addEventListener('mouseenter', function() {
624        isHovering = true;
625        pauseAutoplay();
626      });
627      section.addEventListener('mouseleave', function() {
628        isHovering = false;
629        startAutoplay();
630      });
631    }
632
633    setDotStates(false);
634
635    if ('IntersectionObserver' in window && section) {
636      var observer = new IntersectionObserver(function(entries) {
637        entries.forEach(function(entry) {
638          isVisible = entry.isIntersecting;
639          if (isVisible) {
640            if (!autoplayTimer) { remainingTime = autoplayDelay; setDotStates(true); }
641            startAutoplay();
642          } else {
643            pauseAutoplay();
644          }
645        });
646      }, { threshold: 0.35 });
647      observer.observe(section);
648    } else {
649      isVisible = true;
650      setDotStates(true);
651      startAutoplay();
652    }
653  }
654
655  // ── MCP terminal (§05) — interaktywne demo klienta AI + serwera MCP ──
656  // Dane zmockowane, ale prawdziwe i aktualne (zweryfikowane: art. 476 KC,
657  // art. 113 ust. 1 VAT — 240 000 zł od 01.01.2026, art. 27 ust. 1 UPK).
658  var MCP_SCENARIOS = [
659    {
660      preset: 'Podaj treść art. 476 KC',
661      prompt: 'Podaj treść art. 476 KC',
662      call: 'get_article(article_number: "476", legal_act_code: "KC")',
663      action: 'Pobieram pełną, aktualną treść artykułu z bazy',
664      meta: 'Art. 476 Kodeksu cywilnego · obowiązuje · Dz.U. 2025/1071',
665      quote: 'Dłużnik dopuszcza się zwłoki, gdy nie spełnia świadczenia w terminie, a jeżeli termin nie jest oznaczony — gdy nie spełnia świadczenia niezwłocznie po wezwaniu przez wierzyciela. Nie dotyczy to wypadku, gdy opóźnienie jest następstwem okoliczności, za które dłużnik odpowiedzialności nie ponosi.',
666      answer: 'Art. 476 KC definiuje zwłokę dłużnika. Kluczowe rozróżnienie: zwłoka to opóźnienie zawinione — gdy przyczyna leży poza dłużnikiem, mamy zwykłe opóźnienie, a nie zwłokę.',
667      citation: 'art. 476 KC'
668    },
669    {
670      preset: 'Od jakiej kwoty muszę zarejestrować się do VAT?',
671      prompt: 'Od jakiej kwoty muszę zarejestrować się do VAT?',
672      call: 'search_legal_provisions(query: "limit zwolnienia podmiotowego z VAT")',
673      action: 'Wyszukuję właściwy przepis — dopasowanie po sensie pytania',
674      optimized: 'art. 113 ust. 1 ustawy o VAT',
675      meta: 'Art. 113 ust. 1 ustawy o VAT · obowiązuje · Dz.U. 2025/896',
676      quote: 'Zwalnia się od podatku sprzedaż dokonywaną przez podatników, u których wartość sprzedaży nie przekroczyła łącznie w poprzednim roku podatkowym kwoty 240 000 zł.',
677      answer: 'Limit zwolnienia podmiotowego z VAT to 240 000 zł wartości sprzedaży w poprzednim roku. Uwaga na aktualność: od 1 stycznia 2026 r. próg wzrósł z 200 000 zł do 240 000 zł (ustawa z 24.06.2025). Po jego przekroczeniu rejestracja jako podatnik VAT czynny jest obowiązkowa.',
678      citation: 'art. 113 ust. 1 VAT'
679    },
680    {
681      preset: 'Ile mam czasu na zwrot rzeczy kupionej online?',
682      prompt: 'Ile mam czasu na zwrot rzeczy kupionej w sklepie internetowym?',
683      call: 'search_legal_provisions(query: "odstąpienie od umowy zawartej na odległość")',
684      action: 'Wyszukuję właściwy przepis — dopasowanie po sensie pytania',
685      optimized: 'art. 27 ustawy o prawach konsumenta',
686      meta: 'Art. 27 ust. 1 ustawy o prawach konsumenta · obowiązuje · Dz.U. 2024/1796',
687      quote: 'Konsument, który zawarł umowę na odległość lub poza lokalem przedsiębiorstwa, może w terminie 14 dni odstąpić od niej bez podawania przyczyny i bez ponoszenia kosztów.',
688      answer: 'Masz 14 dni na odstąpienie bez podawania przyczyny, licząc od objęcia rzeczy w posiadanie. Wyjątek: przy nieumówionej wizycie przedsiębiorcy w domu termin wynosi 30 dni. Jeśli sprzedawca nie poinformował Cię o tym prawie — wydłuża się aż do 12 miesięcy.',
689      citation: 'art. 27 ust. 1 UPK'
690    }
691  ];
692
693  function initMcpTerminal() {
694    var root = document.querySelector('[data-mcp-terminal]');
695    if (!root) return;
696    var output = root.querySelector('[data-mcp-output]');
697    var presetsWrap = root.querySelector('[data-mcp-presets]');
698    var presetsGrid = presetsWrap && presetsWrap.querySelector('.mcp__presets-grid');
699    if (!output || !presetsWrap || !presetsGrid) return;
700
701    var prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
702    var SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
703    var runId = 0;
704    var timers = [];
705    var userPicked = false;
706
707    function clearTimers() { timers.forEach(clearTimeout); timers = []; }
708    function wait(ms) { return new Promise(function(res) { timers.push(setTimeout(res, ms)); }); }
709
710    function el(cls, text) {
711      var d = document.createElement('div');
712      d.className = cls;
713      if (text != null) d.textContent = text;
714      return d;
715    }
716    function glyphLine(cls, glyph, text) {
717      var d = el('mcp__ln ' + cls);
718      var g = document.createElement('span');
719      g.className = 'mcp__glyph';
720      g.textContent = glyph;
721      d.appendChild(g);
722      d.appendChild(document.createTextNode(' ' + text));
723      return d;
724    }
725    function promptLine(text, withCursor) {
726      var d = el('mcp__ln mcp__ln--prompt');
727      var caret = document.createElement('span');
728      caret.className = 'mcp__caret';
729      caret.textContent = '>';
730      d.appendChild(caret);
731      d.appendChild(document.createTextNode(' '));
732      var t = document.createElement('span');
733      if (text != null) t.textContent = text;
734      d.appendChild(t);
735      var cursor = null;
736      if (withCursor) {
737        cursor = document.createElement('span');
738        cursor.className = 'mcp__cursor';
739        d.appendChild(cursor);
740      }
741      return { line: d, text: t, cursor: cursor };
742    }
743    function citeChip(text) {
744      var d = el('mcp__cite');
745      var mark = document.createElement('span');
746      mark.className = 'mcp__cite-mark';
747      mark.textContent = '§';
748      d.appendChild(mark);
749      d.appendChild(document.createTextNode(' ' + text));
750      return d;
751    }
752    function appendResults(sc) {
753      if (sc.optimized) {
754        output.appendChild(glyphLine('mcp__ln--opt', '⎿', 'znaleziono dopasowanie w bazie:'));
755        output.appendChild(el('mcp__ln mcp__ln--sub', sc.meta));
756      } else {
757        output.appendChild(glyphLine('mcp__ln--meta', '⎿', sc.meta));
758      }
759    }
760
761    function renderFinal(sc) {
762      output.textContent = '';
763      output.appendChild(promptLine(sc.prompt, false).line);
764      output.appendChild(glyphLine('mcp__ln--tool', '⏺', sc.call));
765      if (sc.action) output.appendChild(glyphLine('mcp__ln--note', '↳', sc.action));
766      appendResults(sc);
767      var q = el('mcp__quote', sc.quote); q.classList.add('is-in');
768      output.appendChild(q);
769      output.appendChild(el('mcp__answer', sc.answer));
770      var c = citeChip(sc.citation); c.classList.add('is-in');
771      output.appendChild(c);
772    }
773
774    function setActive(i) {
775      buttons.forEach(function(b, j) { b.classList.toggle('is-active', j === i); });
776    }
777
778    async function play(i) {
779      var sc = MCP_SCENARIOS[i];
780      if (!sc) return;
781      clearTimers();
782      var my = ++runId;
783      setActive(i);
784      output.textContent = '';
785
786      if (prefersReduced) { renderFinal(sc); return; }
787
788      // 1 — użytkownik „wpisuje" pytanie
789      var p = promptLine('', true);
790      output.appendChild(p.line);
791      for (var c = 0; c < sc.prompt.length; c++) {
792        if (my !== runId) return;
793        p.text.textContent += sc.prompt.charAt(c);
794        await wait(24);
795      }
796      if (p.cursor) p.line.removeChild(p.cursor);
797      await wait(360);
798      if (my !== runId) return;
799
800      // 2 — wywołanie narzędzia MCP
801      output.appendChild(glyphLine('mcp__ln--tool', '⏺', sc.call));
802      await wait(240);
803      if (my !== runId) return;
804
805      // 2b — czytelny dla człowieka opis tego, co robi narzędzie
806      if (sc.action) {
807        output.appendChild(glyphLine('mcp__ln--note', '↳', sc.action));
808        await wait(260);
809        if (my !== runId) return;
810      }
811
812      // 3 — retrieval: sam spinner przy ⎿, lekko wydłużony
813      var spin = el('mcp__ln mcp__ln--running');
814      var corner = document.createElement('span');
815      corner.className = 'mcp__glyph';
816      corner.textContent = '⎿';
817      spin.appendChild(corner);
818      spin.appendChild(document.createTextNode(' '));
819      var sg = document.createElement('span');
820      sg.className = 'mcp__spinner';
821      spin.appendChild(sg);
822      output.appendChild(spin);
823      for (var t = 0; t < 20; t++) {
824        if (my !== runId) return;
825        sg.textContent = SPINNER[t % SPINNER.length];
826        await wait(78);
827      }
828      await wait(220);
829      if (my !== runId) return;
830      output.removeChild(spin);
831
832      // 4 — wynik z metadanymi (aktualność, sygnatura Dz.U.)
833      appendResults(sc);
834      await wait(180);
835      if (my !== runId) return;
836
837      // 5 — cytat przepisu (dane)
838      var quote = el('mcp__quote', sc.quote);
839      output.appendChild(quote);
840      timers.push(setTimeout(function() { quote.classList.add('is-in'); }, 20));
841      await wait(440);
842      if (my !== runId) return;
843
844      // 6 — synteza AI strumieniowo, słowo po słowie
845      var ans = el('mcp__answer');
846      output.appendChild(ans);
847      var words = sc.answer.split(' ');
848      for (var w = 0; w < words.length; w++) {
849        if (my !== runId) return;
850        ans.textContent += (w ? ' ' : '') + words[w];
851        await wait(40);
852      }
853      await wait(200);
854      if (my !== runId) return;
855
856      // 7 — chip z podstawą prawną
857      var chip = citeChip(sc.citation);
858      output.appendChild(chip);
859      timers.push(setTimeout(function() { chip.classList.add('is-in'); }, 20));
860    }
861
862    var buttons = [];
863    MCP_SCENARIOS.forEach(function(sc, i) {
864      var btn = document.createElement('button');
865      btn.type = 'button';
866      btn.className = 'mcp__terminal-preset';
867      btn.textContent = sc.preset;
868      btn.setAttribute('data-mcp-scenario', i);
869      presetsGrid.appendChild(btn);
870      buttons.push(btn);
871    });
872
873    // Delegacja na kontenerze — odporna na przebudowę DOM i pewna w działaniu
874    presetsWrap.addEventListener('click', function(ev) {
875      var b = ev.target.closest('.mcp__terminal-preset');
876      if (!b || !presetsWrap.contains(b)) return;
877      var idx = parseInt(b.getAttribute('data-mcp-scenario'), 10);
878      if (isNaN(idx)) return;
879      userPicked = true;
880      play(idx);
881    });
882
883    // stan początkowy: gotowy transkrypt #1 (bez pustego okna)
884    renderFinal(MCP_SCENARIOS[0]);
885    setActive(0);
886
887    // pierwsze wejście w viewport → odegraj #1 z animacją (raz)
888    if ('IntersectionObserver' in window && !prefersReduced) {
889      var seen = false;
890      var io = new IntersectionObserver(function(entries) {
891        entries.forEach(function(e) {
892          if (e.isIntersecting && !seen && !userPicked) { seen = true; play(0); }
893        });
894      }, { threshold: 0.4 });
895      io.observe(root);
896    }
897  }
898
899  // ── Versus lead — "czytaj więcej" ──
900  function initVersusLeadToggles() {
901    document.querySelectorAll('.versus__lead-col-toggle').forEach(function(btn) {
902      var more = btn.previousElementSibling;
903      if (!more || !more.classList.contains('versus__lead-col-more')) return;
904      btn.addEventListener('click', function() {
905        var expanded = btn.getAttribute('aria-expanded') === 'true';
906        if (expanded) {
907          more.style.maxHeight = '0';
908          btn.setAttribute('aria-expanded', 'false');
909          btn.textContent = 'czytaj więcej +';
910        } else {
911          more.style.maxHeight = more.scrollHeight + 'px';
912          btn.setAttribute('aria-expanded', 'true');
913          btn.textContent = 'zwiń −';
914        }
915      });
916    });
917  }
918
919  // ── Init ──
920  document.addEventListener('DOMContentLoaded', function() {
921    loadTranslations(currentLang);
922    initScrollReveal();
923    initScrollSpy();
924    initMobileMenu();
925    initFaqAccordion();
926    initShowcaseTabs();
927    initLangSwitch();
928    initFoundersCarousel();
929    initFeaturesPersonas();
930    initMcpTerminal();
931    initVersusLeadToggles();
932  });
933})();

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.